From e4a986d02cc6c4385309e11e926ed72305b0ae3c Mon Sep 17 00:00:00 2001 From: ArdenHide Date: Tue, 29 Jul 2025 01:09:06 +0300 Subject: [PATCH 01/20] Refactor Net.Cache.DynamoDb.ERC20 --- .../Api/ApiERC20Service.cs | 86 ------- .../ERC20CacheProvider.cs | 124 ++++----- .../ERC20StorageProvider.cs | 230 ++++++++--------- .../Models/Api/ApiERC20ServiceConfig.cs | 45 ---- .../Models/Api/ApiResponse.cs | 17 -- .../Models/Api/Data.cs | 18 -- .../Models/Api/Item.cs | 38 --- .../Models/ApiRequestFactory.cs | 60 ----- .../Models/ERC20DynamoDbTable.cs | 242 +++++++++--------- .../Models/GetCacheRequest.cs | 156 +++++------ .../Net.Cache.DynamoDb.ERC20.csproj | 2 - .../RPC/ERC20Service.cs | 119 ++++----- .../RPC/Exceptions/Erc20QueryException.cs | 16 ++ .../RPC/Extensions/DecoderHelperExtensions.cs | 15 ++ .../RPC/IERC20Service.cs | 46 +--- .../RPC/Models/Erc20Token.cs | 23 ++ .../RPC/Models/MultiCall.cs | 20 ++ .../RPC/Models/MultiCallFunction.cs | 21 ++ 18 files changed, 520 insertions(+), 758 deletions(-) delete mode 100644 src/Net.Cache.DynamoDb.ERC20/Api/ApiERC20Service.cs delete mode 100644 src/Net.Cache.DynamoDb.ERC20/Models/Api/ApiERC20ServiceConfig.cs delete mode 100644 src/Net.Cache.DynamoDb.ERC20/Models/Api/ApiResponse.cs delete mode 100644 src/Net.Cache.DynamoDb.ERC20/Models/Api/Data.cs delete mode 100644 src/Net.Cache.DynamoDb.ERC20/Models/Api/Item.cs delete mode 100644 src/Net.Cache.DynamoDb.ERC20/Models/ApiRequestFactory.cs create mode 100644 src/Net.Cache.DynamoDb.ERC20/RPC/Exceptions/Erc20QueryException.cs create mode 100644 src/Net.Cache.DynamoDb.ERC20/RPC/Extensions/DecoderHelperExtensions.cs create mode 100644 src/Net.Cache.DynamoDb.ERC20/RPC/Models/Erc20Token.cs create mode 100644 src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCall.cs create mode 100644 src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCallFunction.cs diff --git a/src/Net.Cache.DynamoDb.ERC20/Api/ApiERC20Service.cs b/src/Net.Cache.DynamoDb.ERC20/Api/ApiERC20Service.cs deleted file mode 100644 index 3febd4d..0000000 --- a/src/Net.Cache.DynamoDb.ERC20/Api/ApiERC20Service.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using Flurl.Http; -using System.Numerics; -using Newtonsoft.Json; -using HandlebarsDotNet; -using Net.Web3.EthereumWallet; -using Net.Cache.DynamoDb.ERC20.RPC; -using Net.Cache.DynamoDb.ERC20.Models.Api; - -namespace Net.Cache.DynamoDb.ERC20.Api -{ - /// - /// The ApiERC20Service class provides functionality to interact with ERC20 tokens via an API. - /// This service caches the token data to avoid redundant API calls. - /// - public class ApiERC20Service : IERC20Service - { - private readonly ApiERC20ServiceConfig _config; - private readonly Lazy _tokenDataCache; - - /// - /// Gets the contract address of the ERC20 token. - /// - public EthereumAddress ContractAddress => _config.ContractAddress; - - /// - /// Initializes a new instance of the class using the provided configuration. - /// - /// The configuration containing API key, chain ID, contract address, and API URL. - public ApiERC20Service(ApiERC20ServiceConfig config) - { - _config = config; - _tokenDataCache = new Lazy(() => LoadTokenData(_config.ApiUrl)); - } - - /// - /// Loads the token data from the API and caches it. - /// - /// The API URL template to fetch the token data. - /// The token data as an . - private ApiResponse LoadTokenData(string apiUrl) - { - var template = Handlebars.Compile(apiUrl); - - var url = template(new - { - chainId = _config.ChainId, - contractAddress = _config.ContractAddress, - apiKey = _config.ApiKey - }); - - var responseString = url.GetStringAsync().GetAwaiter().GetResult(); - return JsonConvert.DeserializeObject(responseString); - } - - /// - /// Gets the cached token data. - /// - /// The cached token data as an . - public ApiResponse GetTokenData() => _tokenDataCache.Value; - - /// - /// Retrieves the number of decimals the ERC20 token uses. - /// - /// The number of decimals. - public byte Decimals() => GetTokenData().Data.Items[1].ContractDecimals; - - /// - /// Retrieves the name of the ERC20 token. - /// - /// The name of the token. - public string Name() => GetTokenData().Data.Items[1].ContractName; - - /// - /// Retrieves the symbol of the ERC20 token. - /// - /// The symbol of the token. - public string Symbol() => GetTokenData().Data.Items[1].ContractTickerSymbol; - - /// - /// Retrieves the total supply of the ERC20 token. - /// - /// The total supply as a . - public BigInteger TotalSupply() => BigInteger.Parse(GetTokenData().Data.Items[1].TotalSupply); - } -} \ No newline at end of file diff --git a/src/Net.Cache.DynamoDb.ERC20/ERC20CacheProvider.cs b/src/Net.Cache.DynamoDb.ERC20/ERC20CacheProvider.cs index 18927da..397f439 100644 --- a/src/Net.Cache.DynamoDb.ERC20/ERC20CacheProvider.cs +++ b/src/Net.Cache.DynamoDb.ERC20/ERC20CacheProvider.cs @@ -1,69 +1,69 @@ -using Net.Cryptography.SHA256; -using Net.Cache.DynamoDb.ERC20.Models; -using System.Threading.Tasks; +//using Net.Cryptography.SHA256; +//using Net.Cache.DynamoDb.ERC20.Models; +//using System.Threading.Tasks; -namespace Net.Cache.DynamoDb.ERC20 -{ - /// - /// Provides functionality to manage the caching of ERC20 token information. - /// - /// - /// This provider encapsulates logic for retrieving and storing ERC20 token details - /// within a DynamoDB cache. It supports operations to get existing cache entries or add new entries when required. - /// - public class ERC20CacheProvider - { - private readonly ERC20StorageProvider storageProvider; +//namespace Net.Cache.DynamoDb.ERC20 +//{ +// /// +// /// Provides functionality to manage the caching of ERC20 token information. +// /// +// /// +// /// This provider encapsulates logic for retrieving and storing ERC20 token details +// /// within a DynamoDB cache. It supports operations to get existing cache entries or add new entries when required. +// /// +// public class ERC20CacheProvider +// { +// private readonly ERC20StorageProvider storageProvider; - /// - /// Initializes a new instance of the class with a default storage provider. - /// - public ERC20CacheProvider() - : this(new ERC20StorageProvider()) - { } +// /// +// /// Initializes a new instance of the class with a default storage provider. +// /// +// public ERC20CacheProvider() +// : this(new ERC20StorageProvider()) +// { } - /// - /// Initializes a new instance of the class with a specified storage provider. - /// - /// The storage provider to be used for caching. - public ERC20CacheProvider(ERC20StorageProvider storageProvider) - { - this.storageProvider = storageProvider; - } +// /// +// /// Initializes a new instance of the class with a specified storage provider. +// /// +// /// The storage provider to be used for caching. +// public ERC20CacheProvider(ERC20StorageProvider storageProvider) +// { +// this.storageProvider = storageProvider; +// } - /// - /// Retrieves an ERC20 token information from the cache or adds it to the cache if it does not exist. - /// - /// The request containing details required to retrieve or add the ERC20 token information. - /// The ERC20 token information from the cache. - /// - /// This method checks the cache for an existing entry for the specified ERC20 token. - /// If the entry exists, it is returned. Otherwise, a new entry is created using the provided - /// details in the request and added to the cache. - /// - public virtual ERC20DynamoDbTable GetOrAdd(GetCacheRequest request) - { - if (storageProvider.TryGetValue($"{request.ChainId}-{request.ERC20Service.ContractAddress}".ToSha256(), request, out var storedValue)) - { - return storedValue; - } +// /// +// /// Retrieves an ERC20 token information from the cache or adds it to the cache if it does not exist. +// /// +// /// The request containing details required to retrieve or add the ERC20 token information. +// /// The ERC20 token information from the cache. +// /// +// /// This method checks the cache for an existing entry for the specified ERC20 token. +// /// If the entry exists, it is returned. Otherwise, a new entry is created using the provided +// /// details in the request and added to the cache. +// /// +// public virtual ERC20DynamoDbTable GetOrAdd(GetCacheRequest request) +// { +// if (storageProvider.TryGetValue($"{request.ChainId}-{request.ERC20Service.ContractAddress}".ToSha256(), request, out var storedValue)) +// { +// return storedValue; +// } - storedValue = new ERC20DynamoDbTable(request.ChainId, request.ERC20Service); - storageProvider.Store(storedValue.HashKey, storedValue); - return storedValue; - } +// storedValue = new ERC20DynamoDbTable(request.ChainId, request.ERC20Service); +// storageProvider.Store(storedValue.HashKey, storedValue); +// return storedValue; +// } - public virtual async Task GetOrAddAsync(GetCacheRequest request) - { - var (isExist, storedValue) = await storageProvider.TryGetValueAsync($"{request.ChainId}-{request.ERC20Service.ContractAddress}".ToSha256(), request); - if (isExist && storedValue != null) - { - return storedValue; - } +// public virtual async Task GetOrAddAsync(GetCacheRequest request) +// { +// var (isExist, storedValue) = await storageProvider.TryGetValueAsync($"{request.ChainId}-{request.ERC20Service.ContractAddress}".ToSha256(), request); +// if (isExist && storedValue != null) +// { +// return storedValue; +// } - storedValue = new ERC20DynamoDbTable(request.ChainId, request.ERC20Service); - await storageProvider.StoreAsync(storedValue.HashKey, storedValue); - return storedValue; - } - } -} \ No newline at end of file +// storedValue = new ERC20DynamoDbTable(request.ChainId, request.ERC20Service); +// await storageProvider.StoreAsync(storedValue.HashKey, storedValue); +// return storedValue; +// } +// } +//} \ No newline at end of file diff --git a/src/Net.Cache.DynamoDb.ERC20/ERC20StorageProvider.cs b/src/Net.Cache.DynamoDb.ERC20/ERC20StorageProvider.cs index 2406fb6..9d3a27a 100644 --- a/src/Net.Cache.DynamoDb.ERC20/ERC20StorageProvider.cs +++ b/src/Net.Cache.DynamoDb.ERC20/ERC20StorageProvider.cs @@ -1,127 +1,127 @@ -using System.Threading.Tasks; -using Amazon.DynamoDBv2.DataModel; -using Net.Cache.DynamoDb.ERC20.RPC; -using Net.Cache.DynamoDb.ERC20.Models; -using System.Diagnostics.CodeAnalysis; +//using System.Threading.Tasks; +//using Amazon.DynamoDBv2.DataModel; +//using Net.Cache.DynamoDb.ERC20.RPC; +//using Net.Cache.DynamoDb.ERC20.Models; +//using System.Diagnostics.CodeAnalysis; -namespace Net.Cache.DynamoDb.ERC20 -{ - /// - /// Manages the storage and retrieval of ERC20 token information in a DynamoDB table. - /// - /// - /// This provider extends the to specifically handle the operations - /// related to storing and accessing ERC20 token data. It includes functionalities to update - /// the total supply of a token and to retrieve or store token information based on a given key. - /// - public class ERC20StorageProvider : DynamoDbStorageProvider - { - /// - /// Initializes a new instance of the class with an optional . - /// - /// The name of the DynamoDB table to be used. If not provided, a default table name is used. - public ERC20StorageProvider(string tableName = EmptyString) - : base(tableName) - { } +//namespace Net.Cache.DynamoDb.ERC20 +//{ +// /// +// /// Manages the storage and retrieval of ERC20 token information in a DynamoDB table. +// /// +// /// +// /// This provider extends the to specifically handle the operations +// /// related to storing and accessing ERC20 token data. It includes functionalities to update +// /// the total supply of a token and to retrieve or store token information based on a given key. +// /// +// public class ERC20StorageProvider : DynamoDbStorageProvider +// { +// /// +// /// Initializes a new instance of the class with an optional . +// /// +// /// The name of the DynamoDB table to be used. If not provided, a default table name is used. +// public ERC20StorageProvider(string tableName = EmptyString) +// : base(tableName) +// { } - /// - /// Initializes a new instance of the class with a specific DynamoDB and an optional . - /// - /// The DynamoDB context to be used for operations. - /// The name of the DynamoDB table to be used. If not provided, a default table name is used. - public ERC20StorageProvider(IDynamoDBContext context, string tableName = EmptyString) - : base(context, tableName) - { } +// /// +// /// Initializes a new instance of the class with a specific DynamoDB and an optional . +// /// +// /// The DynamoDB context to be used for operations. +// /// The name of the DynamoDB table to be used. If not provided, a default table name is used. +// public ERC20StorageProvider(IDynamoDBContext context, string tableName = EmptyString) +// : base(context, tableName) +// { } - /// - /// Updates the total supply of an existing ERC20 token information entry. - /// - /// The existing entry of ERC20 token information. - /// The ERC20 service to use for updating the total supply. - /// The updated ERC20 token information entry. - protected virtual ERC20DynamoDbTable UpdateTotalSupply(ERC20DynamoDbTable existValue, IERC20Service erc20Service) - { - var updatedValue = new ERC20DynamoDbTable( - existValue.ChainId, - existValue.Address, - existValue.Name, - existValue.Symbol, - existValue.Decimals, - erc20Service.TotalSupply() - ); - Context.SaveAsync(updatedValue, OperationConfig()) - .GetAwaiter() - .GetResult(); - return updatedValue; - } +// /// +// /// Updates the total supply of an existing ERC20 token information entry. +// /// +// /// The existing entry of ERC20 token information. +// /// The ERC20 service to use for updating the total supply. +// /// The updated ERC20 token information entry. +// protected virtual ERC20DynamoDbTable UpdateTotalSupply(ERC20DynamoDbTable existValue, IERC20Service erc20Service) +// { +// var updatedValue = new ERC20DynamoDbTable( +// existValue.ChainId, +// existValue.Address, +// existValue.Name, +// existValue.Symbol, +// existValue.Decimals, +// erc20Service.TotalSupply() +// ); +// Context.SaveAsync(updatedValue, OperationConfig()) +// .GetAwaiter() +// .GetResult(); +// return updatedValue; +// } - protected virtual async Task UpdateTotalSupplyAsync(ERC20DynamoDbTable existValue, IERC20Service erc20Service) - { - var updatedValue = new ERC20DynamoDbTable( - existValue.ChainId, - existValue.Address, - existValue.Name, - existValue.Symbol, - existValue.Decimals, - erc20Service.TotalSupply() - ); - await Context.SaveAsync(updatedValue, OperationConfig()); - return updatedValue; - } +// protected virtual async Task UpdateTotalSupplyAsync(ERC20DynamoDbTable existValue, IERC20Service erc20Service) +// { +// var updatedValue = new ERC20DynamoDbTable( +// existValue.ChainId, +// existValue.Address, +// existValue.Name, +// existValue.Symbol, +// existValue.Decimals, +// erc20Service.TotalSupply() +// ); +// await Context.SaveAsync(updatedValue, OperationConfig()); +// return updatedValue; +// } - /// - /// Tries to retrieve an ERC20 token information entry from the cache based on a given key. - /// - /// The key associated with the ERC20 token information. - /// The request containing details for the retrieval operation. - /// When this method returns, contains the ERC20 token information associated with the specified key, if the key is found; otherwise, . - /// if the token information is found; otherwise, . - /// - /// This method also updates the total supply of the token information if the flag is set in the . - /// - public virtual bool TryGetValue(string key, GetCacheRequest request, [MaybeNullWhen(false)] out ERC20DynamoDbTable value) - { - if (!base.TryGetValue(key, out value)) - { - return false; - } +// /// +// /// Tries to retrieve an ERC20 token information entry from the cache based on a given key. +// /// +// /// The key associated with the ERC20 token information. +// /// The request containing details for the retrieval operation. +// /// When this method returns, contains the ERC20 token information associated with the specified key, if the key is found; otherwise, . +// /// if the token information is found; otherwise, . +// /// +// /// This method also updates the total supply of the token information if the flag is set in the . +// /// +// public virtual bool TryGetValue(string key, GetCacheRequest request, [MaybeNullWhen(false)] out ERC20DynamoDbTable value) +// { +// if (!base.TryGetValue(key, out value)) +// { +// return false; +// } - if (request.UpdateTotalSupply) - { - value = UpdateTotalSupply(value, request.ERC20Service); - } +// if (request.UpdateTotalSupply) +// { +// value = UpdateTotalSupply(value, request.ERC20Service); +// } - return true; - } +// return true; +// } - public virtual async Task<(bool isExist, ERC20DynamoDbTable? Value)> TryGetValueAsync(string key, GetCacheRequest request) - { - try - { - var storedValue = await Context.LoadAsync(key, OperationConfig()); +// public virtual async Task<(bool isExist, ERC20DynamoDbTable? Value)> TryGetValueAsync(string key, GetCacheRequest request) +// { +// try +// { +// var storedValue = await Context.LoadAsync(key, OperationConfig()); - if (storedValue == null) - { - return (false, null); - } +// if (storedValue == null) +// { +// return (false, null); +// } - if (request.UpdateTotalSupply) - { - storedValue = await UpdateTotalSupplyAsync(storedValue, request.ERC20Service); - } +// if (request.UpdateTotalSupply) +// { +// storedValue = await UpdateTotalSupplyAsync(storedValue, request.ERC20Service); +// } - return (true, storedValue); - } - catch (Amazon.DynamoDBv2.AmazonDynamoDBException) - { - throw; - } - catch - { - return (false, null); - } - } +// return (true, storedValue); +// } +// catch (Amazon.DynamoDBv2.AmazonDynamoDBException) +// { +// throw; +// } +// catch +// { +// return (false, null); +// } +// } - public virtual Task StoreAsync(string key, ERC20DynamoDbTable value) => Context.SaveAsync(value); - } -} \ No newline at end of file +// public virtual Task StoreAsync(string key, ERC20DynamoDbTable value) => Context.SaveAsync(value); +// } +//} \ No newline at end of file diff --git a/src/Net.Cache.DynamoDb.ERC20/Models/Api/ApiERC20ServiceConfig.cs b/src/Net.Cache.DynamoDb.ERC20/Models/Api/ApiERC20ServiceConfig.cs deleted file mode 100644 index b729b1e..0000000 --- a/src/Net.Cache.DynamoDb.ERC20/Models/Api/ApiERC20ServiceConfig.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Net.Web3.EthereumWallet; - -namespace Net.Cache.DynamoDb.ERC20.Models.Api -{ - /// - /// Represents the configuration settings required for the API-based ERC20 service. - /// - public class ApiERC20ServiceConfig - { - /// - /// Initializes a new instance of the class with the specified settings. - /// - /// The API key used to authenticate requests to the ERC20 token service. - /// The blockchain chain ID where the ERC20 token resides. - /// The Ethereum address of the ERC20 token contract. - /// The URL template for making API requests to retrieve ERC20 token information. - public ApiERC20ServiceConfig(string apiKey, long chainId, EthereumAddress contractAddress, string apiUrl) - { - ApiKey = apiKey; - ChainId = chainId; - ContractAddress = contractAddress; - ApiUrl = apiUrl; - } - - /// - /// Gets or sets the API key used to authenticate requests to the ERC20 token service. - /// - public string ApiKey { get; set; } - - /// - /// Gets or sets the blockchain chain ID where the ERC20 token resides. - /// - public long ChainId { get; set; } - - /// - /// Gets or sets the Ethereum address of the ERC20 token contract. - /// - public EthereumAddress ContractAddress { get; set; } - - /// - /// Gets or sets the URL template for making API requests to retrieve ERC20 token information. - /// - public string ApiUrl { get; set; } - } -} \ No newline at end of file diff --git a/src/Net.Cache.DynamoDb.ERC20/Models/Api/ApiResponse.cs b/src/Net.Cache.DynamoDb.ERC20/Models/Api/ApiResponse.cs deleted file mode 100644 index 71ac17b..0000000 --- a/src/Net.Cache.DynamoDb.ERC20/Models/Api/ApiResponse.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Newtonsoft.Json; - -namespace Net.Cache.DynamoDb.ERC20.Models.Api -{ - /// - /// Represents the data container for ERC20 token information retrieved from an API response. - /// - public class ApiResponse - { - /// - /// Gets or sets the list of items containing ERC20 token information. - /// - /// A list of objects that hold the details of ERC20 tokens. - [JsonProperty("data")] - public Data Data { get; set; } = null!; - } -} \ No newline at end of file diff --git a/src/Net.Cache.DynamoDb.ERC20/Models/Api/Data.cs b/src/Net.Cache.DynamoDb.ERC20/Models/Api/Data.cs deleted file mode 100644 index e3204a4..0000000 --- a/src/Net.Cache.DynamoDb.ERC20/Models/Api/Data.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace Net.Cache.DynamoDb.ERC20.Models.Api -{ - /// - /// Represents the data container for ERC20 token information retrieved from an API response. - /// - public class Data - { - /// - /// Gets or sets the list of items containing ERC20 token information. - /// - /// A list of objects that hold the details of ERC20 tokens. - [JsonProperty("items")] - public List Items { get; set; } = null!; - } -} \ No newline at end of file diff --git a/src/Net.Cache.DynamoDb.ERC20/Models/Api/Item.cs b/src/Net.Cache.DynamoDb.ERC20/Models/Api/Item.cs deleted file mode 100644 index 0cf1283..0000000 --- a/src/Net.Cache.DynamoDb.ERC20/Models/Api/Item.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Newtonsoft.Json; - -namespace Net.Cache.DynamoDb.ERC20.Models.Api -{ - /// - /// Represents an item containing ERC20 token information retrieved from an API response. - /// - public class Item - { - /// - /// Gets or sets the number of decimals the ERC20 token uses. - /// - /// The number of decimals for the ERC20 token. - [JsonProperty("contract_decimals")] - public byte ContractDecimals { get; set; } - - /// - /// Gets or sets the name of the ERC20 token. - /// - /// The name of the ERC20 token. - [JsonProperty("contract_name")] - public string ContractName { get; set; } = null!; - - /// - /// Gets or sets the ticker symbol of the ERC20 token. - /// - /// The ticker symbol of the ERC20 token. - [JsonProperty("contract_ticker_symbol")] - public string ContractTickerSymbol { get; set; } = null!; - - /// - /// Gets or sets the total supply of the ERC20 token as a string. - /// - /// The total supply of the ERC20 token. - [JsonProperty("total_supply")] - public string TotalSupply { get; set; } = null!; - } -} \ No newline at end of file diff --git a/src/Net.Cache.DynamoDb.ERC20/Models/ApiRequestFactory.cs b/src/Net.Cache.DynamoDb.ERC20/Models/ApiRequestFactory.cs deleted file mode 100644 index ff38adf..0000000 --- a/src/Net.Cache.DynamoDb.ERC20/Models/ApiRequestFactory.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Net.Web3.EthereumWallet; -using Net.Cache.DynamoDb.ERC20.Api; -using Net.Cache.DynamoDb.ERC20.Models.Api; - -namespace Net.Cache.DynamoDb.ERC20.Models -{ - /// - /// A factory class for creating instances of API-related services and requests. - /// - /// - /// This factory class simplifies the creation of configurations, services, and requests - /// for interacting with ERC20 tokens via API services. - /// - public class ApiRequestFactory - { - /// - /// Creates an instance of using the provided API key, chain ID, contract address, and API URL. - /// - /// The API key for accessing the ERC20 token data. - /// The blockchain chain ID where the ERC20 token resides. - /// The Ethereum address of the ERC20 token contract. - /// The API URL template for fetching the token data. - /// An instance of configured with the provided parameters. - public virtual ApiERC20ServiceConfig CreateApiServiceConfig(string apiKey, long chainId, EthereumAddress contractAddress, string apiUrl) => new ApiERC20ServiceConfig(apiKey, chainId, contractAddress, apiUrl); - - /// - /// Creates an instance of using the provided configuration. - /// - /// The configuration object containing API key, chain ID, contract address, and API URL. - /// An instance of configured with the provided configuration. - public virtual ApiERC20Service CreateApiService(ApiERC20ServiceConfig config) => new ApiERC20Service(config); - - /// - /// Creates an instance of using the provided API service and chain ID. - /// - /// The API service for interacting with the ERC20 token. - /// The blockchain chain ID where the ERC20 token resides. - /// An instance of configured with the provided service and chain ID. - public virtual GetCacheRequest CreateWithApiService(ApiERC20Service apiService, long chainId) => new GetCacheRequest(chainId, apiService); - - /// - /// Creates an instance of by configuring and initializing the necessary API service. - /// - /// The API key used to authenticate requests to the ERC20 token service. - /// The blockchain chain ID where the ERC20 token resides. - /// The Ethereum address of the ERC20 token contract. - /// The API URL template for fetching the token data. - /// An instance of configured with the appropriate service and chain ID. - /// - /// This method simplifies the process of creating a by internally handling the - /// creation of and objects. - /// - public virtual GetCacheRequest CreateCacheRequest(string apiKey, long chainId, EthereumAddress contractAddress, string apiUrl) - { - var config = new ApiERC20ServiceConfig(apiKey, chainId, contractAddress, apiUrl); - var apiService = new ApiERC20Service(config); - return new GetCacheRequest(chainId, apiService); - } - } -} \ No newline at end of file diff --git a/src/Net.Cache.DynamoDb.ERC20/Models/ERC20DynamoDbTable.cs b/src/Net.Cache.DynamoDb.ERC20/Models/ERC20DynamoDbTable.cs index 5b2f98b..bfaf3e5 100644 --- a/src/Net.Cache.DynamoDb.ERC20/Models/ERC20DynamoDbTable.cs +++ b/src/Net.Cache.DynamoDb.ERC20/Models/ERC20DynamoDbTable.cs @@ -1,132 +1,132 @@ -using System.Numerics; -using Net.Web3.EthereumWallet; -using Net.Cryptography.SHA256; -using Amazon.DynamoDBv2.DataModel; -using Net.Cache.DynamoDb.ERC20.RPC; +//using System.Numerics; +//using Net.Web3.EthereumWallet; +//using Net.Cryptography.SHA256; +//using Amazon.DynamoDBv2.DataModel; +//using Net.Cache.DynamoDb.ERC20.RPC; -namespace Net.Cache.DynamoDb.ERC20.Models -{ - /// - /// Represents an ERC20 token information cache entry in DynamoDB. - /// - /// - /// This class is designed to store and manage the basic details of an ERC20 token, - /// including its chain ID, address, name, symbol, decimals, and total supply. It also - /// generates a unique hash key for each token based on its chain ID and address. - /// - [DynamoDBTable("TokensInfoCache")] - public class ERC20DynamoDbTable - { - /// - /// Gets the hash key for the ERC20 token entry, uniquely generated based on the chain ID and token address. - /// - [DynamoDBHashKey] - public string HashKey { get; set; } = string.Empty; +//namespace Net.Cache.DynamoDb.ERC20.Models +//{ +// /// +// /// Represents an ERC20 token information cache entry in DynamoDB. +// /// +// /// +// /// This class is designed to store and manage the basic details of an ERC20 token, +// /// including its chain ID, address, name, symbol, decimals, and total supply. It also +// /// generates a unique hash key for each token based on its chain ID and address. +// /// +// [DynamoDBTable("TokensInfoCache")] +// public class ERC20DynamoDbTable +// { +// /// +// /// Gets the hash key for the ERC20 token entry, uniquely generated based on the chain ID and token address. +// /// +// [DynamoDBHashKey] +// public string HashKey { get; set; } = string.Empty; - /// - /// Gets the block-chain chain ID where the ERC20 token is located. - /// - [DynamoDBProperty] - public long ChainId { get; set; } +// /// +// /// Gets the block-chain chain ID where the ERC20 token is located. +// /// +// [DynamoDBProperty] +// public long ChainId { get; set; } - /// - /// Gets the ERC20 token contract address. - /// - [DynamoDBProperty] - public string Address { get; set; } = string.Empty; +// /// +// /// Gets the ERC20 token contract address. +// /// +// [DynamoDBProperty] +// public string Address { get; set; } = string.Empty; - /// - /// Gets the name of the ERC20 token. - /// - [DynamoDBProperty] - public string Name { get; set; } = string.Empty; +// /// +// /// Gets the name of the ERC20 token. +// /// +// [DynamoDBProperty] +// public string Name { get; set; } = string.Empty; - /// - /// Gets the symbol of the ERC20 token. - /// - [DynamoDBProperty] - public string Symbol { get; set; } = string.Empty; +// /// +// /// Gets the symbol of the ERC20 token. +// /// +// [DynamoDBProperty] +// public string Symbol { get; set; } = string.Empty; - /// - /// Gets the decimals of the ERC20 token, indicating how divisible it is. - /// - [DynamoDBProperty] - public byte Decimals { get; set; } +// /// +// /// Gets the decimals of the ERC20 token, indicating how divisible it is. +// /// +// [DynamoDBProperty] +// public byte Decimals { get; set; } - /// - /// Gets the total supply of the ERC20 token. - /// - [DynamoDBProperty] - public decimal TotalSupply { get; set; } +// /// +// /// Gets the total supply of the ERC20 token. +// /// +// [DynamoDBProperty] +// public decimal TotalSupply { get; set; } - /// - /// Initializes a new instance of the class with specified token details. - /// - /// The block-chain chain ID. - /// The ERC20 token contract address. - /// The name of the ERC20 token. - /// The symbol of the ERC20 token. - /// The decimals of the ERC20 token. - /// The total supply of the ERC20 token. - /// - /// This constructor expects an already calculated based on the total supply value and . - /// - public ERC20DynamoDbTable(long chainId, EthereumAddress address, string name, string symbol, byte decimals, decimal totalSupply) - { - ChainId = chainId; - Address = address; - Name = name; - Symbol = symbol; - Decimals = decimals; - TotalSupply = totalSupply; - HashKey = $"{ChainId}-{Address}".ToSha256(); - } +// /// +// /// Initializes a new instance of the class with specified token details. +// /// +// /// The block-chain chain ID. +// /// The ERC20 token contract address. +// /// The name of the ERC20 token. +// /// The symbol of the ERC20 token. +// /// The decimals of the ERC20 token. +// /// The total supply of the ERC20 token. +// /// +// /// This constructor expects an already calculated based on the total supply value and . +// /// +// public ERC20DynamoDbTable(long chainId, EthereumAddress address, string name, string symbol, byte decimals, decimal totalSupply) +// { +// ChainId = chainId; +// Address = address; +// Name = name; +// Symbol = symbol; +// Decimals = decimals; +// TotalSupply = totalSupply; +// HashKey = $"{ChainId}-{Address}".ToSha256(); +// } - /// - /// Initializes a new instance of the class with specified token details and total supply in . - /// - /// The block-chain chain ID. - /// The ERC20 token contract address. - /// The name of the ERC20 token. - /// The symbol of the ERC20 token. - /// The decimals of the ERC20 token. - /// The total supply of the ERC20 token in format. - /// - /// This constructor converts the from to , considering the token's . - /// - public ERC20DynamoDbTable(long chainId, EthereumAddress address, string name, string symbol, byte decimals, BigInteger totalSupply) - { - ChainId = chainId; - Address = address; - Name = name; - Symbol = symbol; - Decimals = decimals; - TotalSupply = Nethereum.Web3.Web3.Convert.FromWei(totalSupply, decimals); - HashKey = $"{ChainId}-{Address}".ToSha256(); - } +// /// +// /// Initializes a new instance of the class with specified token details and total supply in . +// /// +// /// The block-chain chain ID. +// /// The ERC20 token contract address. +// /// The name of the ERC20 token. +// /// The symbol of the ERC20 token. +// /// The decimals of the ERC20 token. +// /// The total supply of the ERC20 token in format. +// /// +// /// This constructor converts the from to , considering the token's . +// /// +// public ERC20DynamoDbTable(long chainId, EthereumAddress address, string name, string symbol, byte decimals, BigInteger totalSupply) +// { +// ChainId = chainId; +// Address = address; +// Name = name; +// Symbol = symbol; +// Decimals = decimals; +// TotalSupply = Nethereum.Web3.Web3.Convert.FromWei(totalSupply, decimals); +// HashKey = $"{ChainId}-{Address}".ToSha256(); +// } - /// - /// Initializes a new instance of the class using an to populate token details. - /// - /// The block-chain chain ID. - /// The ERC20 service providing access to token details. - /// - /// This constructor retrieves token details such as name, symbol, decimals, and total supply from the provided . - /// - public ERC20DynamoDbTable(long chainId, IERC20Service erc20Service) - { - ChainId = chainId; - Address = erc20Service.ContractAddress; - Name = erc20Service.Name(); - Symbol = erc20Service.Symbol(); - Decimals = erc20Service.Decimals(); - TotalSupply = Nethereum.Web3.Web3.Convert.FromWei(erc20Service.TotalSupply(), Decimals); - HashKey = $"{ChainId}-{Address}".ToSha256(); - } +// /// +// /// Initializes a new instance of the class using an to populate token details. +// /// +// /// The block-chain chain ID. +// /// The ERC20 service providing access to token details. +// /// +// /// This constructor retrieves token details such as name, symbol, decimals, and total supply from the provided . +// /// +// public ERC20DynamoDbTable(long chainId, IERC20Service erc20Service) +// { +// ChainId = chainId; +// Address = erc20Service.ContractAddress; +// Name = erc20Service.Name(); +// Symbol = erc20Service.Symbol(); +// Decimals = erc20Service.Decimals(); +// TotalSupply = Nethereum.Web3.Web3.Convert.FromWei(erc20Service.TotalSupply(), Decimals); +// HashKey = $"{ChainId}-{Address}".ToSha256(); +// } - /// - /// Constructor without parameters for working "Amazon.DynamoDBv2" - /// - public ERC20DynamoDbTable() { } - } -} \ No newline at end of file +// /// +// /// Constructor without parameters for working "Amazon.DynamoDBv2" +// /// +// public ERC20DynamoDbTable() { } +// } +//} \ No newline at end of file diff --git a/src/Net.Cache.DynamoDb.ERC20/Models/GetCacheRequest.cs b/src/Net.Cache.DynamoDb.ERC20/Models/GetCacheRequest.cs index 77fa7b2..68688e2 100644 --- a/src/Net.Cache.DynamoDb.ERC20/Models/GetCacheRequest.cs +++ b/src/Net.Cache.DynamoDb.ERC20/Models/GetCacheRequest.cs @@ -1,85 +1,85 @@ -using System; -using Net.Web3.EthereumWallet; -using Net.Cache.DynamoDb.ERC20.RPC; -using System.Threading.Tasks; +//using System; +//using Net.Web3.EthereumWallet; +//using Net.Cache.DynamoDb.ERC20.RPC; +//using System.Threading.Tasks; -namespace Net.Cache.DynamoDb.ERC20.Models -{ - /// - /// Represents a request to retrieve or update ERC20 token information in the cache. - /// - /// - /// This class encapsulates the details necessary for fetching or updating the cached data of an ERC20 token, - /// including the block-chain chain ID, the , and a flag indicating whether to update the total supply. - /// - public class GetCacheRequest - { - /// - /// Gets the block-chain chain ID for the request. - /// - public long ChainId { get; } +//namespace Net.Cache.DynamoDb.ERC20.Models +//{ +// /// +// /// Represents a request to retrieve or update ERC20 token information in the cache. +// /// +// /// +// /// This class encapsulates the details necessary for fetching or updating the cached data of an ERC20 token, +// /// including the block-chain chain ID, the , and a flag indicating whether to update the total supply. +// /// +// public class GetCacheRequest +// { +// /// +// /// Gets the block-chain chain ID for the request. +// /// +// public long ChainId { get; } - /// - /// Gets the used to interact with the ERC20 token contract. - /// - public IERC20Service ERC20Service { get; } +// /// +// /// Gets the used to interact with the ERC20 token contract. +// /// +// public IERC20Service ERC20Service { get; } - /// - /// Gets a value indicating whether the total supply of the token should be updated in the cache. - /// - public bool UpdateTotalSupply { get; } +// /// +// /// Gets a value indicating whether the total supply of the token should be updated in the cache. +// /// +// public bool UpdateTotalSupply { get; } - /// - /// Initializes a new instance of the class with specified chain ID and contract address. - /// - /// The block-chain chain ID. - /// The ERC20 token contract address. - /// The URL of the RPC endpoint to interact with the block-chain. - /// Optional. Indicates whether to update the total supply of the token in the cache. Defaults to . - /// - /// This constructor creates an instance of the class using the provided RPC URL and contract address. - /// - public GetCacheRequest(long chainId, EthereumAddress contractAddress, string rpcUrl, bool updateTotalSupply = true) - : this(chainId, new ERC20Service(() => rpcUrl, contractAddress), updateTotalSupply) - { } +// /// +// /// Initializes a new instance of the class with specified chain ID and contract address. +// /// +// /// The block-chain chain ID. +// /// The ERC20 token contract address. +// /// The URL of the RPC endpoint to interact with the block-chain. +// /// Optional. Indicates whether to update the total supply of the token in the cache. Defaults to . +// /// +// /// This constructor creates an instance of the class using the provided RPC URL and contract address. +// /// +// public GetCacheRequest(long chainId, EthereumAddress contractAddress, string rpcUrl, bool updateTotalSupply = true) +// : this(chainId, new ERC20Service(() => rpcUrl, contractAddress), updateTotalSupply) +// { } - /// - /// Initializes a new instance of the class with specified chain ID, contract address and RPC URL factory. - /// - /// The block-chain chain ID. - /// The ERC20 token contract address. - /// A function that returns the RPC URL to interact with the block-chain. - /// Optional. Indicates whether to update the total supply of the token in the cache. Defaults to . - public GetCacheRequest(long chainId, EthereumAddress contractAddress, Func rpcUrlFactory, bool updateTotalSupply = true) - : this(chainId, new ERC20Service(rpcUrlFactory, contractAddress), updateTotalSupply) - { } +// /// +// /// Initializes a new instance of the class with specified chain ID, contract address and RPC URL factory. +// /// +// /// The block-chain chain ID. +// /// The ERC20 token contract address. +// /// A function that returns the RPC URL to interact with the block-chain. +// /// Optional. Indicates whether to update the total supply of the token in the cache. Defaults to . +// public GetCacheRequest(long chainId, EthereumAddress contractAddress, Func rpcUrlFactory, bool updateTotalSupply = true) +// : this(chainId, new ERC20Service(rpcUrlFactory, contractAddress), updateTotalSupply) +// { } - /// - /// Initializes a new instance of the class with specified chain ID, contract address and asynchronous RPC URL factory. - /// - /// The block-chain chain ID. - /// The ERC20 token contract address. - /// A function that asynchronously returns the RPC URL to interact with the block-chain. - /// Optional. Indicates whether to update the total supply of the token in the cache. Defaults to . - public GetCacheRequest(long chainId, EthereumAddress contractAddress, Func> rpcUrlFactoryAsync, bool updateTotalSupply = true) - : this(chainId, new ERC20Service(rpcUrlFactoryAsync, contractAddress), updateTotalSupply) - { } +// /// +// /// Initializes a new instance of the class with specified chain ID, contract address and asynchronous RPC URL factory. +// /// +// /// The block-chain chain ID. +// /// The ERC20 token contract address. +// /// A function that asynchronously returns the RPC URL to interact with the block-chain. +// /// Optional. Indicates whether to update the total supply of the token in the cache. Defaults to . +// public GetCacheRequest(long chainId, EthereumAddress contractAddress, Func> rpcUrlFactoryAsync, bool updateTotalSupply = true) +// : this(chainId, new ERC20Service(rpcUrlFactoryAsync, contractAddress), updateTotalSupply) +// { } - /// - /// Initializes a new instance of the class with a specified ERC20 service. - /// - /// The block-chain chain ID. - /// The providing access to the token's details. - /// Optional. Indicates whether to update the total supply of the token in the cache. Defaults to . - /// - /// This constructor allows for more flexibility by accepting an instance of an , - /// enabling the use of customized or mock services for testing purposes. - /// - public GetCacheRequest(long chainId, IERC20Service erc20Service, bool updateTotalSupply = true) - { - ChainId = chainId; - ERC20Service = erc20Service; - UpdateTotalSupply = updateTotalSupply; - } - } -} \ No newline at end of file +// /// +// /// Initializes a new instance of the class with a specified ERC20 service. +// /// +// /// The block-chain chain ID. +// /// The providing access to the token's details. +// /// Optional. Indicates whether to update the total supply of the token in the cache. Defaults to . +// /// +// /// This constructor allows for more flexibility by accepting an instance of an , +// /// enabling the use of customized or mock services for testing purposes. +// /// +// public GetCacheRequest(long chainId, IERC20Service erc20Service, bool updateTotalSupply = true) +// { +// ChainId = chainId; +// ERC20Service = erc20Service; +// UpdateTotalSupply = updateTotalSupply; +// } +// } +//} \ No newline at end of file diff --git a/src/Net.Cache.DynamoDb.ERC20/Net.Cache.DynamoDb.ERC20.csproj b/src/Net.Cache.DynamoDb.ERC20/Net.Cache.DynamoDb.ERC20.csproj index 063b406..13867bf 100644 --- a/src/Net.Cache.DynamoDb.ERC20/Net.Cache.DynamoDb.ERC20.csproj +++ b/src/Net.Cache.DynamoDb.ERC20/Net.Cache.DynamoDb.ERC20.csproj @@ -10,8 +10,6 @@ - - diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs index 910a107..1966143 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs @@ -1,93 +1,60 @@ using System; using Nethereum.Web3; -using System.Numerics; -using System.Threading; +using Nethereum.Contracts; using System.Threading.Tasks; using Net.Web3.EthereumWallet; -using Nethereum.Contracts.Standards.ERC20; +using System.Collections.Generic; +using Net.Cache.DynamoDb.ERC20.RPC.Models; +using Net.Cache.DynamoDb.ERC20.RPC.Extensions; +using Net.Cache.DynamoDb.ERC20.RPC.Exceptions; +using Nethereum.Contracts.Standards.ERC20.ContractDefinition; namespace Net.Cache.DynamoDb.ERC20.RPC { - /// - /// Provides functionalities to interact with ERC20 tokens on the block-chain via RPC calls. - /// - /// - /// This class encapsulates the interactions with an ERC20 token contract, - /// allowing for queries such as retrieving the token's name, symbol, decimals, and total supply. - /// - public class ERC20Service : IERC20Service + public class Erc20Service : IErc20Service { - private readonly Lazy contractService; + private readonly IWeb3 _web3; + private readonly EthereumAddress _multiCall; - /// - public EthereumAddress ContractAddress { get; } - - /// - /// Initializes a new instance of the class using an RPC URL and contract address. - /// - /// The URL of the RPC endpoint to interact with the block-chain. - /// The ERC20 token contract address. - /// - /// This constructor initializes the contract service for interacting with the ERC20 token. - /// - public ERC20Service(string rpcUrl, EthereumAddress contractAddress) - : this(() => rpcUrl, contractAddress) - { } - - /// - /// Initializes a new instance of the class using a function that provides an RPC URL and contract address. - /// - /// A function that returns the RPC URL to interact with the block-chain. - /// The ERC20 token contract address. - /// - /// The RPC URL is retrieved only when the service methods are invoked, allowing for lazy initialization. - /// - public ERC20Service(Func rpcUrlFactory, EthereumAddress contractAddress) + public Erc20Service(IWeb3 web3, EthereumAddress multiCall) { - ContractAddress = contractAddress; - contractService = new Lazy(() => - { - var web3 = new Nethereum.Web3.Web3(rpcUrlFactory()); - return web3.Eth.ERC20.GetContractService(contractAddress); - }); + _web3 = web3 ?? throw new ArgumentNullException(nameof(web3)); + _multiCall = multiCall ?? throw new ArgumentNullException(nameof(multiCall)); } - /// - /// Initializes a new instance of the class using an asynchronous function that provides an RPC URL and contract address. - /// - /// A function that asynchronously returns the RPC URL to interact with the block-chain. - /// The ERC20 token contract address. - /// - /// The RPC URL is retrieved only when the service methods are invoked, allowing for lazy initialization. - /// - public ERC20Service(Func> rpcUrlFactoryAsync, EthereumAddress contractAddress) - : this(() => rpcUrlFactoryAsync().GetAwaiter().GetResult(), contractAddress) - { } - - /// - /// Initializes a new instance of the class using a instance and contract address. - /// - /// The Web3 instance for interacting with the Ethereum network. - /// The ERC20 token contract address. - /// - /// This constructor provides more flexibility by allowing the use of an existing Web3 instance. - /// - public ERC20Service(IWeb3 web3, EthereumAddress contractAddress) + public Erc20Token GetEr20Token(EthereumAddress token) { - ContractAddress = contractAddress; - contractService = new Lazy(() => web3.Eth.ERC20.GetContractService(contractAddress), LazyThreadSafetyMode.ExecutionAndPublication); + return GetEr20TokenAsync(token).GetAwaiter().GetResult(); } - /// - public virtual byte Decimals() => contractService.Value.DecimalsQueryAsync().GetAwaiter().GetResult(); - - /// - public virtual string Name() => contractService.Value.NameQueryAsync().GetAwaiter().GetResult(); - - /// - public virtual string Symbol() => contractService.Value.SymbolQueryAsync().GetAwaiter().GetResult(); - - /// - public virtual BigInteger TotalSupply() => contractService.Value.TotalSupplyQueryAsync().GetAwaiter().GetResult(); + public async Task GetEr20TokenAsync(EthereumAddress token) + { + if (token == null) throw new ArgumentNullException(nameof(token)); + + var multiCallFunction = new MultiCallFunction( + calls: new[] + { + new MultiCall(to: token, data: new NameFunction().GetCallData()), + new MultiCall(to: token, data: new SymbolFunction().GetCallData()), + new MultiCall(to: token, data: new DecimalsFunction().GetCallData()), + new MultiCall(to: token, data: new TotalSupplyFunction().GetCallData()) + } + ); + + var handler = _web3.Eth.GetContractQueryHandler(); + var response = await handler.QueryAsync>(_multiCall, multiCallFunction); + + // TODO: Include FluentValidation lib to validate result + if (response.Count != multiCallFunction.Calls.Length) throw new Erc20QueryException(token, "MultiCall returned unexpected number of results."); + if (response.Exists(r => r == null || r.Length == 0)) + throw new Erc20QueryException(token, "One of ERC20 calls failed (empty return)."); + + var name = response[0].Decode().Name; + var symbol = response[1].Decode().Symbol; + var decimals = response[2].Decode().Decimals; + var supply = response[3].Decode().TotalSupply; + + return new Erc20Token(token, name, symbol, decimals, supply); + } } } diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Exceptions/Erc20QueryException.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Exceptions/Erc20QueryException.cs new file mode 100644 index 0000000..b91add6 --- /dev/null +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Exceptions/Erc20QueryException.cs @@ -0,0 +1,16 @@ +using System; +using Net.Web3.EthereumWallet; + +namespace Net.Cache.DynamoDb.ERC20.RPC.Exceptions +{ + public sealed class Erc20QueryException : Exception + { + public EthereumAddress Token { get; } + + public Erc20QueryException(EthereumAddress token, string message, Exception? inner = null) + : base($"[ERC20 {token}] {message}", inner) + { + Token = token; + } + } +} diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Extensions/DecoderHelperExtensions.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Extensions/DecoderHelperExtensions.cs new file mode 100644 index 0000000..50e2c71 --- /dev/null +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Extensions/DecoderHelperExtensions.cs @@ -0,0 +1,15 @@ +using Nethereum.Contracts; +using Nethereum.Hex.HexConvertors.Extensions; +using Nethereum.ABI.FunctionEncoding.Attributes; + +namespace Net.Cache.DynamoDb.ERC20.RPC.Extensions +{ + public static class DecoderHelperExtensions + { + public static TFunctionOutputDTO Decode(this byte[] data) where TFunctionOutputDTO : IFunctionOutputDTO, new() + { + var dto = new TFunctionOutputDTO(); + return dto.DecodeOutput(data.ToHex()); + } + } +} diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs index 6ddc223..63768f3 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs @@ -1,46 +1,12 @@ -using System.Numerics; +using System.Threading.Tasks; using Net.Web3.EthereumWallet; +using Net.Cache.DynamoDb.ERC20.RPC.Models; namespace Net.Cache.DynamoDb.ERC20.RPC { - /// - /// Defines the basic functionalities for interacting with an ERC20 token contract. - /// - /// - /// This interface provides methods for accessing key properties of an ERC20 token, - /// such as its contract address, decimals, name, symbol, and total supply. Implementations - /// of this interface should encapsulate the logic necessary to query these properties from the block-chain. - /// - public interface IERC20Service + public interface IErc20Service { - /// - /// Gets the contract address of the ERC20 token. - /// - /// The Ethereum address of the ERC20 token contract. - public EthereumAddress ContractAddress { get; } - - /// - /// Retrieves the number of decimals the ERC20 token uses. - /// - /// The number of decimals for the ERC20 token. - public byte Decimals(); - - /// - /// Retrieves the name of the ERC20 token. - /// - /// The name of the ERC20 token. - public string Name(); - - /// - /// Retrieves the symbol of the ERC20 token. - /// - /// The symbol of the ERC20 token. - public string Symbol(); - - /// - /// Retrieves the total supply of the ERC20 token. - /// - /// The total supply of the ERC20 token as a . - public BigInteger TotalSupply(); + public Erc20Token GetEr20Token(EthereumAddress token); + public Task GetEr20TokenAsync(EthereumAddress token); } -} \ No newline at end of file +} diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Models/Erc20Token.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Models/Erc20Token.cs new file mode 100644 index 0000000..a83160b --- /dev/null +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Models/Erc20Token.cs @@ -0,0 +1,23 @@ +using System.Numerics; +using Net.Web3.EthereumWallet; + +namespace Net.Cache.DynamoDb.ERC20.RPC.Models +{ + public class Erc20Token + { + public Erc20Token(EthereumAddress address, string name, string symbol, byte decimals, BigInteger totalSupply) + { + Address = address; + Name = name; + Symbol = symbol; + Decimals = decimals; + TotalSupply = totalSupply; + } + + public EthereumAddress Address { get; } + public string Name { get; } + public string Symbol { get; } + public byte Decimals { get; } + public BigInteger TotalSupply { get; } + } +} diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCall.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCall.cs new file mode 100644 index 0000000..65d62e2 --- /dev/null +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCall.cs @@ -0,0 +1,20 @@ +using Net.Web3.EthereumWallet; +using Nethereum.ABI.FunctionEncoding.Attributes; + +namespace Net.Cache.DynamoDb.ERC20.RPC.Models +{ + public class MultiCall + { + [Parameter("address", "to", order: 1)] + public string To { get; } + + [Parameter("bytes", "data", order: 2)] + public byte[] Data { get; } + + public MultiCall(EthereumAddress to, byte[] data) + { + To = to; + Data = data; + } + } +} diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCallFunction.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCallFunction.cs new file mode 100644 index 0000000..b4fb26c --- /dev/null +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCallFunction.cs @@ -0,0 +1,21 @@ +using System.Linq; +using Nethereum.Contracts; +using System.Collections.Generic; +using Nethereum.ABI.FunctionEncoding.Attributes; + +namespace Net.Cache.DynamoDb.ERC20.RPC.Models +{ + [Function("multicall", "bytes[]")] + public class MultiCallFunction : FunctionMessage + { + [Parameter("tuple[]", "calls", order: 1)] + public MultiCall[] Calls { get; } + + public MultiCallFunction(IEnumerable calls) + { + Calls = calls.ToArray(); + } + + public MultiCallFunction() : this(Enumerable.Empty()) { } + } +} From e41bd1d56c3d0b2556c62760a62c721903db3513 Mon Sep 17 00:00:00 2001 From: ArdenHide Date: Tue, 29 Jul 2025 10:38:56 +0300 Subject: [PATCH 02/20] - include validators, extend validation --- .../Net.Cache.DynamoDb.ERC20.csproj | 1 + .../RPC/ERC20Service.cs | 25 +++++++++--- .../RPC/Validators/Erc20TokenValidator.cs | 28 +++++++++++++ .../Validators/MultiCallResponseValidator.cs | 39 +++++++++++++++++++ 4 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 src/Net.Cache.DynamoDb.ERC20/RPC/Validators/Erc20TokenValidator.cs create mode 100644 src/Net.Cache.DynamoDb.ERC20/RPC/Validators/MultiCallResponseValidator.cs diff --git a/src/Net.Cache.DynamoDb.ERC20/Net.Cache.DynamoDb.ERC20.csproj b/src/Net.Cache.DynamoDb.ERC20/Net.Cache.DynamoDb.ERC20.csproj index 13867bf..9130c80 100644 --- a/src/Net.Cache.DynamoDb.ERC20/Net.Cache.DynamoDb.ERC20.csproj +++ b/src/Net.Cache.DynamoDb.ERC20/Net.Cache.DynamoDb.ERC20.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs index 1966143..c3dae2d 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Nethereum.Web3; using Nethereum.Contracts; using System.Threading.Tasks; @@ -7,6 +8,7 @@ using Net.Cache.DynamoDb.ERC20.RPC.Models; using Net.Cache.DynamoDb.ERC20.RPC.Extensions; using Net.Cache.DynamoDb.ERC20.RPC.Exceptions; +using Net.Cache.DynamoDb.ERC20.RPC.Validators; using Nethereum.Contracts.Standards.ERC20.ContractDefinition; namespace Net.Cache.DynamoDb.ERC20.RPC @@ -43,18 +45,29 @@ public async Task GetEr20TokenAsync(EthereumAddress token) var handler = _web3.Eth.GetContractQueryHandler(); var response = await handler.QueryAsync>(_multiCall, multiCallFunction); - - // TODO: Include FluentValidation lib to validate result - if (response.Count != multiCallFunction.Calls.Length) throw new Erc20QueryException(token, "MultiCall returned unexpected number of results."); - if (response.Exists(r => r == null || r.Length == 0)) - throw new Erc20QueryException(token, "One of ERC20 calls failed (empty return)."); + var responseValidator = new MultiCallResponseValidator(multiCallFunction.Calls.Length); + var validation = await responseValidator.ValidateAsync(response); + if (!validation.IsValid) + { + var error = string.Join(" ", validation.Errors.Select(e => e.ErrorMessage)); + throw new Erc20QueryException(token, error); + } var name = response[0].Decode().Name; var symbol = response[1].Decode().Symbol; var decimals = response[2].Decode().Decimals; var supply = response[3].Decode().TotalSupply; - return new Erc20Token(token, name, symbol, decimals, supply); + var tokenResult = new Erc20Token(token, name, symbol, decimals, supply); + var tokenValidator = new Erc20TokenValidator(); + var tokenValidation = await tokenValidator.ValidateAsync(tokenResult); + if (!tokenValidation.IsValid) + { + var error = string.Join(" ", tokenValidation.Errors.Select(e => e.ErrorMessage)); + throw new Erc20QueryException(token, error); + } + + return tokenResult; } } } diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Validators/Erc20TokenValidator.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Validators/Erc20TokenValidator.cs new file mode 100644 index 0000000..524db37 --- /dev/null +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Validators/Erc20TokenValidator.cs @@ -0,0 +1,28 @@ +using System.Numerics; +using FluentValidation; +using Net.Cache.DynamoDb.ERC20.RPC.Models; + +namespace Net.Cache.DynamoDb.ERC20.RPC.Validators +{ + internal class Erc20TokenValidator : AbstractValidator + { + public Erc20TokenValidator() + { + RuleFor(x => x.Name) + .NotEmpty() + .WithMessage("Name is missing."); + + RuleFor(x => x.Symbol) + .NotEmpty() + .WithMessage("Symbol is missing."); + + RuleFor(x => x.Decimals) + .GreaterThanOrEqualTo((byte)0) + .WithMessage("Decimals is invalid."); + + RuleFor(x => x.TotalSupply) + .GreaterThanOrEqualTo(BigInteger.Zero) + .WithMessage("TotalSupply is negative."); + } + } +} diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Validators/MultiCallResponseValidator.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Validators/MultiCallResponseValidator.cs new file mode 100644 index 0000000..ef307bb --- /dev/null +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Validators/MultiCallResponseValidator.cs @@ -0,0 +1,39 @@ +using FluentValidation; +using System.Collections.Generic; + +namespace Net.Cache.DynamoDb.ERC20.RPC.Validators +{ + internal class MultiCallResponseValidator : AbstractValidator> + { + public MultiCallResponseValidator(int expectedCount) + { + RuleFor(x => x).Custom((list, context) => + { + if (list.Count != expectedCount) + { + context.AddFailure("MultiCall", "MultiCall returned unexpected number of results."); + return; + } + + for (var i = 0; i < list.Count; i++) + { + var data = list[i]; + if (data == null || data.Length == 0) + { + var field = GetFieldName(i); + context.AddFailure(field, $"{field} call returned no data."); + } + } + }); + } + + private static string GetFieldName(int index) => index switch + { + 0 => "Name", + 1 => "Symbol", + 2 => "Decimals", + 3 => "TotalSupply", + _ => $"Call[{index}]" + }; + } +} From a60482dce34ce6a78b85198a982322e9dab78bb8 Mon Sep 17 00:00:00 2001 From: ArdenHide Date: Tue, 5 Aug 2025 21:24:15 +0300 Subject: [PATCH 03/20] - refactor other lib parts --- .../DynamoDb/DynamoDbClient.cs | 45 ++++++ .../DynamoDb/IDynamoDbClient.cs | 12 ++ .../Models/Erc20TokenDynamoDbEntry.cs | 52 +++++++ .../ERC20CacheProvider.cs | 69 --------- .../ERC20StorageProvider.cs | 127 ----------------- .../Erc20CacheService.cs | 45 ++++++ .../IErc20CacheService.cs | 12 ++ .../Models/ERC20DynamoDbTable.cs | 132 ------------------ .../Models/GetCacheRequest.cs | 85 ----------- .../RPC/Erc20ServiceFactory.cs | 14 ++ .../RPC/IErc20ServiceFactory.cs | 10 ++ 11 files changed, 190 insertions(+), 413 deletions(-) create mode 100644 src/Net.Cache.DynamoDb.ERC20/DynamoDb/DynamoDbClient.cs create mode 100644 src/Net.Cache.DynamoDb.ERC20/DynamoDb/IDynamoDbClient.cs create mode 100644 src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs delete mode 100644 src/Net.Cache.DynamoDb.ERC20/ERC20CacheProvider.cs delete mode 100644 src/Net.Cache.DynamoDb.ERC20/ERC20StorageProvider.cs create mode 100644 src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs create mode 100644 src/Net.Cache.DynamoDb.ERC20/IErc20CacheService.cs delete mode 100644 src/Net.Cache.DynamoDb.ERC20/Models/ERC20DynamoDbTable.cs delete mode 100644 src/Net.Cache.DynamoDb.ERC20/Models/GetCacheRequest.cs create mode 100644 src/Net.Cache.DynamoDb.ERC20/RPC/Erc20ServiceFactory.cs create mode 100644 src/Net.Cache.DynamoDb.ERC20/RPC/IErc20ServiceFactory.cs diff --git a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/DynamoDbClient.cs b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/DynamoDbClient.cs new file mode 100644 index 0000000..b32c5d8 --- /dev/null +++ b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/DynamoDbClient.cs @@ -0,0 +1,45 @@ +using Amazon.DynamoDBv2; +using System.Threading.Tasks; +using Amazon.DynamoDBv2.DataModel; +using Net.Cache.DynamoDb.ERC20.DynamoDb.Models; + +namespace Net.Cache.DynamoDb.ERC20.DynamoDb +{ + public class DynamoDbClient : IDynamoDbClient + { + private readonly IDynamoDBContext _dynamoDbContext; + + public DynamoDbClient(IDynamoDBContext dynamoDbContext) + { + _dynamoDbContext = dynamoDbContext; + } + + public DynamoDbClient(IDynamoDBContextBuilder contextBuilder) + : this(contextBuilder.Build()) + { } + + public DynamoDbClient(IAmazonDynamoDB dynamoDb) + : this( + new DynamoDBContextBuilder() + .WithDynamoDBClient(() => dynamoDb) + ) + { } + + public DynamoDbClient() + : this( + new DynamoDBContextBuilder() + .WithDynamoDBClient(() => new AmazonDynamoDBClient()) + ) + { } + + public async Task GetErc20TokenAsync(string hashKey, LoadConfig? config = null) + { + return await _dynamoDbContext.LoadAsync(hashKey, config); + } + + public Task SaveErc20TokenAsync(Erc20TokenDynamoDbEntry entry, SaveConfig? config = null) + { + return _dynamoDbContext.SaveAsync(entry, config); + } + } +} diff --git a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/IDynamoDbClient.cs b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/IDynamoDbClient.cs new file mode 100644 index 0000000..7cbef38 --- /dev/null +++ b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/IDynamoDbClient.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using Amazon.DynamoDBv2.DataModel; +using Net.Cache.DynamoDb.ERC20.DynamoDb.Models; + +namespace Net.Cache.DynamoDb.ERC20.DynamoDb +{ + public interface IDynamoDbClient + { + public Task GetErc20TokenAsync(string hashKey, LoadConfig? config = null); + public Task SaveErc20TokenAsync(Erc20TokenDynamoDbEntry entry, SaveConfig? config = null); + } +} diff --git a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs new file mode 100644 index 0000000..f4d748e --- /dev/null +++ b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs @@ -0,0 +1,52 @@ +using Net.Cryptography.SHA256; +using Net.Web3.EthereumWallet; +using Amazon.DynamoDBv2.DataModel; +using Net.Cache.DynamoDb.ERC20.RPC.Models; +using System; + +namespace Net.Cache.DynamoDb.ERC20.DynamoDb.Models +{ + [DynamoDBTable("TokensInfoCache")] + public class Erc20TokenDynamoDbEntry + { + [DynamoDBHashKey] + public string HashKey { get; set; } = string.Empty; + + [DynamoDBProperty] + public long ChainId { get; set; } + + [DynamoDBProperty] + public string Address { get; set; } = string.Empty; + + [DynamoDBProperty] + public string Name { get; set; } = string.Empty; + + [DynamoDBProperty] + public string Symbol { get; set; } = string.Empty; + + [DynamoDBProperty] + public byte Decimals { get; set; } + + [DynamoDBProperty] + public decimal TotalSupply { get; set; } + + /// + /// Constructor without parameters for working "AWSSDK.DynamoDBv2" + /// + public Erc20TokenDynamoDbEntry() { } + + public Erc20TokenDynamoDbEntry(long chainId, EthereumAddress address, Erc20Token erc20Token) + { + HashKey = GenerateHashKey(chainId, address); + Name = erc20Token.Name; + Symbol = erc20Token.Symbol; + Decimals = erc20Token.Decimals; + TotalSupply = Nethereum.Web3.Web3.Convert.FromWei(erc20Token.TotalSupply, erc20Token.Decimals); + } + + public static string GenerateHashKey(long chainId, EthereumAddress address) + { + return $"{chainId}-{address}".ToSha256(); + } + } +} diff --git a/src/Net.Cache.DynamoDb.ERC20/ERC20CacheProvider.cs b/src/Net.Cache.DynamoDb.ERC20/ERC20CacheProvider.cs deleted file mode 100644 index 397f439..0000000 --- a/src/Net.Cache.DynamoDb.ERC20/ERC20CacheProvider.cs +++ /dev/null @@ -1,69 +0,0 @@ -//using Net.Cryptography.SHA256; -//using Net.Cache.DynamoDb.ERC20.Models; -//using System.Threading.Tasks; - -//namespace Net.Cache.DynamoDb.ERC20 -//{ -// /// -// /// Provides functionality to manage the caching of ERC20 token information. -// /// -// /// -// /// This provider encapsulates logic for retrieving and storing ERC20 token details -// /// within a DynamoDB cache. It supports operations to get existing cache entries or add new entries when required. -// /// -// public class ERC20CacheProvider -// { -// private readonly ERC20StorageProvider storageProvider; - -// /// -// /// Initializes a new instance of the class with a default storage provider. -// /// -// public ERC20CacheProvider() -// : this(new ERC20StorageProvider()) -// { } - -// /// -// /// Initializes a new instance of the class with a specified storage provider. -// /// -// /// The storage provider to be used for caching. -// public ERC20CacheProvider(ERC20StorageProvider storageProvider) -// { -// this.storageProvider = storageProvider; -// } - -// /// -// /// Retrieves an ERC20 token information from the cache or adds it to the cache if it does not exist. -// /// -// /// The request containing details required to retrieve or add the ERC20 token information. -// /// The ERC20 token information from the cache. -// /// -// /// This method checks the cache for an existing entry for the specified ERC20 token. -// /// If the entry exists, it is returned. Otherwise, a new entry is created using the provided -// /// details in the request and added to the cache. -// /// -// public virtual ERC20DynamoDbTable GetOrAdd(GetCacheRequest request) -// { -// if (storageProvider.TryGetValue($"{request.ChainId}-{request.ERC20Service.ContractAddress}".ToSha256(), request, out var storedValue)) -// { -// return storedValue; -// } - -// storedValue = new ERC20DynamoDbTable(request.ChainId, request.ERC20Service); -// storageProvider.Store(storedValue.HashKey, storedValue); -// return storedValue; -// } - -// public virtual async Task GetOrAddAsync(GetCacheRequest request) -// { -// var (isExist, storedValue) = await storageProvider.TryGetValueAsync($"{request.ChainId}-{request.ERC20Service.ContractAddress}".ToSha256(), request); -// if (isExist && storedValue != null) -// { -// return storedValue; -// } - -// storedValue = new ERC20DynamoDbTable(request.ChainId, request.ERC20Service); -// await storageProvider.StoreAsync(storedValue.HashKey, storedValue); -// return storedValue; -// } -// } -//} \ No newline at end of file diff --git a/src/Net.Cache.DynamoDb.ERC20/ERC20StorageProvider.cs b/src/Net.Cache.DynamoDb.ERC20/ERC20StorageProvider.cs deleted file mode 100644 index 9d3a27a..0000000 --- a/src/Net.Cache.DynamoDb.ERC20/ERC20StorageProvider.cs +++ /dev/null @@ -1,127 +0,0 @@ -//using System.Threading.Tasks; -//using Amazon.DynamoDBv2.DataModel; -//using Net.Cache.DynamoDb.ERC20.RPC; -//using Net.Cache.DynamoDb.ERC20.Models; -//using System.Diagnostics.CodeAnalysis; - -//namespace Net.Cache.DynamoDb.ERC20 -//{ -// /// -// /// Manages the storage and retrieval of ERC20 token information in a DynamoDB table. -// /// -// /// -// /// This provider extends the to specifically handle the operations -// /// related to storing and accessing ERC20 token data. It includes functionalities to update -// /// the total supply of a token and to retrieve or store token information based on a given key. -// /// -// public class ERC20StorageProvider : DynamoDbStorageProvider -// { -// /// -// /// Initializes a new instance of the class with an optional . -// /// -// /// The name of the DynamoDB table to be used. If not provided, a default table name is used. -// public ERC20StorageProvider(string tableName = EmptyString) -// : base(tableName) -// { } - -// /// -// /// Initializes a new instance of the class with a specific DynamoDB and an optional . -// /// -// /// The DynamoDB context to be used for operations. -// /// The name of the DynamoDB table to be used. If not provided, a default table name is used. -// public ERC20StorageProvider(IDynamoDBContext context, string tableName = EmptyString) -// : base(context, tableName) -// { } - -// /// -// /// Updates the total supply of an existing ERC20 token information entry. -// /// -// /// The existing entry of ERC20 token information. -// /// The ERC20 service to use for updating the total supply. -// /// The updated ERC20 token information entry. -// protected virtual ERC20DynamoDbTable UpdateTotalSupply(ERC20DynamoDbTable existValue, IERC20Service erc20Service) -// { -// var updatedValue = new ERC20DynamoDbTable( -// existValue.ChainId, -// existValue.Address, -// existValue.Name, -// existValue.Symbol, -// existValue.Decimals, -// erc20Service.TotalSupply() -// ); -// Context.SaveAsync(updatedValue, OperationConfig()) -// .GetAwaiter() -// .GetResult(); -// return updatedValue; -// } - -// protected virtual async Task UpdateTotalSupplyAsync(ERC20DynamoDbTable existValue, IERC20Service erc20Service) -// { -// var updatedValue = new ERC20DynamoDbTable( -// existValue.ChainId, -// existValue.Address, -// existValue.Name, -// existValue.Symbol, -// existValue.Decimals, -// erc20Service.TotalSupply() -// ); -// await Context.SaveAsync(updatedValue, OperationConfig()); -// return updatedValue; -// } - -// /// -// /// Tries to retrieve an ERC20 token information entry from the cache based on a given key. -// /// -// /// The key associated with the ERC20 token information. -// /// The request containing details for the retrieval operation. -// /// When this method returns, contains the ERC20 token information associated with the specified key, if the key is found; otherwise, . -// /// if the token information is found; otherwise, . -// /// -// /// This method also updates the total supply of the token information if the flag is set in the . -// /// -// public virtual bool TryGetValue(string key, GetCacheRequest request, [MaybeNullWhen(false)] out ERC20DynamoDbTable value) -// { -// if (!base.TryGetValue(key, out value)) -// { -// return false; -// } - -// if (request.UpdateTotalSupply) -// { -// value = UpdateTotalSupply(value, request.ERC20Service); -// } - -// return true; -// } - -// public virtual async Task<(bool isExist, ERC20DynamoDbTable? Value)> TryGetValueAsync(string key, GetCacheRequest request) -// { -// try -// { -// var storedValue = await Context.LoadAsync(key, OperationConfig()); - -// if (storedValue == null) -// { -// return (false, null); -// } - -// if (request.UpdateTotalSupply) -// { -// storedValue = await UpdateTotalSupplyAsync(storedValue, request.ERC20Service); -// } - -// return (true, storedValue); -// } -// catch (Amazon.DynamoDBv2.AmazonDynamoDBException) -// { -// throw; -// } -// catch -// { -// return (false, null); -// } -// } - -// public virtual Task StoreAsync(string key, ERC20DynamoDbTable value) => Context.SaveAsync(value); -// } -//} \ No newline at end of file diff --git a/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs b/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs new file mode 100644 index 0000000..176ce3d --- /dev/null +++ b/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Net.Web3.EthereumWallet; +using Net.Cache.DynamoDb.ERC20.RPC; +using Net.Cache.DynamoDb.ERC20.DynamoDb; +using Net.Cache.DynamoDb.ERC20.DynamoDb.Models; + +namespace Net.Cache.DynamoDb.ERC20 +{ + public class Erc20CacheService : IErc20CacheService + { + private readonly IDynamoDbClient _dynamoDbClient; + private readonly IErc20ServiceFactory _erc20ServiceFactory; + + public Erc20CacheService(IDynamoDbClient dynamoDbClient, IErc20ServiceFactory erc20ServiceFactory) + { + _dynamoDbClient = dynamoDbClient; + _erc20ServiceFactory = erc20ServiceFactory; + } + + public Erc20CacheService() + : this( + new DynamoDbClient(), + new Erc20ServiceFactory() + ) + { } + + public async Task GetOrAddAsync(long chainId, EthereumAddress address, Func> rpcUrlFactoryAsync, Func> multiCallFactoryAsync) + { + var value = await _dynamoDbClient.GetErc20TokenAsync(Erc20TokenDynamoDbEntry.GenerateHashKey(chainId, address)); + if (value != null) return value; + + var rpcUrl = await rpcUrlFactoryAsync(); + var multiCall = await multiCallFactoryAsync(); + + var erc20Service = _erc20ServiceFactory.Create(new Nethereum.Web3.Web3(rpcUrl), multiCall); + var erc20Token = await erc20Service.GetEr20TokenAsync(address); + + value = new Erc20TokenDynamoDbEntry(chainId, address, erc20Token); + await _dynamoDbClient.SaveErc20TokenAsync(value); + + return value; + } + } +} \ No newline at end of file diff --git a/src/Net.Cache.DynamoDb.ERC20/IErc20CacheService.cs b/src/Net.Cache.DynamoDb.ERC20/IErc20CacheService.cs new file mode 100644 index 0000000..22e325d --- /dev/null +++ b/src/Net.Cache.DynamoDb.ERC20/IErc20CacheService.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading.Tasks; +using Net.Web3.EthereumWallet; +using Net.Cache.DynamoDb.ERC20.DynamoDb.Models; + +namespace Net.Cache.DynamoDb.ERC20 +{ + public interface IErc20CacheService + { + public Task GetOrAddAsync(long chainId, EthereumAddress address, Func> rpcUrlFactoryAsync, Func> multiCallFactoryAsync); + } +} diff --git a/src/Net.Cache.DynamoDb.ERC20/Models/ERC20DynamoDbTable.cs b/src/Net.Cache.DynamoDb.ERC20/Models/ERC20DynamoDbTable.cs deleted file mode 100644 index bfaf3e5..0000000 --- a/src/Net.Cache.DynamoDb.ERC20/Models/ERC20DynamoDbTable.cs +++ /dev/null @@ -1,132 +0,0 @@ -//using System.Numerics; -//using Net.Web3.EthereumWallet; -//using Net.Cryptography.SHA256; -//using Amazon.DynamoDBv2.DataModel; -//using Net.Cache.DynamoDb.ERC20.RPC; - -//namespace Net.Cache.DynamoDb.ERC20.Models -//{ -// /// -// /// Represents an ERC20 token information cache entry in DynamoDB. -// /// -// /// -// /// This class is designed to store and manage the basic details of an ERC20 token, -// /// including its chain ID, address, name, symbol, decimals, and total supply. It also -// /// generates a unique hash key for each token based on its chain ID and address. -// /// -// [DynamoDBTable("TokensInfoCache")] -// public class ERC20DynamoDbTable -// { -// /// -// /// Gets the hash key for the ERC20 token entry, uniquely generated based on the chain ID and token address. -// /// -// [DynamoDBHashKey] -// public string HashKey { get; set; } = string.Empty; - -// /// -// /// Gets the block-chain chain ID where the ERC20 token is located. -// /// -// [DynamoDBProperty] -// public long ChainId { get; set; } - -// /// -// /// Gets the ERC20 token contract address. -// /// -// [DynamoDBProperty] -// public string Address { get; set; } = string.Empty; - -// /// -// /// Gets the name of the ERC20 token. -// /// -// [DynamoDBProperty] -// public string Name { get; set; } = string.Empty; - -// /// -// /// Gets the symbol of the ERC20 token. -// /// -// [DynamoDBProperty] -// public string Symbol { get; set; } = string.Empty; - -// /// -// /// Gets the decimals of the ERC20 token, indicating how divisible it is. -// /// -// [DynamoDBProperty] -// public byte Decimals { get; set; } - -// /// -// /// Gets the total supply of the ERC20 token. -// /// -// [DynamoDBProperty] -// public decimal TotalSupply { get; set; } - -// /// -// /// Initializes a new instance of the class with specified token details. -// /// -// /// The block-chain chain ID. -// /// The ERC20 token contract address. -// /// The name of the ERC20 token. -// /// The symbol of the ERC20 token. -// /// The decimals of the ERC20 token. -// /// The total supply of the ERC20 token. -// /// -// /// This constructor expects an already calculated based on the total supply value and . -// /// -// public ERC20DynamoDbTable(long chainId, EthereumAddress address, string name, string symbol, byte decimals, decimal totalSupply) -// { -// ChainId = chainId; -// Address = address; -// Name = name; -// Symbol = symbol; -// Decimals = decimals; -// TotalSupply = totalSupply; -// HashKey = $"{ChainId}-{Address}".ToSha256(); -// } - -// /// -// /// Initializes a new instance of the class with specified token details and total supply in . -// /// -// /// The block-chain chain ID. -// /// The ERC20 token contract address. -// /// The name of the ERC20 token. -// /// The symbol of the ERC20 token. -// /// The decimals of the ERC20 token. -// /// The total supply of the ERC20 token in format. -// /// -// /// This constructor converts the from to , considering the token's . -// /// -// public ERC20DynamoDbTable(long chainId, EthereumAddress address, string name, string symbol, byte decimals, BigInteger totalSupply) -// { -// ChainId = chainId; -// Address = address; -// Name = name; -// Symbol = symbol; -// Decimals = decimals; -// TotalSupply = Nethereum.Web3.Web3.Convert.FromWei(totalSupply, decimals); -// HashKey = $"{ChainId}-{Address}".ToSha256(); -// } - -// /// -// /// Initializes a new instance of the class using an to populate token details. -// /// -// /// The block-chain chain ID. -// /// The ERC20 service providing access to token details. -// /// -// /// This constructor retrieves token details such as name, symbol, decimals, and total supply from the provided . -// /// -// public ERC20DynamoDbTable(long chainId, IERC20Service erc20Service) -// { -// ChainId = chainId; -// Address = erc20Service.ContractAddress; -// Name = erc20Service.Name(); -// Symbol = erc20Service.Symbol(); -// Decimals = erc20Service.Decimals(); -// TotalSupply = Nethereum.Web3.Web3.Convert.FromWei(erc20Service.TotalSupply(), Decimals); -// HashKey = $"{ChainId}-{Address}".ToSha256(); -// } - -// /// -// /// Constructor without parameters for working "Amazon.DynamoDBv2" -// /// -// public ERC20DynamoDbTable() { } -// } -//} \ No newline at end of file diff --git a/src/Net.Cache.DynamoDb.ERC20/Models/GetCacheRequest.cs b/src/Net.Cache.DynamoDb.ERC20/Models/GetCacheRequest.cs deleted file mode 100644 index 68688e2..0000000 --- a/src/Net.Cache.DynamoDb.ERC20/Models/GetCacheRequest.cs +++ /dev/null @@ -1,85 +0,0 @@ -//using System; -//using Net.Web3.EthereumWallet; -//using Net.Cache.DynamoDb.ERC20.RPC; -//using System.Threading.Tasks; - -//namespace Net.Cache.DynamoDb.ERC20.Models -//{ -// /// -// /// Represents a request to retrieve or update ERC20 token information in the cache. -// /// -// /// -// /// This class encapsulates the details necessary for fetching or updating the cached data of an ERC20 token, -// /// including the block-chain chain ID, the , and a flag indicating whether to update the total supply. -// /// -// public class GetCacheRequest -// { -// /// -// /// Gets the block-chain chain ID for the request. -// /// -// public long ChainId { get; } - -// /// -// /// Gets the used to interact with the ERC20 token contract. -// /// -// public IERC20Service ERC20Service { get; } - -// /// -// /// Gets a value indicating whether the total supply of the token should be updated in the cache. -// /// -// public bool UpdateTotalSupply { get; } - -// /// -// /// Initializes a new instance of the class with specified chain ID and contract address. -// /// -// /// The block-chain chain ID. -// /// The ERC20 token contract address. -// /// The URL of the RPC endpoint to interact with the block-chain. -// /// Optional. Indicates whether to update the total supply of the token in the cache. Defaults to . -// /// -// /// This constructor creates an instance of the class using the provided RPC URL and contract address. -// /// -// public GetCacheRequest(long chainId, EthereumAddress contractAddress, string rpcUrl, bool updateTotalSupply = true) -// : this(chainId, new ERC20Service(() => rpcUrl, contractAddress), updateTotalSupply) -// { } - -// /// -// /// Initializes a new instance of the class with specified chain ID, contract address and RPC URL factory. -// /// -// /// The block-chain chain ID. -// /// The ERC20 token contract address. -// /// A function that returns the RPC URL to interact with the block-chain. -// /// Optional. Indicates whether to update the total supply of the token in the cache. Defaults to . -// public GetCacheRequest(long chainId, EthereumAddress contractAddress, Func rpcUrlFactory, bool updateTotalSupply = true) -// : this(chainId, new ERC20Service(rpcUrlFactory, contractAddress), updateTotalSupply) -// { } - -// /// -// /// Initializes a new instance of the class with specified chain ID, contract address and asynchronous RPC URL factory. -// /// -// /// The block-chain chain ID. -// /// The ERC20 token contract address. -// /// A function that asynchronously returns the RPC URL to interact with the block-chain. -// /// Optional. Indicates whether to update the total supply of the token in the cache. Defaults to . -// public GetCacheRequest(long chainId, EthereumAddress contractAddress, Func> rpcUrlFactoryAsync, bool updateTotalSupply = true) -// : this(chainId, new ERC20Service(rpcUrlFactoryAsync, contractAddress), updateTotalSupply) -// { } - -// /// -// /// Initializes a new instance of the class with a specified ERC20 service. -// /// -// /// The block-chain chain ID. -// /// The providing access to the token's details. -// /// Optional. Indicates whether to update the total supply of the token in the cache. Defaults to . -// /// -// /// This constructor allows for more flexibility by accepting an instance of an , -// /// enabling the use of customized or mock services for testing purposes. -// /// -// public GetCacheRequest(long chainId, IERC20Service erc20Service, bool updateTotalSupply = true) -// { -// ChainId = chainId; -// ERC20Service = erc20Service; -// UpdateTotalSupply = updateTotalSupply; -// } -// } -//} \ No newline at end of file diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Erc20ServiceFactory.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Erc20ServiceFactory.cs new file mode 100644 index 0000000..2c58da2 --- /dev/null +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Erc20ServiceFactory.cs @@ -0,0 +1,14 @@ +using System; +using Nethereum.Web3; +using Net.Web3.EthereumWallet; + +namespace Net.Cache.DynamoDb.ERC20.RPC +{ + public class Erc20ServiceFactory : IErc20ServiceFactory + { + public IErc20Service Create(IWeb3 web3, EthereumAddress multiCall) + { + return new Erc20Service(web3, multiCall); + } + } +} diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/IErc20ServiceFactory.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/IErc20ServiceFactory.cs new file mode 100644 index 0000000..2ef1c06 --- /dev/null +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/IErc20ServiceFactory.cs @@ -0,0 +1,10 @@ +using Nethereum.Web3; +using Net.Web3.EthereumWallet; + +namespace Net.Cache.DynamoDb.ERC20.RPC +{ + public interface IErc20ServiceFactory + { + public IErc20Service Create(IWeb3 web3, EthereumAddress multiCall); + } +} From 209bc315922c6b89bba45854a17d58e73a2ff7df Mon Sep 17 00:00:00 2001 From: ArdenHide Date: Tue, 5 Aug 2025 21:27:05 +0300 Subject: [PATCH 04/20] - rename `Erc20Token` to `Erc20TokenData` --- .../DynamoDb/Models/Erc20TokenDynamoDbEntry.cs | 2 +- src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs | 6 +++--- src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs | 4 ++-- .../RPC/Models/{Erc20Token.cs => Erc20TokenData.cs} | 4 ++-- .../RPC/Validators/Erc20TokenValidator.cs | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) rename src/Net.Cache.DynamoDb.ERC20/RPC/Models/{Erc20Token.cs => Erc20TokenData.cs} (76%) diff --git a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs index f4d748e..9b44a7c 100644 --- a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs +++ b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs @@ -35,7 +35,7 @@ public class Erc20TokenDynamoDbEntry /// public Erc20TokenDynamoDbEntry() { } - public Erc20TokenDynamoDbEntry(long chainId, EthereumAddress address, Erc20Token erc20Token) + public Erc20TokenDynamoDbEntry(long chainId, EthereumAddress address, Erc20TokenData erc20Token) { HashKey = GenerateHashKey(chainId, address); Name = erc20Token.Name; diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs index c3dae2d..6ded4ad 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs @@ -24,12 +24,12 @@ public Erc20Service(IWeb3 web3, EthereumAddress multiCall) _multiCall = multiCall ?? throw new ArgumentNullException(nameof(multiCall)); } - public Erc20Token GetEr20Token(EthereumAddress token) + public Erc20TokenData GetEr20Token(EthereumAddress token) { return GetEr20TokenAsync(token).GetAwaiter().GetResult(); } - public async Task GetEr20TokenAsync(EthereumAddress token) + public async Task GetEr20TokenAsync(EthereumAddress token) { if (token == null) throw new ArgumentNullException(nameof(token)); @@ -58,7 +58,7 @@ public async Task GetEr20TokenAsync(EthereumAddress token) var decimals = response[2].Decode().Decimals; var supply = response[3].Decode().TotalSupply; - var tokenResult = new Erc20Token(token, name, symbol, decimals, supply); + var tokenResult = new Erc20TokenData(token, name, symbol, decimals, supply); var tokenValidator = new Erc20TokenValidator(); var tokenValidation = await tokenValidator.ValidateAsync(tokenResult); if (!tokenValidation.IsValid) diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs index 63768f3..2571508 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs @@ -6,7 +6,7 @@ namespace Net.Cache.DynamoDb.ERC20.RPC { public interface IErc20Service { - public Erc20Token GetEr20Token(EthereumAddress token); - public Task GetEr20TokenAsync(EthereumAddress token); + public Erc20TokenData GetEr20Token(EthereumAddress token); + public Task GetEr20TokenAsync(EthereumAddress token); } } diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Models/Erc20Token.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Models/Erc20TokenData.cs similarity index 76% rename from src/Net.Cache.DynamoDb.ERC20/RPC/Models/Erc20Token.cs rename to src/Net.Cache.DynamoDb.ERC20/RPC/Models/Erc20TokenData.cs index a83160b..f4e0196 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/Models/Erc20Token.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Models/Erc20TokenData.cs @@ -3,9 +3,9 @@ namespace Net.Cache.DynamoDb.ERC20.RPC.Models { - public class Erc20Token + public class Erc20TokenData { - public Erc20Token(EthereumAddress address, string name, string symbol, byte decimals, BigInteger totalSupply) + public Erc20TokenData(EthereumAddress address, string name, string symbol, byte decimals, BigInteger totalSupply) { Address = address; Name = name; diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Validators/Erc20TokenValidator.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Validators/Erc20TokenValidator.cs index 524db37..23c1912 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/Validators/Erc20TokenValidator.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Validators/Erc20TokenValidator.cs @@ -4,7 +4,7 @@ namespace Net.Cache.DynamoDb.ERC20.RPC.Validators { - internal class Erc20TokenValidator : AbstractValidator + internal class Erc20TokenValidator : AbstractValidator { public Erc20TokenValidator() { From d77d371355aeadf285b89b33d906da95d2bdf28a Mon Sep 17 00:00:00 2001 From: ArdenHide Date: Wed, 6 Aug 2025 12:18:54 +0300 Subject: [PATCH 05/20] - fix `Erc20TokenDynamoDbEntry` - fix naming in `IErc20Service` - add argument null checking --- .../DynamoDb/Models/Erc20TokenDynamoDbEntry.cs | 3 ++- src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs | 9 ++++++--- src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs | 4 ++-- src/Net.Cache.DynamoDb.ERC20/RPC/Erc20ServiceFactory.cs | 3 +-- src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs | 2 +- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs index 9b44a7c..e120f46 100644 --- a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs +++ b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs @@ -2,7 +2,6 @@ using Net.Web3.EthereumWallet; using Amazon.DynamoDBv2.DataModel; using Net.Cache.DynamoDb.ERC20.RPC.Models; -using System; namespace Net.Cache.DynamoDb.ERC20.DynamoDb.Models { @@ -38,6 +37,8 @@ public Erc20TokenDynamoDbEntry() { } public Erc20TokenDynamoDbEntry(long chainId, EthereumAddress address, Erc20TokenData erc20Token) { HashKey = GenerateHashKey(chainId, address); + ChainId = chainId; + Address = address; Name = erc20Token.Name; Symbol = erc20Token.Symbol; Decimals = erc20Token.Decimals; diff --git a/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs b/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs index 176ce3d..c42b4ab 100644 --- a/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs +++ b/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs @@ -14,8 +14,8 @@ public class Erc20CacheService : IErc20CacheService public Erc20CacheService(IDynamoDbClient dynamoDbClient, IErc20ServiceFactory erc20ServiceFactory) { - _dynamoDbClient = dynamoDbClient; - _erc20ServiceFactory = erc20ServiceFactory; + _dynamoDbClient = dynamoDbClient ?? throw new ArgumentNullException(nameof(dynamoDbClient)); + _erc20ServiceFactory = erc20ServiceFactory ?? throw new ArgumentNullException(nameof(erc20ServiceFactory)); } public Erc20CacheService() @@ -27,6 +27,9 @@ public Erc20CacheService() public async Task GetOrAddAsync(long chainId, EthereumAddress address, Func> rpcUrlFactoryAsync, Func> multiCallFactoryAsync) { + if (rpcUrlFactoryAsync == null) throw new ArgumentNullException(nameof(rpcUrlFactoryAsync)); + if (multiCallFactoryAsync == null) throw new ArgumentNullException(nameof(multiCallFactoryAsync)); + var value = await _dynamoDbClient.GetErc20TokenAsync(Erc20TokenDynamoDbEntry.GenerateHashKey(chainId, address)); if (value != null) return value; @@ -34,7 +37,7 @@ public async Task GetOrAddAsync(long chainId, EthereumA var multiCall = await multiCallFactoryAsync(); var erc20Service = _erc20ServiceFactory.Create(new Nethereum.Web3.Web3(rpcUrl), multiCall); - var erc20Token = await erc20Service.GetEr20TokenAsync(address); + var erc20Token = await erc20Service.GetErc20TokenAsync(address); value = new Erc20TokenDynamoDbEntry(chainId, address, erc20Token); await _dynamoDbClient.SaveErc20TokenAsync(value); diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs index 6ded4ad..d8de1e3 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs @@ -26,10 +26,10 @@ public Erc20Service(IWeb3 web3, EthereumAddress multiCall) public Erc20TokenData GetEr20Token(EthereumAddress token) { - return GetEr20TokenAsync(token).GetAwaiter().GetResult(); + return GetErc20TokenAsync(token).GetAwaiter().GetResult(); } - public async Task GetEr20TokenAsync(EthereumAddress token) + public async Task GetErc20TokenAsync(EthereumAddress token) { if (token == null) throw new ArgumentNullException(nameof(token)); diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Erc20ServiceFactory.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Erc20ServiceFactory.cs index 2c58da2..2d70a82 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/Erc20ServiceFactory.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Erc20ServiceFactory.cs @@ -1,5 +1,4 @@ -using System; -using Nethereum.Web3; +using Nethereum.Web3; using Net.Web3.EthereumWallet; namespace Net.Cache.DynamoDb.ERC20.RPC diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs index 2571508..82772f1 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs @@ -7,6 +7,6 @@ namespace Net.Cache.DynamoDb.ERC20.RPC public interface IErc20Service { public Erc20TokenData GetEr20Token(EthereumAddress token); - public Task GetEr20TokenAsync(EthereumAddress token); + public Task GetErc20TokenAsync(EthereumAddress token); } } From 63626dc4edc68d871d46b84918a189e6ae119425 Mon Sep 17 00:00:00 2001 From: ArdenHide Date: Wed, 6 Aug 2025 12:28:17 +0300 Subject: [PATCH 06/20] - typo --- src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs | 2 +- src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs index d8de1e3..09e1275 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs @@ -24,7 +24,7 @@ public Erc20Service(IWeb3 web3, EthereumAddress multiCall) _multiCall = multiCall ?? throw new ArgumentNullException(nameof(multiCall)); } - public Erc20TokenData GetEr20Token(EthereumAddress token) + public Erc20TokenData GetErc20Token(EthereumAddress token) { return GetErc20TokenAsync(token).GetAwaiter().GetResult(); } diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs index 82772f1..b5f2491 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs @@ -6,7 +6,7 @@ namespace Net.Cache.DynamoDb.ERC20.RPC { public interface IErc20Service { - public Erc20TokenData GetEr20Token(EthereumAddress token); + public Erc20TokenData GetErc20Token(EthereumAddress token); public Task GetErc20TokenAsync(EthereumAddress token); } } From 829750e2b9e964b5ea2eb11bcdcf7fce53abd15c Mon Sep 17 00:00:00 2001 From: ArdenHide Date: Wed, 6 Aug 2025 12:44:58 +0300 Subject: [PATCH 07/20] - use `ConfigureAwait(false)` - use `Task.WhenAll` to receive `rpcUrl` and `multiCall` - check if `address` not null - null handling in `DynamoDbClient` --- .../DynamoDb/DynamoDbClient.cs | 19 ++++++++++++------- .../Erc20CacheService.cs | 18 +++++++++++++----- .../RPC/ERC20Service.cs | 12 +++++++++--- 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/DynamoDbClient.cs b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/DynamoDbClient.cs index b32c5d8..bb649da 100644 --- a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/DynamoDbClient.cs +++ b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/DynamoDbClient.cs @@ -1,4 +1,5 @@ -using Amazon.DynamoDBv2; +using System; +using Amazon.DynamoDBv2; using System.Threading.Tasks; using Amazon.DynamoDBv2.DataModel; using Net.Cache.DynamoDb.ERC20.DynamoDb.Models; @@ -11,17 +12,17 @@ public class DynamoDbClient : IDynamoDbClient public DynamoDbClient(IDynamoDBContext dynamoDbContext) { - _dynamoDbContext = dynamoDbContext; + _dynamoDbContext = dynamoDbContext ?? throw new ArgumentNullException(nameof(dynamoDbContext)); } public DynamoDbClient(IDynamoDBContextBuilder contextBuilder) - : this(contextBuilder.Build()) + : this((contextBuilder ?? throw new ArgumentNullException(nameof(contextBuilder))).Build()) { } public DynamoDbClient(IAmazonDynamoDB dynamoDb) : this( new DynamoDBContextBuilder() - .WithDynamoDBClient(() => dynamoDb) + .WithDynamoDBClient(() => dynamoDb ?? throw new ArgumentNullException(nameof(dynamoDb))) ) { } @@ -34,12 +35,16 @@ public DynamoDbClient() public async Task GetErc20TokenAsync(string hashKey, LoadConfig? config = null) { - return await _dynamoDbContext.LoadAsync(hashKey, config); + return await _dynamoDbContext + .LoadAsync(hashKey, config) + .ConfigureAwait(false); } - public Task SaveErc20TokenAsync(Erc20TokenDynamoDbEntry entry, SaveConfig? config = null) + public async Task SaveErc20TokenAsync(Erc20TokenDynamoDbEntry entry, SaveConfig? config = null) { - return _dynamoDbContext.SaveAsync(entry, config); + await _dynamoDbContext + .SaveAsync(entry, config) + .ConfigureAwait(false); } } } diff --git a/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs b/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs index c42b4ab..1e4829a 100644 --- a/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs +++ b/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs @@ -27,20 +27,28 @@ public Erc20CacheService() public async Task GetOrAddAsync(long chainId, EthereumAddress address, Func> rpcUrlFactoryAsync, Func> multiCallFactoryAsync) { + if (address == null) throw new ArgumentNullException(nameof(address)); if (rpcUrlFactoryAsync == null) throw new ArgumentNullException(nameof(rpcUrlFactoryAsync)); if (multiCallFactoryAsync == null) throw new ArgumentNullException(nameof(multiCallFactoryAsync)); - var value = await _dynamoDbClient.GetErc20TokenAsync(Erc20TokenDynamoDbEntry.GenerateHashKey(chainId, address)); + var value = await _dynamoDbClient + .GetErc20TokenAsync(Erc20TokenDynamoDbEntry.GenerateHashKey(chainId, address)) + .ConfigureAwait(false); if (value != null) return value; - var rpcUrl = await rpcUrlFactoryAsync(); - var multiCall = await multiCallFactoryAsync(); + var rpcUrlTask = rpcUrlFactoryAsync(); + var multiCallTask = multiCallFactoryAsync(); + + await Task.WhenAll(rpcUrlTask, multiCallTask); + + var rpcUrl = rpcUrlTask.Result; + var multiCall = multiCallTask.Result; var erc20Service = _erc20ServiceFactory.Create(new Nethereum.Web3.Web3(rpcUrl), multiCall); - var erc20Token = await erc20Service.GetErc20TokenAsync(address); + var erc20Token = await erc20Service.GetErc20TokenAsync(address).ConfigureAwait(false); value = new Erc20TokenDynamoDbEntry(chainId, address, erc20Token); - await _dynamoDbClient.SaveErc20TokenAsync(value); + await _dynamoDbClient.SaveErc20TokenAsync(value).ConfigureAwait(false); return value; } diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs index 09e1275..e2631df 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs @@ -44,9 +44,13 @@ public async Task GetErc20TokenAsync(EthereumAddress token) ); var handler = _web3.Eth.GetContractQueryHandler(); - var response = await handler.QueryAsync>(_multiCall, multiCallFunction); + var response = await handler + .QueryAsync>(_multiCall, multiCallFunction) + .ConfigureAwait(false); var responseValidator = new MultiCallResponseValidator(multiCallFunction.Calls.Length); - var validation = await responseValidator.ValidateAsync(response); + var validation = await responseValidator + .ValidateAsync(response) + .ConfigureAwait(false); if (!validation.IsValid) { var error = string.Join(" ", validation.Errors.Select(e => e.ErrorMessage)); @@ -60,7 +64,9 @@ public async Task GetErc20TokenAsync(EthereumAddress token) var tokenResult = new Erc20TokenData(token, name, symbol, decimals, supply); var tokenValidator = new Erc20TokenValidator(); - var tokenValidation = await tokenValidator.ValidateAsync(tokenResult); + var tokenValidation = await tokenValidator + .ValidateAsync(tokenResult) + .ConfigureAwait(false); if (!tokenValidation.IsValid) { var error = string.Join(" ", tokenValidation.Errors.Select(e => e.ErrorMessage)); From d1f61a7055eb2050456c4e5aab43ec9463e595e2 Mon Sep 17 00:00:00 2001 From: ArdenHide Date: Thu, 7 Aug 2025 12:27:13 +0300 Subject: [PATCH 08/20] - improve naming --- .../Models/Erc20TokenDynamoDbEntry.cs | 2 +- .../Erc20CacheService.cs | 22 +++++++++---------- .../IErc20CacheService.cs | 2 +- .../RPC/ERC20Service.cs | 10 ++++----- .../RPC/Erc20ServiceFactory.cs | 2 +- .../RPC/Exceptions/Erc20QueryException.cs | 2 +- .../RPC/IERC20Service.cs | 4 ++-- .../RPC/IErc20ServiceFactory.cs | 2 +- .../RPC/Models/Erc20TokenData.cs | 2 +- .../RPC/Models/MultiCall.cs | 2 +- .../RPC/Models/MultiCallFunction.cs | 2 +- .../RPC/Validators/Erc20TokenValidator.cs | 4 ++-- .../Validators/MultiCallResponseValidator.cs | 2 +- .../Extensions/DecoderExtensions.cs} | 4 ++-- .../ERC20CacheProviderTests.cs | 2 +- .../RPC/ERC20ServiceTests.cs | 2 +- 16 files changed, 33 insertions(+), 33 deletions(-) rename src/Net.Cache.DynamoDb.ERC20/{RPC/Extensions/DecoderHelperExtensions.cs => Rpc/Extensions/DecoderExtensions.cs} (80%) diff --git a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs index e120f46..5571cca 100644 --- a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs +++ b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs @@ -1,7 +1,7 @@ using Net.Cryptography.SHA256; using Net.Web3.EthereumWallet; using Amazon.DynamoDBv2.DataModel; -using Net.Cache.DynamoDb.ERC20.RPC.Models; +using Net.Cache.DynamoDb.ERC20.Rpc.Models; namespace Net.Cache.DynamoDb.ERC20.DynamoDb.Models { diff --git a/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs b/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs index 1e4829a..d09012f 100644 --- a/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs +++ b/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs @@ -1,7 +1,7 @@ using System; using System.Threading.Tasks; using Net.Web3.EthereumWallet; -using Net.Cache.DynamoDb.ERC20.RPC; +using Net.Cache.DynamoDb.ERC20.Rpc; using Net.Cache.DynamoDb.ERC20.DynamoDb; using Net.Cache.DynamoDb.ERC20.DynamoDb.Models; @@ -25,19 +25,19 @@ public Erc20CacheService() ) { } - public async Task GetOrAddAsync(long chainId, EthereumAddress address, Func> rpcUrlFactoryAsync, Func> multiCallFactoryAsync) + public async Task GetOrAddAsync(long chainId, EthereumAddress address, Func> rpcUrlFactory, Func> multiCallFactory) { if (address == null) throw new ArgumentNullException(nameof(address)); - if (rpcUrlFactoryAsync == null) throw new ArgumentNullException(nameof(rpcUrlFactoryAsync)); - if (multiCallFactoryAsync == null) throw new ArgumentNullException(nameof(multiCallFactoryAsync)); + if (rpcUrlFactory == null) throw new ArgumentNullException(nameof(rpcUrlFactory)); + if (multiCallFactory == null) throw new ArgumentNullException(nameof(multiCallFactory)); - var value = await _dynamoDbClient + var entry = await _dynamoDbClient .GetErc20TokenAsync(Erc20TokenDynamoDbEntry.GenerateHashKey(chainId, address)) .ConfigureAwait(false); - if (value != null) return value; + if (entry != null) return entry; - var rpcUrlTask = rpcUrlFactoryAsync(); - var multiCallTask = multiCallFactoryAsync(); + var rpcUrlTask = rpcUrlFactory(); + var multiCallTask = multiCallFactory(); await Task.WhenAll(rpcUrlTask, multiCallTask); @@ -47,10 +47,10 @@ public async Task GetOrAddAsync(long chainId, EthereumA var erc20Service = _erc20ServiceFactory.Create(new Nethereum.Web3.Web3(rpcUrl), multiCall); var erc20Token = await erc20Service.GetErc20TokenAsync(address).ConfigureAwait(false); - value = new Erc20TokenDynamoDbEntry(chainId, address, erc20Token); - await _dynamoDbClient.SaveErc20TokenAsync(value).ConfigureAwait(false); + entry = new Erc20TokenDynamoDbEntry(chainId, address, erc20Token); + await _dynamoDbClient.SaveErc20TokenAsync(entry).ConfigureAwait(false); - return value; + return entry; } } } \ No newline at end of file diff --git a/src/Net.Cache.DynamoDb.ERC20/IErc20CacheService.cs b/src/Net.Cache.DynamoDb.ERC20/IErc20CacheService.cs index 22e325d..5b9d403 100644 --- a/src/Net.Cache.DynamoDb.ERC20/IErc20CacheService.cs +++ b/src/Net.Cache.DynamoDb.ERC20/IErc20CacheService.cs @@ -7,6 +7,6 @@ namespace Net.Cache.DynamoDb.ERC20 { public interface IErc20CacheService { - public Task GetOrAddAsync(long chainId, EthereumAddress address, Func> rpcUrlFactoryAsync, Func> multiCallFactoryAsync); + public Task GetOrAddAsync(long chainId, EthereumAddress address, Func> rpcUrlFactory, Func> multiCallFactory); } } diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs index e2631df..700e188 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs @@ -5,13 +5,13 @@ using System.Threading.Tasks; using Net.Web3.EthereumWallet; using System.Collections.Generic; -using Net.Cache.DynamoDb.ERC20.RPC.Models; -using Net.Cache.DynamoDb.ERC20.RPC.Extensions; -using Net.Cache.DynamoDb.ERC20.RPC.Exceptions; -using Net.Cache.DynamoDb.ERC20.RPC.Validators; +using Net.Cache.DynamoDb.ERC20.Rpc.Exceptions; +using Net.Cache.DynamoDb.ERC20.Rpc.Extensions; +using Net.Cache.DynamoDb.ERC20.Rpc.Models; +using Net.Cache.DynamoDb.ERC20.Rpc.Validators; using Nethereum.Contracts.Standards.ERC20.ContractDefinition; -namespace Net.Cache.DynamoDb.ERC20.RPC +namespace Net.Cache.DynamoDb.ERC20.Rpc { public class Erc20Service : IErc20Service { diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Erc20ServiceFactory.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Erc20ServiceFactory.cs index 2d70a82..78ae6b7 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/Erc20ServiceFactory.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Erc20ServiceFactory.cs @@ -1,7 +1,7 @@ using Nethereum.Web3; using Net.Web3.EthereumWallet; -namespace Net.Cache.DynamoDb.ERC20.RPC +namespace Net.Cache.DynamoDb.ERC20.Rpc { public class Erc20ServiceFactory : IErc20ServiceFactory { diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Exceptions/Erc20QueryException.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Exceptions/Erc20QueryException.cs index b91add6..24e2eb3 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/Exceptions/Erc20QueryException.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Exceptions/Erc20QueryException.cs @@ -1,7 +1,7 @@ using System; using Net.Web3.EthereumWallet; -namespace Net.Cache.DynamoDb.ERC20.RPC.Exceptions +namespace Net.Cache.DynamoDb.ERC20.Rpc.Exceptions { public sealed class Erc20QueryException : Exception { diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs index b5f2491..c91abb8 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs @@ -1,8 +1,8 @@ using System.Threading.Tasks; +using Net.Cache.DynamoDb.ERC20.Rpc.Models; using Net.Web3.EthereumWallet; -using Net.Cache.DynamoDb.ERC20.RPC.Models; -namespace Net.Cache.DynamoDb.ERC20.RPC +namespace Net.Cache.DynamoDb.ERC20.Rpc { public interface IErc20Service { diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/IErc20ServiceFactory.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/IErc20ServiceFactory.cs index 2ef1c06..ac9f714 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/IErc20ServiceFactory.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/IErc20ServiceFactory.cs @@ -1,7 +1,7 @@ using Nethereum.Web3; using Net.Web3.EthereumWallet; -namespace Net.Cache.DynamoDb.ERC20.RPC +namespace Net.Cache.DynamoDb.ERC20.Rpc { public interface IErc20ServiceFactory { diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Models/Erc20TokenData.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Models/Erc20TokenData.cs index f4e0196..d03ef86 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/Models/Erc20TokenData.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Models/Erc20TokenData.cs @@ -1,7 +1,7 @@ using System.Numerics; using Net.Web3.EthereumWallet; -namespace Net.Cache.DynamoDb.ERC20.RPC.Models +namespace Net.Cache.DynamoDb.ERC20.Rpc.Models { public class Erc20TokenData { diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCall.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCall.cs index 65d62e2..3983b91 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCall.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCall.cs @@ -1,7 +1,7 @@ using Net.Web3.EthereumWallet; using Nethereum.ABI.FunctionEncoding.Attributes; -namespace Net.Cache.DynamoDb.ERC20.RPC.Models +namespace Net.Cache.DynamoDb.ERC20.Rpc.Models { public class MultiCall { diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCallFunction.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCallFunction.cs index b4fb26c..f34f9d9 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCallFunction.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCallFunction.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using Nethereum.ABI.FunctionEncoding.Attributes; -namespace Net.Cache.DynamoDb.ERC20.RPC.Models +namespace Net.Cache.DynamoDb.ERC20.Rpc.Models { [Function("multicall", "bytes[]")] public class MultiCallFunction : FunctionMessage diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Validators/Erc20TokenValidator.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Validators/Erc20TokenValidator.cs index 23c1912..2019144 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/Validators/Erc20TokenValidator.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Validators/Erc20TokenValidator.cs @@ -1,8 +1,8 @@ using System.Numerics; using FluentValidation; -using Net.Cache.DynamoDb.ERC20.RPC.Models; +using Net.Cache.DynamoDb.ERC20.Rpc.Models; -namespace Net.Cache.DynamoDb.ERC20.RPC.Validators +namespace Net.Cache.DynamoDb.ERC20.Rpc.Validators { internal class Erc20TokenValidator : AbstractValidator { diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Validators/MultiCallResponseValidator.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Validators/MultiCallResponseValidator.cs index ef307bb..1dffe85 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/Validators/MultiCallResponseValidator.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Validators/MultiCallResponseValidator.cs @@ -1,7 +1,7 @@ using FluentValidation; using System.Collections.Generic; -namespace Net.Cache.DynamoDb.ERC20.RPC.Validators +namespace Net.Cache.DynamoDb.ERC20.Rpc.Validators { internal class MultiCallResponseValidator : AbstractValidator> { diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Extensions/DecoderHelperExtensions.cs b/src/Net.Cache.DynamoDb.ERC20/Rpc/Extensions/DecoderExtensions.cs similarity index 80% rename from src/Net.Cache.DynamoDb.ERC20/RPC/Extensions/DecoderHelperExtensions.cs rename to src/Net.Cache.DynamoDb.ERC20/Rpc/Extensions/DecoderExtensions.cs index 50e2c71..9fb0e0b 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/Extensions/DecoderHelperExtensions.cs +++ b/src/Net.Cache.DynamoDb.ERC20/Rpc/Extensions/DecoderExtensions.cs @@ -2,9 +2,9 @@ using Nethereum.Hex.HexConvertors.Extensions; using Nethereum.ABI.FunctionEncoding.Attributes; -namespace Net.Cache.DynamoDb.ERC20.RPC.Extensions +namespace Net.Cache.DynamoDb.ERC20.Rpc.Extensions { - public static class DecoderHelperExtensions + public static class DecoderExtensions { public static TFunctionOutputDTO Decode(this byte[] data) where TFunctionOutputDTO : IFunctionOutputDTO, new() { diff --git a/tests/Net.Cache.DynamoDb.ERC20.Tests/ERC20CacheProviderTests.cs b/tests/Net.Cache.DynamoDb.ERC20.Tests/ERC20CacheProviderTests.cs index 31a4c9d..07e7920 100644 --- a/tests/Net.Cache.DynamoDb.ERC20.Tests/ERC20CacheProviderTests.cs +++ b/tests/Net.Cache.DynamoDb.ERC20.Tests/ERC20CacheProviderTests.cs @@ -4,7 +4,7 @@ using FluentAssertions; using Net.Cryptography.SHA256; using Amazon.DynamoDBv2.DataModel; -using Net.Cache.DynamoDb.ERC20.RPC; +using Net.Cache.DynamoDb.ERC20.Rpc; using Net.Cache.DynamoDb.ERC20.Models; namespace Net.Cache.DynamoDb.ERC20.Tests; diff --git a/tests/Net.Cache.DynamoDb.ERC20.Tests/RPC/ERC20ServiceTests.cs b/tests/Net.Cache.DynamoDb.ERC20.Tests/RPC/ERC20ServiceTests.cs index 7b2f335..481a34f 100644 --- a/tests/Net.Cache.DynamoDb.ERC20.Tests/RPC/ERC20ServiceTests.cs +++ b/tests/Net.Cache.DynamoDb.ERC20.Tests/RPC/ERC20ServiceTests.cs @@ -3,7 +3,7 @@ using Nethereum.Web3; using FluentAssertions; using Net.Web3.EthereumWallet; -using Net.Cache.DynamoDb.ERC20.RPC; +using Net.Cache.DynamoDb.ERC20.Rpc; using Nethereum.Contracts.Services; namespace Net.Cache.DynamoDb.ERC20.Tests.RPC; From 6caa45107f03b6cbd2b82d090e9d377fdd6cfa7d Mon Sep 17 00:00:00 2001 From: ArdenHide Date: Thu, 7 Aug 2025 15:09:40 +0300 Subject: [PATCH 09/20] implement inMemoryCache in `Erc20CacheService` --- src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs b/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs index d09012f..67f7c27 100644 --- a/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs +++ b/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Net.Web3.EthereumWallet; using Net.Cache.DynamoDb.ERC20.Rpc; +using System.Collections.Concurrent; using Net.Cache.DynamoDb.ERC20.DynamoDb; using Net.Cache.DynamoDb.ERC20.DynamoDb.Models; @@ -11,11 +12,13 @@ public class Erc20CacheService : IErc20CacheService { private readonly IDynamoDbClient _dynamoDbClient; private readonly IErc20ServiceFactory _erc20ServiceFactory; + private readonly ConcurrentDictionary _inMemoryCache; public Erc20CacheService(IDynamoDbClient dynamoDbClient, IErc20ServiceFactory erc20ServiceFactory) { _dynamoDbClient = dynamoDbClient ?? throw new ArgumentNullException(nameof(dynamoDbClient)); _erc20ServiceFactory = erc20ServiceFactory ?? throw new ArgumentNullException(nameof(erc20ServiceFactory)); + _inMemoryCache = new ConcurrentDictionary(); } public Erc20CacheService() @@ -31,10 +34,18 @@ public async Task GetOrAddAsync(long chainId, EthereumA if (rpcUrlFactory == null) throw new ArgumentNullException(nameof(rpcUrlFactory)); if (multiCallFactory == null) throw new ArgumentNullException(nameof(multiCallFactory)); + var hashKey = Erc20TokenDynamoDbEntry.GenerateHashKey(chainId, address); + + if (_inMemoryCache.TryGetValue(hashKey, out var cachedEntry)) return cachedEntry; + var entry = await _dynamoDbClient - .GetErc20TokenAsync(Erc20TokenDynamoDbEntry.GenerateHashKey(chainId, address)) + .GetErc20TokenAsync(hashKey) .ConfigureAwait(false); - if (entry != null) return entry; + if (entry != null) + { + _inMemoryCache.TryAdd(hashKey, entry); + return entry; + } var rpcUrlTask = rpcUrlFactory(); var multiCallTask = multiCallFactory(); From a9201c12394e49accbb48953523482c10abce873 Mon Sep 17 00:00:00 2001 From: ArdenHide Date: Thu, 7 Aug 2025 15:11:13 +0300 Subject: [PATCH 10/20] - add check for `chainId` in `Erc20CacheService` --- src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs b/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs index 67f7c27..3055b5b 100644 --- a/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs +++ b/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs @@ -30,6 +30,7 @@ public Erc20CacheService() public async Task GetOrAddAsync(long chainId, EthereumAddress address, Func> rpcUrlFactory, Func> multiCallFactory) { + if (chainId <= 0) throw new ArgumentOutOfRangeException(nameof(address)); if (address == null) throw new ArgumentNullException(nameof(address)); if (rpcUrlFactory == null) throw new ArgumentNullException(nameof(rpcUrlFactory)); if (multiCallFactory == null) throw new ArgumentNullException(nameof(multiCallFactory)); From c1ac81495426807466fa8845ea92b3b2b9e22f66 Mon Sep 17 00:00:00 2001 From: ArdenHide Date: Thu, 7 Aug 2025 15:37:11 +0300 Subject: [PATCH 11/20] - create `HashKey` abstraction --- .../DynamoDb/DynamoDbClient.cs | 4 +- .../DynamoDb/IDynamoDbClient.cs | 2 +- .../Models/Erc20TokenDynamoDbEntry.cs | 17 ++--- .../DynamoDb/Models/HashKey.cs | 65 +++++++++++++++++++ .../Erc20CacheService.cs | 15 ++--- .../IErc20CacheService.cs | 2 +- 6 files changed, 80 insertions(+), 25 deletions(-) create mode 100644 src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/HashKey.cs diff --git a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/DynamoDbClient.cs b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/DynamoDbClient.cs index bb649da..e56f5f7 100644 --- a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/DynamoDbClient.cs +++ b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/DynamoDbClient.cs @@ -33,10 +33,10 @@ public DynamoDbClient() ) { } - public async Task GetErc20TokenAsync(string hashKey, LoadConfig? config = null) + public async Task GetErc20TokenAsync(HashKey hashKey, LoadConfig? config = null) { return await _dynamoDbContext - .LoadAsync(hashKey, config) + .LoadAsync(hashKey.Value, config) .ConfigureAwait(false); } diff --git a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/IDynamoDbClient.cs b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/IDynamoDbClient.cs index 7cbef38..b89a16d 100644 --- a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/IDynamoDbClient.cs +++ b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/IDynamoDbClient.cs @@ -6,7 +6,7 @@ namespace Net.Cache.DynamoDb.ERC20.DynamoDb { public interface IDynamoDbClient { - public Task GetErc20TokenAsync(string hashKey, LoadConfig? config = null); + public Task GetErc20TokenAsync(HashKey hashKey, LoadConfig? config = null); public Task SaveErc20TokenAsync(Erc20TokenDynamoDbEntry entry, SaveConfig? config = null); } } diff --git a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs index 5571cca..234b7c4 100644 --- a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs +++ b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs @@ -1,6 +1,4 @@ -using Net.Cryptography.SHA256; -using Net.Web3.EthereumWallet; -using Amazon.DynamoDBv2.DataModel; +using Amazon.DynamoDBv2.DataModel; using Net.Cache.DynamoDb.ERC20.Rpc.Models; namespace Net.Cache.DynamoDb.ERC20.DynamoDb.Models @@ -34,20 +32,15 @@ public class Erc20TokenDynamoDbEntry /// public Erc20TokenDynamoDbEntry() { } - public Erc20TokenDynamoDbEntry(long chainId, EthereumAddress address, Erc20TokenData erc20Token) + public Erc20TokenDynamoDbEntry(HashKey hashKey, Erc20TokenData erc20Token) { - HashKey = GenerateHashKey(chainId, address); - ChainId = chainId; - Address = address; + HashKey = hashKey.Value; + ChainId = hashKey.ChainId; + Address = hashKey.Address; Name = erc20Token.Name; Symbol = erc20Token.Symbol; Decimals = erc20Token.Decimals; TotalSupply = Nethereum.Web3.Web3.Convert.FromWei(erc20Token.TotalSupply, erc20Token.Decimals); } - - public static string GenerateHashKey(long chainId, EthereumAddress address) - { - return $"{chainId}-{address}".ToSha256(); - } } } diff --git a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/HashKey.cs b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/HashKey.cs new file mode 100644 index 0000000..13a5db7 --- /dev/null +++ b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/HashKey.cs @@ -0,0 +1,65 @@ +using System; +using Net.Cryptography.SHA256; +using Net.Web3.EthereumWallet; + +namespace Net.Cache.DynamoDb.ERC20.DynamoDb.Models +{ + /// + /// Represents a unique key that combines a blockchain chain identifier and an ERC20 token address.
+ /// The key value is a SHA256 hash of the combined chain identifier and address. + ///
+ public class HashKey + { + /// + /// Gets the blockchain network identifier. + /// + public long ChainId { get; } + + /// + /// Gets the ERC20 token contract address. + /// + public EthereumAddress Address { get; } + + /// + /// Gets the hashed representation of the and combination. + /// + public string Value { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The blockchain network identifier. + /// The ERC20 token contract address. + /// Thrown when is less than or equal to zero. + /// Thrown when is null. + public HashKey(long chainId, EthereumAddress address) + { + if (chainId <= 0) throw new ArgumentOutOfRangeException(nameof(chainId)); + if (address == null) throw new ArgumentNullException(nameof(address)); + + ChainId = chainId; + Address = address; + Value = Generate(chainId, address); + } + + /// + /// Generates a hashed key for the specified chain identifier and address. + /// + /// The blockchain network identifier. + /// The ERC20 token contract address. + /// A SHA256 hash representing the combined chain identifier and address. + public static string Generate(long chainId, EthereumAddress address) + { + if (chainId <= 0) throw new ArgumentOutOfRangeException(nameof(chainId)); + if (address == null) throw new ArgumentNullException(nameof(address)); + + return $"{chainId}-{address}".ToSha256(); + } + + /// + /// Returns the hash key value. + /// + /// The hashed key string. + public override string ToString() => Value; + } +} diff --git a/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs b/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs index 3055b5b..8ab06ab 100644 --- a/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs +++ b/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs @@ -28,23 +28,20 @@ public Erc20CacheService() ) { } - public async Task GetOrAddAsync(long chainId, EthereumAddress address, Func> rpcUrlFactory, Func> multiCallFactory) + public async Task GetOrAddAsync(HashKey hashKey, Func> rpcUrlFactory, Func> multiCallFactory) { - if (chainId <= 0) throw new ArgumentOutOfRangeException(nameof(address)); - if (address == null) throw new ArgumentNullException(nameof(address)); + if (hashKey == null) throw new ArgumentNullException(nameof(hashKey)); if (rpcUrlFactory == null) throw new ArgumentNullException(nameof(rpcUrlFactory)); if (multiCallFactory == null) throw new ArgumentNullException(nameof(multiCallFactory)); - var hashKey = Erc20TokenDynamoDbEntry.GenerateHashKey(chainId, address); - - if (_inMemoryCache.TryGetValue(hashKey, out var cachedEntry)) return cachedEntry; + if (_inMemoryCache.TryGetValue(hashKey.Value, out var cachedEntry)) return cachedEntry; var entry = await _dynamoDbClient .GetErc20TokenAsync(hashKey) .ConfigureAwait(false); if (entry != null) { - _inMemoryCache.TryAdd(hashKey, entry); + _inMemoryCache.TryAdd(hashKey.Value, entry); return entry; } @@ -57,9 +54,9 @@ public async Task GetOrAddAsync(long chainId, EthereumA var multiCall = multiCallTask.Result; var erc20Service = _erc20ServiceFactory.Create(new Nethereum.Web3.Web3(rpcUrl), multiCall); - var erc20Token = await erc20Service.GetErc20TokenAsync(address).ConfigureAwait(false); + var erc20Token = await erc20Service.GetErc20TokenAsync(hashKey.Address).ConfigureAwait(false); - entry = new Erc20TokenDynamoDbEntry(chainId, address, erc20Token); + entry = new Erc20TokenDynamoDbEntry(hashKey, erc20Token); await _dynamoDbClient.SaveErc20TokenAsync(entry).ConfigureAwait(false); return entry; diff --git a/src/Net.Cache.DynamoDb.ERC20/IErc20CacheService.cs b/src/Net.Cache.DynamoDb.ERC20/IErc20CacheService.cs index 5b9d403..7a66889 100644 --- a/src/Net.Cache.DynamoDb.ERC20/IErc20CacheService.cs +++ b/src/Net.Cache.DynamoDb.ERC20/IErc20CacheService.cs @@ -7,6 +7,6 @@ namespace Net.Cache.DynamoDb.ERC20 { public interface IErc20CacheService { - public Task GetOrAddAsync(long chainId, EthereumAddress address, Func> rpcUrlFactory, Func> multiCallFactory); + public Task GetOrAddAsync(HashKey hashKey, Func> rpcUrlFactory, Func> multiCallFactory); } } From d4f2b2960b712905d11821dece313feafe9e170d Mon Sep 17 00:00:00 2001 From: ArdenHide Date: Thu, 7 Aug 2025 15:46:00 +0300 Subject: [PATCH 12/20] - fix: save into inMemory after DynamoDb save --- src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs b/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs index 8ab06ab..a37c272 100644 --- a/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs +++ b/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs @@ -58,6 +58,7 @@ public async Task GetOrAddAsync(HashKey hashKey, Func Date: Thu, 7 Aug 2025 16:12:25 +0300 Subject: [PATCH 13/20] - write XML docs --- .../DynamoDb/DynamoDbClient.cs | 20 ++++++++++++ .../DynamoDb/IDynamoDbClient.cs | 15 +++++++++ .../Models/Erc20TokenDynamoDbEntry.cs | 32 ++++++++++++++++++- .../Erc20CacheService.cs | 12 +++++++ .../IErc20CacheService.cs | 10 ++++++ .../RPC/ERC20Service.cs | 10 ++++++ .../RPC/Erc20ServiceFactory.cs | 4 +++ .../RPC/Exceptions/Erc20QueryException.cs | 12 +++++++ .../RPC/IERC20Service.cs | 14 ++++++++ .../RPC/IErc20ServiceFactory.cs | 9 ++++++ .../RPC/Models/Erc20TokenData.cs | 30 +++++++++++++++++ .../RPC/Models/MultiCall.cs | 14 ++++++++ .../RPC/Models/MultiCallFunction.cs | 13 ++++++++ .../Rpc/Extensions/DecoderExtensions.cs | 9 ++++++ 14 files changed, 203 insertions(+), 1 deletion(-) diff --git a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/DynamoDbClient.cs b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/DynamoDbClient.cs index e56f5f7..c0e435f 100644 --- a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/DynamoDbClient.cs +++ b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/DynamoDbClient.cs @@ -6,19 +6,34 @@ namespace Net.Cache.DynamoDb.ERC20.DynamoDb { + /// + /// Default implementation of using the AWS SDK. + /// public class DynamoDbClient : IDynamoDbClient { private readonly IDynamoDBContext _dynamoDbContext; + /// + /// Initializes a new instance of the class. + /// + /// The DynamoDB context to operate on. public DynamoDbClient(IDynamoDBContext dynamoDbContext) { _dynamoDbContext = dynamoDbContext ?? throw new ArgumentNullException(nameof(dynamoDbContext)); } + /// + /// Initializes a new instance using a context builder. + /// + /// Builder that produces a DynamoDB context. public DynamoDbClient(IDynamoDBContextBuilder contextBuilder) : this((contextBuilder ?? throw new ArgumentNullException(nameof(contextBuilder))).Build()) { } + /// + /// Initializes a new instance from a raw DynamoDB client. + /// + /// The AWS DynamoDB client. public DynamoDbClient(IAmazonDynamoDB dynamoDb) : this( new DynamoDBContextBuilder() @@ -26,6 +41,9 @@ public DynamoDbClient(IAmazonDynamoDB dynamoDb) ) { } + /// + /// Initializes a new instance using default AWS configuration. + /// public DynamoDbClient() : this( new DynamoDBContextBuilder() @@ -33,6 +51,7 @@ public DynamoDbClient() ) { } + /// public async Task GetErc20TokenAsync(HashKey hashKey, LoadConfig? config = null) { return await _dynamoDbContext @@ -40,6 +59,7 @@ public DynamoDbClient() .ConfigureAwait(false); } + /// public async Task SaveErc20TokenAsync(Erc20TokenDynamoDbEntry entry, SaveConfig? config = null) { await _dynamoDbContext diff --git a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/IDynamoDbClient.cs b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/IDynamoDbClient.cs index b89a16d..3a9f8d3 100644 --- a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/IDynamoDbClient.cs +++ b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/IDynamoDbClient.cs @@ -4,9 +4,24 @@ namespace Net.Cache.DynamoDb.ERC20.DynamoDb { + /// + /// Provides access to ERC20 token metadata stored in DynamoDB. + /// public interface IDynamoDbClient { + /// + /// Retrieves a token entry by its hash key. + /// + /// The composite hash key of the token. + /// Optional DynamoDB load configuration. + /// The token entry if it exists; otherwise, null. public Task GetErc20TokenAsync(HashKey hashKey, LoadConfig? config = null); + + /// + /// Persists a token entry into DynamoDB. + /// + /// The token entry to store. + /// Optional DynamoDB save configuration. public Task SaveErc20TokenAsync(Erc20TokenDynamoDbEntry entry, SaveConfig? config = null); } } diff --git a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs index 234b7c4..823925e 100644 --- a/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs +++ b/src/Net.Cache.DynamoDb.ERC20/DynamoDb/Models/Erc20TokenDynamoDbEntry.cs @@ -3,35 +3,65 @@ namespace Net.Cache.DynamoDb.ERC20.DynamoDb.Models { + /// + /// Represents a persisted ERC20 token entry in the DynamoDB cache table. + /// [DynamoDBTable("TokensInfoCache")] public class Erc20TokenDynamoDbEntry { + /// + /// Gets or sets the composite hash key value. + /// [DynamoDBHashKey] public string HashKey { get; set; } = string.Empty; + /// + /// Gets or sets the blockchain network identifier. + /// [DynamoDBProperty] public long ChainId { get; set; } + /// + /// Gets or sets the ERC20 token contract address. + /// [DynamoDBProperty] public string Address { get; set; } = string.Empty; + /// + /// Gets or sets the token name. + /// [DynamoDBProperty] public string Name { get; set; } = string.Empty; + /// + /// Gets or sets the token symbol. + /// [DynamoDBProperty] public string Symbol { get; set; } = string.Empty; + /// + /// Gets or sets the number of decimal places used by the token. + /// [DynamoDBProperty] public byte Decimals { get; set; } + /// + /// Gets or sets the total supply of the token. + /// [DynamoDBProperty] public decimal TotalSupply { get; set; } /// - /// Constructor without parameters for working "AWSSDK.DynamoDBv2" + /// Initializes a new instance of the class.
+ /// Constructor without parameters for working "AWSSDK.DynamoDBv2" library. ///
public Erc20TokenDynamoDbEntry() { } + /// + /// Initializes a new instance of the class with specified values. + /// + /// The hash key identifying the token. + /// The token data retrieved from RPC. public Erc20TokenDynamoDbEntry(HashKey hashKey, Erc20TokenData erc20Token) { HashKey = hashKey.Value; diff --git a/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs b/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs index a37c272..bf38efe 100644 --- a/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs +++ b/src/Net.Cache.DynamoDb.ERC20/Erc20CacheService.cs @@ -8,12 +8,20 @@ namespace Net.Cache.DynamoDb.ERC20 { + /// + /// Provides caching of ERC20 token information with a backing store in DynamoDB and an in-memory layer. + /// public class Erc20CacheService : IErc20CacheService { private readonly IDynamoDbClient _dynamoDbClient; private readonly IErc20ServiceFactory _erc20ServiceFactory; private readonly ConcurrentDictionary _inMemoryCache; + /// + /// Initializes a new instance of the class. + /// + /// The DynamoDB client used for persistent storage. + /// Factory for creating RPC services to query token metadata. public Erc20CacheService(IDynamoDbClient dynamoDbClient, IErc20ServiceFactory erc20ServiceFactory) { _dynamoDbClient = dynamoDbClient ?? throw new ArgumentNullException(nameof(dynamoDbClient)); @@ -21,6 +29,9 @@ public Erc20CacheService(IDynamoDbClient dynamoDbClient, IErc20ServiceFactory er _inMemoryCache = new ConcurrentDictionary(); } + /// + /// Initializes a new instance of the class using default implementations. + /// public Erc20CacheService() : this( new DynamoDbClient(), @@ -28,6 +39,7 @@ public Erc20CacheService() ) { } + /// public async Task GetOrAddAsync(HashKey hashKey, Func> rpcUrlFactory, Func> multiCallFactory) { if (hashKey == null) throw new ArgumentNullException(nameof(hashKey)); diff --git a/src/Net.Cache.DynamoDb.ERC20/IErc20CacheService.cs b/src/Net.Cache.DynamoDb.ERC20/IErc20CacheService.cs index 7a66889..10d0566 100644 --- a/src/Net.Cache.DynamoDb.ERC20/IErc20CacheService.cs +++ b/src/Net.Cache.DynamoDb.ERC20/IErc20CacheService.cs @@ -5,8 +5,18 @@ namespace Net.Cache.DynamoDb.ERC20 { + /// + /// Provides caching operations for ERC20 token metadata backed by DynamoDB. + /// public interface IErc20CacheService { + /// + /// Retrieves a cached ERC20 token entry or adds a new one when it is missing. + /// + /// The composite key of chain identifier and token address. + /// Factory used to resolve the RPC endpoint URL. + /// Factory used to resolve the address of the multicall contract. + /// The cached or newly created instance. public Task GetOrAddAsync(HashKey hashKey, Func> rpcUrlFactory, Func> multiCallFactory); } } diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs index 700e188..dc77a47 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs @@ -13,22 +13,32 @@ namespace Net.Cache.DynamoDb.ERC20.Rpc { + /// + /// Retrieves ERC20 token information via blockchain RPC calls. + /// public class Erc20Service : IErc20Service { private readonly IWeb3 _web3; private readonly EthereumAddress _multiCall; + /// + /// Initializes a new instance of the class. + /// + /// The web3 client used for RPC communication. + /// The address of the multicall contract. public Erc20Service(IWeb3 web3, EthereumAddress multiCall) { _web3 = web3 ?? throw new ArgumentNullException(nameof(web3)); _multiCall = multiCall ?? throw new ArgumentNullException(nameof(multiCall)); } + /// public Erc20TokenData GetErc20Token(EthereumAddress token) { return GetErc20TokenAsync(token).GetAwaiter().GetResult(); } + /// public async Task GetErc20TokenAsync(EthereumAddress token) { if (token == null) throw new ArgumentNullException(nameof(token)); diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Erc20ServiceFactory.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Erc20ServiceFactory.cs index 78ae6b7..c1cfa3a 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/Erc20ServiceFactory.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Erc20ServiceFactory.cs @@ -3,8 +3,12 @@ namespace Net.Cache.DynamoDb.ERC20.Rpc { + /// + /// Default implementation of . + /// public class Erc20ServiceFactory : IErc20ServiceFactory { + /// public IErc20Service Create(IWeb3 web3, EthereumAddress multiCall) { return new Erc20Service(web3, multiCall); diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Exceptions/Erc20QueryException.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Exceptions/Erc20QueryException.cs index 24e2eb3..9d38ffa 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/Exceptions/Erc20QueryException.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Exceptions/Erc20QueryException.cs @@ -3,10 +3,22 @@ namespace Net.Cache.DynamoDb.ERC20.Rpc.Exceptions { + /// + /// Represents errors that occur during ERC20 token queries. + /// public sealed class Erc20QueryException : Exception { + /// + /// Gets the address of the token that caused the error. + /// public EthereumAddress Token { get; } + /// + /// Initializes a new instance of the class. + /// + /// The token contract address. + /// The error message. + /// An optional inner exception. public Erc20QueryException(EthereumAddress token, string message, Exception? inner = null) : base($"[ERC20 {token}] {message}", inner) { diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs index c91abb8..88eb2e6 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs @@ -4,9 +4,23 @@ namespace Net.Cache.DynamoDb.ERC20.Rpc { + /// + /// Defines operations for retrieving ERC20 token information via RPC calls. + /// public interface IErc20Service { + /// + /// Retrieves token information synchronously. + /// + /// The token contract address. + /// The token data. public Erc20TokenData GetErc20Token(EthereumAddress token); + + /// + /// Retrieves token information asynchronously. + /// + /// The token contract address. + /// A task that resolves to the token data. public Task GetErc20TokenAsync(EthereumAddress token); } } diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/IErc20ServiceFactory.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/IErc20ServiceFactory.cs index ac9f714..15bf0cf 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/IErc20ServiceFactory.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/IErc20ServiceFactory.cs @@ -3,8 +3,17 @@ namespace Net.Cache.DynamoDb.ERC20.Rpc { + /// + /// Factory abstraction for creating instances of . + /// public interface IErc20ServiceFactory { + /// + /// Creates an ERC20 service for the specified web3 client and multicall address. + /// + /// The web3 client used to perform RPC calls. + /// The address of the multicall contract. + /// A new instance. public IErc20Service Create(IWeb3 web3, EthereumAddress multiCall); } } diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Models/Erc20TokenData.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Models/Erc20TokenData.cs index d03ef86..df0ce8b 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/Models/Erc20TokenData.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Models/Erc20TokenData.cs @@ -3,8 +3,19 @@ namespace Net.Cache.DynamoDb.ERC20.Rpc.Models { + /// + /// Represents ERC20 token metadata retrieved from the blockchain. + /// public class Erc20TokenData { + /// + /// Initializes a new instance of the class. + /// + /// The token contract address. + /// The token name. + /// The token symbol. + /// The number of decimal places used by the token. + /// The total token supply. public Erc20TokenData(EthereumAddress address, string name, string symbol, byte decimals, BigInteger totalSupply) { Address = address; @@ -14,10 +25,29 @@ public Erc20TokenData(EthereumAddress address, string name, string symbol, byte TotalSupply = totalSupply; } + /// + /// Gets the token contract address. + /// public EthereumAddress Address { get; } + + /// + /// Gets the token name. + /// public string Name { get; } + + /// + /// Gets the token symbol. + /// public string Symbol { get; } + + /// + /// Gets the number of decimal places used by the token. + /// public byte Decimals { get; } + + /// + /// Gets the total token supply. + /// public BigInteger TotalSupply { get; } } } diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCall.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCall.cs index 3983b91..308fdb8 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCall.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCall.cs @@ -3,14 +3,28 @@ namespace Net.Cache.DynamoDb.ERC20.Rpc.Models { + /// + /// Represents a single call item for the Multicall contract. + /// public class MultiCall { + /// + /// Gets the address of the contract to call. + /// [Parameter("address", "to", order: 1)] public string To { get; } + /// + /// Gets the encoded call data. + /// [Parameter("bytes", "data", order: 2)] public byte[] Data { get; } + /// + /// Initializes a new instance of the class. + /// + /// The contract address to call. + /// The encoded function data. public MultiCall(EthereumAddress to, byte[] data) { To = to; diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCallFunction.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCallFunction.cs index f34f9d9..c3bee62 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCallFunction.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/Models/MultiCallFunction.cs @@ -5,17 +5,30 @@ namespace Net.Cache.DynamoDb.ERC20.Rpc.Models { + /// + /// Represents a multicall function message that aggregates multiple calls. + /// [Function("multicall", "bytes[]")] public class MultiCallFunction : FunctionMessage { + /// + /// Gets the collection of calls to execute. + /// [Parameter("tuple[]", "calls", order: 1)] public MultiCall[] Calls { get; } + /// + /// Initializes a new instance of the class with the specified calls. + /// + /// The calls to execute within the multicall. public MultiCallFunction(IEnumerable calls) { Calls = calls.ToArray(); } + /// + /// Initializes a new instance of the class with no calls. + /// public MultiCallFunction() : this(Enumerable.Empty()) { } } } diff --git a/src/Net.Cache.DynamoDb.ERC20/Rpc/Extensions/DecoderExtensions.cs b/src/Net.Cache.DynamoDb.ERC20/Rpc/Extensions/DecoderExtensions.cs index 9fb0e0b..0be3fb6 100644 --- a/src/Net.Cache.DynamoDb.ERC20/Rpc/Extensions/DecoderExtensions.cs +++ b/src/Net.Cache.DynamoDb.ERC20/Rpc/Extensions/DecoderExtensions.cs @@ -4,8 +4,17 @@ namespace Net.Cache.DynamoDb.ERC20.Rpc.Extensions { + /// + /// Provides helper methods for decoding multicall responses. + /// public static class DecoderExtensions { + /// + /// Decodes raw call data into the specified DTO type. + /// + /// The DTO type to decode into. + /// The raw byte data returned by the contract call. + /// The decoded DTO instance. public static TFunctionOutputDTO Decode(this byte[] data) where TFunctionOutputDTO : IFunctionOutputDTO, new() { var dto = new TFunctionOutputDTO(); From ecf0fcdf6154111e3220637c1aebff8320e00bc9 Mon Sep 17 00:00:00 2001 From: ArdenHide Date: Thu, 7 Aug 2025 16:13:08 +0300 Subject: [PATCH 14/20] - remove exist tests --- .../Api/ApiERC20ServiceTests.cs | 121 --------------- .../ERC20CacheProviderTests.cs | 146 ------------------ .../Models/GetCacheRequestTests.cs | 41 ----- .../RPC/ERC20ServiceTests.cs | 30 ---- 4 files changed, 338 deletions(-) delete mode 100644 tests/Net.Cache.DynamoDb.ERC20.Tests/Api/ApiERC20ServiceTests.cs delete mode 100644 tests/Net.Cache.DynamoDb.ERC20.Tests/ERC20CacheProviderTests.cs delete mode 100644 tests/Net.Cache.DynamoDb.ERC20.Tests/Models/GetCacheRequestTests.cs delete mode 100644 tests/Net.Cache.DynamoDb.ERC20.Tests/RPC/ERC20ServiceTests.cs diff --git a/tests/Net.Cache.DynamoDb.ERC20.Tests/Api/ApiERC20ServiceTests.cs b/tests/Net.Cache.DynamoDb.ERC20.Tests/Api/ApiERC20ServiceTests.cs deleted file mode 100644 index a4065bd..0000000 --- a/tests/Net.Cache.DynamoDb.ERC20.Tests/Api/ApiERC20ServiceTests.cs +++ /dev/null @@ -1,121 +0,0 @@ -using Xunit; -using System.Numerics; -using FluentAssertions; -using Flurl.Http.Testing; -using Newtonsoft.Json.Linq; -using Net.Web3.EthereumWallet; -using Net.Cache.DynamoDb.ERC20.Api; -using Net.Cache.DynamoDb.ERC20.Models; -using Net.Cache.DynamoDb.ERC20.Models.Api; - -namespace Net.Cache.DynamoDb.ERC20.Tests.Api; - -public class ApiERC20ServiceTests -{ - private readonly long _chainId = 1; - private readonly string _apiKey = "test-api-key"; - private readonly ApiERC20Service _apiErc20Service; - private readonly string _apiUrl = "https://api.covalenthq.com/v1/{{chainId}}/tokens/{{contractAddress}}/token_holders_v2/?page-size=100&page-number=0&key={{apiKey}}"; - private readonly string _contractAddress; - - public ApiERC20ServiceTests() - { - _contractAddress = EthereumAddress.ZeroAddress; - _apiErc20Service = new ApiERC20Service(new ApiERC20ServiceConfig(_apiKey, _chainId, _contractAddress, _apiUrl)); - } - - private static JObject CreateMockResponse(byte decimals, string name, string symbol, string totalSupply) - { - return new JObject - { - ["data"] = new JObject - { - ["items"] = new JArray - { - new JObject - { - ["contract_decimals"] = decimals + 1, - ["contract_name"] = name + " 2", - ["contract_ticker_symbol"] = symbol + "2", - ["total_supply"] = totalSupply - }, - new JObject - { - ["contract_decimals"] = decimals, - ["contract_name"] = name, - ["contract_ticker_symbol"] = symbol, - ["total_supply"] = totalSupply - } - } - } - }; - } - - [Fact] - public void ApiERC20Service_Methods_ShouldReturnExpectedValues() - { - using var httpTest = new HttpTest(); - const byte expectedDecimals = 6; - const string expectedName = "Test Token"; - const string expectedSymbol = "TTT"; - const string expectedTotalSupply = "1000000"; - var jsonResponse = CreateMockResponse(expectedDecimals, expectedName, expectedSymbol, expectedTotalSupply); - - httpTest.RespondWith(jsonResponse.ToString()); - - var decimals = _apiErc20Service.Decimals(); - var name = _apiErc20Service.Name(); - var symbol = _apiErc20Service.Symbol(); - var totalSupply = _apiErc20Service.TotalSupply(); - - decimals.Should().Be(expectedDecimals); - name.Should().Be(expectedName); - symbol.Should().Be(expectedSymbol); - totalSupply.Should().Be(BigInteger.Parse(expectedTotalSupply)); - } - - [Fact] - public void ApiRequestFactory_ShouldCreateCorrectlyConfiguredServices() - { - var ethereumAddress = (EthereumAddress)_contractAddress; - - var apiRequestFactory = new ApiRequestFactory(); - var config = apiRequestFactory.CreateApiServiceConfig(_apiKey, _chainId, ethereumAddress, _apiUrl); - var apiService = apiRequestFactory.CreateApiService(config); - var cacheRequest = apiRequestFactory.CreateWithApiService(apiService, _chainId); - - cacheRequest.ChainId.Should().Be(_chainId); - cacheRequest.ERC20Service.Should().NotBeNull(); - cacheRequest.ERC20Service.ContractAddress.Should().Be(ethereumAddress); - } - - [Fact] - public void GetTokenData_ShouldReturnCorrectData() - { - using var httpTest = new HttpTest(); - var expectedJson = CreateMockResponse(18, "Test Token", "TTT", "1000000"); - - httpTest.RespondWith(expectedJson.ToString()); - - var tokenData = _apiErc20Service.GetTokenData(); - - var result = JObject.FromObject(tokenData); - - result.Should().BeEquivalentTo(expectedJson); - - var expectedUrl = $"https://api.covalenthq.com/v1/1/tokens/0x0000000000000000000000000000000000000000/token_holders_v2/?page-size=100&page-number=0&key=test-api-key"; - - httpTest.ShouldHaveCalled(expectedUrl) - .WithVerb(HttpMethod.Get); - } - - - [Fact] - public void Constructor_ShouldInitializeFieldsCorrectly() - { - var service = new ApiERC20Service(new ApiERC20ServiceConfig(_apiKey, _chainId, _contractAddress, _apiUrl)); - - service.ContractAddress.Should().Be((EthereumAddress)_contractAddress); - service.Should().NotBeNull(); - } -} \ No newline at end of file diff --git a/tests/Net.Cache.DynamoDb.ERC20.Tests/ERC20CacheProviderTests.cs b/tests/Net.Cache.DynamoDb.ERC20.Tests/ERC20CacheProviderTests.cs deleted file mode 100644 index 07e7920..0000000 --- a/tests/Net.Cache.DynamoDb.ERC20.Tests/ERC20CacheProviderTests.cs +++ /dev/null @@ -1,146 +0,0 @@ -using Moq; -using Xunit; -using System.Numerics; -using FluentAssertions; -using Net.Cryptography.SHA256; -using Amazon.DynamoDBv2.DataModel; -using Net.Cache.DynamoDb.ERC20.Rpc; -using Net.Cache.DynamoDb.ERC20.Models; - -namespace Net.Cache.DynamoDb.ERC20.Tests; - -public class ERC20CacheProviderTests -{ - private const long chainId = 56; - private const string contractAddress = "0x0000000000000000000000000000000000000000"; - private const byte decimals = 18; - private const string name = "Test"; - private const string symbol = "TST"; - private const long totalSupply = 5500000; - private const long updatedTotalSupply = 5000000; - private readonly string key = $"{chainId}-{contractAddress}".ToSha256(); - private readonly IERC20Service mockErc20Service; - - public ERC20CacheProviderTests() - { - Environment.SetEnvironmentVariable("AWS_REGION", "us-east-1"); - mockErc20Service = MockERC20Service(); - } - - [Fact] - internal void GetOrAdd_ItemReceivedFromCache_TotalSupplyHasBeenUpdated() - { - var erc20StorageProvider = new ERC20StorageProvider(MockContext(true)); - var erc20CacheProvider = new ERC20CacheProvider(erc20StorageProvider); - - var addedItem = erc20CacheProvider.GetOrAdd(new GetCacheRequest(chainId, mockErc20Service)); - var updatedItem = erc20CacheProvider.GetOrAdd(new GetCacheRequest(chainId, mockErc20Service)); - - addedItem.Should().BeEquivalentTo(new ERC20DynamoDbTable( - chainId, contractAddress, name, symbol, decimals, 0.0000000000055m - )); - updatedItem.Should().BeEquivalentTo(new ERC20DynamoDbTable( - chainId, contractAddress, name, symbol, decimals, 0.0000000000050m - )); - } - - [Fact] - internal void GetOrAdd_ItemSavedToCache() - { - var erc20StorageProvider = new ERC20StorageProvider(MockContext(false)); - var erc20CacheProvider = new ERC20CacheProvider(erc20StorageProvider); - - var addedItem = erc20CacheProvider.GetOrAdd(new GetCacheRequest(chainId, mockErc20Service)); - - addedItem.Should().BeEquivalentTo(new ERC20DynamoDbTable( - chainId, contractAddress, name, symbol, decimals, 0.0000000000055m - )); - } - - [Fact] - internal async Task GetOrAddAsync_ItemReceivedFromCache_TotalSupplyHasBeenUpdated() - { - var erc20StorageProvider = new ERC20StorageProvider(MockContext(true)); - var erc20CacheProvider = new ERC20CacheProvider(erc20StorageProvider); - - var addedItem = await erc20CacheProvider.GetOrAddAsync(new GetCacheRequest(chainId, mockErc20Service)); - var updatedItem = await erc20CacheProvider.GetOrAddAsync(new GetCacheRequest(chainId, mockErc20Service)); - - addedItem.Should().BeEquivalentTo(new ERC20DynamoDbTable( - chainId, contractAddress, name, symbol, decimals, 0.0000000000055m - )); - updatedItem.Should().BeEquivalentTo(new ERC20DynamoDbTable( - chainId, contractAddress, name, symbol, decimals, 0.0000000000050m - )); - } - - [Fact] - internal async Task GetOrAddAsync_ItemSavedToCache() - { - var erc20StorageProvider = new ERC20StorageProvider(MockContext(false)); - var erc20CacheProvider = new ERC20CacheProvider(erc20StorageProvider); - - var addedItem = await erc20CacheProvider.GetOrAddAsync(new GetCacheRequest(chainId, mockErc20Service)); - - addedItem.Should().BeEquivalentTo(new ERC20DynamoDbTable( - chainId, contractAddress, name, symbol, decimals, 0.0000000000055m - )); - } - - [Fact] - internal void GetOrAdd_ApiERC20ServiceIntegration_WorksCorrectly() - { - const byte expectedDecimals = 6; - const string expectedName = "USD Coin"; - const string expectedSymbol = "USDC"; - var expectedTotalSupply = new BigInteger(1000000); - - var mockApiERC20Service = new Mock(); - mockApiERC20Service.Setup(x => x.ContractAddress).Returns(contractAddress); - mockApiERC20Service.Setup(x => x.Decimals()).Returns(expectedDecimals); - mockApiERC20Service.Setup(x => x.Name()).Returns(expectedName); - mockApiERC20Service.Setup(x => x.Symbol()).Returns(expectedSymbol); - mockApiERC20Service.Setup(x => x.TotalSupply()).Returns(expectedTotalSupply); - - var erc20StorageProvider = new ERC20StorageProvider(MockContext(false)); - var erc20CacheProvider = new ERC20CacheProvider(erc20StorageProvider); - - var addedItem = erc20CacheProvider.GetOrAdd(new GetCacheRequest(chainId, mockApiERC20Service.Object)); - - addedItem.Should().BeEquivalentTo(new ERC20DynamoDbTable( - chainId, contractAddress, expectedName, expectedSymbol, expectedDecimals, - Nethereum.Web3.Web3.Convert.FromWei(expectedTotalSupply, expectedDecimals) - )); - } - - private static IERC20Service MockERC20Service() - { - var mock = new Mock(); - mock.Setup(x => x.ContractAddress) - .Returns(contractAddress); - mock.Setup(x => x.Decimals()) - .Returns(decimals); - mock.Setup(x => x.Name()) - .Returns(name); - mock.Setup(x => x.Symbol()) - .Returns(symbol); - mock.SetupSequence(x => x.TotalSupply()) - .Returns(totalSupply) - .Returns(updatedTotalSupply); - - return mock.Object; - } - - private IDynamoDBContext MockContext(bool setupLoad) - { - var mock = new Mock(); - if (setupLoad) - { - var value = new ERC20DynamoDbTable(chainId, contractAddress, name, symbol, decimals, 0.0000000000055m); - mock.SetupSequence(x => x.LoadAsync(key, It.IsAny(), CancellationToken.None)) - .ReturnsAsync(value) - .ReturnsAsync(value); - } - return mock.Object; - } -} \ No newline at end of file diff --git a/tests/Net.Cache.DynamoDb.ERC20.Tests/Models/GetCacheRequestTests.cs b/tests/Net.Cache.DynamoDb.ERC20.Tests/Models/GetCacheRequestTests.cs deleted file mode 100644 index 93574ff..0000000 --- a/tests/Net.Cache.DynamoDb.ERC20.Tests/Models/GetCacheRequestTests.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Xunit; -using FluentAssertions; -using Net.Web3.EthereumWallet; -using Net.Cache.DynamoDb.ERC20.Models; - -namespace Net.Cache.DynamoDb.ERC20.Tests.Models; - -public class GetCacheRequestTests -{ - [Fact] - public void Constructor_WithRpcUrlFactory_ShouldNotInvokeFactory() - { - var called = false; - - var request = new GetCacheRequest(1, EthereumAddress.ZeroAddress, () => - { - called = true; - return "http://localhost"; - }, false); - - called.Should().BeFalse(); - request.ChainId.Should().Be(1); - request.ERC20Service.Should().NotBeNull(); - } - - [Fact] - public void Constructor_WithAsyncRpcUrlFactory_ShouldNotInvokeFactory() - { - var called = false; - - var request = new GetCacheRequest(1, EthereumAddress.ZeroAddress, () => - { - called = true; - return Task.FromResult("http://localhost"); - }, false); - - called.Should().BeFalse(); - request.ChainId.Should().Be(1); - request.ERC20Service.Should().NotBeNull(); - } -} \ No newline at end of file diff --git a/tests/Net.Cache.DynamoDb.ERC20.Tests/RPC/ERC20ServiceTests.cs b/tests/Net.Cache.DynamoDb.ERC20.Tests/RPC/ERC20ServiceTests.cs deleted file mode 100644 index 481a34f..0000000 --- a/tests/Net.Cache.DynamoDb.ERC20.Tests/RPC/ERC20ServiceTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Moq; -using Xunit; -using Nethereum.Web3; -using FluentAssertions; -using Net.Web3.EthereumWallet; -using Net.Cache.DynamoDb.ERC20.Rpc; -using Nethereum.Contracts.Services; - -namespace Net.Cache.DynamoDb.ERC20.Tests.RPC; - -public class ERC20ServiceTests -{ - [Fact] - internal void Ctor() - { - var eth = new Mock(); - var erc20 = new Mock(eth.Object).Object; - - eth.Setup(x => x.ERC20).Returns(erc20); - - var web3 = new Mock(); - web3.Setup(x => x.Eth).Returns(eth.Object); - - var contractAddress = EthereumAddress.ZeroAddress; - - var erc20Service = new ERC20Service(web3.Object, contractAddress); - - erc20Service.Should().NotBeNull(); - } -} \ No newline at end of file From bfcff987ccaa3c301be14fd13b738a96a1106e4f Mon Sep 17 00:00:00 2001 From: ArdenHide Date: Thu, 7 Aug 2025 16:30:32 +0300 Subject: [PATCH 15/20] - write tests for DynamoDb/Models --- .../Models/Erc20TokenDynamoDbEntryTests.cs | 31 ++++++++++ .../DynamoDb/Models/HashKeyTests.cs | 60 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 tests/Net.Cache.DynamoDb.ERC20.Tests/DynamoDb/Models/Erc20TokenDynamoDbEntryTests.cs create mode 100644 tests/Net.Cache.DynamoDb.ERC20.Tests/DynamoDb/Models/HashKeyTests.cs diff --git a/tests/Net.Cache.DynamoDb.ERC20.Tests/DynamoDb/Models/Erc20TokenDynamoDbEntryTests.cs b/tests/Net.Cache.DynamoDb.ERC20.Tests/DynamoDb/Models/Erc20TokenDynamoDbEntryTests.cs new file mode 100644 index 0000000..99fbcad --- /dev/null +++ b/tests/Net.Cache.DynamoDb.ERC20.Tests/DynamoDb/Models/Erc20TokenDynamoDbEntryTests.cs @@ -0,0 +1,31 @@ +using Xunit; +using System.Numerics; +using FluentAssertions; +using Net.Web3.EthereumWallet; +using Net.Cache.DynamoDb.ERC20.Rpc.Models; +using Net.Cache.DynamoDb.ERC20.DynamoDb.Models; + +namespace Net.Cache.DynamoDb.ERC20.Tests.DynamoDb.Models +{ + public class Erc20TokenDynamoDbEntryTests + { + [Fact] + public void Constructor_ShouldMapFields() + { + var address = new EthereumAddress("0x0000000000000000000000000000000000000001"); + var chainId = 1; + var hashKey = new HashKey(chainId, address); + var token = new Erc20TokenData(address, "Token", "TKN", 2, new BigInteger(1000)); + + var entry = new Erc20TokenDynamoDbEntry(hashKey, token); + + entry.HashKey.Should().Be(hashKey.Value); + entry.ChainId.Should().Be(chainId); + entry.Address.Should().Be(address); + entry.Name.Should().Be("Token"); + entry.Symbol.Should().Be("TKN"); + entry.Decimals.Should().Be(2); + entry.TotalSupply.Should().Be(10m); + } + } +} diff --git a/tests/Net.Cache.DynamoDb.ERC20.Tests/DynamoDb/Models/HashKeyTests.cs b/tests/Net.Cache.DynamoDb.ERC20.Tests/DynamoDb/Models/HashKeyTests.cs new file mode 100644 index 0000000..9c6da1a --- /dev/null +++ b/tests/Net.Cache.DynamoDb.ERC20.Tests/DynamoDb/Models/HashKeyTests.cs @@ -0,0 +1,60 @@ +using Xunit; +using FluentAssertions; +using Net.Cryptography.SHA256; +using Net.Web3.EthereumWallet; +using Net.Cache.DynamoDb.ERC20.DynamoDb.Models; + +namespace Net.Cache.DynamoDb.ERC20.Tests.DynamoDb.Models; + +public class HashKeyTests +{ + public const long chainId = 1; + public const string address = EthereumAddress.ZeroAddress; + + public class Constructor + { + [Theory] + [InlineData(0)] + [InlineData(-1)] + public void WhenChainIdIsInvalid_ShouldThrow(long chainId) + { + var act = () => new HashKey(chainId, address); + + act.Should().Throw(); + } + + [Fact] + public void WhenAddressIsNull_ShouldThrow() + { + var act = () => new HashKey(1, null!); + + act.Should().Throw(); + } + + [Fact] + public void ShouldInitializeProperties() + { + var expected = $"{chainId}-{address}".ToSha256(); + + var key = new HashKey(chainId, address); + + key.ChainId.Should().Be(chainId); + key.Address.Should().Be(address); + key.Value.Should().Be(expected); + key.ToString().Should().Be(expected); + } + } + + public class Generate + { + [Fact] + public void ShouldReturnSha256Value() + { + var expected = $"{chainId}-{address}".ToSha256(); + + var value = HashKey.Generate(chainId, address); + + value.Should().Be(expected); + } + } +} \ No newline at end of file From b5e38f7ec7c835e2553ceb354ee2291c55d77705 Mon Sep 17 00:00:00 2001 From: ArdenHide Date: Thu, 7 Aug 2025 18:19:45 +0300 Subject: [PATCH 16/20] - add `AssemblyInfo` - fix `HashKeyTests` - write `DynamoDbClientTests` - write `DecoderExtensionsTests` - write `Erc20TokenValidatorTests` and `MultiCallResponseValidatorTests` - write `Erc20ServiceFactoryTests` - write `Erc20ServiceTests` --- .../Properties/AssemblyInfo.cs | 3 + .../DynamoDb/DynamoDbClientTests.cs | 99 +++++++++++++++++ .../DynamoDb/Models/HashKeyTests.cs | 2 +- .../Rpc/Erc20ServiceFactoryTests.cs | 37 +++++++ .../Rpc/Erc20ServiceTests.cs | 104 ++++++++++++++++++ .../Rpc/Extensions/DecoderExtensionsTests.cs | 43 ++++++++ .../Validators/Erc20TokenValidatorTests.cs | 59 ++++++++++ .../MultiCallResponseValidatorTests.cs | 46 ++++++++ 8 files changed, 392 insertions(+), 1 deletion(-) create mode 100644 src/Net.Cache.DynamoDb.ERC20/Properties/AssemblyInfo.cs create mode 100644 tests/Net.Cache.DynamoDb.ERC20.Tests/DynamoDb/DynamoDbClientTests.cs create mode 100644 tests/Net.Cache.DynamoDb.ERC20.Tests/Rpc/Erc20ServiceFactoryTests.cs create mode 100644 tests/Net.Cache.DynamoDb.ERC20.Tests/Rpc/Erc20ServiceTests.cs create mode 100644 tests/Net.Cache.DynamoDb.ERC20.Tests/Rpc/Extensions/DecoderExtensionsTests.cs create mode 100644 tests/Net.Cache.DynamoDb.ERC20.Tests/Rpc/Validators/Erc20TokenValidatorTests.cs create mode 100644 tests/Net.Cache.DynamoDb.ERC20.Tests/Rpc/Validators/MultiCallResponseValidatorTests.cs diff --git a/src/Net.Cache.DynamoDb.ERC20/Properties/AssemblyInfo.cs b/src/Net.Cache.DynamoDb.ERC20/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..86ae4f3 --- /dev/null +++ b/src/Net.Cache.DynamoDb.ERC20/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Net.Cache.DynamoDb.ERC20.Tests")] \ No newline at end of file diff --git a/tests/Net.Cache.DynamoDb.ERC20.Tests/DynamoDb/DynamoDbClientTests.cs b/tests/Net.Cache.DynamoDb.ERC20.Tests/DynamoDb/DynamoDbClientTests.cs new file mode 100644 index 0000000..e155708 --- /dev/null +++ b/tests/Net.Cache.DynamoDb.ERC20.Tests/DynamoDb/DynamoDbClientTests.cs @@ -0,0 +1,99 @@ +using Moq; +using Xunit; +using System.Numerics; +using FluentAssertions; +using Amazon.DynamoDBv2; +using Net.Web3.EthereumWallet; +using Amazon.DynamoDBv2.DataModel; +using Net.Cache.DynamoDb.ERC20.DynamoDb; +using Net.Cache.DynamoDb.ERC20.Rpc.Models; +using Net.Cache.DynamoDb.ERC20.DynamoDb.Models; + +namespace Net.Cache.DynamoDb.ERC20.Tests.DynamoDb; + +public class DynamoDbClientTests +{ + public class Constructor + { + [Fact] + public void WhenContextIsNull_ShouldThrow() + { + var act = () => new DynamoDbClient((IDynamoDBContext)null!); + + act.Should().Throw(); + } + + [Fact] + public void WhenContextBuilderIsNull_ShouldThrow() + { + var act = () => new DynamoDbClient((IDynamoDBContextBuilder)null!); + + act.Should().Throw(); + } + + [Fact] + public void WhenDynamoDbClientIsNull_ShouldThrow() + { + var act = () => new DynamoDbClient((IAmazonDynamoDB)null!); + + act.Should().Throw(); + } + + [Fact] + public void ShouldBuildContextFromBuilder() + { + Environment.SetEnvironmentVariable("AWS_REGION", "us-east-1"); + + var mockBuilder = new Mock(); + var mockContext = new DynamoDBContext(new AmazonDynamoDBClient()); + mockBuilder.Setup(b => b.Build()).Returns(mockContext); + + _ = new DynamoDbClient(mockBuilder.Object); + + mockBuilder.Verify(b => b.Build(), Times.Once); + } + } + + public class GetErc20TokenAsync + { + [Fact] + public async Task ShouldCallLoadAsyncWithCorrectParameters() + { + var mockContext = new Mock(); + var client = new DynamoDbClient(mockContext.Object); + var hashKey = new HashKey(1, EthereumAddress.ZeroAddress); + var token = new Erc20TokenData(EthereumAddress.ZeroAddress, "Token", "TKN", 2, new BigInteger(1000)); + var expectedEntry = new Erc20TokenDynamoDbEntry(hashKey, token); + var config = new LoadConfig(); + mockContext + .Setup(c => c.LoadAsync(hashKey.Value, config, CancellationToken.None)) + .ReturnsAsync(expectedEntry); + + var result = await client.GetErc20TokenAsync(hashKey, config); + + result.Should().Be(expectedEntry); + mockContext.Verify(c => c.LoadAsync(hashKey.Value, config, CancellationToken.None), Times.Once); + } + } + + public class SaveErc20TokenAsync + { + [Fact] + public async Task ShouldCallSaveAsyncWithCorrectParameters() + { + var mockContext = new Mock(); + var client = new DynamoDbClient(mockContext.Object); + var hashKey = new HashKey(1, EthereumAddress.ZeroAddress); + var token = new Erc20TokenData(EthereumAddress.ZeroAddress, "Token", "TKN", 2, new BigInteger(1000)); + var entry = new Erc20TokenDynamoDbEntry(hashKey, token); + var config = new SaveConfig(); + mockContext + .Setup(c => c.SaveAsync(entry, config, CancellationToken.None)) + .Returns(Task.CompletedTask); + + await client.SaveErc20TokenAsync(entry, config); + + mockContext.Verify(c => c.SaveAsync(entry, config, CancellationToken.None), Times.Once); + } + } +} \ No newline at end of file diff --git a/tests/Net.Cache.DynamoDb.ERC20.Tests/DynamoDb/Models/HashKeyTests.cs b/tests/Net.Cache.DynamoDb.ERC20.Tests/DynamoDb/Models/HashKeyTests.cs index 9c6da1a..3bf1183 100644 --- a/tests/Net.Cache.DynamoDb.ERC20.Tests/DynamoDb/Models/HashKeyTests.cs +++ b/tests/Net.Cache.DynamoDb.ERC20.Tests/DynamoDb/Models/HashKeyTests.cs @@ -39,7 +39,7 @@ public void ShouldInitializeProperties() var key = new HashKey(chainId, address); key.ChainId.Should().Be(chainId); - key.Address.Should().Be(address); + key.Address.Should().Be(new EthereumAddress(address)); key.Value.Should().Be(expected); key.ToString().Should().Be(expected); } diff --git a/tests/Net.Cache.DynamoDb.ERC20.Tests/Rpc/Erc20ServiceFactoryTests.cs b/tests/Net.Cache.DynamoDb.ERC20.Tests/Rpc/Erc20ServiceFactoryTests.cs new file mode 100644 index 0000000..f7fd42b --- /dev/null +++ b/tests/Net.Cache.DynamoDb.ERC20.Tests/Rpc/Erc20ServiceFactoryTests.cs @@ -0,0 +1,37 @@ +using Moq; +using Xunit; +using Nethereum.Web3; +using FluentAssertions; +using Net.Web3.EthereumWallet; +using Net.Cache.DynamoDb.ERC20.Rpc; + +namespace Net.Cache.DynamoDb.ERC20.Tests.Rpc; + +public class Erc20ServiceFactoryTests +{ + public class Create + { + [Fact] + public void ShouldReturnErc20Service() + { + var web3 = new Mock(); + var factory = new Erc20ServiceFactory(); + + var service = factory.Create(web3.Object, EthereumAddress.ZeroAddress); + + service.Should().BeOfType(); + } + + [Fact] + public void WhenDependencyNull_ShouldThrow() + { + var factory = new Erc20ServiceFactory(); + + var act1 = () => factory.Create(null!, EthereumAddress.ZeroAddress); + var act2 = () => factory.Create(new Mock().Object, null!); + + act1.Should().Throw(); + act2.Should().Throw(); + } + } +} \ No newline at end of file diff --git a/tests/Net.Cache.DynamoDb.ERC20.Tests/Rpc/Erc20ServiceTests.cs b/tests/Net.Cache.DynamoDb.ERC20.Tests/Rpc/Erc20ServiceTests.cs new file mode 100644 index 0000000..f4f8dff --- /dev/null +++ b/tests/Net.Cache.DynamoDb.ERC20.Tests/Rpc/Erc20ServiceTests.cs @@ -0,0 +1,104 @@ +using Moq; +using Xunit; +using Nethereum.ABI; +using Nethereum.Web3; +using System.Numerics; +using FluentAssertions; +using Nethereum.RPC.Eth.DTOs; +using Net.Web3.EthereumWallet; +using Nethereum.Contracts.Services; +using Net.Cache.DynamoDb.ERC20.Rpc; +using Net.Cache.DynamoDb.ERC20.Rpc.Models; +using Nethereum.Contracts.ContractHandlers; +using Net.Cache.DynamoDb.ERC20.Rpc.Exceptions; + +namespace Net.Cache.DynamoDb.ERC20.Tests.Rpc; + +public class Erc20ServiceTests +{ + public class Constructor + { + [Fact] + public void WhenWeb3Null_ShouldThrow() + { + var act = () => new Erc20Service(null!, EthereumAddress.ZeroAddress); + act.Should().Throw(); + } + + [Fact] + public void WhenMultiCallNull_ShouldThrow() + { + var web3 = Mock.Of(); + var act = () => new Erc20Service(web3, null!); + act.Should().Throw(); + } + } + + public class GetErc20TokenAsync + { + [Fact] + public async Task WhenTokenNull_ShouldThrow() + { + var handler = new Mock>(); + var service = CreateService(handler.Object); + var act = async () => await service.GetErc20TokenAsync(null!); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task WhenResponseValid_ShouldReturnToken() + { + var response = BuildResponse("Token", "TKN", 18, new BigInteger(1000)); + var handlerMock = new Mock>(); + handlerMock + .Setup(h => h.QueryAsync>(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(response); + var service = CreateService(handlerMock.Object); + + var result = await service.GetErc20TokenAsync(EthereumAddress.ZeroAddress); + + result.Address.Should().Be(new EthereumAddress(EthereumAddress.ZeroAddress)); + result.Name.Should().Be("Token"); + result.Symbol.Should().Be("TKN"); + result.Decimals.Should().Be(18); + result.TotalSupply.Should().Be(new BigInteger(1000)); + } + + [Fact] + public async Task WhenTokenInvalid_ShouldThrow() + { + var response = BuildResponse(string.Empty, "TKN", 18, new BigInteger(1000)); + var handlerMock = new Mock>(); + handlerMock + .Setup(h => h.QueryAsync>(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(response); + var service = CreateService(handlerMock.Object); + + var act = async () => await service.GetErc20TokenAsync(EthereumAddress.ZeroAddress); + + await act.Should().ThrowAsync() + .WithMessage("*[ERC20*Name is missing.*"); + } + } + + private static Erc20Service CreateService(IContractQueryHandler handler) + { + var web3Mock = new Mock(); + var ethMock = new Mock(); + ethMock.Setup(e => e.GetContractQueryHandler()).Returns(handler); + web3Mock.SetupGet(w => w.Eth).Returns(ethMock.Object); + return new Erc20Service(web3Mock.Object, EthereumAddress.ZeroAddress); + } + + private static List BuildResponse(string name, string symbol, byte decimals, BigInteger supply) + { + var abiEncode = new ABIEncode(); + return + [ + abiEncode.GetABIEncoded(new ABIValue("string", name)), + abiEncode.GetABIEncoded(new ABIValue("string", symbol)), + abiEncode.GetABIEncoded(new ABIValue("uint8", decimals)), + abiEncode.GetABIEncoded(new ABIValue("uint256", supply)) + ]; + } +} diff --git a/tests/Net.Cache.DynamoDb.ERC20.Tests/Rpc/Extensions/DecoderExtensionsTests.cs b/tests/Net.Cache.DynamoDb.ERC20.Tests/Rpc/Extensions/DecoderExtensionsTests.cs new file mode 100644 index 0000000..6ad7bc6 --- /dev/null +++ b/tests/Net.Cache.DynamoDb.ERC20.Tests/Rpc/Extensions/DecoderExtensionsTests.cs @@ -0,0 +1,43 @@ +using Xunit; +using System.Text; +using System.Numerics; +using FluentAssertions; +using Nethereum.Hex.HexConvertors.Extensions; +using Net.Cache.DynamoDb.ERC20.Rpc.Extensions; +using Nethereum.Contracts.Standards.ERC20.ContractDefinition; + +namespace Net.Cache.DynamoDb.ERC20.Tests.Rpc.Extensions +{ + public class DecoderExtensionsTests + { + [Fact] + public void ShouldDecodeErc20Outputs() + { + var nameData = EncodeString("TokenName"); + var symbolData = EncodeString("TN"); + var decimalsData = EncodeNumber(18); + var supplyData = EncodeNumber(new BigInteger(1000)); + + var responses = new[] { nameData, symbolData, decimalsData, supplyData }; + + responses[0].Decode().Name.Should().Be("TokenName"); + responses[1].Decode().Symbol.Should().Be("TN"); + responses[2].Decode().Decimals.Should().Be(18); + responses[3].Decode().TotalSupply.Should().Be(new BigInteger(1000)); + } + + private static byte[] EncodeString(string value) + { + var bytes = Encoding.UTF8.GetBytes(value); + var lengthHex = bytes.Length.ToString("x").PadLeft(64, '0'); + var dataHex = bytes.ToHex().PadRight(((bytes.Length + 31) / 32) * 64, '0'); + var hex = "0000000000000000000000000000000000000000000000000000000000000020" + lengthHex + dataHex; + return hex.HexToByteArray(); + } + + private static byte[] EncodeNumber(BigInteger value) + { + return value.ToString("x").PadLeft(64, '0').HexToByteArray(); + } + } +} diff --git a/tests/Net.Cache.DynamoDb.ERC20.Tests/Rpc/Validators/Erc20TokenValidatorTests.cs b/tests/Net.Cache.DynamoDb.ERC20.Tests/Rpc/Validators/Erc20TokenValidatorTests.cs new file mode 100644 index 0000000..d8ab2d5 --- /dev/null +++ b/tests/Net.Cache.DynamoDb.ERC20.Tests/Rpc/Validators/Erc20TokenValidatorTests.cs @@ -0,0 +1,59 @@ +using Xunit; +using System.Numerics; +using FluentAssertions; +using Net.Web3.EthereumWallet; +using Net.Cache.DynamoDb.ERC20.Rpc.Models; +using Net.Cache.DynamoDb.ERC20.Rpc.Validators; + +namespace Net.Cache.DynamoDb.ERC20.Tests.Rpc.Validators; + +public class Erc20TokenValidatorTests +{ + public class Validate + { + private readonly Erc20TokenValidator validator = new(); + + [Fact] + public void WhenTokenIsValid_ShouldReturnValid() + { + var token = new Erc20TokenData(EthereumAddress.ZeroAddress, "Token", "TKN", 18, BigInteger.Zero); + + var result = validator.Validate(token); + + result.IsValid.Should().BeTrue(); + } + + [Fact] + public void WhenNameMissing_ShouldReturnError() + { + var token = new Erc20TokenData(EthereumAddress.ZeroAddress, "", "TKN", 18, BigInteger.Zero); + + var result = validator.Validate(token); + + result.IsValid.Should().BeFalse(); + result.Errors.Should().ContainSingle(e => e.PropertyName == "Name" && e.ErrorMessage == "Name is missing."); + } + + [Fact] + public void WhenSymbolMissing_ShouldReturnError() + { + var token = new Erc20TokenData(EthereumAddress.ZeroAddress, "Token", "", 18, BigInteger.Zero); + + var result = validator.Validate(token); + + result.IsValid.Should().BeFalse(); + result.Errors.Should().ContainSingle(e => e.PropertyName == "Symbol" && e.ErrorMessage == "Symbol is missing."); + } + + [Fact] + public void WhenTotalSupplyIsNegative_ShouldReturnError() + { + var token = new Erc20TokenData(EthereumAddress.ZeroAddress, "Token", "TKN", 18, BigInteger.MinusOne); + + var result = validator.Validate(token); + + result.IsValid.Should().BeFalse(); + result.Errors.Should().ContainSingle(e => e.PropertyName == "TotalSupply" && e.ErrorMessage == "TotalSupply is negative."); + } + } +} \ No newline at end of file diff --git a/tests/Net.Cache.DynamoDb.ERC20.Tests/Rpc/Validators/MultiCallResponseValidatorTests.cs b/tests/Net.Cache.DynamoDb.ERC20.Tests/Rpc/Validators/MultiCallResponseValidatorTests.cs new file mode 100644 index 0000000..ae66f25 --- /dev/null +++ b/tests/Net.Cache.DynamoDb.ERC20.Tests/Rpc/Validators/MultiCallResponseValidatorTests.cs @@ -0,0 +1,46 @@ +using Xunit; +using FluentAssertions; +using Net.Cache.DynamoDb.ERC20.Rpc.Validators; + +namespace Net.Cache.DynamoDb.ERC20.Tests.Rpc.Validators; + +public class MultiCallResponseValidatorTests +{ + public class Validate + { + [Fact] + public void WhenCountIsUnexpected_ShouldReturnError() + { + var validator = new MultiCallResponseValidator(2); + var response = new List { new byte[] { 1 } }; + + var result = validator.Validate(response); + + result.IsValid.Should().BeFalse(); + result.Errors.Should().ContainSingle(e => e.PropertyName == "MultiCall" && e.ErrorMessage == "MultiCall returned unexpected number of results."); + } + + [Fact] + public void WhenCallReturnsNoData_ShouldReturnError() + { + var validator = new MultiCallResponseValidator(2); + var response = new List { new byte[] { 1 }, Array.Empty() }; + + var result = validator.Validate(response); + + result.IsValid.Should().BeFalse(); + result.Errors.Should().ContainSingle(e => e.PropertyName == "Symbol" && e.ErrorMessage == "Symbol call returned no data."); + } + + [Fact] + public void WhenAllCallsReturnData_ShouldBeValid() + { + var validator = new MultiCallResponseValidator(2); + var response = new List { new byte[] { 1 }, new byte[] { 2 } }; + + var result = validator.Validate(response); + + result.IsValid.Should().BeTrue(); + } + } +} \ No newline at end of file From 53ec619e6a2456505c0b71575a6bf9e6149fe33b Mon Sep 17 00:00:00 2001 From: ArdenHide Date: Thu, 7 Aug 2025 18:40:06 +0300 Subject: [PATCH 17/20] - write `Erc20CacheServiceTests` --- .../Erc20CacheServiceTests.cs | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 tests/Net.Cache.DynamoDb.ERC20.Tests/Erc20CacheServiceTests.cs diff --git a/tests/Net.Cache.DynamoDb.ERC20.Tests/Erc20CacheServiceTests.cs b/tests/Net.Cache.DynamoDb.ERC20.Tests/Erc20CacheServiceTests.cs new file mode 100644 index 0000000..0b9698d --- /dev/null +++ b/tests/Net.Cache.DynamoDb.ERC20.Tests/Erc20CacheServiceTests.cs @@ -0,0 +1,116 @@ +using Moq; +using Xunit; +using System.Numerics; +using FluentAssertions; +using System.Reflection; +using Net.Web3.EthereumWallet; +using Net.Cache.DynamoDb.ERC20.Rpc; +using System.Collections.Concurrent; +using Net.Cache.DynamoDb.ERC20.DynamoDb; +using Net.Cache.DynamoDb.ERC20.Rpc.Models; +using Net.Cache.DynamoDb.ERC20.DynamoDb.Models; + +namespace Net.Cache.DynamoDb.ERC20.Tests; + +public class Erc20CacheServiceTests +{ + public class GetOrAddAsync + { + [Fact] + public async Task ReturnsEntryFromInMemoryCache_WhenPresent() + { + var dynamoDbClientMock = new Mock(MockBehavior.Strict); + var erc20FactoryMock = new Mock(MockBehavior.Strict); + var service = new Erc20CacheService(dynamoDbClientMock.Object, erc20FactoryMock.Object); + + var hashKey = new HashKey(1, EthereumAddress.ZeroAddress); + var token = new Erc20TokenData(EthereumAddress.ZeroAddress, "Token", "TKN", 18, new BigInteger(1000)); + var entry = new Erc20TokenDynamoDbEntry(hashKey, token); + + var cacheField = typeof(Erc20CacheService).GetField("_inMemoryCache", BindingFlags.NonPublic | BindingFlags.Instance); + var cache = (ConcurrentDictionary)cacheField!.GetValue(service)!; + cache.TryAdd(hashKey.Value, entry); + + var rpcUrlFactoryMock = new Mock>>(MockBehavior.Strict); + var multiCallFactoryMock = new Mock>>(MockBehavior.Strict); + + var result = await service.GetOrAddAsync(hashKey, rpcUrlFactoryMock.Object, multiCallFactoryMock.Object); + + result.Should().BeSameAs(entry); + } + + [Fact] + public async Task ReturnsEntryFromDynamoDb_WhenCacheMisses() + { + var hashKey = new HashKey(1, EthereumAddress.ZeroAddress); + var token = new Erc20TokenData(EthereumAddress.ZeroAddress, "Token", "TKN", 18, new BigInteger(1000)); + var entry = new Erc20TokenDynamoDbEntry(hashKey, token); + + var dynamoDbClientMock = new Mock(MockBehavior.Strict); + dynamoDbClientMock + .Setup(x => x.GetErc20TokenAsync(hashKey, null)) + .ReturnsAsync(entry); + + var erc20FactoryMock = new Mock(MockBehavior.Strict); + var service = new Erc20CacheService(dynamoDbClientMock.Object, erc20FactoryMock.Object); + + var rpcUrlFactoryMock = new Mock>>(MockBehavior.Strict); + var multiCallFactoryMock = new Mock>>(MockBehavior.Strict); + + var result = await service.GetOrAddAsync(hashKey, rpcUrlFactoryMock.Object, multiCallFactoryMock.Object); + + result.Should().BeEquivalentTo(entry); + + var cacheField = typeof(Erc20CacheService).GetField("_inMemoryCache", BindingFlags.NonPublic | BindingFlags.Instance); + var cache = (ConcurrentDictionary)cacheField!.GetValue(service)!; + cache[hashKey.Value].Should().BeEquivalentTo(entry); + + dynamoDbClientMock.Verify(x => x.GetErc20TokenAsync(hashKey, null), Times.Once); + } + + [Fact] + public async Task FetchesFromRpcAndSaves_WhenNotInCacheOrDb() + { + var hashKey = new HashKey(1, EthereumAddress.ZeroAddress); + var token = new Erc20TokenData(EthereumAddress.ZeroAddress, "Token", "TKN", 18, new BigInteger(1000)); + + var dynamoDbClientMock = new Mock(MockBehavior.Strict); + dynamoDbClientMock + .Setup(x => x.GetErc20TokenAsync(hashKey, null)) + .ReturnsAsync((Erc20TokenDynamoDbEntry?)null); + dynamoDbClientMock + .Setup(x => x.SaveErc20TokenAsync(It.Is(e => e.HashKey == hashKey.Value), null)) + .Returns(Task.CompletedTask); + + var erc20ServiceMock = new Mock(MockBehavior.Strict); + erc20ServiceMock + .Setup(x => x.GetErc20TokenAsync(EthereumAddress.ZeroAddress)) + .ReturnsAsync(token); + + var erc20FactoryMock = new Mock(MockBehavior.Strict); + var multiCall = new EthereumAddress("0x0000000000000000000000000000000000000001"); + erc20FactoryMock + .Setup(x => x.Create(It.IsAny(), multiCall)) + .Returns(erc20ServiceMock.Object); + + var service = new Erc20CacheService(dynamoDbClientMock.Object, erc20FactoryMock.Object); + + var rpcUrlFactoryMock = new Mock>>(); + rpcUrlFactoryMock.Setup(f => f()).ReturnsAsync("https://rpc"); + var multiCallFactoryMock = new Mock>>(); + multiCallFactoryMock.Setup(f => f()).ReturnsAsync(multiCall); + + var result = await service.GetOrAddAsync(hashKey, rpcUrlFactoryMock.Object, multiCallFactoryMock.Object); + + result.HashKey.Should().Be(hashKey.Value); + result.Name.Should().Be("Token"); + + dynamoDbClientMock.Verify(x => x.GetErc20TokenAsync(hashKey, null), Times.Once); + dynamoDbClientMock.Verify(x => x.SaveErc20TokenAsync(It.IsAny(), null), Times.Once); + erc20FactoryMock.Verify(x => x.Create(It.IsAny(), multiCall), Times.Once); + erc20ServiceMock.Verify(x => x.GetErc20TokenAsync(EthereumAddress.ZeroAddress), Times.Once); + rpcUrlFactoryMock.Verify(f => f(), Times.Once); + multiCallFactoryMock.Verify(f => f(), Times.Once); + } + } +} \ No newline at end of file From ae6a505f307e80cdaab6b9b23cd6ee10e8c8ce24 Mon Sep 17 00:00:00 2001 From: ArdenHide Date: Thu, 7 Aug 2025 18:54:01 +0300 Subject: [PATCH 18/20] - up coverage - remove `GetErc20Token` from `Erc20Service` --- src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs | 6 ------ src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs | 7 ------- .../DynamoDb/DynamoDbClientTests.cs | 10 ++++++++++ .../Erc20CacheServiceTests.cs | 13 +++++++++++++ 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs index dc77a47..10dd35d 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/ERC20Service.cs @@ -32,12 +32,6 @@ public Erc20Service(IWeb3 web3, EthereumAddress multiCall) _multiCall = multiCall ?? throw new ArgumentNullException(nameof(multiCall)); } - /// - public Erc20TokenData GetErc20Token(EthereumAddress token) - { - return GetErc20TokenAsync(token).GetAwaiter().GetResult(); - } - /// public async Task GetErc20TokenAsync(EthereumAddress token) { diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs index 88eb2e6..b135e59 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs @@ -9,13 +9,6 @@ namespace Net.Cache.DynamoDb.ERC20.Rpc /// public interface IErc20Service { - /// - /// Retrieves token information synchronously. - /// - /// The token contract address. - /// The token data. - public Erc20TokenData GetErc20Token(EthereumAddress token); - /// /// Retrieves token information asynchronously. /// diff --git a/tests/Net.Cache.DynamoDb.ERC20.Tests/DynamoDb/DynamoDbClientTests.cs b/tests/Net.Cache.DynamoDb.ERC20.Tests/DynamoDb/DynamoDbClientTests.cs index e155708..f37b7de 100644 --- a/tests/Net.Cache.DynamoDb.ERC20.Tests/DynamoDb/DynamoDbClientTests.cs +++ b/tests/Net.Cache.DynamoDb.ERC20.Tests/DynamoDb/DynamoDbClientTests.cs @@ -52,6 +52,16 @@ public void ShouldBuildContextFromBuilder() mockBuilder.Verify(b => b.Build(), Times.Once); } + + [Fact] + public void Default() + { + Environment.SetEnvironmentVariable("AWS_REGION", "us-east-1"); + + var dynamoDbClient = new DynamoDbClient(); + + dynamoDbClient.Should().NotBeNull(); + } } public class GetErc20TokenAsync diff --git a/tests/Net.Cache.DynamoDb.ERC20.Tests/Erc20CacheServiceTests.cs b/tests/Net.Cache.DynamoDb.ERC20.Tests/Erc20CacheServiceTests.cs index 0b9698d..cdba996 100644 --- a/tests/Net.Cache.DynamoDb.ERC20.Tests/Erc20CacheServiceTests.cs +++ b/tests/Net.Cache.DynamoDb.ERC20.Tests/Erc20CacheServiceTests.cs @@ -14,6 +14,19 @@ namespace Net.Cache.DynamoDb.ERC20.Tests; public class Erc20CacheServiceTests { + public class Constructor + { + [Fact] + public void Default() + { + Environment.SetEnvironmentVariable("AWS_REGION", "us-east-1"); + + var cacheService = new Erc20CacheService(); + + cacheService.Should().NotBeNull(); + } + } + public class GetOrAddAsync { [Fact] From e7981683951f8d15d4ecf783105c499383a3a39c Mon Sep 17 00:00:00 2001 From: ArdenHide Date: Thu, 7 Aug 2025 18:54:10 +0300 Subject: [PATCH 19/20] - cleanup --- src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs b/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs index b135e59..da771a3 100644 --- a/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs +++ b/src/Net.Cache.DynamoDb.ERC20/RPC/IERC20Service.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; -using Net.Cache.DynamoDb.ERC20.Rpc.Models; using Net.Web3.EthereumWallet; +using Net.Cache.DynamoDb.ERC20.Rpc.Models; namespace Net.Cache.DynamoDb.ERC20.Rpc { From a86023f4c808bc0fd6d6823de72f3f9c0607f50b Mon Sep 17 00:00:00 2001 From: ArdenHide Date: Fri, 8 Aug 2025 11:44:08 +0300 Subject: [PATCH 20/20] - update README.md --- src/Net.Cache.DynamoDb.ERC20/README.md | 36 ++++++++++++++------------ 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/Net.Cache.DynamoDb.ERC20/README.md b/src/Net.Cache.DynamoDb.ERC20/README.md index d1579af..2f50ea0 100644 --- a/src/Net.Cache.DynamoDb.ERC20/README.md +++ b/src/Net.Cache.DynamoDb.ERC20/README.md @@ -9,8 +9,8 @@ It integrates seamlessly with Ethereum blockchain networks to provide efficient - `ERC20 Token Information Caching`: Efficiently caches ERC20 token information, reducing the need for repeated blockchain queries. - `Automatic Key Generation`: Generates unique hash keys for each token based on its chain ID and address, ensuring efficient data retrieval. -- `Flexible Initialization Options`: Supports initialization with custom or default DynamoDB client settings. -- `Update Total Supply`: Offers functionality to update the total supply of tokens in the cache dynamically. +- `Flexible Initialization Options`: Supports initialization with custom or default DynamoDB client and RPC service settings. +- `In-Memory Layer`: Uses an in-memory cache alongside DynamoDB for faster repeated access. - `Integration with Net.Cache.DynamoDb`: Builds upon the robust caching capabilities of Net.Cache.DynamoDb, providing a specific solution for ERC20 tokens. @@ -23,35 +23,39 @@ Then, include Net.Cache.DynamoDb.ERC20 in your project ### Usage -Initialize ERC20CacheProvider +#### Initialize `Erc20CacheService` -You can initialize the `ERC20CacheProvider` using the default constructor or by passing an instance of `ERC20StorageProvider` for more customized settings. +You can initialize the `Erc20CacheService` using the default constructor or by passing custom implementations of `IDynamoDbClient` and `IErc20ServiceFactory`: ```csharp -var erc20CacheProvider = new ERC20CacheProvider(); +var cacheService = new Erc20CacheService(); ``` -Or with a custom storage provider: +Or with custom dependencies: ```csharp -var customContext = new DynamoDBContext(customClient); -var erc20StorageProvider = new ERC20StorageProvider(customContext); -var erc20CacheProvider = new ERC20CacheProvider(erc20StorageProvider); +var context = new DynamoDBContext(customClient); +var dynamoDbClient = new DynamoDbClient(context); +var erc20ServiceFactory = new Erc20ServiceFactory(); +var cacheService = new Erc20CacheService(dynamoDbClient, erc20ServiceFactory); ``` -Caching ERC20 Token Information +#### Caching ERC20 Token Information -To cache ERC20 token information, create a `GetCacheRequest` and use the `GetOrAdd` method of `ERC20CacheProvider`: +To cache ERC20 token information, create a `HashKey` and use the `GetOrAddAsync` method of `Erc20CacheService`: ```csharp -var chainId = BigInteger.Parse("1"); // Ethereum mainnet +var chainId = 1L; // Ethereum mainnet var contractAddress = new EthereumAddress("0x..."); // ERC20 token contract address -var rpcUrl = "https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID"; +var hashKey = new HashKey(chainId, contractAddress); -var request = new GetCacheRequest(chainId, contractAddress, rpcUrl); -var tokenInfo = erc20CacheProvider.GetOrAdd(contractAddress, request); +var tokenInfo = await cacheService.GetOrAddAsync( + hashKey, + rpcUrlFactory: () => Task.FromResult("https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID"), + multiCallFactory: () => Task.FromResult(new EthereumAddress("0x...multicall")) +); Console.WriteLine($"Token Name: {tokenInfo.Name}, Symbol: {tokenInfo.Symbol}"); ``` -This method retrieves the token information from the cache if it exists or fetches it from the blockchain and adds it to the cache otherwise. +This method retrieves the token information from the cache if it exists, or fetches it from the blockchain and stores it in both the DynamoDB table and the in-memory cache otherwise. \ No newline at end of file