diff --git a/Lexplorer/Pages/AccountsOverview.razor b/Lexplorer/Pages/AccountsOverview.razor new file mode 100644 index 0000000..b7c47dd --- /dev/null +++ b/Lexplorer/Pages/AccountsOverview.razor @@ -0,0 +1,128 @@ +@using Lexplorer.Helpers; +@using Lexplorer.Components; +@using System.Diagnostics; +@inject Lexplorer.Services.LoopringGraphQLService LoopringGraphQLService; +@inject NavigationManager NavigationManager; +@inject IAppCache AppCache; +@page "/account"; + +The Lexplorer - Accounts + + + + Latest accounts + + + + + + + + + + + + + + + Id + Type + L1 address + Created At (UTC) + + + @LinkHelper.GetObjectLink(context) + @context.typeName + + @context.createdAtTransaction?.verifiedAt + + + + +@code { + [Parameter] + [SupplyParameterFromQuery] + public string pageNumber { get; set; } = "1"; + + public int gotoPage + { + get + { + return Int32.Parse(pageNumber ?? "1"); + } + set + { + navigateTo(value); + } + } + + public bool isLoading = true; + public readonly int pageSize = 25; + + private IList? accounts { get; set; } = new List(); + + [Parameter] + [SupplyParameterFromQuery] + public string? type { get; set; } + + public string? filterAccounts + { + get + { + return type ?? "All"; + } + set + { + type = (string.Equals(value ?? "All", "All", StringComparison.InvariantCultureIgnoreCase)) ? null : value; + navigateTo(gotoPage); + } + } + + private void navigateTo(int page) + { + string URL = $"/account?pageNumber={page}"; + if (type != null) + URL += $"&type={type}"; + NavigationManager.NavigateTo(URL); + } + + private CancellationTokenSource? cts; + + protected override async Task OnParametersSetAsync() + { + + //cancel any previous OnParametersSetAsync which might still be running + cts?.Cancel(); + + using (CancellationTokenSource localCTS = new CancellationTokenSource()) + { + //give future calls a chance to cancel us; it is now safe to replace + //any previous value of cts, since we already cancelled it above + cts = localCTS; + + try + { + isLoading = true; + + string accountCacheKey = $"transactions-page{gotoPage}-type{type}"; + accounts = await AppCache.GetOrAddAsyncNonNull(accountCacheKey, + async () => await LoopringGraphQLService.GetAccounts((gotoPage - 1) * pageSize, pageSize, type, localCTS.Token), + DateTimeOffset.UtcNow.AddMinutes(10)); + isLoading = false; + StateHasChanged(); + } + catch (OperationCanceledException) + { + } + catch (Exception e) + { + Trace.WriteLine(e.StackTrace + "\n" + e.Message); + } + //now for cleanup, we must clear cts, but only if it is still our localCTS, which we're about to dispose + //otherwise a new call has already replaced cts with it's own localCTS + Interlocked.CompareExchange(ref cts, null, localCTS); + } + } + +} + diff --git a/Shared/Services/LoopringGraphQLService.cs b/Shared/Services/LoopringGraphQLService.cs index 90db293..20ae7e7 100644 --- a/Shared/Services/LoopringGraphQLService.cs +++ b/Shared/Services/LoopringGraphQLService.cs @@ -468,6 +468,70 @@ query transactions( return null; } } + public async Task?> GetAccounts(int skip, int first, string? typeName = null, CancellationToken cancellationToken = default) + { + var accountsQuery = @" + query accounts( + $skip: Int + $first: Int + $orderBy: Account_orderBy + $orderDirection: OrderDirection + ) { + accounts( + skip: $skip + first: $first + orderBy: $orderBy + orderDirection: $orderDirection + ) { + ...AccountFragment + ...PoolFragment + ...UserFragment + } + }" + + GraphQLFragments.AccountFragment + + GraphQLFragments.TokenFragment + + GraphQLFragments.PoolFragment + + GraphQLFragments.UserFragment + + GraphQLFragments.AccountCreatedAtFragment; + + var request = new RestRequest(); + request.AddHeader("Content-Type", "application/json"); + + //since there is no way to filter for typename in Accounts_filter, we simply + //query a different entity, i.e. pools instead of accounts + string typePluralName = "accounts"; + if (!String.IsNullOrEmpty(typeName)) + { + //lower case, but first char only + typePluralName = Char.ToLower(typeName[0]) + typeName.Substring(1) + "s"; + accountsQuery = accountsQuery.Replace("accounts(", $"{typePluralName}("); + } + + request.AddJsonBody(new + { + query = accountsQuery, + variables = new + { + skip = skip, + first = first, + orderBy = "internalID", + orderDirection = "desc" + } + }); + + try + { + var response = await _client.PostAsync(request, cancellationToken); + JObject jresponse = JObject.Parse(response.Content!); + JToken? token = jresponse["data"]![typePluralName]; + return token!.ToObject>(); + } + catch (Exception ex) + { + Debug.WriteLine(ex.Message); + return null; + } + } public async Task GetAccount(string accountId, CancellationToken cancellationToken = default) { var accountQuery = @" diff --git a/xUnitTests/LoopringGraphTests/TestAccount.cs b/xUnitTests/LoopringGraphTests/TestAccount.cs index 5357fa7..4fd8600 100644 --- a/xUnitTests/LoopringGraphTests/TestAccount.cs +++ b/xUnitTests/LoopringGraphTests/TestAccount.cs @@ -51,5 +51,26 @@ public async void GetAccountNFTSlots() Assert.NotNull(slot.nft); } } + + [Fact] + public async void GetAccounts() + { + IList? accounts = await service.GetAccounts(0, 10); + Assert.NotEmpty(accounts); + foreach (var account in accounts!) + { + Assert.NotNull(account); + } + } + [Fact] + public async void GetPools() + { + IList? accounts = await service.GetAccounts(0, 10, "Pool"); + Assert.NotEmpty(accounts); + foreach (var account in accounts!) + { + Assert.IsType(account); + } + } } }