diff --git a/README.md b/README.md index bce3a04..f83f984 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,8 @@ Kadena Cabinet is a Community Advisory Board (CAB) where KDA token holders come - **Chainweb Node Instance:** - An instance of the [Chainweb Node](https://github.com/kadena-io/chainweb-node) with the `allowReadsInLocal: true` parameter set. -- **One of the Following:** - - A [Kadena GraphQL](https://github.com/kadena-community/kadena.js/tree/main/packages/apps/graph) server. - - Access to the `events` endpoint from [Chainweb Data](https://github.com/kadena-io/chainweb-data). - - SQL access to the PostgreSQL database from [Chainweb Data](https://github.com/kadena-io/chainweb-data). - - +- A **[Kadena GraphQL](https://github.com/kadena-community/kadena.js/tree/main/packages/apps/graph)** server + ## Project Structure - `pact/`: Smart contracts and related files for on-chain governance. diff --git a/backend/API/Interfaces/IBondService.cs b/backend/API/Interfaces/IBondService.cs index 79373ba..ef40425 100644 --- a/backend/API/Interfaces/IBondService.cs +++ b/backend/API/Interfaces/IBondService.cs @@ -9,6 +9,7 @@ public interface IBondService Task> GetAllLockups(bool ignoreCache = false); Task> GetAllLockupEvents(bool ignoreCache = false); Task> GetAllClaimEvents(bool ignoreCache = false); + Task> GetAllVoteEvents(bool ignoreCache = false); Task GetLockup(string lockupId, bool ignoreCache = false); Task> GetAccountLockups(string account, bool ignoreCache = false); Task> GetAllLockupsFromBond(string bondId, bool ignoreCache = false); diff --git a/backend/API/Interfaces/IChainwebDataRetriever.cs b/backend/API/Interfaces/IChainwebDataRetriever.cs deleted file mode 100644 index 4573389..0000000 --- a/backend/API/Interfaces/IChainwebDataRetriever.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Dab.API.Models.Events; - -namespace Dab.API.Interfaces; - -public interface IChainwebDataRetriever -{ - Task> RetrieveLockData(); - Task> RetrieveClaimData(); -} diff --git a/backend/API/Models/BondDashboard.cs b/backend/API/Models/BondDashboard.cs index 00a2c84..f855ade 100644 --- a/backend/API/Models/BondDashboard.cs +++ b/backend/API/Models/BondDashboard.cs @@ -1,3 +1,5 @@ +using Dab.API.Models.Events; + namespace Dab.API.Models.Dashboard; public class LockDTO @@ -17,6 +19,17 @@ public LockDTO(EventDTO evt) RequestKey = evt.RequestKey; LockTime = DateTime.TryParse(evt.BlockTime, out var lockTime) ? lockTime : default; } + + public LockDTO(LockEvent evt) + { + BondId = evt.BondId; + Account = evt.Account; + LockedAmount = evt.Amount; + MaxRewards = evt.Rewards; + RequestKey = evt.RequestKey; + LockTime = evt.Timestamp; + } + } public class ClaimDTO @@ -36,6 +49,16 @@ public ClaimDTO(EventDTO evt) RequestKey = evt.RequestKey; ClaimTime = DateTime.TryParse(evt.BlockTime, out var lockTime) ? lockTime : default; } + public ClaimDTO(ClaimEvent evt) + { + BondId = evt.BondId; + Account = evt.Account; + Amount = evt.OriginalAmount; + TotalAmount = evt.TotalAmount; + RequestKey = evt.RequestKey; + ClaimTime = evt.Timestamp; + } + } public class PollVoteEventDTO @@ -47,12 +70,21 @@ public class PollVoteEventDTO public string RequestKey { get; set; } = ""; public PollVoteEventDTO(EventDTO evt) { - PollId = evt.Params[0]?.ToString(); - Account = evt.Params[1]?.ToString(); + PollId = evt.Params[1]?.ToString(); + Account = evt.Params[0]?.ToString(); Action = evt.Params[2]?.ToString(); RequestKey = evt.RequestKey; VoteTime = DateTime.TryParse(evt.BlockTime, out var lockTime) ? lockTime : default; } + public PollVoteEventDTO(VoteEvent evt) + { + PollId = evt.PollId; + Account = evt.Account; + Action = evt.Action; + RequestKey = evt.RequestKey; + VoteTime = evt.Timestamp; + } + } @@ -84,6 +116,7 @@ public class BondDashboard public List LatestLocks { get; set; } = new(); public List LatestClaims { get; set; } = new(); public List LatestVotes { get; set; } = new(); + public bool ActivePool {get; set;} } public class VoteDistributionDTO diff --git a/backend/API/Models/Bonder.cs b/backend/API/Models/Bonder.cs index 2a8f269..57b0fd3 100644 --- a/backend/API/Models/Bonder.cs +++ b/backend/API/Models/Bonder.cs @@ -4,8 +4,6 @@ namespace Dab.API.Models.Bonder; -//FIXME these types require more testing (not handled {decimal:}) - public class LockupOption { [JsonPropertyName("option-name")] diff --git a/backend/API/Models/Cache.cs b/backend/API/Models/Cache.cs index 5903c01..6837a01 100644 --- a/backend/API/Models/Cache.cs +++ b/backend/API/Models/Cache.cs @@ -77,6 +77,8 @@ public class CacheKeys public static string AllVotes() => $"all-votes"; + public static string AllVoteEvents() => $"all-vote-events"; + public static string ApiAnalytics() => $"api-dashboard"; } } diff --git a/backend/API/Models/Events.cs b/backend/API/Models/Events.cs index 6209562..722c982 100644 --- a/backend/API/Models/Events.cs +++ b/backend/API/Models/Events.cs @@ -37,3 +37,12 @@ public class ClaimEvent : IBondEvent public decimal Amount => TotalAmount; } + +public class VoteEvent +{ + public string Account { get; set; } = ""; + public string PollId { get; set; } = ""; + public string Action { get; set; } = ""; + public DateTime Timestamp { get; set; } + public string RequestKey { get; set; } = ""; +} diff --git a/backend/API/Program.cs b/backend/API/Program.cs index 5e32f9b..6c2ed76 100644 --- a/backend/API/Program.cs +++ b/backend/API/Program.cs @@ -98,7 +98,6 @@ // Business logic services // builder.Services.AddSingleton(); builder.Services.AddSingleton(); -builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/backend/API/Services/AnalyticsService.cs b/backend/API/Services/AnalyticsService.cs index 9d625c7..9d2833e 100644 --- a/backend/API/Services/AnalyticsService.cs +++ b/backend/API/Services/AnalyticsService.cs @@ -68,13 +68,9 @@ private async Task> CacheEventSearch(string evt, bool ignoreCache public async Task> GetLatestLocks(bool ignoreCache = false) { - var lockupsInContract = await _bondService.GetAllLockups(); - if (!lockupsInContract.Any()) return new List(); + var lockupsInContract = await _bondService.GetAllLockupEvents(); - var evt = $"{ns}.bonder.LOCK"; - var query = await CacheEventSearch(evt, ignoreCache); - - return query + return lockupsInContract .Select(evt => new LockDTO(evt)) .OrderByDescending(dto => dto.LockTime) .Take(3) @@ -83,13 +79,9 @@ public async Task> GetLatestLocks(bool ignoreCache = false) public async Task> GetLatestClaims(bool ignoreCache = false) { - var lockupsInContract = await _bondService.GetAllLockups(); - if (!lockupsInContract.Any()) return new List(); - - var evt = $"{ns}.bonder.CLAIM"; - var query = await CacheEventSearch(evt, ignoreCache); + var claimsInContract = await _bondService.GetAllClaimEvents(); - return query + return claimsInContract .Select(evt => new ClaimDTO(evt)) .OrderByDescending(dto => dto.ClaimTime) .Take(3) @@ -98,13 +90,9 @@ public async Task> GetLatestClaims(bool ignoreCache = false) public async Task> GetLatestVotes(bool ignoreCache = false) { - var lockupsInContract = await _bondService.GetAllLockups(); - if (!lockupsInContract.Any()) return new List(); - - var evt = $"{ns}.poller.VOTE"; - var query = await CacheEventSearch(evt, ignoreCache); + var votesInContract = await _bondService.GetAllVoteEvents(); - return query + return votesInContract .Select(evt => new PollVoteEventDTO(evt)) .OrderByDescending(dto => dto.VoteTime) .Take(3) @@ -240,7 +228,8 @@ public async Task GetApiAnalytics(bool ignoreCache = false) MostVotedPoll = await SafeExecute(async () => await GetMostVotedPoll(ignoreCache), ""), AverageLockup = await SafeExecute(async () => await GetAverageLockup(ignoreCache), ""), MaxReturnRate = await SafeExecute(async () => await GetMaxReturnRate(ignoreCache), 0m), - TotalLockedAmount = await SafeExecute(async () => (await _bondService.GetAllLockupEvents(ignoreCache)).Sum(x => x.Amount + x.Rewards), 0m) + TotalLockedAmount = await SafeExecute(async () => (await _bondService.GetAllLockupEvents(ignoreCache)).Sum(x => x.Amount + x.Rewards), 0m), + ActivePool = await SafeExecute(async () => (await IsPollActive(ignoreCache)), false) }; var jsonResult = Utils.JsonPrettify(ret); @@ -268,6 +257,19 @@ private async Task SafeExecute(Func> func, T defaultValue) } } + private async Task IsPollActive(bool ignoreCache = false) + { + + try + { + var pools = await _pollService.GetActivePolls(ignoreCache); + return pools.Any(); + } + catch + { + return false; + } + } private async Task GetGlobalGivenRewards(bool ignoreCache = false) { diff --git a/backend/API/Services/BondService.cs b/backend/API/Services/BondService.cs index a3de5dd..c204a3d 100644 --- a/backend/API/Services/BondService.cs +++ b/backend/API/Services/BondService.cs @@ -13,19 +13,17 @@ public class BondService : IBondService private readonly ILogger _logger; private readonly ICacheService _cacheService; private readonly IPactService _pactService; - private readonly IChainwebDataRetriever _dataRetriever; private readonly ChainwebGraphQLRetriever _graphRetriever; private readonly DabContractConfig _dabConfig; private readonly string chain; private readonly string ns; private readonly int expirySeconds = 20; - public BondService(ILogger logger, ICacheService cacheService, IPactService pactService, IConfiguration configuration, IChainwebDataRetriever dataRetriever, ChainwebGraphQLRetriever graphRetriever) + public BondService(ILogger logger, ICacheService cacheService, IPactService pactService, IConfiguration configuration, ChainwebGraphQLRetriever graphRetriever) { _logger = logger; _cacheService = cacheService; _pactService = pactService; - _dataRetriever = dataRetriever; _graphRetriever = graphRetriever; _dabConfig = (configuration.GetSection("DabContractConfig").Get() ?? @@ -34,47 +32,55 @@ public BondService(ILogger logger, ICacheService cacheService, IPac ns = _dabConfig.Namespace; } -public async Task> GetAllLockupEvents(bool ignoreCache = false) -{ - // var locksInContract = await GetAllLockups(ignoreCache); + public async Task> GetAllLockupEvents(bool ignoreCache = false) + { + var cacheKey = CacheKeys.AllLockupEvents(); + + if (!ignoreCache && await _cacheService.HasItem(cacheKey)) + { + var cached = await _cacheService.GetItem(cacheKey); + return JsonSerializer.Deserialize>(cached) ?? new(); + } + + var lockEvents = (await _graphRetriever.RetrieveLockData()) + .GroupBy(e => e.RequestKey) + .Select(g => g.First()) + .ToList(); - // if (!locksInContract.Any()) return new List(); + var ret = Utils.JsonPrettify(lockEvents); - var cacheKey = CacheKeys.AllLockupEvents(); + await _cacheService.SetItem(cacheKey, ret, 30 * expirySeconds); + return lockEvents; + } - if (!ignoreCache && await _cacheService.HasItem(cacheKey)) + public async Task> GetAllVoteEvents(bool ignoreCache = false) { - var cached = await _cacheService.GetItem(cacheKey); - return JsonSerializer.Deserialize>(cached) ?? new(); + var cacheKey = CacheKeys.AllVoteEvents(); + + if (!ignoreCache && await _cacheService.HasItem(cacheKey)) + { + var cached = await _cacheService.GetItem(cacheKey); + return JsonSerializer.Deserialize>(cached) ?? new(); + } + + var voteEvents = (await _graphRetriever.RetrieveVoteData()) + .GroupBy(e => e.RequestKey) + .Select(g => g.First()) + .ToList(); + + + var ret = Utils.JsonPrettify(voteEvents); + + await _cacheService.SetItem(cacheKey, ret, 30 * expirySeconds); + return voteEvents; } - // Temporarily commented out - var lockEvents = await _graphRetriever.RetrieveLockData(); - - // var lockEvents = locksInContract.Select(lockup => new LockEvent - // { - // BondId = lockup.BondId, - // Account = lockup.Account, - // Amount = lockup.KdaLocked, - // Rewards = lockup.MaxKdaRewards, - // LockupLength = lockup.LockupOption.OptionLength, - // Timestamp = lockup.LockupStartTime.Date, - // RequestKey = "", //lockup.LockupId, - // Type = "Lock" - // }).ToList(); - - var ret = Utils.JsonPrettify(lockEvents); - - await _cacheService.SetItem(cacheKey, ret, 30 * expirySeconds); - return lockEvents; -} public async Task> GetAllClaimEvents(bool ignoreCache = false) { - var cacheKey = CacheKeys.AllClaimEvents(); if (!ignoreCache && await _cacheService.HasItem(cacheKey)) @@ -83,7 +89,11 @@ public async Task> GetAllClaimEvents(bool ignoreCache = false) return JsonSerializer.Deserialize>(cached) ?? new(); } - var claimEvents = await _graphRetriever.RetrieveClaimData(); + var claimEvents = (await _graphRetriever.RetrieveClaimData()) + .GroupBy(e => e.RequestKey) + .Select(g => g.First()) + .ToList(); + var ret = Utils.JsonPrettify(claimEvents); diff --git a/backend/API/Services/DataRetriever.cs b/backend/API/Services/DataRetriever.cs deleted file mode 100644 index 5f693ac..0000000 --- a/backend/API/Services/DataRetriever.cs +++ /dev/null @@ -1,270 +0,0 @@ -using Npgsql; -using Dab.API.Interfaces; -using Dab.API.Models.Events; -using System.Text.Json; -using Dab.API.Models.Cache; -using Dab.API.Models.Dashboard; - -namespace Dab.API.Services -{ - public class ChainwebDataRetriever : IChainwebDataRetriever - { - private readonly ILogger _logger; - private readonly ICacheService _cacheService; - private readonly string _connectionString; - private readonly string _namespace; - private NpgsqlDataSource? dataSource; - private readonly string ns; - private readonly string chainwebDataUrl; - private readonly int expirySeconds = 120; - - public ChainwebDataRetriever(IConfiguration configuration, ILogger logger, ICacheService cacheService) - { - _logger = logger; - _connectionString = configuration.GetConnectionString("DefaultConnection"); - _namespace = configuration.GetSection("DabContractConfig").GetValue("Namespace") ?? throw new Exception("Namespace not defined in configuration."); - _cacheService = cacheService; - chainwebDataUrl = configuration.GetSection("DabContractConfig").GetValue("ChainwebDataUrl") ?? throw new Exception("Chainweb Data URL not defined in configuration."); - ns = _namespace; - } - - private void OpenSqlConnection() - { - if (!string.IsNullOrEmpty(_connectionString)) - { - dataSource = NpgsqlDataSource.Create(_connectionString); - } - } - - public async Task> RetrieveLockData() - { - if (!string.IsNullOrEmpty(_connectionString)) - { - try - { - _logger.LogInformation($"Retrieving LOCK event data from SQL via \n {_connectionString}"); - - OpenSqlConnection(); - var sql = $@"SELECT params ->> 0 as ""BondId"", - params ->> 1 as ""Account"", - coalesce(params -> 2 ->> 'decimal', params ->> 2) as ""Amount"", - coalesce(params -> 3 ->> 'decimal', params ->> 3) as ""Rewards"", - coalesce(params -> 4 ->> 'int', params ->> 4) as ""LockupLength"", - replace(translate(to_char(creationtime, 'YYYY-MM-DD HH:MI:SS.MSOF'), ' ', 'T'), '+00', 'Z') as ""Timestamp"", - requestkey as ""RequestKey"" - FROM events as a - JOIN blocks as b - on a.block = b.hash - WHERE a.qualname = '{_namespace}.bonder.LOCK' - ORDER BY creationtime ASC;"; - - await using var cmd = dataSource.CreateCommand(sql); - await using var rdr = await cmd.ExecuteReaderAsync(); - - var list = new List(); - - while (await rdr.ReadAsync()) - { - var lockEvent = new LockEvent - { - BondId = rdr.GetString(0), - Account = rdr.GetString(1), - Amount = decimal.Parse(rdr.GetString(2)), - Rewards = decimal.Parse(rdr.GetString(3)), - LockupLength = decimal.Parse(rdr.GetString(4)), - Timestamp = DateTime.Parse(rdr.GetString(5)), - RequestKey = rdr.GetString(6) - }; - - list.Add(lockEvent); - } - - return list; - } - catch (Exception ex) - { - _logger.LogError(ex, "SQL retrieval failed, falling back to ChainwebData API"); - } - } - - // Fallback to ChainwebData API - return new(); // await GetAllLocks(); - } - - public async Task> RetrieveClaimData() - { - if (!string.IsNullOrEmpty(_connectionString)) - { - try - { - _logger.LogInformation("Retrieving CLAIM event data from SQL"); - - OpenSqlConnection(); - var sql = $@"SELECT params ->> 0 as ""BondId"", - params ->> 1 as ""Account"", - coalesce(params -> 2 ->> 'decimal', params ->> 2) as ""OriginalAmount"", - coalesce(params -> 3 ->> 'decimal', params ->> 3) as ""TotalAmount"", - replace(translate(to_char(creationtime, 'YYYY-MM-DD HH:MI:SS.MSOF'), ' ', 'T'), '+00', 'Z') as ""Timestamp"", - requestkey as ""RequestKey"" - FROM events as a - JOIN blocks as b - on a.block = b.hash - WHERE a.qualname = '{_namespace}.bonder.CLAIM' - ORDER BY creationtime ASC;"; - - await using var cmd = dataSource.CreateCommand(sql); - await using var rdr = await cmd.ExecuteReaderAsync(); - - var list = new List(); - - while (await rdr.ReadAsync()) - { - _logger.LogInformation(rdr.ToString()); - var claimEvent = new ClaimEvent - { - BondId = rdr.GetString(0), - Account = rdr.GetString(1), - OriginalAmount = decimal.Parse(rdr.GetString(2)), - TotalAmount = decimal.Parse(rdr.GetString(3)), - Timestamp = DateTime.Parse(rdr.GetString(4)), - RequestKey = rdr.GetString(5) - }; - - list.Add(claimEvent); - } - - return list; - } - catch (Exception ex) - { - _logger.LogError(ex, "SQL retrieval failed, falling back to ChainwebData API"); - } - } - - // Fallback to ChainwebData API - return await GetAllClaims(); - } - - - private int ParseLockupLength(object lockupLengthParam) - { - if (lockupLengthParam is JsonElement jsonElement && jsonElement.ValueKind == JsonValueKind.Object) - { - // Assuming the "int" field is present in the JSON object - if (jsonElement.TryGetProperty("int", out var intElement)) - { - return intElement.GetInt32(); - } - } - - // Fallback for direct int values or if the object is not in expected format - return int.TryParse(lockupLengthParam?.ToString(), out var result) ? result : 0; - } - - public async Task> GetAllLocks(bool ignoreCache = false) - { - var evt = $"{ns}.bonder.LOCK"; - var query = await CacheEventSearchAll(evt, ignoreCache); - - return query - .Select(e => new LockEvent - { - BondId = e.Params[0]?.ToString() ?? "", - Account = e.Params[1]?.ToString() ?? "", - Amount = decimal.Parse(e.Params[2]?.ToString() ?? "0"), - Rewards = decimal.Parse(e.Params[3]?.ToString() ?? "0"), - LockupLength = ParseLockupLength(e.Params[4]), - Timestamp = DateTime.Parse(e.BlockTime), - RequestKey = e.RequestKey - }) - .OrderByDescending(le => le.Timestamp) - .ToList(); - } - - public async Task> GetAllClaims(bool ignoreCache = false) - { - var evt = $"{ns}.bonder.CLAIM"; - var query = await CacheEventSearchAll(evt, ignoreCache); - - return query - .Select(e => new ClaimEvent - { - BondId = e.Params[0]?.ToString() ?? "", - Account = e.Params[1]?.ToString() ?? "", - OriginalAmount = decimal.Parse(e.Params[2]?.ToString() ?? "0"), - TotalAmount = decimal.Parse(e.Params[3]?.ToString() ?? "0"), - Timestamp = DateTime.Parse(e.BlockTime), - RequestKey = e.RequestKey - }) - .OrderByDescending(ce => ce.Timestamp) - .ToList(); - } - - private async Task> EventSearchAll(string evt) - { - List allResults = new(); - string nextToken = null; - - do - { - var result = await EventSearchWithToken(evt, nextToken); - - // Add the items to the allResults list if there are any - if (result.Items != null && result.Items.Count > 0) - { - allResults.AddRange(result.Items); - } - - if (!result.Items.Any()) break; - - // Update nextToken to determine if the loop should continue - nextToken = result.NextToken ?? String.Empty; - - } while (!string.IsNullOrEmpty(nextToken)); // Continue if nextToken is not null or empty - - return allResults; - } - - private async Task<(List Items, string? NextToken)> EventSearchWithToken(string evt, string nextToken = null) - { - var queryUri = $"{chainwebDataUrl}/txs/events?search={evt}"; - if (!string.IsNullOrEmpty(nextToken)) - { - queryUri += $"&next={nextToken}"; - } - - _logger.LogDebug($"Querying {queryUri} for {evt}"); - using (HttpClient client = new()) - { - using var request = new HttpRequestMessage(HttpMethod.Get, queryUri); - using var response = await client.SendAsync(request); - response.EnsureSuccessStatusCode(); - - var events = await response.Content.ReadFromJsonAsync>(); - var next = response.Headers.Contains("Chainweb-Next") - ? response.Headers.GetValues("Chainweb-Next").FirstOrDefault() - : null; - - return (events ?? new List(), next); - } - } - - private async Task> CacheEventSearchAll(string evt, bool ignoreCache = false) - { - var cacheKey = CacheKeys.EventSearch(evt); - if (!ignoreCache && await _cacheService.HasItem(cacheKey)) - { - var cached = await _cacheService.GetItem(cacheKey); - return JsonSerializer.Deserialize>(cached) ?? new(); - } - - var evs = await EventSearchAll(evt); - var ret = Utils.JsonPrettify(evs); - - _logger.LogDebug($"Event Search: {ret}"); - - await _cacheService.SetItem(cacheKey, ret, expirySeconds); - return evs; - } - } -} diff --git a/backend/API/Services/GraphQLRetriever.cs b/backend/API/Services/GraphQLRetriever.cs index b1d9292..b046b90 100644 --- a/backend/API/Services/GraphQLRetriever.cs +++ b/backend/API/Services/GraphQLRetriever.cs @@ -5,7 +5,7 @@ namespace Dab.API.Services { - public class ChainwebGraphQLRetriever : IChainwebDataRetriever + public class ChainwebGraphQLRetriever { private readonly ILogger _logger; private readonly ICacheService _cacheService; @@ -21,9 +21,9 @@ public ChainwebGraphQLRetriever(IConfiguration configuration, ILogger("GraphQLEndpoint") ?? "https://graph.kadena.network/graphql"; } - public async Task> RetrieveLockData() + public async Task> RetrieveEventDataAsync(string qualifiedEventName, Func parseFunction) { - var allLockEvents = new List(); + var allEvents = new List(); string endCursor = null; bool hasNextPage = true; @@ -31,12 +31,12 @@ public async Task> RetrieveLockData() { try { - _logger.LogInformation("Retrieving LOCK event data via GraphQL"); + _logger.LogInformation($"Retrieving {qualifiedEventName} event data via GraphQL"); var query = $@" - query LockEventSearch($after: String) {{ + query EventSearch($after: String) {{ events( - qualifiedEventName: ""{_namespace}.bonder.LOCK"" + qualifiedEventName: ""{qualifiedEventName}"" first: 1000 after: $after ) {{ @@ -59,19 +59,27 @@ query LockEventSearch($after: String) {{ var variables = new { after = endCursor }; - var graphQLResponse = await ExecuteGraphQLQueryAsync(query, "LockEventSearch", variables); + var graphQLResponse = await ExecuteGraphQLQueryAsync(query, "EventSearch", variables); - var lockEvents = ParseLockEvents(graphQLResponse); - - allLockEvents.AddRange(lockEvents); - - // Update pagination info + // Parse events using the provided parse function if (graphQLResponse.TryGetProperty("data", out var dataElement) && dataElement.TryGetProperty("events", out var eventsElement) && - eventsElement.TryGetProperty("pageInfo", out var pageInfoElement)) + eventsElement.TryGetProperty("edges", out var edgesElement)) { - hasNextPage = pageInfoElement.GetProperty("hasNextPage").GetBoolean(); - endCursor = pageInfoElement.GetProperty("endCursor").GetString(); + foreach (var edge in edgesElement.EnumerateArray()) + { + if (edge.TryGetProperty("node", out var nodeElement)) + { + var parsedEvent = parseFunction(nodeElement); + allEvents.Add(parsedEvent); + } + } + + if (eventsElement.TryGetProperty("pageInfo", out var pageInfoElement)) + { + hasNextPage = pageInfoElement.GetProperty("hasNextPage").GetBoolean(); + endCursor = pageInfoElement.GetProperty("endCursor").GetString(); + } } else { @@ -85,84 +93,26 @@ query LockEventSearch($after: String) {{ } } - return allLockEvents; + return allEvents; } - public async Task> RetrieveClaimData() - { - var allClaimEvents = new List(); - string endCursor = null; - bool hasNextPage = true; - - while (hasNextPage) - { - try - { - _logger.LogInformation("Retrieving CLAIM event data via GraphQL"); + public async Task> RetrieveLockData() => + await RetrieveEventDataAsync($"{_namespace}.bonder.LOCK", ParseLockEvent); - var query = $@" - query ClaimEventSearch($after: String) {{ - events( - qualifiedEventName: ""{_namespace}.bonder.CLAIM"" - first: 1000 - after: $after - ) {{ - edges {{ - cursor - node {{ - parameters - requestKey - block {{ - creationTime - }} - }} - }} - pageInfo {{ - hasNextPage - endCursor - }} - }} - }}"; - - var variables = new { after = endCursor }; - - var graphQLResponse = await ExecuteGraphQLQueryAsync(query, "ClaimEventSearch", variables); - - var claimEvents = ParseClaimEvents(graphQLResponse); - - allClaimEvents.AddRange(claimEvents); - - // Update pagination info - if (graphQLResponse.TryGetProperty("data", out var dataElement) && - dataElement.TryGetProperty("events", out var eventsElement) && - eventsElement.TryGetProperty("pageInfo", out var pageInfoElement)) - { - hasNextPage = pageInfoElement.GetProperty("hasNextPage").GetBoolean(); - endCursor = pageInfoElement.GetProperty("endCursor").GetString(); - } - else - { - hasNextPage = false; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "GraphQL retrieval failed"); - break; - } - } + public async Task> RetrieveClaimData() => + await RetrieveEventDataAsync($"{_namespace}.bonder.CLAIM", ParseClaimEvent); - return allClaimEvents; - } + public async Task> RetrieveVoteData() => + await RetrieveEventDataAsync($"{_namespace}.poller.VOTE", ParseVoteEvent); private async Task ExecuteGraphQLQueryAsync(string query, string operationName, object variables) { using var client = new HttpClient(); var requestBody = new { - query = query, - operationName = operationName, - variables = variables, + query, + operationName, + variables, extensions = new { } }; @@ -180,82 +130,47 @@ private async Task ExecuteGraphQLQueryAsync(string query, string op using var jsonDocument = JsonDocument.Parse(responseString); - return jsonDocument.RootElement.Clone(); // Clone to prevent disposal issues + return jsonDocument.RootElement.Clone(); } - private List ParseLockEvents(JsonElement root) - { - var lockEvents = new List(); - - if (root.TryGetProperty("data", out var dataElement) && - dataElement.TryGetProperty("events", out var eventsElement) && - eventsElement.TryGetProperty("edges", out var edgesElement)) + private LockEvent ParseLockEvent(JsonElement nodeElement) => + ParseEvent(nodeElement, parameters => new LockEvent { - foreach (var edge in edgesElement.EnumerateArray()) - { - if (edge.TryGetProperty("node", out var nodeElement)) - { - var parametersString = nodeElement.GetProperty("parameters").GetString(); - var parametersJson = JsonSerializer.Deserialize(parametersString); - - var lockEvent = new LockEvent - { - BondId = parametersJson.Length > 0 ? parametersJson[0].GetString() : "", - Account = parametersJson.Length > 1 ? parametersJson[1].GetString() : "", - Amount = parametersJson.Length > 2 ? decimal.Parse(parametersJson[2].GetRawText()) : 0, - Rewards = parametersJson.Length > 3 ? decimal.Parse(parametersJson[3].GetRawText()) : 0, - LockupLength = parametersJson.Length > 4 ? ParseLockupLength(parametersJson[4]) : 0, - Timestamp = DateTime.Parse(nodeElement.GetProperty("block").GetProperty("creationTime").GetString()), - RequestKey = nodeElement.GetProperty("requestKey").GetString() ?? "" - }; - - lockEvents.Add(lockEvent); - } - } - } - else + BondId = parameters.Length > 0 ? parameters[0].GetString() : "", + Account = parameters.Length > 1 ? parameters[1].GetString() : "", + Amount = parameters.Length > 2 ? decimal.Parse(parameters[2].GetRawText()) : 0, + Rewards = parameters.Length > 3 ? decimal.Parse(parameters[3].GetRawText()) : 0, + LockupLength = parameters.Length > 4 ? ParseLockupLength(parameters[4]) : 0, + Timestamp = DateTime.Parse(nodeElement.GetProperty("block").GetProperty("creationTime").GetString()), + RequestKey = nodeElement.GetProperty("requestKey").GetString() ?? "" + }); + + private ClaimEvent ParseClaimEvent(JsonElement nodeElement) => + ParseEvent(nodeElement, parameters => new ClaimEvent { - _logger.LogError("Unexpected JSON structure in ParseLockEvents."); - } - - return lockEvents; - } - - private List ParseClaimEvents(JsonElement root) - { - var claimEvents = new List(); - - if (root.TryGetProperty("data", out var dataElement) && - dataElement.TryGetProperty("events", out var eventsElement) && - eventsElement.TryGetProperty("edges", out var edgesElement)) + BondId = parameters.Length > 0 ? parameters[0].GetString() : "", + Account = parameters.Length > 1 ? parameters[1].GetString() : "", + OriginalAmount = parameters.Length > 2 ? decimal.Parse(parameters[2].GetRawText()) : 0, + TotalAmount = parameters.Length > 3 ? decimal.Parse(parameters[3].GetRawText()) : 0, + Timestamp = DateTime.Parse(nodeElement.GetProperty("block").GetProperty("creationTime").GetString()), + RequestKey = nodeElement.GetProperty("requestKey").GetString() ?? "" + }); + + private VoteEvent ParseVoteEvent(JsonElement nodeElement) => + ParseEvent(nodeElement, parameters => new VoteEvent { - foreach (var edge in edgesElement.EnumerateArray()) - { - if (edge.TryGetProperty("node", out var nodeElement)) - { - var parametersString = nodeElement.GetProperty("parameters").GetString(); - var parametersJson = JsonSerializer.Deserialize(parametersString); - - var claimEvent = new ClaimEvent - { - BondId = parametersJson.Length > 0 ? parametersJson[0].GetString() : "", - Account = parametersJson.Length > 1 ? parametersJson[1].GetString() : "", - OriginalAmount = parametersJson.Length > 2 ? decimal.Parse(parametersJson[2].GetRawText()) : 0, - TotalAmount = parametersJson.Length > 3 ? decimal.Parse(parametersJson[3].GetRawText()) : 0, - Timestamp = DateTime.Parse(nodeElement.GetProperty("block").GetProperty("creationTime").GetString()), - RequestKey = nodeElement.GetProperty("requestKey").GetString() ?? "" - }; - - claimEvents.Add(claimEvent); - } - } - } - else - { - _logger.LogError("Unexpected JSON structure in ParseClaimEvents."); - } - - return claimEvents; + Account = parameters.Length > 0 ? parameters[0].GetString() : "", + PollId = parameters.Length > 1 ? parameters[1].GetString() : "", + Action = parameters.Length > 2 ? parameters[2].GetString() : "", + Timestamp = DateTime.Parse(nodeElement.GetProperty("block").GetProperty("creationTime").GetString()), + RequestKey = nodeElement.GetProperty("requestKey").GetString() ?? "" + }); + + private T ParseEvent(JsonElement nodeElement, Func createEvent) + { + var parametersString = nodeElement.GetProperty("parameters").GetString(); + var parameters = JsonSerializer.Deserialize(parametersString); + return createEvent(parameters); } private int ParseLockupLength(JsonElement lockupLengthParam) @@ -277,7 +192,5 @@ private int ParseLockupLength(JsonElement lockupLengthParam) return 0; } } - - // Implement other methods from IChainwebDataRetriever as needed } } diff --git a/backend/README.md b/backend/README.md index 4740591..54df1a4 100644 --- a/backend/README.md +++ b/backend/README.md @@ -7,10 +7,7 @@ Cabinet Backend API serves as the middle layer consumed by the Cabinet UI. It is To run the Cabinet Backend API, ensure you have the following: - **Chainweb Node Instance**: An instance of the [Chainweb Node](https://github.com/kadena-io/chainweb-node) with the parameter `allowReadsInLocal: true` set for local reads. -- **One of the Following**: - - A [Kadena GraphQL](https://github.com/kadena-community/kadena.js/tree/main/packages/apps/graph) server. - - Access to the `events` endpoint from [Chainweb Data](https://github.com/kadena-io/chainweb-data). - - SQL access to the PostgreSQL database from [Chainweb Data](https://github.com/kadena-io/chainweb-data). +- A **[Kadena GraphQL](https://github.com/kadena-community/kadena.js/tree/main/packages/apps/graph)** server. - **.NET SDK**: Version 7.0 or 8.0 with all required dependencies installed. - **Redis**: A running Redis instance for caching purposes. - **Docker & Docker Compose**: Alternatively, you can deploy the entire stack using Docker Compose. @@ -45,11 +42,7 @@ To run the Cabinet Backend API, ensure you have the following: "DabContractConfig": { "ContractChain": "0", "Namespace": "n_yournamespace", - "ChainwebDataUrl": "http://127.0.0.1:8080", "GraphQLEndpoint": "https://127.0.0.1:4000/graph" - }, - "ConnectionStrings": { - "DefaultConnection": "Host=127.0.0.1;Port=5432;Database=devnet;Username=devnet;Password=;" # only if you exposed port 5432 } } ``` @@ -81,10 +74,7 @@ To run the Cabinet Backend API, ensure you have the following: - **DabContractConfig**: - `ContractChain`: The specific chain ID for contract interactions. - `Namespace`: Namespace used for the contracts. - - `ChainwebDataUrl`: URL for accessing Chainweb Data API. - `GraphQLEndpoint`: The endpoint for Kadena's GraphQL server. -- **ConnectionStrings**: - - `DefaultConnection`: PostgreSQL connection string for accessing historical data directly from the database. ## Getting Started @@ -114,9 +104,5 @@ Here's a concise breakdown of the folder names and their specific purposes: ## Data Sources -The API fetches and processes blockchain historical data from the following sources: - -1. **Chainweb Data**: Retrieves transaction and event history directly from Chainweb’s API endpoints. -2. **PostgreSQL Database**: Queries historical data stored locally for faster access and caching. -3. **GraphQL**: Pulls data using GraphQL queries from Kadena’s testnet or mainnet. +The API fetches and processes blockchain historical data using **GraphQL** event queries from Kadena’s testnet or mainnet. diff --git a/docker-compose.yaml b/docker-compose.yaml index 2a6489d..e9c1937 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,4 +1,3 @@ -version: "3" services: cache: image: redis diff --git a/web/package.json b/web/package.json index 2be243c..9486757 100644 --- a/web/package.json +++ b/web/package.json @@ -9,28 +9,28 @@ "lint": "next lint" }, "dependencies": { - "@kadena/client": "^1.12.0", + "@kadena/client": "^1.15.0", "@reach/dialog": "^0.18.0", "@react-hook/window-scroll": "^1.3.0", "@rebass/forms": "^4.0.6", - "@reduxjs/toolkit": "^2.2.1", + "@reduxjs/toolkit": "^2.3.0", "@svgr/webpack": "^8.1.0", - "@tailwindcss/typography": "^0.5.12", - "@types/rebass": "^4.0.14", + "@tailwindcss/typography": "^0.5.15", + "@types/rebass": "^4.0.15", "@types/ua-parser-js": "^0.7.39", - "@vercel/analytics": "^1.3.1", + "@vercel/analytics": "^1.3.2", "@walletconnect/client": "^1.8.0", - "@walletconnect/modal": "^2.6.2", + "@walletconnect/modal": "^2.7.0", "@walletconnect/qrcode-modal": "^1.8.0", - "@walletconnect/types": "^2.13.3", - "@walletconnect/utils": "^2.13.3", - "axios": "^1.6.8", + "@walletconnect/types": "^2.17.1", + "@walletconnect/utils": "^2.17.1", + "axios": "^1.7.7", "babel-plugin-styled-components": "^2.1.4", "camelcase-keys": "^9.1.3", "copy-to-clipboard": "^3.3.3", - "date-fns": "^3.6.0", - "lucide-react": "^0.428.0", - "next": "14.1.3", + "date-fns": "^4.1.0", + "lucide-react": "^0.454.0", + "next": "15.0.2", "next-redux-wrapper": "^8.1.0", "next-svg": "^1.0.7", "next-svgr": "^0.0.2", @@ -40,33 +40,32 @@ "react-debounce-input": "^3.3.0", "react-dom": "^18", "react-feather": "^2.0.10", - "react-icons": "^5.2.1", - "react-spring": "^9.7.3", + "react-icons": "^5.3.0", + "react-spring": "^9.7.4", "react-use-gesture": "^9.1.3", "rebass": "^4.0.7", - "recharts": "^2.12.7", - "sharp": "^0.33.4", - "styled-components": "^6.1.8", + "recharts": "^2.13.2", + "sharp": "^0.33.5", + "styled-components": "^6.1.13", "svgo": "^3.2.0", - "tailwind-merge": "^2.4.0", - "ua-parser-js": "^1.0.37", - "web-vitals": "^3.5.2" + "tailwind-merge": "^2.5.4", + "ua-parser-js": "^2.0.0-rc.1" }, "devDependencies": { - "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", - "autoprefixer": "^10.0.1", - "eslint": "^8", - "eslint-config-next": "14.1.3", + "@types/node": "^22.8.5", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "autoprefixer": "^10.4.20", + "eslint": "^9.13.0", + "eslint-config-next": "15.0.2", "pact-lang-api": "^4.3.6", - "postcss": "^8", + "postcss": "^8.4.47", "reac": "^0.0.0", "react": "^18.2.0", "react-redux": "^9.1.0", - "react-router-dom": "^6.22.3", - "tailwindcss": "^3.3.0", - "typescript": "^5", + "react-router-dom": "^6.27.0", + "tailwindcss": "^3.4.14", + "typescript": "^5.6.3", "zustand": "^4.5.2" } } diff --git a/web/src/features/bond/AllBondLockups.tsx b/web/src/features/bond/AllBondLockups.tsx index ed37241..26bc0aa 100644 --- a/web/src/features/bond/AllBondLockups.tsx +++ b/web/src/features/bond/AllBondLockups.tsx @@ -127,7 +127,7 @@ const AllBondLockupsComponent: React.FC<{ bondId: string }> = ({ bondId }) => { Status Amount + MAX rewards Date - {/* Explorer*/} + Explorer {bondLockups.length === 0 ? (
No results found
@@ -161,7 +161,7 @@ const AllBondLockupsComponent: React.FC<{ bondId: string }> = ({ bondId }) => { {new Date(lockup.timestamp).toLocaleString()}

- {/* )) )} diff --git a/web/src/features/bond/BondDetailsModal.tsx b/web/src/features/bond/BondDetailsModal.tsx index 12358fc..a9f70b5 100644 --- a/web/src/features/bond/BondDetailsModal.tsx +++ b/web/src/features/bond/BondDetailsModal.tsx @@ -32,7 +32,10 @@ const BondDetailsModal: React.FC = ({ useEffect(() => { const handleClickOutside = (event: MouseEvent) => { - if (modalRef.current && !modalRef.current.contains(event.target as Node)) { + if ( + modalRef.current && + !modalRef.current.contains(event.target as Node) + ) { onClose(); } }; @@ -53,16 +56,16 @@ const BondDetailsModal: React.FC = ({ } function formatLockupId(id: string) { - const regex = /^LOCKUP_SALE-(\d+)$/; - const match = id.match(regex); + const regex = /^LOCKUP_SALE-(\d+)$/; + const match = id.match(regex); - if (match) { - const number = match[1]; - return `Lockup ${number}`; - } + if (match) { + const number = match[1]; + return `Lockup ${number}`; + } - // Return the original id if it doesn't match the pattern - return id; + // Return the original id if it doesn't match the pattern + return id; } const handleToggle = () => { @@ -81,69 +84,70 @@ const BondDetailsModal: React.FC = ({ alignItems: "center", }} > -
-
-
-

{formatLockupId(bond.bondId)} Details

- -
+
+
+
+

+ {formatLockupId(bond.bondId)} Details +

+ +
-
-
-

Min Amount

-

{formatNumber(bond.minAmount)}

-
-
-

Max Amount

-

{formatNumber(bond.maxAmount)}

-
-
-

Creator

-
-

{shortenKAddress(bond.creator)}

- +
+
+

Min Amount

+

{formatNumber(bond.minAmount)}

+
+
+

Max Amount

+

{formatNumber(bond.maxAmount)}

+
+
+

Creator

+
+

{shortenKAddress(bond.creator)}

+ +
+
+ +
+

Active Cabinet Members

+

{bond.activeBonders}

+
+
+

Total Polls

+

{bond.totalPolls}

+
+
+

Total Rewards

+

{formatNumber(bond.totalRewards)}

+
-
-
-

Active Cabinet Members

-

{bond.activeBonders}

-
-
-

Total Polls

-

{bond.totalPolls}

-
-
-

Total Rewards

-

{formatNumber(bond.totalRewards)}

+
+

Lockup Options

+ +
+
+ + + +
+
+ +
+
+ + +
+
- - - -
-

Lockup Options

- -
-
- -
-
- - - -
-
- - -
- -
-
); }; diff --git a/web/src/features/bond/BondRewardsRadialBarChart.tsx b/web/src/features/bond/BondRewardsRadialBarChart.tsx index b4508a4..e6c7b6f 100644 --- a/web/src/features/bond/BondRewardsRadialBarChart.tsx +++ b/web/src/features/bond/BondRewardsRadialBarChart.tsx @@ -18,8 +18,7 @@ const COLORS = ["#4A9079", "#E41968", "#FF8042"]; interface CustomTooltipProps extends TooltipProps { active?: boolean; payload?: any; - label?: string; -} + label?: string; } const BondRewardsHorizontalBarChart: React.FC<{ bond: any }> = ({ bond }) => { const { totalRewards, lockedRewards, givenRewards } = bond; diff --git a/web/src/features/bond/LockupDensityChart.tsx b/web/src/features/bond/LockupDensityChart.tsx index 0a8b881..8579abf 100644 --- a/web/src/features/bond/LockupDensityChart.tsx +++ b/web/src/features/bond/LockupDensityChart.tsx @@ -23,6 +23,12 @@ const COLORS: string[] = [ "#0B1D2E", ]; + +interface CustomTooltipProps extends TooltipProps { + active?: boolean; + payload?: any; + label?: string; } + const LockupDensityChart: React.FC = () => { const lockupDensity = useAppSelector(selectLockupDensity) || []; //console.log(lockupDensity); @@ -31,17 +37,11 @@ const LockupDensityChart: React.FC = () => { return
Loading...
; } - interface CustomTooltipProps extends TooltipProps { - active?: boolean; - payload?: any; - label?: string; - } - const CustomTooltip: React.FC = ({ - active, - payload, - label, - }) => { + active, + payload, + label, +}) => { if (active && payload && payload.length) { return (
diff --git a/web/src/features/bond/LockupStatusChart.tsx b/web/src/features/bond/LockupStatusChart.tsx index 647abaa..054c2b4 100644 --- a/web/src/features/bond/LockupStatusChart.tsx +++ b/web/src/features/bond/LockupStatusChart.tsx @@ -27,12 +27,11 @@ interface BondLockupDistributionBarChartProps { interface CustomTooltipProps extends TooltipProps { active?: boolean; payload?: any; - label?: string; -} + label?: string; } -const BondLockupDistributionBarChart: React.FC = ({ - bond, -}) => { + const BondLockupDistributionBarChart: React.FC = ({ + bond, + }) => { const { lockedCount, claimedCount } = bond; diff --git a/web/src/features/bond/LockupsOverTimeBarChart.tsx b/web/src/features/bond/LockupsOverTimeBarChart.tsx index 89c5bec..b322cd0 100644 --- a/web/src/features/bond/LockupsOverTimeBarChart.tsx +++ b/web/src/features/bond/LockupsOverTimeBarChart.tsx @@ -5,51 +5,51 @@ import { XAxis, YAxis, Tooltip, - ResponsiveContainer, TooltipProps, + ResponsiveContainer, } from "recharts"; import { useAppSelector } from "@/app/hooks"; import { selectLockupSummaryBar, selectDisplayAmount } from "./bondSlice"; import styles from "@/styles/main.module.css"; import { LockupDailyAmount } from "./types"; -import {AppLoader} from '@/features/components/Loader'; +import { AppLoader } from "@/features/components/Loader"; const COLORS = { amount: "#4A9079", lockupCount: "#E41968", }; - interface CustomTooltipProps extends TooltipProps { active?: boolean; payload?: any; label?: string; } -const CustomTooltip: React.FC = ({ - active, - payload, - label, -}) => { - if (active && payload && payload.length) { - return ( -
-

{`${label} : ${payload[0].value.toLocaleString()} KDA`}

-
- ); - } - return null; -}; - - - const LockupsOverTimeBarChart: React.FC = () => { const lockupSummaryBar = useAppSelector(selectLockupSummaryBar) || []; const displayAmount = useAppSelector(selectDisplayAmount); + const CustomTooltip: React.FC = ({ + active, + payload, + label, + }) => { + if (active && payload && payload.length) { + return ( +
+

{`${label} : ${payload[0].value.toLocaleString()} ${displayAmount ? "KDA" : "lockups"} `}

+
+ ); + } + return null; + }; + if (!lockupSummaryBar) { - return
-; + return ( +
+ +
+ ); } const data = lockupSummaryBar.map((lockup: LockupDailyAmount) => ({ @@ -72,14 +72,20 @@ const LockupsOverTimeBarChart: React.FC = () => { data={sortedData} margin={{ top: 20, right: 30, left: 20, bottom: 20 }} > - } /> { const dispatch = useAppDispatch(); const { account } = useKadenaReact(); - if (!account) return null; const isCoreMember = useAppSelector((state: RootState) => selectIsCoreMember(state), @@ -24,7 +23,7 @@ const CreateBondComponent: React.FC = () => { }, [dispatch, account]); const handleSubmit = useCreateBond(); - + const safeAccount = account ? account.account : ""; const [lockupOptions, setLockupOptions] = useState([]); const [newBond, setNewBond] = useState({ startTime: "", @@ -34,7 +33,7 @@ const CreateBondComponent: React.FC = () => { maxAmount: 100000.0, minAmount: 1000.0, totalRewards: 10000.0, - creator: account?.account, + creator: safeAccount, }); const handleLockupOptionChange = ( @@ -75,7 +74,7 @@ const CreateBondComponent: React.FC = () => { setLockupOptions((prevOptions) => { const newOption: PactLockupOption = { "option-name": "10 minutes", - "option-length": 600, + "option-length": { int: 600 }, // Initialize as an object with "int" "time-multiplier": 1.1, "poller-max-boost": 1.0, "polling-power-multiplier": 1.0, @@ -99,20 +98,22 @@ const CreateBondComponent: React.FC = () => { } }; + if (!account) return; + if (!isCoreMember) { return (

- You must be a core member to create a lockup oportunity. + You must be a core member to create a lockup.

); } return (
-

Create New Lockup Oportunity

+

Create New Lockup

-
-
-
-
-
-