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);
+ }
+ }
}
}