From 62704633abbc631221bfc5d631326127cde7c92e Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Wed, 17 Apr 2024 14:11:15 +0200 Subject: [PATCH 01/63] First setup for Action Page --- src/aoWebWallet/Models/ActionParam.cs | 44 +++++++++ src/aoWebWallet/Pages/ActionPage.razor | 61 ++++++++++++ src/aoWebWallet/Pages/ActionPage.razor.cs | 97 +++++++++++++++++++ .../Components/ActionInputComponent.razor | 71 ++++++++++++++ src/aoWebWallet/Shared/SendTokenDialog.razor | 2 +- 5 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 src/aoWebWallet/Models/ActionParam.cs create mode 100644 src/aoWebWallet/Pages/ActionPage.razor create mode 100644 src/aoWebWallet/Pages/ActionPage.razor.cs create mode 100644 src/aoWebWallet/Shared/Components/ActionInputComponent.razor diff --git a/src/aoWebWallet/Models/ActionParam.cs b/src/aoWebWallet/Models/ActionParam.cs new file mode 100644 index 0000000..98a2f1f --- /dev/null +++ b/src/aoWebWallet/Models/ActionParam.cs @@ -0,0 +1,44 @@ +namespace aoWebWallet.Models +{ + public class AoAction + { + public List Params { get; set; } = new(); + + public ActionParam? Target => Params.Where(x => x.ParamType == ActionParamType.Target).FirstOrDefault(); + public IEnumerable Filled => Params.Where(x => x.ParamType == ActionParamType.Filled); + public IEnumerable AllInputs => Params.Where(x => + x.ParamType != ActionParamType.Filled + && x.ParamType != ActionParamType.Target); + + public string? IsValid() + { + if (Target == null) + return "No Target process specified."; + + return null; + } + } + + public class ActionParam + { + public required string Key { get; set; } + public string? Value { get; set; } + + public string? TokenId { get; set; } + + public ActionParamType ParamType { get; set; } + + } + + public enum ActionParamType + { + None = 0, + Target, + Filled, + Input, + Integer, + Process, + Balance, //Must have balance + Quantity, //Does not care about balance + } +} diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor new file mode 100644 index 0000000..975cfc6 --- /dev/null +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -0,0 +1,61 @@ +@page "/action" +@using aoWebWallet.Models +@inherits MvvmComponentBase +@inject IDialogService DialogService +@inject ISnackbar Snackbar +@inject NavigationManager NavigationManager; + +@Program.PageTitlePostFix + + + + Action Page + + + TODO: Select a wallet, or create a new wallet + + + + Target: @AoAction.Target?.Value + + + + @foreach(var value in AoAction.Filled) + { +

@value.Key = @value.Value | @value.ParamType

+ } + +
+ + + @foreach (var param in AoAction.AllInputs) + { + if(param.ParamType == ActionParamType.Input + || param.ParamType == ActionParamType.Integer + || param.ParamType == ActionParamType.Process + ) + { + + } + else if (param.ParamType == ActionParamType.Quantity || param.ParamType == ActionParamType.Balance) + { + //TODO: Quantity, with token and denomination and enough balance check + } + } + + + + +
+ +@code +{ + + private void OnValidSubmit(EditContext context) + { + //success = true; + StateHasChanged(); + } + + +} diff --git a/src/aoWebWallet/Pages/ActionPage.razor.cs b/src/aoWebWallet/Pages/ActionPage.razor.cs new file mode 100644 index 0000000..29de8ac --- /dev/null +++ b/src/aoWebWallet/Pages/ActionPage.razor.cs @@ -0,0 +1,97 @@ +using aoWebWallet.Models; +using aoWebWallet.ViewModels; +using Microsoft.AspNetCore.Components.Routing; +using System.Net; + +namespace aoWebWallet.Pages +{ + public partial class ActionPage : MvvmComponentBase + { + public AoAction AoAction { get; set; } = new(); + + protected override void OnInitialized() + { + GetQueryStringValues(); + WatchDataLoaderVM(BindingContext.TokenList); + NavigationManager.LocationChanged += NavigationManager_LocationChanged; + } + + private void NavigationManager_LocationChanged(object? sender, LocationChangedEventArgs e) + { + GetQueryStringValues(); + StateHasChanged(); + } + + void GetQueryStringValues() + { + var uri = new Uri(NavigationManager.Uri); + var query = uri.Query; + + // Parsing query string + var queryStringValues = System.Web.HttpUtility.ParseQueryString(query); + + AoAction = new AoAction(); + + foreach (var key in queryStringValues.AllKeys) + { + if (key == null) + continue; + + var values = queryStringValues.GetValues(key); + if (values == null || !values.Any()) + continue; + + foreach(var val in values) + { + string actionKey = key; + string? actionValue = val.ToString(); + ActionParamType actionParamType = ActionParamType.Filled; + + if (key.Equals("Target", StringComparison.InvariantCultureIgnoreCase)) + actionParamType = ActionParamType.Target; + if (key.Equals("X-Quantity", StringComparison.InvariantCultureIgnoreCase)) + actionParamType = ActionParamType.Quantity; + else if (key.Equals("X-Process", StringComparison.InvariantCultureIgnoreCase)) + actionParamType = ActionParamType.Process; + else if (key.Equals("X-Int", StringComparison.InvariantCultureIgnoreCase)) + actionParamType = ActionParamType.Integer; + else if (key.Equals("X-Input", StringComparison.InvariantCultureIgnoreCase)) + actionParamType = ActionParamType.Input; + + if (actionParamType != ActionParamType.Filled + && actionParamType != ActionParamType.Target) + { + actionKey = val; + actionValue = null; + } + + AoAction.Params.Add(new ActionParam + { + Key = actionKey, + Value = actionValue, + ParamType = actionParamType + }); + + } + } + + StateHasChanged(); + } + + + + public void Dispose() + { + NavigationManager.LocationChanged -= NavigationManager_LocationChanged; + } + + protected override async Task LoadDataAsync() + { + await BindingContext.LoadTokenList(); + + await base.LoadDataAsync(); + + } + + } +} diff --git a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor new file mode 100644 index 0000000..5c67913 --- /dev/null +++ b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor @@ -0,0 +1,71 @@ +@using aoWebWallet.Models +

@ActionParam.Key = @ActionParam.Value | @ActionParam.ParamType

+ +@if (ActionParam.ParamType == ActionParamType.Input) +{ + +} +else if (ActionParam.ParamType == ActionParamType.Process) +{ + +} +else if (ActionParam.ParamType == ActionParamType.Integer) +{ + +} + + +@code { + + [Parameter] + public required ActionParam ActionParam { get; set; } + + MudTextField? mudTextField; + MudTextField? mudProcessField; + MudTextField? mudIntField; + + public string? stringValue = null; + public int? intValue = null; + + public async void UpdateStringValue(string e) + { + if (mudTextField != null) + await mudTextField.Validate(); + if(mudProcessField != null) + await mudProcessField.Validate(); + + if (!(mudTextField?.ValidationErrors.Any() ?? false) + && !(mudProcessField?.ValidationErrors.Any() ?? false)) + { + ActionParam.Value = e; + } + else + ActionParam.Value = null; + + StateHasChanged(); + } + + public IEnumerable ValidateProcess(string input) + { + if (input.Length != 43) + { + Console.WriteLine("Invalid"); + + yield return "Address must have length of 43 characters."; + } + } + + public async void UpdateIntValue(int e) + { + if (mudIntField != null) + await mudIntField.Validate(); + + if (!(mudIntField?.ValidationErrors.Any() ?? false)) + ActionParam.Value = e.ToString(); + else + ActionParam.Value = null; + + StateHasChanged(); + } + +} diff --git a/src/aoWebWallet/Shared/SendTokenDialog.razor b/src/aoWebWallet/Shared/SendTokenDialog.razor index e92b2a0..10eec0f 100644 --- a/src/aoWebWallet/Shared/SendTokenDialog.razor +++ b/src/aoWebWallet/Shared/SendTokenDialog.razor @@ -90,7 +90,7 @@ public MudButton? confButtonRef; - public int Denomination => BindingContext.SelectedBalanceDataVM?.Token?.TokenData?.Denomination ?? 0; + //public int Denomination => BindingContext.SelectedBalanceDataVM?.Token?.TokenData?.Denomination ?? 0; public string DenominationFormat => "F" + (BindingContext.SelectedBalanceDataVM?.Token?.TokenData?.Denomination ?? 1).ToString(); public async Task Submit() From 252362cb717778fdbbc67ce22b4e11f16597e77b Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Wed, 17 Apr 2024 15:32:49 +0200 Subject: [PATCH 02/63] wallet select work in progress --- src/aoWebWallet/Models/ActionParam.cs | 9 ++-- src/aoWebWallet/Pages/ActionPage.razor | 57 +++++++++++++++++++---- src/aoWebWallet/Pages/ActionPage.razor.cs | 19 ++++++++ 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/src/aoWebWallet/Models/ActionParam.cs b/src/aoWebWallet/Models/ActionParam.cs index 98a2f1f..ffa8900 100644 --- a/src/aoWebWallet/Models/ActionParam.cs +++ b/src/aoWebWallet/Models/ActionParam.cs @@ -24,7 +24,10 @@ public class ActionParam public required string Key { get; set; } public string? Value { get; set; } - public string? TokenId { get; set; } + /// + /// Arguments (like TokenId) + /// + public List Args { get; set; } = new(); public ActionParamType ParamType { get; set; } @@ -38,7 +41,7 @@ public enum ActionParamType Input, Integer, Process, - Balance, //Must have balance - Quantity, //Does not care about balance + Balance, //Arg1: TokenId //Must have balance + Quantity, //Arg1: TokenId //Does not care about balance } } diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor index 975cfc6..d24a4fa 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -11,18 +11,55 @@ Action Page - - TODO: Select a wallet, or create a new wallet - + + TODO: Select a wallet, or create a new wallet + + @if (BindingContext.WalletList.Data != null) + { + if (BindingContext.WalletList.Data.Any()) + { + Add Wallet + } + else + { + + @foreach (var wallet in BindingContext.WalletList.Data ?? new()) + { + + + @* *@ + +
+ + @wallet.Address + +
+
+ @wallet.Name +
+
+
+ +
+ } +
+ } + } + +
Target: @AoAction.Target?.Value - @foreach(var value in AoAction.Filled) + @foreach (var value in AoAction.Filled) {

@value.Key = @value.Value | @value.ParamType

+ @foreach (var arg in value.Args) + { + @arg + } }
@@ -30,7 +67,7 @@ @foreach (var param in AoAction.AllInputs) { - if(param.ParamType == ActionParamType.Input + if (param.ParamType == ActionParamType.Input || param.ParamType == ActionParamType.Integer || param.ParamType == ActionParamType.Process ) @@ -45,17 +82,17 @@ - +
@code { + private string? selectedWallet; - private void OnValidSubmit(EditContext context) + private void OpenDialog() { - //success = true; - StateHasChanged(); + var options = new DialogOptions { CloseOnEscapeKey = true }; + DialogService.Show("Add Wallet", options); } - } diff --git a/src/aoWebWallet/Pages/ActionPage.razor.cs b/src/aoWebWallet/Pages/ActionPage.razor.cs index 29de8ac..d108d71 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor.cs +++ b/src/aoWebWallet/Pages/ActionPage.razor.cs @@ -13,9 +13,23 @@ protected override void OnInitialized() { GetQueryStringValues(); WatchDataLoaderVM(BindingContext.TokenList); + WatchDataLoaderVM(BindingContext.WalletList); NavigationManager.LocationChanged += NavigationManager_LocationChanged; } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await BindingContext.CheckHasArConnectExtension(); + + await BindingContext.LoadWalletList(); + await BindingContext.LoadTokenList(); + } + + await base.OnAfterRenderAsync(firstRender); + } + private void NavigationManager_LocationChanged(object? sender, LocationChangedEventArgs e) { GetQueryStringValues(); @@ -47,6 +61,10 @@ void GetQueryStringValues() string? actionValue = val.ToString(); ActionParamType actionParamType = ActionParamType.Filled; + var actionValueSplit = actionValue.Split(';', StringSplitOptions.RemoveEmptyEntries); + actionValue = actionValueSplit.FirstOrDefault(); + List args = actionValueSplit.Skip(1).ToList(); + if (key.Equals("Target", StringComparison.InvariantCultureIgnoreCase)) actionParamType = ActionParamType.Target; if (key.Equals("X-Quantity", StringComparison.InvariantCultureIgnoreCase)) @@ -69,6 +87,7 @@ void GetQueryStringValues() { Key = actionKey, Value = actionValue, + Args = args, ParamType = actionParamType }); From a2fc7daa9b65fcdf25b898923b98ca805b6144c3 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Wed, 17 Apr 2024 19:59:49 +0200 Subject: [PATCH 03/63] Bugfix and cleanup --- src/aoWebWallet/Pages/ActionPage.razor | 3 ++- src/aoWebWallet/Pages/ActionPage.razor.cs | 18 ++++++++++++------ src/aoWebWallet/Pages/MvvmComponentBase.cs | 8 +------- src/aoWebWallet/Services/GraphqlClient.cs | 2 -- src/aoWebWallet/ViewModels/MainViewModel.cs | 2 -- 5 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor index d24a4fa..7928bd4 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -13,10 +13,11 @@ TODO: Select a wallet, or create a new wallet + @if (BindingContext.WalletList.Data != null) { - if (BindingContext.WalletList.Data.Any()) + if (!BindingContext.WalletList.Data.Any()) { Add Wallet } diff --git a/src/aoWebWallet/Pages/ActionPage.razor.cs b/src/aoWebWallet/Pages/ActionPage.razor.cs index d108d71..340d22b 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor.cs +++ b/src/aoWebWallet/Pages/ActionPage.razor.cs @@ -14,7 +14,11 @@ protected override void OnInitialized() GetQueryStringValues(); WatchDataLoaderVM(BindingContext.TokenList); WatchDataLoaderVM(BindingContext.WalletList); + WatchDataLoaderVM(BindingContext.BalanceDataList); + NavigationManager.LocationChanged += NavigationManager_LocationChanged; + + base.OnInitialized(); } protected override async Task OnAfterRenderAsync(bool firstRender) @@ -99,18 +103,20 @@ void GetQueryStringValues() - public void Dispose() + public override void Dispose() { NavigationManager.LocationChanged -= NavigationManager_LocationChanged; + + base.Dispose(); } - protected override async Task LoadDataAsync() - { - await BindingContext.LoadTokenList(); + //protected override async Task LoadDataAsync() + //{ + // await BindingContext.LoadTokenList(); - await base.LoadDataAsync(); + // await base.LoadDataAsync(); - } + //} } } diff --git a/src/aoWebWallet/Pages/MvvmComponentBase.cs b/src/aoWebWallet/Pages/MvvmComponentBase.cs index 0687f07..a141a8d 100644 --- a/src/aoWebWallet/Pages/MvvmComponentBase.cs +++ b/src/aoWebWallet/Pages/MvvmComponentBase.cs @@ -47,15 +47,9 @@ internal async void BindingContext_PropertyChanged(object? sender, System.Compon } } - internal async void ObjWatch_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + internal void ObjWatch_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { this.StateHasChanged(); - await ChartRenderAsync(); - } - - protected virtual Task ChartRenderAsync() - { - return Task.CompletedTask; } protected virtual Task LoadDataAsync() diff --git a/src/aoWebWallet/Services/GraphqlClient.cs b/src/aoWebWallet/Services/GraphqlClient.cs index 7e9e41a..c8b6aa0 100644 --- a/src/aoWebWallet/Services/GraphqlClient.cs +++ b/src/aoWebWallet/Services/GraphqlClient.cs @@ -318,8 +318,6 @@ public async Task> GetAoProcessesForAddress(string address) var result = new List(); - Console.WriteLine("AOresult:" + (queryResult?.Data?.Transactions?.Edges.Count ?? 0).ToString()); - foreach (var edge in queryResult?.Data?.Transactions?.Edges ?? new()) { AoProcessInfo? processInfo = GetAoProcessInfo(edge); diff --git a/src/aoWebWallet/ViewModels/MainViewModel.cs b/src/aoWebWallet/ViewModels/MainViewModel.cs index 68374a9..1be2e4a 100644 --- a/src/aoWebWallet/ViewModels/MainViewModel.cs +++ b/src/aoWebWallet/ViewModels/MainViewModel.cs @@ -326,8 +326,6 @@ public async Task LoadWalletList(bool force = false) WalletList.Data = list; - - await LoadProcessesDataList(); } } From bd116f04984664f1db8554957df4f922adbd67f9 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Wed, 17 Apr 2024 23:04:34 +0200 Subject: [PATCH 04/63] Added ActionQuantityComponent.razor --- src/aoWebWallet/Pages/ActionPage.razor | 15 +++++- src/aoWebWallet/Pages/ActionPage.razor.cs | 12 ++++- src/aoWebWallet/Services/StorageService.cs | 22 +++++++- .../Components/ActionInputComponent.razor | 9 ++-- .../Components/ActionQuantityComponent.razor | 51 +++++++++++++++++++ src/aoWebWallet/ViewModels/MainViewModel.cs | 5 +- src/aoWebWallet/aoWebWallet.csproj | 6 +++ 7 files changed, 108 insertions(+), 12 deletions(-) create mode 100644 src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor index 7928bd4..ab4a77e 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -77,7 +77,20 @@ } else if (param.ParamType == ActionParamType.Quantity || param.ParamType == ActionParamType.Balance) { - //TODO: Quantity, with token and denomination and enough balance check + var tokenId = param.Args.FirstOrDefault(); + var token = BindingContext.TokenList.Data?.Where(x => x.TokenId == tokenId && x.TokenData != null).FirstOrDefault(); + + if(token != null) + { + //TODO: Quantity, with token and denomination and enough balance check + + } + else { + Loading Token Data... + } + + + } } diff --git a/src/aoWebWallet/Pages/ActionPage.razor.cs b/src/aoWebWallet/Pages/ActionPage.razor.cs index 340d22b..2559d58 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor.cs +++ b/src/aoWebWallet/Pages/ActionPage.razor.cs @@ -40,7 +40,7 @@ private void NavigationManager_LocationChanged(object? sender, LocationChangedEv StateHasChanged(); } - void GetQueryStringValues() + private async void GetQueryStringValues() { var uri = new Uri(NavigationManager.Uri); var query = uri.Query; @@ -98,6 +98,16 @@ void GetQueryStringValues() } } + //Add and load tokens + var tokens = AoAction + .AllInputs + .Where(x => x.ParamType == ActionParamType.Balance || x.ParamType == ActionParamType.Quantity) + .Select(x => x.Args.FirstOrDefault()) + .Distinct() + .ToList(); + + await BindingContext.TryAddTokenIds(tokens); + StateHasChanged(); } diff --git a/src/aoWebWallet/Services/StorageService.cs b/src/aoWebWallet/Services/StorageService.cs index 2f01b0b..18bc2a9 100644 --- a/src/aoWebWallet/Services/StorageService.cs +++ b/src/aoWebWallet/Services/StorageService.cs @@ -40,7 +40,25 @@ private void AddSystemToken(List list, string tokenId) list.Add(new Token { TokenId = tokenId, IsSystemToken = true }); } - public async ValueTask AddToken(string tokenId, TokenData data, bool isUserAdded) + public async Task AddTokenId(string tokenId, bool isUserAdded = true, bool isVisible = false) + { + if(tokenId.Length != 43) + return; + + var list = await GetTokenIds(); + + var existing = list.Where(x => x.TokenId == tokenId).FirstOrDefault(); + if (existing != null) + return; + + + existing = new Token { TokenId = tokenId, IsUserAdded = isUserAdded, IsVisible = isVisible }; + list.Add(existing); + + await SaveTokenList(list); + } + + public async ValueTask AddToken(string tokenId, TokenData data, bool isUserAdded, bool isVisible = true) { var list = await GetTokenIds(); @@ -54,7 +72,7 @@ public async ValueTask AddToken(string tokenId, TokenData data, bool isUs } else { - existing = new Token { TokenId = tokenId, TokenData = data, IsUserAdded = isUserAdded }; + existing = new Token { TokenId = tokenId, TokenData = data, IsUserAdded = isUserAdded, IsVisible = isVisible }; list.Add(existing); } diff --git a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor index 5c67913..9d6bd7d 100644 --- a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor +++ b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor @@ -24,10 +24,7 @@ else if (ActionParam.ParamType == ActionParamType.Integer) MudTextField? mudProcessField; MudTextField? mudIntField; - public string? stringValue = null; - public int? intValue = null; - - public async void UpdateStringValue(string e) + public async void UpdateStringValue(string? e) { if (mudTextField != null) await mudTextField.Validate(); @@ -45,9 +42,9 @@ else if (ActionParam.ParamType == ActionParamType.Integer) StateHasChanged(); } - public IEnumerable ValidateProcess(string input) + public IEnumerable ValidateProcess(string? input) { - if (input.Length != 43) + if (input == null || input.Length != 43) { Console.WriteLine("Invalid"); diff --git a/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor b/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor new file mode 100644 index 0000000..d6c1a9f --- /dev/null +++ b/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor @@ -0,0 +1,51 @@ +@using ArweaveAO.Models.Token +@using aoWebWallet.Models +

@ActionParam.Key = @ActionParam.Value | @ActionParam.ParamType

+ +@if (ActionParam.ParamType == ActionParamType.Quantity +|| ActionParam.ParamType == ActionParamType.Balance) +{ + +} + + + +@code { + + [Parameter] + public required ActionParam ActionParam { get; set; } + + [Parameter] + public required Token Token { get; set; } + + [Parameter] + public BalanceData? BalanceData { get; set; } + + public string DenominationFormat => "F" + (Token.TokenData?.Denomination ?? 1).ToString(); + + MudTextField? mudTextField; + + public async void UpdateDecimalValue(decimal e) + { + if (mudTextField != null) + await mudTextField.Validate(); + + if (Token.TokenData?.Denomination == null) + { + ActionParam.Value = null; + return; + } + + if (!(mudTextField?.ValidationErrors.Any() ?? false)) + { + long amountLong = BalanceHelper.DecimalToTokenAmount(e, Token.TokenData.Denomination.Value); + + ActionParam.Value = amountLong.ToString(); + } + else + ActionParam.Value = null; + + StateHasChanged(); + } + +} diff --git a/src/aoWebWallet/ViewModels/MainViewModel.cs b/src/aoWebWallet/ViewModels/MainViewModel.cs index 1be2e4a..2ac70f6 100644 --- a/src/aoWebWallet/ViewModels/MainViewModel.cs +++ b/src/aoWebWallet/ViewModels/MainViewModel.cs @@ -428,11 +428,11 @@ public async Task ClearUserData() BalanceDataList.Data = null; } - private async Task TryAddTokenIds(List allTokenIds) + public async Task TryAddTokenIds(List allTokenIds) { foreach (var tokenId in allTokenIds) { - if (string.IsNullOrEmpty(tokenId)) + if (string.IsNullOrEmpty(tokenId) || tokenId.Length != 43) continue; var exist = TokenList.Data?.Where(x => x.TokenId == tokenId).Any() ?? false; @@ -800,5 +800,6 @@ public async Task SetIsDarkMode(bool isDarkMode) await SaveUserSettings(); } } + } } diff --git a/src/aoWebWallet/aoWebWallet.csproj b/src/aoWebWallet/aoWebWallet.csproj index 47688c6..de28eec 100644 --- a/src/aoWebWallet/aoWebWallet.csproj +++ b/src/aoWebWallet/aoWebWallet.csproj @@ -27,4 +27,10 @@ + + + true + + + From acf2ae374ce79a94777b3d34d69ddab392b3855e Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Thu, 18 Apr 2024 14:40:07 +0200 Subject: [PATCH 05/63] ViewModel refactoring --- src/aoWebWallet/Pages/ActionPage.razor | 6 +- src/aoWebWallet/Pages/ActionPage.razor.cs | 10 +- src/aoWebWallet/Pages/MvvmComponentBase.cs | 28 +- src/aoWebWallet/Pages/TokenDetail.razor | 9 +- src/aoWebWallet/Pages/TokenDetail.razor.cs | 17 +- src/aoWebWallet/Pages/Tokens.razor | 25 +- src/aoWebWallet/Pages/Tokens.razor.cs | 5 +- src/aoWebWallet/Pages/TransactionDetail.razor | 7 +- .../Pages/TransactionDetail.razor.cs | 16 +- src/aoWebWallet/Pages/WalletDetail.razor | 51 +- src/aoWebWallet/Pages/WalletDetail.razor.cs | 22 +- src/aoWebWallet/Pages/Wallets.razor | 6 +- src/aoWebWallet/Pages/Wallets.razor.cs | 4 +- src/aoWebWallet/Program.cs | 6 +- src/aoWebWallet/Services/ClipboardService.cs | 26 + src/aoWebWallet/Services/DataService.cs | 49 -- src/aoWebWallet/Services/TokenDataService.cs | 164 ++++++ .../Shared/AddArConnectComponent.razor | 1 - .../Shared/AddGenerateWalletComponent.razor | 1 - src/aoWebWallet/Shared/AddTokenDialog.razor | 7 +- .../Shared/AddUploadWalletComponent.razor | 1 - .../Shared/AddWalletComponent.razor | 1 - src/aoWebWallet/Shared/AddWalletDialog.razor | 2 - .../Components/TransactionComponent.razor | 3 +- .../Shared/ReceiveTokenDialog.razor | 15 +- src/aoWebWallet/Shared/SendTokenDialog.razor | 55 +- src/aoWebWallet/ViewModels/MainViewModel.cs | 491 +----------------- .../ViewModels/TokenDetailViewModel.cs | 62 +++ .../ViewModels/TransactionDetailViewModel.cs | 49 ++ .../ViewModels/WalletDetailViewModel.cs | 346 ++++++++++++ 30 files changed, 819 insertions(+), 666 deletions(-) create mode 100644 src/aoWebWallet/Services/ClipboardService.cs delete mode 100644 src/aoWebWallet/Services/DataService.cs create mode 100644 src/aoWebWallet/Services/TokenDataService.cs create mode 100644 src/aoWebWallet/ViewModels/TokenDetailViewModel.cs create mode 100644 src/aoWebWallet/ViewModels/TransactionDetailViewModel.cs create mode 100644 src/aoWebWallet/ViewModels/WalletDetailViewModel.cs diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor index ab4a77e..db72f28 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -3,7 +3,8 @@ @inherits MvvmComponentBase @inject IDialogService DialogService @inject ISnackbar Snackbar -@inject NavigationManager NavigationManager; +@inject NavigationManager NavigationManager +@inject TokenDataService dataService @Program.PageTitlePostFix @@ -13,7 +14,6 @@ TODO: Select a wallet, or create a new wallet - @if (BindingContext.WalletList.Data != null) { @@ -78,7 +78,7 @@ else if (param.ParamType == ActionParamType.Quantity || param.ParamType == ActionParamType.Balance) { var tokenId = param.Args.FirstOrDefault(); - var token = BindingContext.TokenList.Data?.Where(x => x.TokenId == tokenId && x.TokenData != null).FirstOrDefault(); + var token = dataService.TokenList.Where(x => x.TokenId == tokenId && x.TokenData != null).FirstOrDefault(); if(token != null) { diff --git a/src/aoWebWallet/Pages/ActionPage.razor.cs b/src/aoWebWallet/Pages/ActionPage.razor.cs index 2559d58..d829305 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor.cs +++ b/src/aoWebWallet/Pages/ActionPage.razor.cs @@ -1,4 +1,5 @@ using aoWebWallet.Models; +using aoWebWallet.Services; using aoWebWallet.ViewModels; using Microsoft.AspNetCore.Components.Routing; using System.Net; @@ -12,9 +13,8 @@ public partial class ActionPage : MvvmComponentBase protected override void OnInitialized() { GetQueryStringValues(); - WatchDataLoaderVM(BindingContext.TokenList); + //WatchDataLoaderVM(BindingContext.TokenList); WatchDataLoaderVM(BindingContext.WalletList); - WatchDataLoaderVM(BindingContext.BalanceDataList); NavigationManager.LocationChanged += NavigationManager_LocationChanged; @@ -28,7 +28,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) await BindingContext.CheckHasArConnectExtension(); await BindingContext.LoadWalletList(); - await BindingContext.LoadTokenList(); + await dataService.LoadTokenList(); } await base.OnAfterRenderAsync(firstRender); @@ -104,9 +104,9 @@ private async void GetQueryStringValues() .Where(x => x.ParamType == ActionParamType.Balance || x.ParamType == ActionParamType.Quantity) .Select(x => x.Args.FirstOrDefault()) .Distinct() - .ToList(); + .ToList(); - await BindingContext.TryAddTokenIds(tokens); + await dataService.TryAddTokenIds(tokens); StateHasChanged(); } diff --git a/src/aoWebWallet/Pages/MvvmComponentBase.cs b/src/aoWebWallet/Pages/MvvmComponentBase.cs index a141a8d..b7303d2 100644 --- a/src/aoWebWallet/Pages/MvvmComponentBase.cs +++ b/src/aoWebWallet/Pages/MvvmComponentBase.cs @@ -1,6 +1,7 @@ using aoWebWallet.ViewModels; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.AspNetCore.Components; +using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; using webvNext.DataLoader; @@ -13,6 +14,7 @@ public abstract class MvvmComponentBase : ComponentBase, IDisposable where T public T BindingContext { get; set; } = default!; public List ObjWatch { get; set; } = new(); + public List CollectionWatch { get; set; } = new(); protected override void OnInitialized() { @@ -23,9 +25,14 @@ protected override void OnInitialized() obj.PropertyChanged += ObjWatch_PropertyChanged; } + foreach (var obj in CollectionWatch) + { + obj.CollectionChanged += Obj_CollectionChanged; + } + base.OnInitialized(); } - + protected override async Task OnInitializedAsync() { await LoadDataAsync(); @@ -51,17 +58,27 @@ internal void ObjWatch_PropertyChanged(object? sender, System.ComponentModel.Pro { this.StateHasChanged(); } + private void Obj_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + Console.WriteLine("Collection change " + this.GetType()); + this.StateHasChanged(); + } protected virtual Task LoadDataAsync() { return Task.CompletedTask; } - protected void WatchObject(D obj) where D : ObservableObject + protected void WatchObject(D obj) where D : INotifyPropertyChanged { ObjWatch.Add(obj); } + protected void WatchCollection(D obj) where D : INotifyCollectionChanged + { + CollectionWatch.Add(obj); + } + protected void WatchDataLoaderVM(DataLoaderViewModel vm) where D : class { ObjWatch.Add(vm); @@ -76,6 +93,13 @@ public virtual void Dispose() { obj.PropertyChanged -= ObjWatch_PropertyChanged; } + + foreach (var obj in CollectionWatch) + { + obj.CollectionChanged -= Obj_CollectionChanged; + } + + Console.WriteLine("Dispose " + this.GetType().FullName); } } } diff --git a/src/aoWebWallet/Pages/TokenDetail.razor b/src/aoWebWallet/Pages/TokenDetail.razor index 9adb983..982643e 100644 --- a/src/aoWebWallet/Pages/TokenDetail.razor +++ b/src/aoWebWallet/Pages/TokenDetail.razor @@ -1,9 +1,10 @@ @page "/token/{tokenId}" @using aoWebWallet.Models -@inherits MvvmComponentBase +@inherits MvvmComponentBase @inject IDialogService DialogService @inject ISnackbar Snackbar @inject NavigationManager NavigationManager; +@inject TokenDataService dataService; @Program.PageTitlePostFix @@ -11,12 +12,12 @@ Token Explorer - + - @if (BindingContext.TokenList.Data != null) + @if (dataService.TokenList != null) { - var token = BindingContext.TokenList.Data.Where(x => x.TokenId == TokenId).FirstOrDefault(); + var token = dataService.TokenList.Where(x => x.TokenId == TokenId).FirstOrDefault(); if (token != null) { diff --git a/src/aoWebWallet/Pages/TokenDetail.razor.cs b/src/aoWebWallet/Pages/TokenDetail.razor.cs index 406530a..02fe71b 100644 --- a/src/aoWebWallet/Pages/TokenDetail.razor.cs +++ b/src/aoWebWallet/Pages/TokenDetail.razor.cs @@ -3,31 +3,32 @@ namespace aoWebWallet.Pages { - public partial class TokenDetail : MvvmComponentBase + public partial class TokenDetail : MvvmComponentBase { protected override void OnInitialized() { - WatchDataLoaderVM(BindingContext.TokenList); + //WatchDataLoaderVM(dataService.TokenList); WatchDataLoaderVM(BindingContext.TokenTransferList); base.OnInitialized(); } - protected override void OnParametersSet() + protected override async Task OnParametersSetAsync() { - BindingContext.SelectedTokenId = null; - if (TokenId != null && TokenId.Length != 43) + if (TokenId == null || TokenId.Length != 43) { NavigationManager.NavigateTo(""); } - BindingContext.SelectedTokenId = TokenId; - base.OnParametersSet(); + if(TokenId != null) + await BindingContext.Initialize(TokenId); + + base.OnParametersSetAsync(); } protected override async Task LoadDataAsync() { - await BindingContext.LoadTokenList(); + await dataService.LoadTokenList(); await base.LoadDataAsync(); diff --git a/src/aoWebWallet/Pages/Tokens.razor b/src/aoWebWallet/Pages/Tokens.razor index 8df22a2..cb9f44d 100644 --- a/src/aoWebWallet/Pages/Tokens.razor +++ b/src/aoWebWallet/Pages/Tokens.razor @@ -2,6 +2,7 @@ @using aoWebWallet.Models @inherits MvvmComponentBase @inject IDialogService DialogService +@inject TokenDataService dataService @inject ISnackbar Snackbar @Program.PageTitlePostFix @@ -10,20 +11,20 @@ Token Explorer - + - @if (BindingContext.TokenList.Data != null) + @if (dataService.TokenList != null) { - var autoAddedTokens = BindingContext.TokenList.Data.Where(x => !x.IsVisible); + var autoAddedTokens = dataService.TokenList.Where(x => !x.IsVisible); - @foreach (var token in BindingContext.TokenList.Data.Where(x => x.IsVisible)) + @foreach (var token in dataService.TokenList.Where(x => x.IsVisible)) { } @@ -35,17 +36,6 @@ } - @* else - { - foreach (var token in BindingContext.TokenList.Data) - { - - } - } *@ - - - - } @@ -61,8 +51,7 @@ private async void ToggleVisibility(Token token) { - - await BindingContext.TokenToggleVisibility(token.TokenId); + await dataService.TokenToggleVisibility(token.TokenId); StateHasChanged(); } @@ -76,7 +65,7 @@ if (result != null) { - await BindingContext.DeleteToken(token.TokenId); + await dataService.DeleteToken(token.TokenId); Snackbar.Add($"Token {token.TokenData?.Name} deleted ({token.TokenId})", Severity.Info); } diff --git a/src/aoWebWallet/Pages/Tokens.razor.cs b/src/aoWebWallet/Pages/Tokens.razor.cs index 1ee069f..b51bbca 100644 --- a/src/aoWebWallet/Pages/Tokens.razor.cs +++ b/src/aoWebWallet/Pages/Tokens.razor.cs @@ -6,17 +6,14 @@ public partial class Tokens : MvvmComponentBase { protected override void OnInitialized() { - WatchDataLoaderVM(BindingContext.TokenList); - base.OnInitialized(); } protected override async Task LoadDataAsync() { - await BindingContext.LoadTokenList(); + await dataService.LoadTokenList(); await base.LoadDataAsync(); - } } diff --git a/src/aoWebWallet/Pages/TransactionDetail.razor b/src/aoWebWallet/Pages/TransactionDetail.razor index e591eeb..662fada 100644 --- a/src/aoWebWallet/Pages/TransactionDetail.razor +++ b/src/aoWebWallet/Pages/TransactionDetail.razor @@ -1,7 +1,8 @@ @page "/transaction/{txid}" @using aoWebWallet.Models -@inherits MvvmComponentBase +@inherits MvvmComponentBase @inject NavigationManager NavigationManager; +@inject TokenDataService dataService @TxId - @Program.PageTitlePostFix @@ -9,7 +10,7 @@ - + @if (BindingContext.SelectedTransaction.Data != null) @@ -21,7 +22,7 @@ @transfer.Id - var tokenData = BindingContext.TokenList.Data?.Where(x => x.TokenId == transfer.TokenId).Select(x => x.TokenData).FirstOrDefault(); + var tokenData = dataService.TokenList.Where(x => x.TokenId == transfer.TokenId).Select(x => x.TokenData).FirstOrDefault(); diff --git a/src/aoWebWallet/Pages/TransactionDetail.razor.cs b/src/aoWebWallet/Pages/TransactionDetail.razor.cs index b96d054..1c3907a 100644 --- a/src/aoWebWallet/Pages/TransactionDetail.razor.cs +++ b/src/aoWebWallet/Pages/TransactionDetail.razor.cs @@ -1,11 +1,12 @@ -using aoWebWallet.ViewModels; +using aoWebWallet.Models; +using aoWebWallet.ViewModels; using Microsoft.AspNetCore.Components; using MudBlazor; using System.Net; namespace aoWebWallet.Pages { - public partial class TransactionDetail : MvvmComponentBase + public partial class TransactionDetail : MvvmComponentBase { [Parameter] public string? TxId { get; set; } @@ -18,23 +19,22 @@ protected override void OnInitialized() base.OnInitialized(); } - protected override void OnParametersSet() + protected override async Task OnParametersSetAsync() { - BindingContext.SelectedTransactionId = null; - if (TxId != null && TxId.Length != 43) { NavigationManager.NavigateTo(""); } - BindingContext.SelectedTransactionId = this.TxId; + if (TxId != null) + await BindingContext.Initialize(TxId); - base.OnParametersSet(); + base.OnParametersSetAsync(); } protected override async Task LoadDataAsync() { - await BindingContext.LoadTokenList(); + await dataService.LoadTokenList(); //if (!string.IsNullOrEmpty(Address)) //{ diff --git a/src/aoWebWallet/Pages/WalletDetail.razor b/src/aoWebWallet/Pages/WalletDetail.razor index 55e876d..b164aa8 100644 --- a/src/aoWebWallet/Pages/WalletDetail.razor +++ b/src/aoWebWallet/Pages/WalletDetail.razor @@ -1,9 +1,12 @@ @page "/wallet/{address}" @using aoWebWallet.Models -@inherits MvvmComponentBase +@inherits MvvmComponentBase @inject IDialogService DialogService @inject ISnackbar Snackbar -@inject NavigationManager NavigationManager; +@inject NavigationManager NavigationManager +@inject TokenDataService dataService +@inject MainViewModel MainViewModel +@inject ClipboardService ClipboardService @Address - @Program.PageTitlePostFix @@ -35,7 +38,7 @@ } - + @if (!string.IsNullOrEmpty(BindingContext.SelectedWallet?.Wallet.Jwk)) { @@ -49,7 +52,7 @@ @if (BindingContext.SelectedWallet?.Wallet.Source == WalletTypes.Explorer) { - + } @@ -59,10 +62,10 @@ { - @if (BindingContext.UserSettings != null) + @if (MainViewModel.UserSettings != null) { - @if (BindingContext.UserSettings.Claimed1 && BindingContext.UserSettings.Claimed2 && BindingContext.UserSettings.Claimed3) + @if (MainViewModel.UserSettings.Claimed1 && MainViewModel.UserSettings.Claimed2 && MainViewModel.UserSettings.Claimed3) { All Rewards Claimed (3/3) @@ -70,25 +73,25 @@ else { - if (BindingContext.UserSettings.Claimed1) + if (MainViewModel.UserSettings.Claimed1) { } else { - Claim Reward (1/3) + Claim Reward (1/3) } - if (BindingContext.UserSettings.Claimed2) + if (MainViewModel.UserSettings.Claimed2) { } else { - Claim Reward (2/3) + Claim Reward (2/3) } - Claim Reward (3/3) + Claim Reward (3/3) } } @@ -106,7 +109,7 @@ - + @@ -175,7 +178,7 @@ @foreach (var transfer in BindingContext.TokenTransferList.Data) { - + } } @@ -228,33 +231,29 @@ private void Receive(BalanceDataViewModel? balanceDataVM) { - BindingContext.SelectedBalanceDataVM = balanceDataVM; + var parameters = new DialogParameters { { x => x.SelectedBalanceDataVM, balanceDataVM } }; var options = new DialogOptions { CloseOnEscapeKey = true }; - DialogService.Show("Receive Token", options); + DialogService.Show("Receive Token", parameters, options); } private void Send(BalanceDataViewModel? balanceDataVM) { - BindingContext.SelectedBalanceDataVM = balanceDataVM; + var parameters = new DialogParameters { + { x => x.SelectedBalanceDataVM, balanceDataVM }, + { x => x.SelectedWallet, BindingContext.SelectedWallet } + }; var options = new DialogOptions { CloseOnEscapeKey = true }; - DialogService.Show("Transfer Token", options); + DialogService.Show("Transfer Token", parameters, options); } private async Task RefreshBalances() { - if (BindingContext.SelectedAddress != null) - await BindingContext.LoadBalanceDataList(BindingContext.SelectedAddress); + await BindingContext.RefreshBalanceDataList(); } private async Task RefreshTransactions() { - if (BindingContext.SelectedAddress != null) - await BindingContext.LoadTokenTransferList(BindingContext.SelectedAddress); - } - - private async Task AddWalletAsReadonly() - { - await BindingContext.AddWalletAsReadonly(); + await BindingContext.RefreshTokenTransferList(); } private async Task Claim1() diff --git a/src/aoWebWallet/Pages/WalletDetail.razor.cs b/src/aoWebWallet/Pages/WalletDetail.razor.cs index 9ddfe91..a06ddb3 100644 --- a/src/aoWebWallet/Pages/WalletDetail.razor.cs +++ b/src/aoWebWallet/Pages/WalletDetail.razor.cs @@ -5,12 +5,12 @@ namespace aoWebWallet.Pages { - public partial class WalletDetail : MvvmComponentBase + public partial class WalletDetail : MvvmComponentBase { protected override void OnInitialized() { - WatchDataLoaderVM(BindingContext.TokenList); - WatchDataLoaderVM(BindingContext.WalletList); + WatchCollection(dataService.TokenList); + WatchDataLoaderVM(MainViewModel.WalletList); WatchDataLoaderVM(BindingContext.BalanceDataList); WatchDataLoaderVM(BindingContext.TokenTransferList); WatchDataLoaderVM(BindingContext.SelectedProcessData); @@ -18,24 +18,22 @@ protected override void OnInitialized() base.OnInitialized(); } - protected override void OnParametersSet() + protected override async Task OnParametersSetAsync() { - BindingContext.SelectedWallet = null; - //BindingContext.SelectedAddress = null; - - if(Address != null && Address.Length != 43) + if (Address != null && Address.Length != 43) { NavigationManager.NavigateTo(""); } - BindingContext.SelectedAddress = Address; + if (Address != null) + await BindingContext.Initialize(Address); - base.OnParametersSet(); + base.OnParametersSetAsync(); } protected override async Task LoadDataAsync() { - BindingContext.LoadTokenList(); + dataService.LoadTokenList(); //if (!string.IsNullOrEmpty(Address)) //{ @@ -47,7 +45,7 @@ protected override async Task LoadDataAsync() private async void DownloadWallet(Wallet wallet) { - await BindingContext.DownloadWallet(wallet); + await MainViewModel.DownloadWallet(wallet); StateHasChanged(); } diff --git a/src/aoWebWallet/Pages/Wallets.razor b/src/aoWebWallet/Pages/Wallets.razor index d1acb1a..436c5e8 100644 --- a/src/aoWebWallet/Pages/Wallets.razor +++ b/src/aoWebWallet/Pages/Wallets.razor @@ -3,6 +3,8 @@ @inherits MvvmComponentBase @inject IDialogService DialogService @inject ISnackbar Snackbar +@inject TokenDataService dataService +@inject ClipboardService ClipboardService @Program.PageTitlePostFix @@ -18,7 +20,7 @@ Wallet } - + @if (BindingContext.WalletList.Data != null && BindingContext.WalletList.Data.Any()) @@ -48,7 +50,7 @@ @wallet.Address - +
@if (wallet.IsReadOnly) diff --git a/src/aoWebWallet/Pages/Wallets.razor.cs b/src/aoWebWallet/Pages/Wallets.razor.cs index 17980e8..701ad89 100644 --- a/src/aoWebWallet/Pages/Wallets.razor.cs +++ b/src/aoWebWallet/Pages/Wallets.razor.cs @@ -6,7 +6,7 @@ public partial class Wallets : MvvmComponentBase { protected override void OnInitialized() { - WatchDataLoaderVM(BindingContext.TokenList); + //WatchDataLoaderVM(BindingContext.TokenList); WatchDataLoaderVM(BindingContext.WalletList); WatchDataLoaderVM(BindingContext.ProcessesDataList); @@ -20,7 +20,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) await BindingContext.CheckHasArConnectExtension(); await BindingContext.LoadWalletList(); - await BindingContext.LoadTokenList(); + await dataService.LoadTokenList(); } await base.OnAfterRenderAsync(firstRender); diff --git a/src/aoWebWallet/Program.cs b/src/aoWebWallet/Program.cs index e69cd66..74e27aa 100644 --- a/src/aoWebWallet/Program.cs +++ b/src/aoWebWallet/Program.cs @@ -88,8 +88,9 @@ private static void ConfigureServices(IServiceCollection services, string baseAd services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(baseAddress) }); //Services - services.AddScoped(); + services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddSingleton(); @@ -101,6 +102,9 @@ private static void ConfigureServices(IServiceCollection services, string baseAd //Register ViewModels services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddBlazoredLocalStorage(); diff --git a/src/aoWebWallet/Services/ClipboardService.cs b/src/aoWebWallet/Services/ClipboardService.cs new file mode 100644 index 0000000..aa8919e --- /dev/null +++ b/src/aoWebWallet/Services/ClipboardService.cs @@ -0,0 +1,26 @@ +using ClipLazor.Components; +using ClipLazor.Enums; +using MudBlazor; + +namespace aoWebWallet.Services +{ + public class ClipboardService(IClipLazor clipboard, ISnackbar snackbar) + { + public async Task CopyToClipboard(string? text) + { + bool isSupported = await clipboard.IsClipboardSupported(); + bool isWritePermitted = await clipboard.IsPermitted(PermissionCommand.Write); + if (isSupported && !string.IsNullOrEmpty(text)) + { + if (isWritePermitted) + { + var isCopied = await clipboard.WriteTextAsync(text.AsMemory()); + if (isCopied) + { + snackbar.Add("Address copied to clipboard", Severity.Success); + } + } + } + } + } +} diff --git a/src/aoWebWallet/Services/DataService.cs b/src/aoWebWallet/Services/DataService.cs deleted file mode 100644 index 105f414..0000000 --- a/src/aoWebWallet/Services/DataService.cs +++ /dev/null @@ -1,49 +0,0 @@ - -using aoWebWallet.Models; -using ArweaveAO; -using ArweaveAO.Models.Token; - -namespace aoWebWallet.Services -{ - public class DataService - { - private readonly StorageService storageService; - private readonly TokenClient tokenClient; - - public DataService(StorageService storageService, TokenClient tokenClient) - { - this.storageService = storageService; - this.tokenClient = tokenClient; - } - - internal void Init(string value) - { - // throw new NotImplementedException(); - } - - public async IAsyncEnumerable LoadTokenDataAsync() - { - var tokens = await storageService.GetTokenIds(); - foreach (var token in tokens) - { - if (token.TokenData == null) - { - //Load metadata - try - { - var data = await tokenClient.GetTokenMetaData(token.TokenId); - token.TokenData = data; - } - catch { } - } - - if (token.TokenData != null) - yield return token; - } - - await storageService.SaveTokenList(tokens); - - } - - } -} diff --git a/src/aoWebWallet/Services/TokenDataService.cs b/src/aoWebWallet/Services/TokenDataService.cs new file mode 100644 index 0000000..e26d84b --- /dev/null +++ b/src/aoWebWallet/Services/TokenDataService.cs @@ -0,0 +1,164 @@ + +using aoWebWallet.Models; +using aoWebWallet.Pages; +using ArweaveAO; +using ArweaveAO.Models.Token; +using System.Collections.ObjectModel; +using webvNext.DataLoader; + +namespace aoWebWallet.Services +{ + public class TokenDataService + { + public DataLoader TokenDataLoader { get; set; } = new(); + public ObservableCollection TokenList { get; set; } = new(); + + + private readonly StorageService storageService; + private readonly TokenClient tokenClient; + + public TokenDataService(StorageService storageService, TokenClient tokenClient) + { + this.storageService = storageService; + this.tokenClient = tokenClient; + } + + public async Task TryAddTokenIds(List allTokenIds) + { + + foreach (var tokenId in allTokenIds) + { + if (string.IsNullOrEmpty(tokenId) || tokenId.Length != 43) + continue; + + var exist = TokenList.Where(x => x.TokenId == tokenId).Any(); + if (exist) + continue; + + var data = await TokenDataLoader.LoadAsync(async () => + { + var data = await tokenClient.GetTokenMetaData(tokenId); + return data; + }); + + if (data != null) + { + await storageService.AddToken(tokenId, data, isUserAdded: false); + } + } + } + + public async Task LoadTokenDataAsync(string tokenId) + { + var tokens = await storageService.GetTokenIds(); + + var token = tokens.Where(x => x.TokenId.Equals(tokenId, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); + if(token == null) + { + token = new Token() + { + TokenId = tokenId, + }; + + tokens.Add(token); + } + + if (token.TokenData == null) + { + var data = await TokenDataLoader.LoadAsync(async () => + { + var data = await tokenClient.GetTokenMetaData(tokenId); + return data; + }); + + token.TokenData = data; + + await storageService.SaveTokenList(tokens); + } + + return token.TokenData; + } + + public async Task LoadTokenList(bool force = false) + { + if (!TokenList.Any() || force) + { + TokenList = new(); + await foreach (var item in LoadTokenDataAsync()) + { + TokenList.Add(item); + } + } + } + + public async IAsyncEnumerable LoadTokenDataAsync() + { + var tokens = await storageService.GetTokenIds(); + foreach (var token in tokens) + { + if (token.TokenData == null) + { + //Load metadata + try + { + var data = await TokenDataLoader.LoadAsync(async () => + { + var data = await tokenClient.GetTokenMetaData(token.TokenId); + return data; + }); + + token.TokenData = data; + } + catch { } + } + + if (token.TokenData != null) + yield return token; + } + + await storageService.SaveTokenList(tokens); + + } + + public async Task DeleteToken(string tokenId) + { + await storageService.DeleteToken(tokenId); + await this.LoadTokenList(force: true); + } + + public async Task TokenToggleVisibility(string tokenId) + { + var all = TokenList ?? new(); + var token = all.Where(x => x.TokenId == tokenId).FirstOrDefault(); + if (token != null) + { + token.IsVisible = !token.IsVisible; + await storageService.SaveTokenList(all.ToList()); + await this.LoadTokenList(force: true); + } + } + + public async Task AddToken(string tokenId, TokenData data, bool isUserAdded) + { + var newToken = await storageService.AddToken(tokenId, data, isUserAdded); + var existing = TokenList.Where(x => x.TokenId == newToken.TokenId).FirstOrDefault(); + if (existing == null) + { + if (TokenList == null) + TokenList = new(); + + TokenList.Add(newToken); + } + else + { + existing = newToken; + } + } + + public async Task Clear() + { + await storageService.SaveTokenList(new()); + TokenList.Clear(); + } + } +} diff --git a/src/aoWebWallet/Shared/AddArConnectComponent.razor b/src/aoWebWallet/Shared/AddArConnectComponent.razor index 8a5bf8a..fe82624 100644 --- a/src/aoWebWallet/Shared/AddArConnectComponent.razor +++ b/src/aoWebWallet/Shared/AddArConnectComponent.razor @@ -1,6 +1,5 @@ @using aoWebWallet.Models @inherits MvvmComponentBase -@inject TokenClient TokenClient @inject ISnackbar Snackbar @inject ArweaveService ArweaveService diff --git a/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor b/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor index 4dc145c..49d3338 100644 --- a/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor +++ b/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor @@ -1,6 +1,5 @@ @using aoWebWallet.Models @inherits MvvmComponentBase -@inject TokenClient TokenClient @inject ArweaveService ArweaveService @inject ISnackbar Snackbar diff --git a/src/aoWebWallet/Shared/AddTokenDialog.razor b/src/aoWebWallet/Shared/AddTokenDialog.razor index 5d5c47d..ec12608 100644 --- a/src/aoWebWallet/Shared/AddTokenDialog.razor +++ b/src/aoWebWallet/Shared/AddTokenDialog.razor @@ -1,5 +1,4 @@ -@inherits MvvmComponentBase -@inject TokenClient TokenClient +@inject TokenDataService dataService @inject ISnackbar Snackbar @@ -37,10 +36,10 @@ Progress = "Checking metadata..."; try { - var data = await TokenClient.GetTokenMetaData(TokenId); + var data = await dataService.LoadTokenDataAsync(TokenId); if (data != null) { - await BindingContext.AddToken(TokenId, data, isUserAdded: true); + await dataService.AddToken(TokenId, data, isUserAdded: true); Snackbar.Add($"Token added ({data.Name})", Severity.Info); diff --git a/src/aoWebWallet/Shared/AddUploadWalletComponent.razor b/src/aoWebWallet/Shared/AddUploadWalletComponent.razor index 68f84d0..3005a85 100644 --- a/src/aoWebWallet/Shared/AddUploadWalletComponent.razor +++ b/src/aoWebWallet/Shared/AddUploadWalletComponent.razor @@ -1,6 +1,5 @@ @using aoWebWallet.Models @inherits MvvmComponentBase -@inject TokenClient TokenClient @inject ArweaveService ArweaveService @inject ISnackbar Snackbar diff --git a/src/aoWebWallet/Shared/AddWalletComponent.razor b/src/aoWebWallet/Shared/AddWalletComponent.razor index 9a681d1..722b476 100644 --- a/src/aoWebWallet/Shared/AddWalletComponent.razor +++ b/src/aoWebWallet/Shared/AddWalletComponent.razor @@ -1,6 +1,5 @@ @using aoWebWallet.Models @inherits MvvmComponentBase -@inject TokenClient TokenClient @inject ISnackbar Snackbar diff --git a/src/aoWebWallet/Shared/AddWalletDialog.razor b/src/aoWebWallet/Shared/AddWalletDialog.razor index 47e4575..6ba01fd 100644 --- a/src/aoWebWallet/Shared/AddWalletDialog.razor +++ b/src/aoWebWallet/Shared/AddWalletDialog.razor @@ -1,7 +1,5 @@ @using aoWebWallet.Models @using aoWebWallet.Shared -@inherits MvvmComponentBase -@inject TokenClient TokenClient @inject ISnackbar Snackbar diff --git a/src/aoWebWallet/Shared/Components/TransactionComponent.razor b/src/aoWebWallet/Shared/Components/TransactionComponent.razor index 10082a4..2b734be 100644 --- a/src/aoWebWallet/Shared/Components/TransactionComponent.razor +++ b/src/aoWebWallet/Shared/Components/TransactionComponent.razor @@ -1,9 +1,10 @@ @using aoWebWallet.Models @inherits MvvmComponentBase +@inject TokenDataService dataService @if (transfer != null) { - var tokenData = BindingContext.TokenList.Data?.Where(x => x.TokenId == transfer.TokenId).Select(x => x.TokenData).FirstOrDefault(); + var tokenData = dataService.TokenList.Where(x => x.TokenId == transfer.TokenId).Select(x => x.TokenData).FirstOrDefault(); var isSend = SelectedAddress == transfer.From; var isReceive = SelectedAddress == transfer.To; string txUrl = $"transaction/{transfer.Id}"; diff --git a/src/aoWebWallet/Shared/ReceiveTokenDialog.razor b/src/aoWebWallet/Shared/ReceiveTokenDialog.razor index 7e473aa..d997fd7 100644 --- a/src/aoWebWallet/Shared/ReceiveTokenDialog.razor +++ b/src/aoWebWallet/Shared/ReceiveTokenDialog.razor @@ -1,16 +1,14 @@ @using aoWebWallet.Models @using aoWebWallet.Shared -@inherits MvvmComponentBase -@inject TokenClient TokenClient @inject ISnackbar Snackbar - + - @BindingContext.SelectedBalanceDataVM?.Token?.TokenData?.Name - @BindingContext.SelectedBalanceDataVM?.Token?.TokenData?.Ticker + @SelectedBalanceDataVM?.Token?.TokenData?.Name + @SelectedBalanceDataVM?.Token?.TokenData?.Ticker
@@ -18,7 +16,7 @@ How to receive tokens? Send tokens to this address: - @BindingContext.SelectedBalanceDataVM?.BalanceData?.Account + @SelectedBalanceDataVM?.BalanceData?.Account @@ -27,7 +25,7 @@ From aos: Command: - Send({ Target = "@BindingContext.SelectedBalanceDataVM?.Token?.TokenId", Action = "Transfer", Recipient = "@BindingContext.SelectedBalanceDataVM?.BalanceData?.Account", Quantity = "TOKEN_AMOUNT"}) + Send({ Target = "@SelectedBalanceDataVM?.Token?.TokenId", Action = "Transfer", Recipient = "@SelectedBalanceDataVM?.BalanceData?.Account", Quantity = "TOKEN_AMOUNT"})
@@ -39,6 +37,9 @@ @code { [CascadingParameter] MudDialogInstance MudDialog { get; set; } = default!; + [Parameter] + public BalanceDataViewModel? SelectedBalanceDataVM { get; set; } + public async Task Submit() { MudDialog.Close(DialogResult.Ok(true)); diff --git a/src/aoWebWallet/Shared/SendTokenDialog.razor b/src/aoWebWallet/Shared/SendTokenDialog.razor index 10eec0f..109dffe 100644 --- a/src/aoWebWallet/Shared/SendTokenDialog.razor +++ b/src/aoWebWallet/Shared/SendTokenDialog.razor @@ -1,19 +1,20 @@ @using aoWebWallet.Models @using aoWebWallet.Shared -@inherits MvvmComponentBase @inject TokenClient TokenClient @inject ISnackbar Snackbar +@inject MainViewModel MainViewModel +@inject WalletDetailViewModel WalletDetailViewModel - + - @BindingContext.SelectedBalanceDataVM?.Token?.TokenData?.Name - @BindingContext.SelectedBalanceDataVM?.Token?.TokenData?.Ticker + @SelectedBalanceDataVM?.Token?.TokenData?.Name + @SelectedBalanceDataVM?.Token?.TokenData?.Ticker - Available balance
@BalanceHelper.FormatBalance(BindingContext.SelectedBalanceDataVM?.BalanceData?.Balance, BindingContext.SelectedBalanceDataVM?.Token?.TokenData?.Denomination ?? 0)
+ Available balance
@BalanceHelper.FormatBalance(SelectedBalanceDataVM?.BalanceData?.Balance, SelectedBalanceDataVM?.Token?.TokenData?.Denomination ?? 0)
@if (!isConfirm) { @@ -34,14 +35,14 @@ } - Amount: @Amount @BindingContext.SelectedBalanceDataVM?.Token?.TokenData?.Ticker + Amount: @Amount @SelectedBalanceDataVM?.Token?.TokenData?.Ticker Receiver: @Address - + if (!string.IsNullOrEmpty(TransactionId)) { @@ -65,7 +66,7 @@ { if (string.IsNullOrEmpty(TransactionId)) { - if (!BindingContext.LastTransactionId.DataLoader.IsLoading) + if (!MainViewModel.LastTransactionId.DataLoader.IsLoading) { Confirm } @@ -80,6 +81,12 @@ @code { [CascadingParameter] MudDialogInstance MudDialog { get; set; } = default!; + [Parameter] + public BalanceDataViewModel? SelectedBalanceDataVM { get; set; } + + [Parameter] + public WalletDetailsViewModel? SelectedWallet { get; set; } + public string? Progress { get; set; } public string? Address { get; set; } public decimal Amount { get; set; } @@ -91,7 +98,7 @@ public MudButton? confButtonRef; //public int Denomination => BindingContext.SelectedBalanceDataVM?.Token?.TokenData?.Denomination ?? 0; - public string DenominationFormat => "F" + (BindingContext.SelectedBalanceDataVM?.Token?.TokenData?.Denomination ?? 1).ToString(); + public string DenominationFormat => "F" + (SelectedBalanceDataVM?.Token?.TokenData?.Denomination ?? 1).ToString(); public async Task Submit() { @@ -108,19 +115,19 @@ return; } - if (string.IsNullOrEmpty(BindingContext.SelectedBalanceDataVM?.BalanceData?.TokenId) - || BindingContext.SelectedBalanceDataVM.Token?.TokenData?.Denomination == null - || !BindingContext.SelectedBalanceDataVM.Token.TokenData.Denomination.HasValue) + if (string.IsNullOrEmpty(SelectedBalanceDataVM?.BalanceData?.TokenId) + || SelectedBalanceDataVM.Token?.TokenData?.Denomination == null + || !SelectedBalanceDataVM.Token.TokenData.Denomination.HasValue) return; - long amountLong = BalanceHelper.DecimalToTokenAmount(Amount, BindingContext.SelectedBalanceDataVM.Token.TokenData.Denomination!.Value); + long amountLong = BalanceHelper.DecimalToTokenAmount(Amount, SelectedBalanceDataVM.Token.TokenData.Denomination!.Value); if (amountLong <= 0) { Progress = "Amount has to be higher than 0."; StateHasChanged(); return; } - if (amountLong > BindingContext.SelectedBalanceDataVM?.BalanceData?.Balance) + if (amountLong > SelectedBalanceDataVM?.BalanceData?.Balance) { Progress = "Not enough balance available."; StateHasChanged(); @@ -141,9 +148,9 @@ return; } - if (string.IsNullOrEmpty(BindingContext.SelectedBalanceDataVM?.BalanceData?.TokenId) - || BindingContext.SelectedBalanceDataVM.Token?.TokenData?.Denomination == null - || !BindingContext.SelectedBalanceDataVM.Token.TokenData.Denomination.HasValue) + if (string.IsNullOrEmpty(SelectedBalanceDataVM?.BalanceData?.TokenId) + || SelectedBalanceDataVM.Token?.TokenData?.Denomination == null + || !SelectedBalanceDataVM.Token.TokenData.Denomination.HasValue) return; if (confButtonRef != null) @@ -151,24 +158,20 @@ this.StateHasChanged(); - if (BindingContext.SelectedWallet == null) + if (SelectedWallet == null) { throw new Exception("SelectedWallet is null"); } - long amountLong = BalanceHelper.DecimalToTokenAmount(Amount, BindingContext.SelectedBalanceDataVM.Token.TokenData.Denomination!.Value); - var result = await BindingContext.SendToken(BindingContext.SelectedWallet.Wallet, BindingContext.SelectedBalanceDataVM.Token.TokenId, Address, amountLong); + long amountLong = BalanceHelper.DecimalToTokenAmount(Amount, SelectedBalanceDataVM.Token.TokenData.Denomination!.Value); + var result = await MainViewModel.SendToken(SelectedWallet.Wallet, SelectedBalanceDataVM.Token.TokenId, Address, amountLong); TransactionId = result?.Id; - if (!string.IsNullOrEmpty(BindingContext.SelectedAddress)) - { - await BindingContext.LoadBalanceDataList(BindingContext.SelectedAddress); - await BindingContext.LoadTokenTransferList(BindingContext.SelectedAddress); - } + await WalletDetailViewModel.RefreshBalance(); if (TransactionId != null) { - await BindingContext.AddToLog(ActivityLogType.SendTransaction, TransactionId); + await MainViewModel.AddToLog(ActivityLogType.SendTransaction, TransactionId); } } diff --git a/src/aoWebWallet/ViewModels/MainViewModel.cs b/src/aoWebWallet/ViewModels/MainViewModel.cs index 2ac70f6..d2a28dd 100644 --- a/src/aoWebWallet/ViewModels/MainViewModel.cs +++ b/src/aoWebWallet/ViewModels/MainViewModel.cs @@ -20,13 +20,12 @@ public partial class MainViewModel : ObservableRecipient { private const string CLAIM_PROCESS_ID = "5Mv1TBYZvKjNlWUpH78hWORIhqj1uqn_wdkJrA7emfU"; - private readonly DataService dataService; + private readonly TokenDataService dataService; private readonly TokenClient tokenClient; private readonly StorageService storageService; private readonly ArweaveService arweaveService; private readonly GraphqlClient graphqlClient; private readonly ISnackbar snackbar; - private readonly IClipLazor clipboard; private readonly MemoryDataCache memoryDataCache; [ObservableProperty] @@ -42,58 +41,24 @@ public partial class MainViewModel : ObservableRecipient [NotifyPropertyChangedRecipients] private string? computeUnitUrl; - [ObservableProperty] - private string? selectedAddress; - - [ObservableProperty] - private WalletDetailsViewModel? selectedWallet; - - [ObservableProperty] - private int? selectedWalletIndex; - - [ObservableProperty] - private BalanceDataViewModel? selectedBalanceDataVM; - - [ObservableProperty] - private string? selectedTransactionId; - - [ObservableProperty] - private string? selectedTokenId; - [ObservableProperty] private UserSettings? userSettings; - [ObservableProperty] - private bool canClaim1; - [ObservableProperty] - private bool canClaim2; - [ObservableProperty] - private bool canClaim3; public DataLoaderViewModel LastTransactionId { get; set; } = new(); - public DataLoaderViewModel> TokenList { get; set; } = new(); - public DataLoaderViewModel>> BalanceDataList { get; set; } = new(); public DataLoaderViewModel> WalletList { get; set; } = new(); - public DataLoaderViewModel> TokenTransferList { get; set; } = new(); - public DataLoaderViewModel SelectedTransaction { get; set; } = new(); public DataLoaderViewModel>> ProcessesDataList { get; set; } = new(); - public DataLoaderViewModel SelectedProcessData { get; set; } = new(); - - - //TODO: - //Actions List (optional? address) /// /// Gets the responsible for loading the source markdown docs. /// - public MainViewModel(DataService dataService, + public MainViewModel(TokenDataService dataService, TokenClient tokenClient, StorageService storageService, ArweaveService arweaveService, GraphqlClient graphqlClient, ISnackbar snackbar, - IClipLazor clipboard, MemoryDataCache memoryDataCache) : base() { this.dataService = dataService; @@ -102,119 +67,15 @@ public MainViewModel(DataService dataService, this.arweaveService = arweaveService; this.graphqlClient = graphqlClient; this.snackbar = snackbar; - this.clipboard = clipboard; this.memoryDataCache = memoryDataCache; } public async Task AddToLog(ActivityLogType type, string id) { await storageService.AddToLog(type, id); - await SetClaims(); } - public Task LoadTokenTransferList(string address) => TokenTransferList.DataLoader.LoadAsync(async () => - { - TokenTransferList.Data = new(); - - var incoming = await graphqlClient.GetTransactionsIn(address); - var outgoing = await graphqlClient.GetTransactionsOut(address); - var outgoingProcess = await graphqlClient.GetTransactionsOutFromProcess(address); - - var all = incoming.Concat(outgoing).Concat(outgoingProcess); - - TokenTransferList.Data = all.OrderByDescending(x => x.Timestamp).ToList(); - - var allTokenIds = all.Select(x => x.TokenId).Distinct().ToList(); - TryAddTokenIds(allTokenIds); - - return TokenTransferList.Data; - }); - - - public Task LoadTokenTransferListForToken(string? tokenId) => TokenTransferList.DataLoader.LoadAsync(async () => - { - TokenTransferList.Data = new(); - - if (!string.IsNullOrWhiteSpace(tokenId)) - { - var all = await graphqlClient.GetTransactionsForToken(tokenId); - - TokenTransferList.Data = all.ToList(); - - var allTokenIds = all.Select(x => x.TokenId).Distinct().ToList(); - TryAddTokenIds(allTokenIds); - - return TokenTransferList.Data; - } - else - { - return null; - } - }); - - public Task LoadTokenList(bool force = false) => TokenList.DataLoader.LoadAsync(async () => - { - if (TokenList.Data == null || force) - { - TokenList.Data = new(); - await foreach (var item in dataService.LoadTokenDataAsync()) - { - TokenList.Data.Add(item); - TokenList.ForcePropertyChanged(); - } - } - TokenList.ForcePropertyChanged(); - return TokenList.Data; - }); - - public Task LoadSelectedTokenTransfer() => SelectedTransaction.DataLoader.LoadAsync(async () => - { - SelectedTransaction.Data = null; - if (!string.IsNullOrEmpty(SelectedTransactionId)) - { - var result = await graphqlClient.GetTransactionsById(SelectedTransactionId); - - SelectedTransaction.Data = result; - - if (result != null) - TryAddTokenIds(new List() { result.TokenId }); - - return result; - } - else - { - return null; - } - }); - - - public Task LoadBalanceDataList(string address) => BalanceDataList.DataLoader.LoadAsync(async () => - { - //First clear - BalanceDataList.Data = null; - var tokens = TokenList.Data ?? new(); - - var result = new List>(); - - foreach (var token in tokens.Where(x => x.IsVisible)) - { - var balanceData = new DataLoaderViewModel(); - balanceData.Data = new BalanceDataViewModel { Token = token }; - - balanceData.DataLoader.LoadAsync(async () => - { - var balanceData = await tokenClient.GetBalance(token.TokenId, address); - return new BalanceDataViewModel() { BalanceData = balanceData, Token = token }; - }, (x) => { balanceData.Data = x; BalanceDataList.ForcePropertyChanged(); }); - result.Add(balanceData); - } - - BalanceDataList.Data = result; - - return result; - - }); - + public Task LoadProcessesDataList() => ProcessesDataList.DataLoader.LoadAsync(async () => { ProcessesDataList.Data = null; @@ -246,67 +107,7 @@ public Task LoadProcessesDataList() => ProcessesDataList.DataLoader.LoadAsync(as }); - public async Task LoadSelectedWalletProcessData() - { - if (string.IsNullOrEmpty(SelectedAddress)) - return; - - SelectedProcessData.Data = null; - - var address = SelectedAddress; - - SelectedProcessData.Data = new WalletProcessDataViewModel { Address = address }; - - SelectedProcessData.DataLoader.LoadAsync(() => - { - return memoryDataCache!.GetAsync($"{nameof(LoadProcessesDataList)}-{address}", async () => - { - var data = await graphqlClient.GetAoProcessesForAddress(address); - return new WalletProcessDataViewModel() { Address = address, Processes = data }; - }, TimeSpan.FromMinutes(1)); - - - }, (x) => { SelectedProcessData.Data = x; }); - } - - public async Task LoadSelectedWalletOwnerData() - { - if (string.IsNullOrEmpty(SelectedAddress)) - return; - - if (SelectedWallet?.Wallet.OwnerAddress != null) - { - CheckCanOwnerOfSelectedWalletSend(); - return; - } - - var address = SelectedAddress; - - var ownerAddress = await memoryDataCache!.GetAsync($"{nameof(LoadSelectedWalletOwnerData)}-{address}", async () => - { - var data = await graphqlClient.GetOwnerForAoProcessAddress(address); - return data?.Owner; - }, TimeSpan.FromMinutes(1)); - - if (SelectedWallet != null) - SelectedWallet.Wallet.OwnerAddress = ownerAddress; - - CheckCanOwnerOfSelectedWalletSend(); - } - - private void CheckCanOwnerOfSelectedWalletSend() - { - if (!string.IsNullOrEmpty(SelectedWallet?.Wallet.OwnerAddress)) - { - var owner = WalletList.Data?.Where(x => x.Address == SelectedWallet.Wallet.OwnerAddress).FirstOrDefault(); - if (owner != null) - { - var details = new WalletDetailsViewModel(owner); - var canOwnerSend = details?.CanSend ?? false; - SelectedWallet.OwnerCanSend = canOwnerSend; - } - } - } + public async Task LoadWalletList(bool force = false) { @@ -330,60 +131,10 @@ public async Task LoadWalletList(bool force = false) } } - public async Task AddToken(string tokenId, TokenData data, bool isUserAdded = false) - { - BalanceDataList.Data = null; - var newToken = await storageService.AddToken(tokenId, data, isUserAdded); - var existing = TokenList.Data?.Where(x => x.TokenId == newToken.TokenId).FirstOrDefault(); - if (existing == null) - { - if (TokenList.Data == null) - TokenList.Data = new(); - - TokenList.Data.Add(newToken); - TokenList.ForcePropertyChanged(); - } - else - { - existing = newToken; - } - - if (!string.IsNullOrEmpty(SelectedAddress)) - { - await LoadBalanceDataList(SelectedAddress); - } - - if (!string.IsNullOrEmpty(SelectedTransactionId)) - { - await this.LoadSelectedTokenTransfer(); - } - - await this.SetClaims(); - } - public async Task DeleteToken(string tokenId) - { - BalanceDataList.Data = null; - await storageService.DeleteToken(tokenId); - await this.LoadTokenList(force: true); - } - - public async Task TokenToggleVisibility(string tokenId) - { - var all = TokenList.Data ?? new(); - var token = all.Where(x => x.TokenId == tokenId).FirstOrDefault(); - if (token != null) - { - token.IsVisible = !token.IsVisible; - await storageService.SaveTokenList(all); - await this.LoadTokenList(force: true); - } - } - public async Task SaveWallet(Wallet wallet) { await storageService.SaveWallet(wallet); await LoadWalletList(force: true); - await SetClaims(); } public async Task DeleteWallet(Wallet wallet) @@ -417,110 +168,21 @@ public async Task ClearUserData() { memoryDataCache.Clear(); - await storageService.SaveTokenList(new()); + await dataService.Clear(); await storageService.SaveWalletList(new()); await storageService.SaveUserSettings(new()); await storageService.ClearActivityLog(); //Clear all data - TokenList.Data = null; WalletList = new(); - BalanceDataList.Data = null; - } - - public async Task TryAddTokenIds(List allTokenIds) - { - foreach (var tokenId in allTokenIds) - { - if (string.IsNullOrEmpty(tokenId) || tokenId.Length != 43) - continue; - - var exist = TokenList.Data?.Where(x => x.TokenId == tokenId).Any() ?? false; - if (exist) - continue; - - var data = await tokenClient.GetTokenMetaData(tokenId); - if (data != null) - { - await AddToken(tokenId, data, isUserAdded: false); - } - } - } - - - partial void OnSelectedAddressChanged(string? value) - { - SelectWallet(value); - LoadSelectedWalletProcessData(); - LoadSelectedWalletOwnerData(); - - if (value != null) - this.AddToLog(ActivityLogType.ViewAddress, value); - } - - partial void OnSelectedWalletChanged(WalletDetailsViewModel? value) - { - CheckHasArConnectExtension(); - } - - private async Task SelectWallet(string? address) - { - if (!string.IsNullOrEmpty(address)) - { - if (this.WalletList.Data == null) - { - await LoadWalletList(); - } - - var all = this.WalletList.Data ?? new(); - var current = all.Where(x => x.Address == address).FirstOrDefault(); - if (current != null) - { - SelectedWallet = new WalletDetailsViewModel(current); - var indexOf = all.IndexOf(current); - SelectedWalletIndex = (indexOf % 5) + 1; - - } - else - { - var tempWallet = new Wallet - { - Address = address, - AddedDate = DateTimeOffset.Now, - LastUsedDate = DateTimeOffset.UtcNow, - Name = null, - Source = WalletTypes.Explorer - }; - SelectedWallet = new WalletDetailsViewModel(tempWallet); - SelectedWalletIndex = 5; - } - - this.LoadBalanceDataList(address); - this.LoadTokenTransferList(address); - - } - else - { - SelectedWallet = null; - SelectedWalletIndex = null; - } + //BalanceDataList.Data = null; } - partial void OnSelectedTransactionIdChanged(string? value) - { - this.LoadSelectedTokenTransfer(); + - if (value != null) - this.AddToLog(ActivityLogType.ViewTransaction, value); - } - - partial void OnSelectedTokenIdChanged(string? value) - { - this.LoadTokenTransferListForToken(value); - if (value != null) - this.AddToLog(ActivityLogType.ViewToken, value); - } + + partial void OnComputeUnitUrlChanged(string? value) { @@ -530,7 +192,6 @@ partial void OnComputeUnitUrlChanged(string? value) { //storageService.SetApiUrl(value); - dataService.Init(value); } } @@ -553,103 +214,9 @@ public async Task SaveUserSettings() } } - public async Task AddWalletAsReadonly() - { - if (SelectedWallet != null) - { - SelectedWallet.Wallet.Source = WalletTypes.Manual; - if(SelectedWallet.Wallet.OwnerAddress != null) - SelectedWallet.Wallet.Source = WalletTypes.AoProcess; - - await storageService.SaveWallet(SelectedWallet.Wallet); - await LoadWalletList(force: true); + - snackbar.Add("Wallet added to list.", Severity.Info); - - } - } - - public async Task SetClaims() - { - var viewTokenActivity = await storageService.GetLog(ActivityLogType.ViewToken); - var viewTransactionctivity = await storageService.GetLog(ActivityLogType.ViewTransaction); - var viewAddressActivity = await storageService.GetLog(ActivityLogType.ViewAddress); - var sendTransactionActivity = await storageService.GetLog(ActivityLogType.SendTransaction); - - CanClaim1 = sendTransactionActivity.Count > 0; - CanClaim2 = CanClaim1 && sendTransactionActivity.Count > 1 && (WalletList.Data?.Count() > 1 || TokenList.Data?.Count() > 6); - CanClaim3 = CanClaim2 && sendTransactionActivity.Count > 1 && WalletList.Data?.Count() > 2 && viewTokenActivity.Count > 0 && viewAddressActivity.Count > 2 && viewTransactionctivity.Count > 1; - - Console.WriteLine("1:" + CanClaim1); - } - - public async Task Claim1() - { - if (UserSettings != null) - { - var tx = await Claim(1); - if (tx != null && !string.IsNullOrEmpty(tx.Id)) - { - UserSettings.Claimed1 = true; - await storageService.SaveUserSettings(UserSettings); - - snackbar.Add("Claim 1 successful. You received 10 AOWW!", Severity.Info); - - if (SelectedAddress != null) - await LoadBalanceDataList(this.SelectedAddress); - - } - else - { - snackbar.Add("Claim was not successful.", Severity.Error); - } - } - - } - public async Task Claim2() - { - if (UserSettings != null) - { - var tx = await Claim(2); - if (tx != null && !string.IsNullOrEmpty(tx.Id)) - { - UserSettings.Claimed2 = true; - await storageService.SaveUserSettings(UserSettings); - - snackbar.Add("Claim 2 successful. You received 20 AOWW!", Severity.Info); - - if (SelectedAddress != null) - await LoadBalanceDataList(this.SelectedAddress); - - } - else - { - snackbar.Add("Claim was not successful.", Severity.Error); - } - } - } - public async Task Claim3() - { - if (UserSettings != null) - { - var tx = await Claim(3); - if (tx != null && !string.IsNullOrEmpty(tx.Id)) - { - UserSettings.Claimed3 = true; - await storageService.SaveUserSettings(UserSettings); - - snackbar.Add("Claim 3 successful. You received 30 AOWW!", Severity.Info); - - if (SelectedAddress != null) - await LoadBalanceDataList(this.SelectedAddress); - - } - else - { - snackbar.Add("Claim was not successful.", Severity.Error); - } - } - } + public async Task DisconnectArWallet() { @@ -666,44 +233,18 @@ public async Task CheckHasArConnectExtension() public async Task GetActiveArConnectAddress() { - //if (this.WalletList.Data != null) - //{ - // var wallets = this.WalletList.Data.Where(x => x.IsConnected && x.Source == WalletTypes.ArConnect); - // foreach (var wallet in wallets) - // { - // wallet.IsConnected = false; - // } - // this.WalletList.ForcePropertyChanged(); - // await storageService.SaveWalletList(this.WalletList.Data); - //} - if (HasArConnectExtension.HasValue && HasArConnectExtension.Value) { ActiveArConnectAddress = await arweaveService.GetActiveAddress(); - if (this.SelectedWallet != null) - { - this.SelectedWallet.IsConnected = SelectedWallet.Wallet.Address == ActiveArConnectAddress; - } + //if (this.SelectedWallet != null) + //{ + // this.SelectedWallet.IsConnected = SelectedWallet.Wallet.Address == ActiveArConnectAddress; + //} } } - public async Task CopyToClipboard(string? text) - { - bool isSupported = await clipboard.IsClipboardSupported(); - bool isWritePermitted = await clipboard.IsPermitted(PermissionCommand.Write); - if (isSupported && !string.IsNullOrEmpty(text)) - { - if (isWritePermitted) - { - var isCopied = await clipboard.WriteTextAsync(text.AsMemory()); - if (isCopied) - { - snackbar.Add("Address copied to clipboard", Severity.Success); - } - } - } - } + public Task SendToken(Wallet wallet, string tokenId, string address, long amount) { diff --git a/src/aoWebWallet/ViewModels/TokenDetailViewModel.cs b/src/aoWebWallet/ViewModels/TokenDetailViewModel.cs new file mode 100644 index 0000000..ad7e3e4 --- /dev/null +++ b/src/aoWebWallet/ViewModels/TokenDetailViewModel.cs @@ -0,0 +1,62 @@ +using aoWebWallet.Models; +using aoWebWallet.Services; +using CommunityToolkit.Mvvm.ComponentModel; +using webvNext.DataLoader; +using static MudBlazor.Colors; + +namespace aoWebWallet.ViewModels +{ + public class TokenDetailViewModel : ObservableObject + { + private readonly MainViewModel mainViewModel; + private readonly GraphqlClient graphqlClient; + private readonly TokenDataService dataService; + + public DataLoaderViewModel Token { get; set; } = new(); + + public DataLoaderViewModel> TokenTransferList { get; set; } = new(); + + public TokenDetailViewModel(MainViewModel mainViewModel, GraphqlClient graphqlClient, TokenDataService dataService) + { + this.mainViewModel = mainViewModel; + this.graphqlClient = graphqlClient; + this.dataService = dataService; + } + + + public async Task Initialize(string tokenId) + { + this.LoadTokenData(tokenId); + + this.LoadTokenTransferListForToken(tokenId); + + mainViewModel.AddToLog(ActivityLogType.ViewToken, tokenId); + } + + public Task LoadTokenData(string tokenId) => Token.DataLoader.LoadAsync(async () => + { + Token.Data = null; + + await dataService.LoadTokenList(); + + return dataService.TokenList; + + }); + + public Task LoadTokenTransferListForToken(string tokenId) => TokenTransferList.DataLoader.LoadAsync(async () => + { + TokenTransferList.Data = new(); + + var all = await graphqlClient.GetTransactionsForToken(tokenId); + + TokenTransferList.Data = all.ToList(); + + var allTokenIds = all.Select(x => x.TokenId).Distinct().ToList(); + dataService.TryAddTokenIds(allTokenIds); + + return TokenTransferList.Data; + + }); + + } +} diff --git a/src/aoWebWallet/ViewModels/TransactionDetailViewModel.cs b/src/aoWebWallet/ViewModels/TransactionDetailViewModel.cs new file mode 100644 index 0000000..e43cf28 --- /dev/null +++ b/src/aoWebWallet/ViewModels/TransactionDetailViewModel.cs @@ -0,0 +1,49 @@ +using aoWebWallet.Models; +using aoWebWallet.Services; +using CommunityToolkit.Mvvm.ComponentModel; +using webvNext.DataLoader; + +namespace aoWebWallet.ViewModels +{ + public class TransactionDetailViewModel : ObservableObject + { + private readonly MainViewModel mainViewModel; + private readonly GraphqlClient graphqlClient; + private readonly TokenDataService dataService; + + public DataLoaderViewModel SelectedTransaction { get; set; } = new(); + public DataLoaderViewModel> TokenTransferList { get; set; } = new(); + + public TransactionDetailViewModel(MainViewModel mainViewModel, GraphqlClient graphqlClient, TokenDataService dataService) + { + this.mainViewModel = mainViewModel; + this.graphqlClient = graphqlClient; + this.dataService = dataService; + } + + + public async Task Initialize(string txId) + { + this.LoadSelectedTokenTransfer(txId); + + if (txId != null) + mainViewModel.AddToLog(ActivityLogType.ViewTransaction, txId); + } + + + + public Task LoadSelectedTokenTransfer(string txId) => SelectedTransaction.DataLoader.LoadAsync(async () => + { + SelectedTransaction.Data = null; + var result = await graphqlClient.GetTransactionsById(txId); + + SelectedTransaction.Data = result; + + if (result != null) + dataService.TryAddTokenIds(new List() { result.TokenId }); + + return result; + }); + + } +} diff --git a/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs b/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs new file mode 100644 index 0000000..31c9944 --- /dev/null +++ b/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs @@ -0,0 +1,346 @@ +using aoWebWallet.Models; +using aoWebWallet.Services; +using ArweaveAO; +using CommunityToolkit.Mvvm.ComponentModel; +using MudBlazor; +using System.Net; +using webvNext.DataLoader; +using webvNext.DataLoader.Cache; +using static MudBlazor.Colors; + +namespace aoWebWallet.ViewModels +{ + public partial class WalletDetailViewModel : ObservableObject + { + private readonly MainViewModel mainViewModel; + private readonly GraphqlClient graphqlClient; + private readonly TokenDataService dataService; + private readonly TokenClient tokenClient; + private readonly StorageService storageService; + private readonly ISnackbar snackbar; + private readonly MemoryDataCache memoryDataCache; + + private string? selectedAddress = null; + + + [ObservableProperty] + private bool canClaim1; + [ObservableProperty] + private bool canClaim2; + [ObservableProperty] + private bool canClaim3; + + public int? SelectedWalletIndex { get; set; } + + + public WalletDetailsViewModel? SelectedWallet { get; set; } + + + public DataLoaderViewModel SelectedProcessData { get; set; } = new(); + + public DataLoaderViewModel>> BalanceDataList { get; set; } = new(); + + public DataLoaderViewModel> TokenTransferList { get; set; } = new(); + + + public WalletDetailViewModel(MainViewModel mainViewModel, + GraphqlClient graphqlClient, + TokenDataService dataService, + TokenClient tokenClient, + StorageService storageService, + ISnackbar snackbar, + MemoryDataCache memoryDataCache) + { + this.mainViewModel = mainViewModel; + this.graphqlClient = graphqlClient; + this.dataService = dataService; + this.tokenClient = tokenClient; + this.storageService = storageService; + this.snackbar = snackbar; + this.memoryDataCache = memoryDataCache; + } + + + public async Task Initialize(string address) + { + selectedAddress = address; + + await SelectWallet(address); + + await LoadSelectedWalletProcessData(address); + await LoadSelectedWalletOwnerData(address); + + mainViewModel.CheckHasArConnectExtension(); + + SetClaims(); + + mainViewModel.AddToLog(ActivityLogType.ViewAddress, address); + } + + public async Task RefreshTokenTransferList() + { + if (selectedAddress != null) + { + await LoadTokenTransferList(selectedAddress); + } + } + + public async Task RefreshBalanceDataList() + { + if (selectedAddress != null) + { + await LoadBalanceDataList(selectedAddress); + } + } + + public async Task RefreshBalance() + { + if (selectedAddress != null) + { + await LoadTokenTransferList(selectedAddress); + await LoadBalanceDataList(selectedAddress); + } + } + + //public async Task Refresh() + //{ + // if (selectedAddress != null) + // await Initialize(selectedAddress); + //} + + private async Task SelectWallet(string? address) + { + if (!string.IsNullOrEmpty(address)) + { + if (mainViewModel.WalletList.Data == null) + { + await mainViewModel.LoadWalletList(); + } + + var all = mainViewModel.WalletList.Data ?? new(); + var current = all.Where(x => x.Address == address).FirstOrDefault(); + if (current != null) + { + SelectedWallet = new WalletDetailsViewModel(current); + var indexOf = all.IndexOf(current); + SelectedWalletIndex = (indexOf % 5) + 1; + + } + else + { + var tempWallet = new Wallet + { + Address = address, + AddedDate = DateTimeOffset.Now, + LastUsedDate = DateTimeOffset.UtcNow, + Name = null, + Source = WalletTypes.Explorer + }; + SelectedWallet = new WalletDetailsViewModel(tempWallet); + SelectedWalletIndex = 5; + } + + this.LoadBalanceDataList(address); + this.LoadTokenTransferList(address); + + } + else + { + SelectedWallet = null; + SelectedWalletIndex = null; + } + } + + public Task LoadTokenTransferList(string address) => TokenTransferList.DataLoader.LoadAsync(async () => + { + TokenTransferList.Data = new(); + + var incoming = await graphqlClient.GetTransactionsIn(address); + var outgoing = await graphqlClient.GetTransactionsOut(address); + var outgoingProcess = await graphqlClient.GetTransactionsOutFromProcess(address); + + var all = incoming.Concat(outgoing).Concat(outgoingProcess); + + TokenTransferList.Data = all.OrderByDescending(x => x.Timestamp).ToList(); + + var allTokenIds = all.Select(x => x.TokenId).Distinct().ToList(); + dataService.TryAddTokenIds(allTokenIds); + + return TokenTransferList.Data; + }); + + + + public Task LoadBalanceDataList(string address) => BalanceDataList.DataLoader.LoadAsync(async () => + { + //First clear + BalanceDataList.Data = null; + + var result = new List>(); + + foreach (var token in dataService.TokenList.Where(x => x.IsVisible)) + { + var balanceData = new DataLoaderViewModel(); + balanceData.Data = new BalanceDataViewModel { Token = token }; + + balanceData.DataLoader.LoadAsync(async () => + { + var balanceData = await tokenClient.GetBalance(token.TokenId, address); + return new BalanceDataViewModel() { BalanceData = balanceData, Token = token }; + }, (x) => { balanceData.Data = x; BalanceDataList.ForcePropertyChanged(); }); + result.Add(balanceData); + } + + BalanceDataList.Data = result; + + return result; + + }); + + public async Task LoadSelectedWalletProcessData(string address) + { + SelectedProcessData.Data = new WalletProcessDataViewModel { Address = address }; + + SelectedProcessData.DataLoader.LoadAsync(() => + { + return memoryDataCache!.GetAsync($"{nameof(MainViewModel.LoadProcessesDataList)}-{address}", async () => + { + var data = await graphqlClient.GetAoProcessesForAddress(address); + return new WalletProcessDataViewModel() { Address = address, Processes = data }; + }, TimeSpan.FromMinutes(1)); + + + }, (x) => { SelectedProcessData.Data = x; }); + } + + public async Task LoadSelectedWalletOwnerData(string address) + { + var ownerAddress = await memoryDataCache!.GetAsync($"{nameof(LoadSelectedWalletOwnerData)}-{address}", async () => + { + var data = await graphqlClient.GetOwnerForAoProcessAddress(address); + return data?.Owner; + }, TimeSpan.FromMinutes(1)); + + if (SelectedWallet != null) + SelectedWallet.Wallet.OwnerAddress = ownerAddress; + + CheckCanOwnerOfSelectedWalletSend(); + } + + private void CheckCanOwnerOfSelectedWalletSend() + { + if (!string.IsNullOrEmpty(SelectedWallet?.Wallet.OwnerAddress)) + { + var owner = mainViewModel.WalletList.Data?.Where(x => x.Address == SelectedWallet.Wallet.OwnerAddress).FirstOrDefault(); + if (owner != null) + { + var details = new WalletDetailsViewModel(owner); + var canOwnerSend = details?.CanSend ?? false; + SelectedWallet.OwnerCanSend = canOwnerSend; + } + } + } + + public async Task AddWalletAsReadonly() + { + if (SelectedWallet != null) + { + SelectedWallet.Wallet.Source = WalletTypes.Manual; + if (SelectedWallet.Wallet.OwnerAddress != null) + SelectedWallet.Wallet.Source = WalletTypes.AoProcess; + + await storageService.SaveWallet(SelectedWallet.Wallet); + await mainViewModel.LoadWalletList(force: true); + + snackbar.Add("Wallet added to list.", Severity.Info); + + } + } + + + + + public async Task SetClaims() + { + var viewTokenActivity = await storageService.GetLog(ActivityLogType.ViewToken); + var viewTransactionctivity = await storageService.GetLog(ActivityLogType.ViewTransaction); + var viewAddressActivity = await storageService.GetLog(ActivityLogType.ViewAddress); + var sendTransactionActivity = await storageService.GetLog(ActivityLogType.SendTransaction); + + CanClaim1 = sendTransactionActivity.Count > 0; + CanClaim2 = CanClaim1 && sendTransactionActivity.Count > 1 && (mainViewModel.WalletList.Data?.Count() > 1 || dataService.TokenList.Count() > 6); + CanClaim3 = CanClaim2 && sendTransactionActivity.Count > 1 && mainViewModel.WalletList.Data?.Count() > 2 && viewTokenActivity.Count > 0 && viewAddressActivity.Count > 2 && viewTransactionctivity.Count > 1; + + Console.WriteLine("1:" + CanClaim1); + } + + public async Task Claim1() + { + if (mainViewModel.UserSettings != null) + { + var tx = await mainViewModel.Claim(1); + if (tx != null && !string.IsNullOrEmpty(tx.Id)) + { + mainViewModel.UserSettings.Claimed1 = true; + await storageService.SaveUserSettings(mainViewModel.UserSettings); + + snackbar.Add("Claim 1 successful. You received 10 AOWW!", Severity.Info); + + if (SelectedWallet != null) + await LoadBalanceDataList(this.SelectedWallet.Wallet.Address); + + } + else + { + snackbar.Add("Claim was not successful.", Severity.Error); + } + } + + } + public async Task Claim2() + { + if (mainViewModel.UserSettings != null) + { + var tx = await mainViewModel.Claim(2); + if (tx != null && !string.IsNullOrEmpty(tx.Id)) + { + mainViewModel.UserSettings.Claimed2 = true; + await storageService.SaveUserSettings(mainViewModel.UserSettings); + + snackbar.Add("Claim 2 successful. You received 20 AOWW!", Severity.Info); + + if (SelectedWallet != null) + await LoadBalanceDataList(this.SelectedWallet.Wallet.Address); + + } + else + { + snackbar.Add("Claim was not successful.", Severity.Error); + } + } + } + public async Task Claim3() + { + if (mainViewModel.UserSettings != null) + { + var tx = await mainViewModel.Claim(3); + if (tx != null && !string.IsNullOrEmpty(tx.Id)) + { + mainViewModel.UserSettings.Claimed3 = true; + await storageService.SaveUserSettings(mainViewModel.UserSettings); + + snackbar.Add("Claim 3 successful. You received 30 AOWW!", Severity.Info); + + if (SelectedWallet != null) + await LoadBalanceDataList(this.SelectedWallet.Wallet.Address); + + } + else + { + snackbar.Add("Claim was not successful.", Severity.Error); + } + } + } + + } +} From 3755e49da64228e6c78b18b636dcf0abde698df2 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Fri, 19 Apr 2024 17:37:34 +0200 Subject: [PATCH 06/63] Fixing the build --- src/aoWebWallet/Pages/Tokens.razor.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/aoWebWallet/Pages/Tokens.razor.cs b/src/aoWebWallet/Pages/Tokens.razor.cs index b51bbca..51a579d 100644 --- a/src/aoWebWallet/Pages/Tokens.razor.cs +++ b/src/aoWebWallet/Pages/Tokens.razor.cs @@ -9,11 +9,14 @@ protected override void OnInitialized() base.OnInitialized(); } - protected override async Task LoadDataAsync() + protected override async Task OnAfterRenderAsync(bool firstRender) { - await dataService.LoadTokenList(); + if (firstRender) + { + await dataService.LoadTokenList(); + } - await base.LoadDataAsync(); + await base.OnAfterRenderAsync(firstRender); } } From b3fe20d2e1a87a83f907d8035d12e43d61cee051 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Tue, 23 Apr 2024 13:17:01 +0200 Subject: [PATCH 07/63] ViewModel refactoring bug fixes --- src/aoWebWallet/Pages/MvvmComponentBase.cs | 4 +- src/aoWebWallet/Pages/TokenDetail.razor | 36 +++++++------ src/aoWebWallet/Pages/TokenDetail.razor.cs | 5 +- src/aoWebWallet/Pages/Tokens.razor.cs | 2 + src/aoWebWallet/Pages/TransactionDetail.razor | 1 - .../Pages/TransactionDetail.razor.cs | 3 +- src/aoWebWallet/Pages/WalletDetail.razor | 25 +++++----- src/aoWebWallet/Pages/WalletDetail.razor.cs | 20 +++++++- src/aoWebWallet/Services/TokenDataService.cs | 17 ++++--- src/aoWebWallet/Shared/AddTokenDialog.razor | 3 +- .../Components/ActionInputComponent.razor | 2 - .../Shared/ReceiveTokenDialog.razor | 4 +- src/aoWebWallet/Shared/SendTokenDialog.razor | 8 +-- .../ViewModels/BalanceDataViewModel.cs | 3 +- src/aoWebWallet/ViewModels/MainViewModel.cs | 16 ------ .../ViewModels/TokenDetailViewModel.cs | 8 +-- .../ViewModels/TransactionDetailViewModel.cs | 2 +- .../ViewModels/WalletDetailViewModel.cs | 50 ++++++++++++------- .../ViewModels/WalletDetailsViewModel.cs | 1 - src/webvNext.DataLoader/DataLoader.cs | 46 +++++++++-------- 20 files changed, 136 insertions(+), 120 deletions(-) diff --git a/src/aoWebWallet/Pages/MvvmComponentBase.cs b/src/aoWebWallet/Pages/MvvmComponentBase.cs index b7303d2..02abfff 100644 --- a/src/aoWebWallet/Pages/MvvmComponentBase.cs +++ b/src/aoWebWallet/Pages/MvvmComponentBase.cs @@ -57,10 +57,10 @@ internal async void BindingContext_PropertyChanged(object? sender, System.Compon internal void ObjWatch_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { this.StateHasChanged(); + Console.WriteLine("Obj State changed"); } private void Obj_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { - Console.WriteLine("Collection change " + this.GetType()); this.StateHasChanged(); } @@ -98,8 +98,6 @@ public virtual void Dispose() { obj.CollectionChanged -= Obj_CollectionChanged; } - - Console.WriteLine("Dispose " + this.GetType().FullName); } } } diff --git a/src/aoWebWallet/Pages/TokenDetail.razor b/src/aoWebWallet/Pages/TokenDetail.razor index 982643e..f83ee85 100644 --- a/src/aoWebWallet/Pages/TokenDetail.razor +++ b/src/aoWebWallet/Pages/TokenDetail.razor @@ -12,25 +12,23 @@ Token Explorer - + - @if (dataService.TokenList != null) + @if (BindingContext.Token.Data != null) { - var token = dataService.TokenList.Where(x => x.TokenId == TokenId).FirstOrDefault(); - if (token != null) - { - - - - - @token.TokenData?.Name - @token.TokenData?.Ticker - @token.TokenId - + var token = BindingContext.Token.Data; + + + + + + @token.TokenData?.Name + @token.TokenData?.Ticker + @token.TokenId - - } + + } @@ -43,10 +41,10 @@ Transactions - @foreach (var transfer in BindingContext.TokenTransferList.Data) - { - - } + @foreach (var transfer in BindingContext.TokenTransferList.Data) + { + + } } diff --git a/src/aoWebWallet/Pages/TokenDetail.razor.cs b/src/aoWebWallet/Pages/TokenDetail.razor.cs index 02fe71b..28ff237 100644 --- a/src/aoWebWallet/Pages/TokenDetail.razor.cs +++ b/src/aoWebWallet/Pages/TokenDetail.razor.cs @@ -7,7 +7,8 @@ public partial class TokenDetail : MvvmComponentBase { protected override void OnInitialized() { - //WatchDataLoaderVM(dataService.TokenList); + WatchCollection(dataService.TokenList); + WatchDataLoaderVM(BindingContext.Token); WatchDataLoaderVM(BindingContext.TokenTransferList); base.OnInitialized(); @@ -28,7 +29,7 @@ protected override async Task OnParametersSetAsync() protected override async Task LoadDataAsync() { - await dataService.LoadTokenList(); + //await dataService.LoadTokenList(); await base.LoadDataAsync(); diff --git a/src/aoWebWallet/Pages/Tokens.razor.cs b/src/aoWebWallet/Pages/Tokens.razor.cs index 51a579d..9904aa6 100644 --- a/src/aoWebWallet/Pages/Tokens.razor.cs +++ b/src/aoWebWallet/Pages/Tokens.razor.cs @@ -6,6 +6,8 @@ public partial class Tokens : MvvmComponentBase { protected override void OnInitialized() { + WatchCollection(dataService.TokenList); + base.OnInitialized(); } diff --git a/src/aoWebWallet/Pages/TransactionDetail.razor b/src/aoWebWallet/Pages/TransactionDetail.razor index 662fada..ef1da07 100644 --- a/src/aoWebWallet/Pages/TransactionDetail.razor +++ b/src/aoWebWallet/Pages/TransactionDetail.razor @@ -10,7 +10,6 @@ - @if (BindingContext.SelectedTransaction.Data != null) diff --git a/src/aoWebWallet/Pages/TransactionDetail.razor.cs b/src/aoWebWallet/Pages/TransactionDetail.razor.cs index 1c3907a..a68298e 100644 --- a/src/aoWebWallet/Pages/TransactionDetail.razor.cs +++ b/src/aoWebWallet/Pages/TransactionDetail.razor.cs @@ -13,6 +13,7 @@ public partial class TransactionDetail : MvvmComponentBase - - @if (BindingContext.BalanceDataList.Data != null) + @if (BindingContext.BalanceDataList != null) { - @foreach (var balance in BindingContext.BalanceDataList.Data) + @foreach (var balance in BindingContext.BalanceDataList) { - if (balance.Data?.Token?.TokenData == null) + if (balance.Token?.TokenData == null) continue;
- + - @balance.Data?.Token?.TokenData?.Name - @balance.Data?.Token?.TokenData?.Ticker + @balance.Token?.TokenData?.Name + @balance.Token?.TokenData?.Ticker
- - @if (balance.Data?.BalanceData != null) + + @if (balance.BalanceDataLoader.Data != null) { - @BalanceHelper.FormatBalance(balance.Data.BalanceData.Balance, balance.Data.Token?.TokenData?.Denomination ?? 0) + @BalanceHelper.FormatBalance(balance.BalanceDataLoader.Data?.Balance, balance.Token?.TokenData?.Denomination ?? 0) } - + @if ((BindingContext.SelectedWallet?.CanSend ?? false)) { - var hasBalance = balance.Data?.BalanceData?.Balance ?? 0; + var hasBalance = balance.BalanceDataLoader.Data?.Balance ?? 0; - + } diff --git a/src/aoWebWallet/Pages/WalletDetail.razor.cs b/src/aoWebWallet/Pages/WalletDetail.razor.cs index a06ddb3..f1c48b9 100644 --- a/src/aoWebWallet/Pages/WalletDetail.razor.cs +++ b/src/aoWebWallet/Pages/WalletDetail.razor.cs @@ -9,15 +9,24 @@ public partial class WalletDetail : MvvmComponentBase { protected override void OnInitialized() { + //WatchObject(dataService.TokenList); + WatchObject(BindingContext.BalanceDataList); WatchCollection(dataService.TokenList); + WatchCollection(BindingContext.BalanceDataList); WatchDataLoaderVM(MainViewModel.WalletList); - WatchDataLoaderVM(BindingContext.BalanceDataList); WatchDataLoaderVM(BindingContext.TokenTransferList); WatchDataLoaderVM(BindingContext.SelectedProcessData); + dataService.TokenList.CollectionChanged += TokenList_CollectionChanged; + base.OnInitialized(); } + private void TokenList_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + { + BindingContext.TokenAddedRefresh(); + } + protected override async Task OnParametersSetAsync() { if (Address != null && Address.Length != 43) @@ -28,7 +37,7 @@ protected override async Task OnParametersSetAsync() if (Address != null) await BindingContext.Initialize(Address); - base.OnParametersSetAsync(); + await base.OnParametersSetAsync(); } protected override async Task LoadDataAsync() @@ -49,5 +58,12 @@ private async void DownloadWallet(Wallet wallet) StateHasChanged(); } + public override void Dispose() + { + dataService.TokenList.CollectionChanged -= TokenList_CollectionChanged; + + base.Dispose(); + } + } } diff --git a/src/aoWebWallet/Services/TokenDataService.cs b/src/aoWebWallet/Services/TokenDataService.cs index e26d84b..225a428 100644 --- a/src/aoWebWallet/Services/TokenDataService.cs +++ b/src/aoWebWallet/Services/TokenDataService.cs @@ -11,7 +11,7 @@ namespace aoWebWallet.Services public class TokenDataService { public DataLoader TokenDataLoader { get; set; } = new(); - public ObservableCollection TokenList { get; set; } = new(); + public ObservableCollection TokenList { get; } = new(); private readonly StorageService storageService; @@ -25,7 +25,6 @@ public TokenDataService(StorageService storageService, TokenClient tokenClient) public async Task TryAddTokenIds(List allTokenIds) { - foreach (var tokenId in allTokenIds) { if (string.IsNullOrEmpty(tokenId) || tokenId.Length != 43) @@ -46,9 +45,11 @@ public async Task TryAddTokenIds(List allTokenIds) await storageService.AddToken(tokenId, data, isUserAdded: false); } } + + LoadTokenList(); } - public async Task LoadTokenDataAsync(string tokenId) + public async Task LoadTokenAsync(string tokenId) { var tokens = await storageService.GetTokenIds(); @@ -74,16 +75,19 @@ public async Task TryAddTokenIds(List allTokenIds) token.TokenData = data; await storageService.SaveTokenList(tokens); + + } - return token.TokenData; + return token; } public async Task LoadTokenList(bool force = false) { if (!TokenList.Any() || force) { - TokenList = new(); + + TokenList.Clear(); await foreach (var item in LoadTokenDataAsync()) { TokenList.Add(item); @@ -144,9 +148,6 @@ public async Task AddToken(string tokenId, TokenData data, bool isUserAdded) var existing = TokenList.Where(x => x.TokenId == newToken.TokenId).FirstOrDefault(); if (existing == null) { - if (TokenList == null) - TokenList = new(); - TokenList.Add(newToken); } else diff --git a/src/aoWebWallet/Shared/AddTokenDialog.razor b/src/aoWebWallet/Shared/AddTokenDialog.razor index ec12608..4a538ea 100644 --- a/src/aoWebWallet/Shared/AddTokenDialog.razor +++ b/src/aoWebWallet/Shared/AddTokenDialog.razor @@ -36,7 +36,8 @@ Progress = "Checking metadata..."; try { - var data = await dataService.LoadTokenDataAsync(TokenId); + var token = await dataService.LoadTokenAsync(TokenId); + var data = token.TokenData; if (data != null) { await dataService.AddToken(TokenId, data, isUserAdded: true); diff --git a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor index 9d6bd7d..f707bbf 100644 --- a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor +++ b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor @@ -46,8 +46,6 @@ else if (ActionParam.ParamType == ActionParamType.Integer) { if (input == null || input.Length != 43) { - Console.WriteLine("Invalid"); - yield return "Address must have length of 43 characters."; } } diff --git a/src/aoWebWallet/Shared/ReceiveTokenDialog.razor b/src/aoWebWallet/Shared/ReceiveTokenDialog.razor index d997fd7..fd19fbf 100644 --- a/src/aoWebWallet/Shared/ReceiveTokenDialog.razor +++ b/src/aoWebWallet/Shared/ReceiveTokenDialog.razor @@ -16,7 +16,7 @@ How to receive tokens? Send tokens to this address: - @SelectedBalanceDataVM?.BalanceData?.Account + @SelectedBalanceDataVM?.BalanceDataLoader.Data?.Account
@@ -25,7 +25,7 @@ From aos: Command: - Send({ Target = "@SelectedBalanceDataVM?.Token?.TokenId", Action = "Transfer", Recipient = "@SelectedBalanceDataVM?.BalanceData?.Account", Quantity = "TOKEN_AMOUNT"}) + Send({ Target = "@SelectedBalanceDataVM?.Token?.TokenId", Action = "Transfer", Recipient = "@SelectedBalanceDataVM?.BalanceDataLoader.Data?.Account", Quantity = "TOKEN_AMOUNT"})
diff --git a/src/aoWebWallet/Shared/SendTokenDialog.razor b/src/aoWebWallet/Shared/SendTokenDialog.razor index 109dffe..482c39e 100644 --- a/src/aoWebWallet/Shared/SendTokenDialog.razor +++ b/src/aoWebWallet/Shared/SendTokenDialog.razor @@ -14,7 +14,7 @@ @SelectedBalanceDataVM?.Token?.TokenData?.Ticker - Available balance
@BalanceHelper.FormatBalance(SelectedBalanceDataVM?.BalanceData?.Balance, SelectedBalanceDataVM?.Token?.TokenData?.Denomination ?? 0)
+ Available balance
@BalanceHelper.FormatBalance(SelectedBalanceDataVM?.BalanceDataLoader.Data?.Balance, SelectedBalanceDataVM?.Token?.TokenData?.Denomination ?? 0)
@if (!isConfirm) { @@ -115,7 +115,7 @@ return; } - if (string.IsNullOrEmpty(SelectedBalanceDataVM?.BalanceData?.TokenId) + if (string.IsNullOrEmpty(SelectedBalanceDataVM?.BalanceDataLoader.Data?.TokenId) || SelectedBalanceDataVM.Token?.TokenData?.Denomination == null || !SelectedBalanceDataVM.Token.TokenData.Denomination.HasValue) return; @@ -127,7 +127,7 @@ StateHasChanged(); return; } - if (amountLong > SelectedBalanceDataVM?.BalanceData?.Balance) + if (amountLong > SelectedBalanceDataVM?.BalanceDataLoader.Data?.Balance) { Progress = "Not enough balance available."; StateHasChanged(); @@ -148,7 +148,7 @@ return; } - if (string.IsNullOrEmpty(SelectedBalanceDataVM?.BalanceData?.TokenId) + if (string.IsNullOrEmpty(SelectedBalanceDataVM?.BalanceDataLoader.Data?.TokenId) || SelectedBalanceDataVM.Token?.TokenData?.Denomination == null || !SelectedBalanceDataVM.Token.TokenData.Denomination.HasValue) return; diff --git a/src/aoWebWallet/ViewModels/BalanceDataViewModel.cs b/src/aoWebWallet/ViewModels/BalanceDataViewModel.cs index d33c121..14c599c 100644 --- a/src/aoWebWallet/ViewModels/BalanceDataViewModel.cs +++ b/src/aoWebWallet/ViewModels/BalanceDataViewModel.cs @@ -1,11 +1,12 @@ using aoWebWallet.Models; using ArweaveAO.Models.Token; +using webvNext.DataLoader; namespace aoWebWallet.ViewModels { public class BalanceDataViewModel { - public BalanceData? BalanceData { get; set; } + public DataLoaderViewModel BalanceDataLoader { get; set; } = new DataLoaderViewModel(); public Token? Token { get; set; } } } diff --git a/src/aoWebWallet/ViewModels/MainViewModel.cs b/src/aoWebWallet/ViewModels/MainViewModel.cs index 18f50c8..f88a740 100644 --- a/src/aoWebWallet/ViewModels/MainViewModel.cs +++ b/src/aoWebWallet/ViewModels/MainViewModel.cs @@ -21,11 +21,9 @@ public partial class MainViewModel : ObservableRecipient private const string CLAIM_PROCESS_ID = "5Mv1TBYZvKjNlWUpH78hWORIhqj1uqn_wdkJrA7emfU"; private readonly TokenDataService dataService; - private readonly TokenClient tokenClient; private readonly StorageService storageService; private readonly ArweaveService arweaveService; private readonly GraphqlClient graphqlClient; - private readonly ISnackbar snackbar; private readonly MemoryDataCache memoryDataCache; [ObservableProperty] @@ -54,19 +52,15 @@ public partial class MainViewModel : ObservableRecipient /// Gets the responsible for loading the source markdown docs. /// public MainViewModel(TokenDataService dataService, - TokenClient tokenClient, StorageService storageService, ArweaveService arweaveService, GraphqlClient graphqlClient, - ISnackbar snackbar, MemoryDataCache memoryDataCache) : base() { this.dataService = dataService; - this.tokenClient = tokenClient; this.storageService = storageService; this.arweaveService = arweaveService; this.graphqlClient = graphqlClient; - this.snackbar = snackbar; this.memoryDataCache = memoryDataCache; } @@ -179,11 +173,6 @@ public async Task ClearUserData() } - - - - - partial void OnComputeUnitUrlChanged(string? value) { //ClearUserData(); @@ -213,9 +202,6 @@ public async Task SaveUserSettings() IsDarkMode = UserSettings.IsDarkMode ?? true; } } - - - public async Task DisconnectArWallet() @@ -244,8 +230,6 @@ public async Task GetActiveArConnectAddress() } } - - public Task SendToken(Wallet wallet, string tokenId, string address, long amount) { if (wallet.Source == WalletTypes.ArConnect) diff --git a/src/aoWebWallet/ViewModels/TokenDetailViewModel.cs b/src/aoWebWallet/ViewModels/TokenDetailViewModel.cs index ad7e3e4..e714d80 100644 --- a/src/aoWebWallet/ViewModels/TokenDetailViewModel.cs +++ b/src/aoWebWallet/ViewModels/TokenDetailViewModel.cs @@ -26,7 +26,7 @@ public TokenDetailViewModel(MainViewModel mainViewModel, GraphqlClient graphqlCl public async Task Initialize(string tokenId) { - this.LoadTokenData(tokenId); + await this.LoadTokenData(tokenId); this.LoadTokenTransferListForToken(tokenId); @@ -37,11 +37,11 @@ public Task LoadTokenData(string tokenId) => Token.DataLoader.LoadAsync(async () { Token.Data = null; - await dataService.LoadTokenList(); + var result = await dataService.LoadTokenAsync(tokenId); - return dataService.TokenList; + return result; - }); + }, (x) => Token.Data = x); public Task LoadTokenTransferListForToken(string tokenId) => TokenTransferList.DataLoader.LoadAsync(async () => { diff --git a/src/aoWebWallet/ViewModels/TransactionDetailViewModel.cs b/src/aoWebWallet/ViewModels/TransactionDetailViewModel.cs index e43cf28..e447bef 100644 --- a/src/aoWebWallet/ViewModels/TransactionDetailViewModel.cs +++ b/src/aoWebWallet/ViewModels/TransactionDetailViewModel.cs @@ -43,7 +43,7 @@ public Task LoadSelectedTokenTransfer(string txId) => SelectedTransaction.DataLo dataService.TryAddTokenIds(new List() { result.TokenId }); return result; - }); + }, (x) => SelectedTransaction.Data = x); } } diff --git a/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs b/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs index 31c9944..df882c2 100644 --- a/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs +++ b/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs @@ -3,6 +3,8 @@ using ArweaveAO; using CommunityToolkit.Mvvm.ComponentModel; using MudBlazor; +using System.Collections.ObjectModel; +using System.ComponentModel; using System.Net; using webvNext.DataLoader; using webvNext.DataLoader.Cache; @@ -38,7 +40,7 @@ public partial class WalletDetailViewModel : ObservableObject public DataLoaderViewModel SelectedProcessData { get; set; } = new(); - public DataLoaderViewModel>> BalanceDataList { get; set; } = new(); + public ObservableCollection BalanceDataList { get; } = new(); public DataLoaderViewModel> TokenTransferList { get; set; } = new(); @@ -102,6 +104,14 @@ public async Task RefreshBalance() } } + public async Task TokenAddedRefresh() + { + if (selectedAddress != null) + { + await LoadBalanceDataList(selectedAddress, onlyNew: true); + } + } + //public async Task Refresh() //{ // if (selectedAddress != null) @@ -171,31 +181,37 @@ public Task LoadTokenTransferList(string address) => TokenTransferList.DataLoade - public Task LoadBalanceDataList(string address) => BalanceDataList.DataLoader.LoadAsync(async () => + public async Task LoadBalanceDataList(string address, bool onlyNew = false) { //First clear - BalanceDataList.Data = null; + if (!onlyNew) + BalanceDataList.Clear(); - var result = new List>(); + var result = new List(); foreach (var token in dataService.TokenList.Where(x => x.IsVisible)) { - var balanceData = new DataLoaderViewModel(); - balanceData.Data = new BalanceDataViewModel { Token = token }; - - balanceData.DataLoader.LoadAsync(async () => + if (onlyNew) { - var balanceData = await tokenClient.GetBalance(token.TokenId, address); - return new BalanceDataViewModel() { BalanceData = balanceData, Token = token }; - }, (x) => { balanceData.Data = x; BalanceDataList.ForcePropertyChanged(); }); - result.Add(balanceData); - } + if (BalanceDataList.Where(x => x.Token?.TokenId == token.TokenId).Any()) + continue; - BalanceDataList.Data = result; + } + var balanceData = new BalanceDataViewModel { Token = token }; - return result; + balanceData.BalanceDataLoader.DataLoader.LoadAsync(async () => + { + var balanceData = await tokenClient.GetBalance(token.TokenId, address); + return balanceData; + }, (x) => + { + balanceData.BalanceDataLoader.Data = x; + TokenTransferList.ForcePropertyChanged(); + }); - }); + BalanceDataList.Add(balanceData); + } + } public async Task LoadSelectedWalletProcessData(string address) { @@ -271,7 +287,6 @@ public async Task SetClaims() CanClaim2 = CanClaim1 && sendTransactionActivity.Count > 1 && (mainViewModel.WalletList.Data?.Count() > 1 || dataService.TokenList.Count() > 6); CanClaim3 = CanClaim2 && sendTransactionActivity.Count > 1 && mainViewModel.WalletList.Data?.Count() > 2 && viewTokenActivity.Count > 0 && viewAddressActivity.Count > 2 && viewTransactionctivity.Count > 1; - Console.WriteLine("1:" + CanClaim1); } public async Task Claim1() @@ -342,5 +357,6 @@ public async Task Claim3() } } + } } diff --git a/src/aoWebWallet/ViewModels/WalletDetailsViewModel.cs b/src/aoWebWallet/ViewModels/WalletDetailsViewModel.cs index 94082e1..e849a3c 100644 --- a/src/aoWebWallet/ViewModels/WalletDetailsViewModel.cs +++ b/src/aoWebWallet/ViewModels/WalletDetailsViewModel.cs @@ -29,7 +29,6 @@ partial void OnIsConnectedChanged(bool value) partial void OnOwnerCanSendChanged(bool value) { - Console.WriteLine("Owner can send: " + value); SetCanSend(); } diff --git a/src/webvNext.DataLoader/DataLoader.cs b/src/webvNext.DataLoader/DataLoader.cs index 0d58d22..5d6b29c 100644 --- a/src/webvNext.DataLoader/DataLoader.cs +++ b/src/webvNext.DataLoader/DataLoader.cs @@ -57,36 +57,38 @@ public DataLoader(bool swallowExceptions = true) //await semaphoreSlim.WaitAsync(); //try //{ - //Set loading state - LoadingState = LoadingState.Loading; + //Set loading state + LoadingState = LoadingState.Loading; - T? result = default; + T? result = default; - try - { - result = await loadingMethod(); + try + { + result = await loadingMethod(); - //Set finished state - LoadingState = LoadingState.Finished; - LoadedDateTime = DateTimeOffset.UtcNow; - - if (resultCallback != null) - resultCallback(result); + //Set finished state + LoadingState = LoadingState.Finished; + LoadedDateTime = DateTimeOffset.UtcNow; - } - catch (Exception e) + if (resultCallback != null) { - //Set error state - LoadingState = LoadingState.Error; + resultCallback(result); + } - if (errorCallback != null) - errorCallback(e); - else if (!_swallowExceptions) //swallow exception if _swallowExceptions is true - throw; //throw error if no callback is defined + } + catch (Exception e) + { + //Set error state + LoadingState = LoadingState.Error; - } + if (errorCallback != null) + errorCallback(e); + else if (!_swallowExceptions) //swallow exception if _swallowExceptions is true + throw; //throw error if no callback is defined + + } - return result; + return result; //} //finally //{ From 1eb0800eb13ea00014a9ce07970b1491501a8fef Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Wed, 24 Apr 2024 16:45:03 +0200 Subject: [PATCH 08/63] ActionEditor improvements with TokenData and balance data --- src/aoWebWallet/Pages/ActionPage.razor | 73 +++++-------- src/aoWebWallet/Pages/ActionPage.razor.cs | 2 + src/aoWebWallet/Services/TokenDataService.cs | 7 +- src/aoWebWallet/Shared/ActionEditor.razor | 54 ++++++++++ .../Components/ActionInputComponent.razor | 53 +++++++-- .../Components/ActionQuantityComponent.razor | 101 ++++++++++++++++-- src/aoWebWallet/aoWebWallet.csproj | 6 -- 7 files changed, 229 insertions(+), 67 deletions(-) create mode 100644 src/aoWebWallet/Shared/ActionEditor.razor diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor index db72f28..85711a1 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -49,52 +49,22 @@ - - Target: @AoAction.Target?.Value - - - - @foreach (var value in AoAction.Filled) - { -

@value.Key = @value.Value | @value.ParamType

- @foreach (var arg in value.Args) - { - @arg - } - } - -
- - - @foreach (var param in AoAction.AllInputs) - { - if (param.ParamType == ActionParamType.Input - || param.ParamType == ActionParamType.Integer - || param.ParamType == ActionParamType.Process - ) - { - - } - else if (param.ParamType == ActionParamType.Quantity || param.ParamType == ActionParamType.Balance) - { - var tokenId = param.Args.FirstOrDefault(); - var token = dataService.TokenList.Where(x => x.TokenId == tokenId && x.TokenData != null).FirstOrDefault(); - - if(token != null) - { - //TODO: Quantity, with token and denomination and enough balance check - - } - else { - Loading Token Data... - } - - + @if(readOnly) + { + Please review your transaction: + } - } - } + - + @if (!readOnly) + { + Preview + } + else + { + Cancel + Submit + } @@ -102,6 +72,21 @@ @code { private string? selectedWallet; + private bool readOnly = false; + + private void Preview() + { + readOnly = true; + } + private void Cancel() + { + readOnly = false; + } + + private void Submit() + { + readOnly = false; + } private void OpenDialog() { diff --git a/src/aoWebWallet/Pages/ActionPage.razor.cs b/src/aoWebWallet/Pages/ActionPage.razor.cs index d829305..bacd88d 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor.cs +++ b/src/aoWebWallet/Pages/ActionPage.razor.cs @@ -73,6 +73,8 @@ private async void GetQueryStringValues() actionParamType = ActionParamType.Target; if (key.Equals("X-Quantity", StringComparison.InvariantCultureIgnoreCase)) actionParamType = ActionParamType.Quantity; + if (key.Equals("X-Balance", StringComparison.InvariantCultureIgnoreCase)) + actionParamType = ActionParamType.Balance; else if (key.Equals("X-Process", StringComparison.InvariantCultureIgnoreCase)) actionParamType = ActionParamType.Process; else if (key.Equals("X-Int", StringComparison.InvariantCultureIgnoreCase)) diff --git a/src/aoWebWallet/Services/TokenDataService.cs b/src/aoWebWallet/Services/TokenDataService.cs index 225a428..b64770a 100644 --- a/src/aoWebWallet/Services/TokenDataService.cs +++ b/src/aoWebWallet/Services/TokenDataService.cs @@ -72,10 +72,13 @@ public async Task LoadTokenAsync(string tokenId) return data; }); - token.TokenData = data; + if (data != null) + { - await storageService.SaveTokenList(tokens); + token.TokenData = data; + await storageService.SaveTokenList(tokens); + } } diff --git a/src/aoWebWallet/Shared/ActionEditor.razor b/src/aoWebWallet/Shared/ActionEditor.razor new file mode 100644 index 0000000..fa19e30 --- /dev/null +++ b/src/aoWebWallet/Shared/ActionEditor.razor @@ -0,0 +1,54 @@ +@using aoWebWallet.Models + + + Target: @AoAction.Target?.Value + + + + @foreach (var value in AoAction.Filled) + { +

@value.Key = @value.Value | @value.ParamType

+ @foreach (var arg in value.Args) + { + @arg + } + } + +
+ + + @foreach (var param in AoAction.AllInputs) + { + if (param.ParamType == ActionParamType.Input + || param.ParamType == ActionParamType.Integer + || param.ParamType == ActionParamType.Process + ) + { + + } + else if (param.ParamType == ActionParamType.Quantity || param.ParamType == ActionParamType.Balance) + { + var tokenId = param.Args.FirstOrDefault(); + if (tokenId != null) + { + + } + + + + } + } + + + + +@code { + [Parameter] + public required AoAction AoAction { get; set; } + + [Parameter] + public bool ReadOnly { get; set; } + + [Parameter] + public string? Address { get; set; } +} diff --git a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor index f707bbf..5390175 100644 --- a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor +++ b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor @@ -1,17 +1,24 @@ @using aoWebWallet.Models

@ActionParam.Key = @ActionParam.Value | @ActionParam.ParamType

-@if (ActionParam.ParamType == ActionParamType.Input) +@if(ReadOnly) { - +

@ActionParam.Key = @ActionParam.Value

} -else if (ActionParam.ParamType == ActionParamType.Process) +else { - -} -else if (ActionParam.ParamType == ActionParamType.Integer) -{ - + if (ActionParam.ParamType == ActionParamType.Input) + { + + } + else if (ActionParam.ParamType == ActionParamType.Process) + { + + } + else if (ActionParam.ParamType == ActionParamType.Integer) + { + + } } @@ -20,10 +27,40 @@ else if (ActionParam.ParamType == ActionParamType.Integer) [Parameter] public required ActionParam ActionParam { get; set; } + [Parameter] + public bool ReadOnly { get; set; } + + private string? textValue; + MudTextField? mudTextField; MudTextField? mudProcessField; MudTextField? mudIntField; + // protected override void OnParametersSet() + // { + // mudTextField?.SetText("TESTAAA"); + // Console.WriteLine("Param Set"); + + // base.OnParametersSet(); + // } + + // protected override void OnInitialized() + // { + // mudTextField?.SetText("TESTAAA"); + // Console.WriteLine("Init"); + + // base.OnInitialized(); + // } + + protected override void OnAfterRender(bool firstRender) + { + mudTextField?.SetText(ActionParam.Value); + mudProcessField?.SetText(ActionParam.Value); + mudIntField?.SetText(ActionParam.Value); + + base.OnAfterRender(firstRender); + } + public async void UpdateStringValue(string? e) { if (mudTextField != null) diff --git a/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor b/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor index d6c1a9f..3425241 100644 --- a/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor +++ b/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor @@ -1,13 +1,42 @@ @using ArweaveAO.Models.Token @using aoWebWallet.Models +@inject TokenDataService tokenDataService +@inject TokenClient tokenClient

@ActionParam.Key = @ActionParam.Value | @ActionParam.ParamType

-@if (ActionParam.ParamType == ActionParamType.Quantity -|| ActionParam.ParamType == ActionParamType.Balance) +@if(Token == null) +{ + Loading token data... + return; +} +@if (ActionParam.ParamType == ActionParamType.Balance && string.IsNullOrEmpty(Address)) +{ + Please select a wallet... + return; +} +@if (ActionParam.ParamType == ActionParamType.Balance && BalanceData == null) { - + Loading balance... + return; } +@if (ReadOnly) +{ +

@ActionParam.Key = @BalanceHelper.FormatBalance(long.Parse(ActionParam.Value ?? "0"), Token?.TokenData?.Denomination ?? 0)

+} +else +{ + if (ActionParam.ParamType == ActionParamType.Quantity +|| ActionParam.ParamType == ActionParamType.Balance) + { + + + if (ActionParam.ParamType == ActionParamType.Balance) + { + Balance available: @BalanceHelper.FormatBalance(BalanceData?.Balance, Token?.TokenData?.Denomination ?? 1) + } + } +} @code { @@ -16,30 +45,88 @@ public required ActionParam ActionParam { get; set; } [Parameter] - public required Token Token { get; set; } + public string? Address { get; set; } [Parameter] + public bool ReadOnly { get; set; } + + [Parameter] + public required string TokenId { get; set; } + + public Token? Token { get; set; } + public BalanceData? BalanceData { get; set; } - public string DenominationFormat => "F" + (Token.TokenData?.Denomination ?? 1).ToString(); + public string DenominationFormat => "F" + (Token?.TokenData?.Denomination ?? 1).ToString(); MudTextField? mudTextField; + protected override void OnAfterRender(bool firstRender) + { + mudTextField?.SetText(ActionParam.Value); + + base.OnAfterRender(firstRender); + } + + protected override async Task OnParametersSetAsync() + { + var token = await tokenDataService.LoadTokenAsync(TokenId); + if (token.TokenData?.Denomination != null) + Token = token; + + if (ActionParam.ParamType == ActionParamType.Balance && !string.IsNullOrEmpty(Address)) + { + BalanceData = await tokenClient.GetBalance(token.TokenId, Address); + } + + base.OnParametersSetAsync(); + } + + public IEnumerable ValidateBalance(decimal e) + { + // if (e == null) + // { + // yield return "Please enter a value."; + // } + + if(e > 0) + { + + if (ActionParam.ParamType == ActionParamType.Balance) + { + if (Token?.TokenData?.Denomination.HasValue ?? false) + { + long amountLong = BalanceHelper.DecimalToTokenAmount(e, Token.TokenData.Denomination.Value); + + if (BalanceData?.Balance < amountLong) + { + yield return "Not enough balance available."; + } + } + else + { + yield return "Token data is not available."; + } + } + } + } + public async void UpdateDecimalValue(decimal e) { if (mudTextField != null) await mudTextField.Validate(); - if (Token.TokenData?.Denomination == null) + if (Token?.TokenData?.Denomination == null) { ActionParam.Value = null; return; } + if (!(mudTextField?.ValidationErrors.Any() ?? false)) { long amountLong = BalanceHelper.DecimalToTokenAmount(e, Token.TokenData.Denomination.Value); - + ActionParam.Value = amountLong.ToString(); } else diff --git a/src/aoWebWallet/aoWebWallet.csproj b/src/aoWebWallet/aoWebWallet.csproj index dc8a86d..a1937b7 100644 --- a/src/aoWebWallet/aoWebWallet.csproj +++ b/src/aoWebWallet/aoWebWallet.csproj @@ -27,10 +27,4 @@ - - - true - - - From 4a658ee512ef33bff9d54cd232ea142272e76fc0 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Wed, 24 Apr 2024 17:01:48 +0200 Subject: [PATCH 09/63] ActionPage validation --- src/aoWebWallet/Pages/ActionPage.razor | 2 +- .../Components/ActionInputComponent.razor | 17 ++++++++++++++--- .../Components/ActionQuantityComponent.razor | 13 ++++++++----- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor index 85711a1..4c4a14f 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -23,7 +23,7 @@ } else { - + @foreach (var wallet in BindingContext.WalletList.Data ?? new()) { diff --git a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor index 5390175..dd0f083 100644 --- a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor +++ b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor @@ -54,9 +54,20 @@ else protected override void OnAfterRender(bool firstRender) { - mudTextField?.SetText(ActionParam.Value); - mudProcessField?.SetText(ActionParam.Value); - mudIntField?.SetText(ActionParam.Value); + if (!(mudTextField?.ValidationErrors.Any() ?? false)) + { + mudTextField?.SetText(ActionParam.Value); + } + + if (!(mudProcessField?.ValidationErrors.Any() ?? false)) + { + mudProcessField?.SetText(ActionParam.Value); + } + + if (!(mudIntField?.ValidationErrors.Any() ?? false)) + { + mudIntField?.SetText(ActionParam.Value); + } base.OnAfterRender(firstRender); } diff --git a/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor b/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor index 3425241..9c5c6e5 100644 --- a/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor +++ b/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor @@ -63,7 +63,10 @@ else protected override void OnAfterRender(bool firstRender) { - mudTextField?.SetText(ActionParam.Value); + if (!(mudTextField?.ValidationErrors.Any() ?? false)) + { + mudTextField?.SetText(ActionParam.Value); + } base.OnAfterRender(firstRender); } @@ -84,10 +87,10 @@ else public IEnumerable ValidateBalance(decimal e) { - // if (e == null) - // { - // yield return "Please enter a value."; - // } + if (e < 0) + { + yield return "Must be greater or equal than 0."; + } if(e > 0) { From ddac9a86a3f32d3c98d65908fe466ea4ee634b59 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Wed, 24 Apr 2024 21:03:12 +0200 Subject: [PATCH 10/63] Setup for sending transactions --- src/aoWebWallet/Models/ActionParam.cs | 17 ++- .../{ViewModels => Models}/Transaction.cs | 2 +- src/aoWebWallet/Pages/ActionPage.razor | 15 ++- src/aoWebWallet/Pages/ActionPage.razor.cs | 7 ++ src/aoWebWallet/Program.cs | 1 + .../Services/TransactionService.cs | 114 ++++++++++++++++++ 6 files changed, 152 insertions(+), 4 deletions(-) rename src/aoWebWallet/{ViewModels => Models}/Transaction.cs (72%) create mode 100644 src/aoWebWallet/Services/TransactionService.cs diff --git a/src/aoWebWallet/Models/ActionParam.cs b/src/aoWebWallet/Models/ActionParam.cs index ffa8900..6798bc6 100644 --- a/src/aoWebWallet/Models/ActionParam.cs +++ b/src/aoWebWallet/Models/ActionParam.cs @@ -1,10 +1,15 @@ -namespace aoWebWallet.Models + +using aoWebWallet.Pages; +using System.Net; + +namespace aoWebWallet.Models { public class AoAction { public List Params { get; set; } = new(); public ActionParam? Target => Params.Where(x => x.ParamType == ActionParamType.Target).FirstOrDefault(); + public IEnumerable AllWithoutTarget => Params.Where(x => x.ParamType != ActionParamType.Target); public IEnumerable Filled => Params.Where(x => x.ParamType == ActionParamType.Filled); public IEnumerable AllInputs => Params.Where(x => x.ParamType != ActionParamType.Filled @@ -17,6 +22,16 @@ public class AoAction return null; } + + public List ToEvalTags() + { + return Params.Select(x => new ArweaveBlazor.Models.Tag { Name = x.Key, Value = x.Value ?? string.Empty }).ToList(); + } + + public List ToTags() + { + return AllWithoutTarget.Select(x => new ArweaveBlazor.Models.Tag { Name = x.Key, Value = x.Value ?? string.Empty }).ToList(); + } } public class ActionParam diff --git a/src/aoWebWallet/ViewModels/Transaction.cs b/src/aoWebWallet/Models/Transaction.cs similarity index 72% rename from src/aoWebWallet/ViewModels/Transaction.cs rename to src/aoWebWallet/Models/Transaction.cs index 3c8e316..78000c7 100644 --- a/src/aoWebWallet/ViewModels/Transaction.cs +++ b/src/aoWebWallet/Models/Transaction.cs @@ -1,4 +1,4 @@ -namespace aoWebWallet.ViewModels +namespace aoWebWallet.Models { public class Transaction { diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor index 4c4a14f..371a5b7 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -83,9 +83,20 @@ readOnly = false; } - private void Submit() + private async Task Submit() { - readOnly = false; + if (BindingContext.WalletList.Data == null) + return; + + var wallet = BindingContext.WalletList.Data.Where(x => x.Address == selectedWallet).FirstOrDefault(); + + if (wallet == null) + return; + + //Do we need the owner wallet? + Wallet? ownerWallet = BindingContext.WalletList.Data.Where(x => x.Address == wallet.OwnerAddress).FirstOrDefault(); + + await transactionService.SendAction(wallet, ownerWallet, AoAction); } private void OpenDialog() diff --git a/src/aoWebWallet/Pages/ActionPage.razor.cs b/src/aoWebWallet/Pages/ActionPage.razor.cs index bacd88d..a1bb6b2 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor.cs +++ b/src/aoWebWallet/Pages/ActionPage.razor.cs @@ -8,8 +8,15 @@ namespace aoWebWallet.Pages { public partial class ActionPage : MvvmComponentBase { + private readonly TransactionService transactionService; + public AoAction AoAction { get; set; } = new(); + public ActionPage(TransactionService transactionService) + { + this.transactionService = transactionService; + } + protected override void OnInitialized() { GetQueryStringValues(); diff --git a/src/aoWebWallet/Program.cs b/src/aoWebWallet/Program.cs index 74e27aa..515a58d 100644 --- a/src/aoWebWallet/Program.cs +++ b/src/aoWebWallet/Program.cs @@ -91,6 +91,7 @@ private static void ConfigureServices(IServiceCollection services, string baseAd services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddSingleton(); diff --git a/src/aoWebWallet/Services/TransactionService.cs b/src/aoWebWallet/Services/TransactionService.cs new file mode 100644 index 0000000..8673c44 --- /dev/null +++ b/src/aoWebWallet/Services/TransactionService.cs @@ -0,0 +1,114 @@ +using aoWebWallet.Extensions; +using aoWebWallet.Models; +using ArweaveBlazor; +using webvNext.DataLoader; + +namespace aoWebWallet.Services +{ + public class TransactionService(ArweaveService arweaveService) + { + public DataLoaderViewModel LastTransactionId { get; set; } = new(); + + public async Task GetActiveArConnectAddress() + { + bool hasArConnectExtension = await arweaveService.HasArConnectAsync(); + + if (hasArConnectExtension) + { + var address = await arweaveService.GetActiveAddress(); + + return address; + + } + + return null; + } + + public async Task SendAction(Wallet wallet, Wallet? ownerWallet, AoAction action) + { + if (wallet.Source == WalletTypes.ArConnect) + { + var activeAddress = await GetActiveArConnectAddress(); + if(activeAddress == wallet.Address) + return await SendActionWithArConnect(action); + } + + if (ownerWallet?.Source == WalletTypes.ArConnect) + { + var activeAddress = await GetActiveArConnectAddress(); + if (activeAddress == ownerWallet.Address) + return await SendActionWithEvalWithArConnect(wallet.Address, action); + } + + if (!string.IsNullOrEmpty(wallet.OwnerAddress) && ownerWallet?.Address == wallet.Address + && !string.IsNullOrEmpty(ownerWallet?.Jwk)) + { + return await SendActionWithEval(ownerWallet.Jwk, wallet.Address, action); + + } + + if (!string.IsNullOrEmpty(wallet.Jwk)) + return await SendActionWithJwk(wallet.Jwk, action); + + return null; + + } + + private Task SendActionWithEvalWithArConnect(string processId, AoAction action) + => LastTransactionId.DataLoader.LoadAsync(async () => + { + var activeAddress = await GetActiveArConnectAddress(); + if (string.IsNullOrEmpty(activeAddress)) + return null; + + return await SendActionWithEval(null, processId, action); + }); + + private Task SendActionWithEval(string? jwk, string processId, AoAction action) + => LastTransactionId.DataLoader.LoadAsync(async () => + { + + var transferTags = action.ToEvalTags(); + + var data = $"Send({transferTags.ToSendCommand()})"; + + var evalTags = new List + { + new ArweaveBlazor.Models.Tag() { Name = "Action", Value = "Eval"}, + new ArweaveBlazor.Models.Tag() { Name = "X-Wallet", Value = "aoww"}, + }; + + var idResult = await arweaveService.SendAsync(jwk, processId, null, data, evalTags); + + return new Transaction { Id = idResult }; + }); + + private Task SendActionWithArConnect(AoAction action) + => LastTransactionId.DataLoader.LoadAsync(async () => + { + var activeAddress = await GetActiveArConnectAddress(); + if (string.IsNullOrEmpty(activeAddress)) + return null; + + return await SendActionWithJwk(null, action); + }); + + private Task SendActionWithJwk(string? jwk, AoAction action) + => LastTransactionId.DataLoader.LoadAsync(async () => + { + if (action.Target?.Value == null) + return null; + + var transferTags = action.ToTags(); + transferTags.Add(new ArweaveBlazor.Models.Tag() { Name = "X-Wallet", Value = "aoww" }); + + var idResult = await arweaveService.SendAsync(jwk, action.Target.Value, null, null, transferTags); + + return new Transaction { Id = idResult }; + }); + + + + + } +} From 5e409b02f92a4909b18b74b0cd0975d14c12fa20 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Fri, 26 Apr 2024 15:48:15 +0200 Subject: [PATCH 11/63] Action improvements --- src/aoWebWallet/Models/ActionParam.cs | 117 +++++++++++++++++- src/aoWebWallet/Pages/ActionPage.razor | 11 +- src/aoWebWallet/Pages/ActionPage.razor.cs | 68 +--------- src/aoWebWallet/Pages/WalletDetail.razor | 19 ++- src/aoWebWallet/Shared/ActionEditor.razor | 16 ++- .../Components/ActionInputComponent.razor | 7 +- .../Components/ActionQuantityComponent.razor | 32 +++-- .../ViewModels/WalletDetailViewModel.cs | 36 +++++- 8 files changed, 206 insertions(+), 100 deletions(-) diff --git a/src/aoWebWallet/Models/ActionParam.cs b/src/aoWebWallet/Models/ActionParam.cs index 6798bc6..822c03f 100644 --- a/src/aoWebWallet/Models/ActionParam.cs +++ b/src/aoWebWallet/Models/ActionParam.cs @@ -1,6 +1,5 @@  -using aoWebWallet.Pages; -using System.Net; +using System.Text; namespace aoWebWallet.Models { @@ -20,6 +19,12 @@ public class AoAction if (Target == null) return "No Target process specified."; + foreach(var input in AllInputs) + { + if (string.IsNullOrEmpty(input.Value)) + return $"Please enter a value for {input.Key}"; + } + return null; } @@ -32,6 +37,114 @@ public class AoAction { return AllWithoutTarget.Select(x => new ArweaveBlazor.Models.Tag { Name = x.Key, Value = x.Value ?? string.Empty }).ToList(); } + + + public string ToQueryString() + { + if (Target == null) + return string.Empty; + + StringBuilder sb = new StringBuilder(); + + sb.Append($"{Target.Key}={Target.Value}&"); + + foreach (var param in this.Filled) + { + sb.Append($"{param.Key}={param.Value}&"); + } + + foreach (var param in this.AllInputs) + { + var args = string.Join(';', param.Args); + if (args.Length > 0) + { + sb.Append($"X-{param.ParamType}={param.Key};{args}&"); + } + else + { + sb.Append($"X-{param.ParamType}={param.Key}&"); + } + } + + return sb.ToString().TrimEnd('&'); + } + + public static AoAction CreateFromQueryString(string qstring) + { + // Parsing query string + var queryStringValues = System.Web.HttpUtility.ParseQueryString(qstring); + + AoAction action = new AoAction(); + + foreach (var key in queryStringValues.AllKeys) + { + if (key == null) + continue; + + var values = queryStringValues.GetValues(key); + if (values == null || !values.Any()) + continue; + + foreach (var val in values) + { + string actionKey = key; + string? actionValue = val.ToString(); + ActionParamType actionParamType = ActionParamType.Filled; + + var actionValueSplit = actionValue.Split(';', StringSplitOptions.RemoveEmptyEntries); + actionValue = actionValueSplit.First(); + List args = actionValueSplit.Skip(1).ToList(); + + if (key.Equals("Target", StringComparison.InvariantCultureIgnoreCase)) + actionParamType = ActionParamType.Target; + if (key.Equals("X-Quantity", StringComparison.InvariantCultureIgnoreCase)) + actionParamType = ActionParamType.Quantity; + if (key.Equals("X-Balance", StringComparison.InvariantCultureIgnoreCase)) + actionParamType = ActionParamType.Balance; + else if (key.Equals("X-Process", StringComparison.InvariantCultureIgnoreCase)) + actionParamType = ActionParamType.Process; + else if (key.Equals("X-Integer", StringComparison.InvariantCultureIgnoreCase)) + actionParamType = ActionParamType.Integer; + else if (key.Equals("X-Input", StringComparison.InvariantCultureIgnoreCase)) + actionParamType = ActionParamType.Input; + + if (actionParamType != ActionParamType.Filled + && actionParamType != ActionParamType.Target) + { + actionKey = actionValue; + actionValue = null; + } + + Console.WriteLine($"Val: {actionValue} args: {args.Count()}"); + + action.Params.Add(new ActionParam + { + Key = actionKey, + Value = actionValue, + Args = args, + ParamType = actionParamType + }); + + } + } + + return action; + } + + public static AoAction CreateForTokenTransaction(string tokenId) + { + return new AoAction + { + Params = new List + { + new ActionParam { Key= "Target", ParamType = ActionParamType.Target, Value= tokenId }, + new ActionParam { Key= "Action", ParamType = ActionParamType.Filled, Value= "Transfer" }, + new ActionParam { Key= "Recipient", ParamType = ActionParamType.Process }, + new ActionParam { Key= "Quantity", ParamType = ActionParamType.Balance, Args = new List { tokenId } } + } + + }; + } } public class ActionParam diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor index 371a5b7..fc964cf 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -5,16 +5,16 @@ @inject ISnackbar Snackbar @inject NavigationManager NavigationManager @inject TokenDataService dataService +@inject TransactionService transactionService; +@inject WalletDetailViewModel WalletDetailViewModel @Program.PageTitlePostFix - Action Page + New transaction - TODO: Select a wallet, or create a new wallet - @if (BindingContext.WalletList.Data != null) { if (!BindingContext.WalletList.Data.Any()) @@ -59,6 +59,7 @@ @if (!readOnly) { Preview + @validation } else { @@ -71,12 +72,14 @@ @code { + private string? validation; private string? selectedWallet; private bool readOnly = false; private void Preview() { - readOnly = true; + validation = AoAction.IsValid(); + readOnly = string.IsNullOrEmpty(validation); } private void Cancel() { diff --git a/src/aoWebWallet/Pages/ActionPage.razor.cs b/src/aoWebWallet/Pages/ActionPage.razor.cs index a1bb6b2..03a4344 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor.cs +++ b/src/aoWebWallet/Pages/ActionPage.razor.cs @@ -1,28 +1,23 @@ using aoWebWallet.Models; -using aoWebWallet.Services; using aoWebWallet.ViewModels; using Microsoft.AspNetCore.Components.Routing; -using System.Net; namespace aoWebWallet.Pages { public partial class ActionPage : MvvmComponentBase { - private readonly TransactionService transactionService; - public AoAction AoAction { get; set; } = new(); - public ActionPage(TransactionService transactionService) - { - this.transactionService = transactionService; - } - protected override void OnInitialized() { GetQueryStringValues(); //WatchDataLoaderVM(BindingContext.TokenList); WatchDataLoaderVM(BindingContext.WalletList); + //Auto select wallet + if(!string.IsNullOrEmpty(WalletDetailViewModel.SelectedWallet?.Wallet.Address)) + selectedWallet = WalletDetailViewModel.SelectedWallet?.Wallet.Address; + NavigationManager.LocationChanged += NavigationManager_LocationChanged; base.OnInitialized(); @@ -52,60 +47,7 @@ private async void GetQueryStringValues() var uri = new Uri(NavigationManager.Uri); var query = uri.Query; - // Parsing query string - var queryStringValues = System.Web.HttpUtility.ParseQueryString(query); - - AoAction = new AoAction(); - - foreach (var key in queryStringValues.AllKeys) - { - if (key == null) - continue; - - var values = queryStringValues.GetValues(key); - if (values == null || !values.Any()) - continue; - - foreach(var val in values) - { - string actionKey = key; - string? actionValue = val.ToString(); - ActionParamType actionParamType = ActionParamType.Filled; - - var actionValueSplit = actionValue.Split(';', StringSplitOptions.RemoveEmptyEntries); - actionValue = actionValueSplit.FirstOrDefault(); - List args = actionValueSplit.Skip(1).ToList(); - - if (key.Equals("Target", StringComparison.InvariantCultureIgnoreCase)) - actionParamType = ActionParamType.Target; - if (key.Equals("X-Quantity", StringComparison.InvariantCultureIgnoreCase)) - actionParamType = ActionParamType.Quantity; - if (key.Equals("X-Balance", StringComparison.InvariantCultureIgnoreCase)) - actionParamType = ActionParamType.Balance; - else if (key.Equals("X-Process", StringComparison.InvariantCultureIgnoreCase)) - actionParamType = ActionParamType.Process; - else if (key.Equals("X-Int", StringComparison.InvariantCultureIgnoreCase)) - actionParamType = ActionParamType.Integer; - else if (key.Equals("X-Input", StringComparison.InvariantCultureIgnoreCase)) - actionParamType = ActionParamType.Input; - - if (actionParamType != ActionParamType.Filled - && actionParamType != ActionParamType.Target) - { - actionKey = val; - actionValue = null; - } - - AoAction.Params.Add(new ActionParam - { - Key = actionKey, - Value = actionValue, - Args = args, - ParamType = actionParamType - }); - - } - } + AoAction = AoAction.CreateFromQueryString(query); //Add and load tokens var tokens = AoAction diff --git a/src/aoWebWallet/Pages/WalletDetail.razor b/src/aoWebWallet/Pages/WalletDetail.razor index ef6c05b..afbbf28 100644 --- a/src/aoWebWallet/Pages/WalletDetail.razor +++ b/src/aoWebWallet/Pages/WalletDetail.razor @@ -237,12 +237,19 @@ private void Send(BalanceDataViewModel? balanceDataVM) { - var parameters = new DialogParameters { - { x => x.SelectedBalanceDataVM, balanceDataVM }, - { x => x.SelectedWallet, BindingContext.SelectedWallet } - }; - var options = new DialogOptions { CloseOnEscapeKey = true }; - DialogService.Show("Transfer Token", parameters, options); + if(balanceDataVM?.Token == null) + return; + + var aoAction = AoAction.CreateForTokenTransaction(balanceDataVM.Token.TokenId); + + NavigationManager.NavigateTo($"/action?{aoAction.ToQueryString()}"); + + // var parameters = new DialogParameters { + // { x => x.SelectedBalanceDataVM, balanceDataVM }, + // { x => x.SelectedWallet, BindingContext.SelectedWallet } + // }; + // var options = new DialogOptions { CloseOnEscapeKey = true }; + // DialogService.Show("Transfer Token", parameters, options); } private async Task RefreshBalances() diff --git a/src/aoWebWallet/Shared/ActionEditor.razor b/src/aoWebWallet/Shared/ActionEditor.razor index fa19e30..6eccb07 100644 --- a/src/aoWebWallet/Shared/ActionEditor.razor +++ b/src/aoWebWallet/Shared/ActionEditor.razor @@ -1,17 +1,15 @@ @using aoWebWallet.Models - - Target: @AoAction.Target?.Value - + + Target @AoAction.Target?.Value + - @foreach (var value in AoAction.Filled) + @foreach (var ActionParam in AoAction.Filled) { -

@value.Key = @value.Value | @value.ParamType

- @foreach (var arg in value.Args) - { - @arg - } + + @ActionParam.Key @ActionParam.Value + }
diff --git a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor index dd0f083..4a3d20c 100644 --- a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor +++ b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor @@ -1,9 +1,10 @@ @using aoWebWallet.Models -

@ActionParam.Key = @ActionParam.Value | @ActionParam.ParamType

+@*

@ActionParam.Key = @ActionParam.Value | @ActionParam.ParamType

*@ + @if(ReadOnly) { -

@ActionParam.Key = @ActionParam.Value

+ @ActionParam.Key @ActionParam.Value } else { @@ -20,7 +21,7 @@ else } } - +
@code { diff --git a/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor b/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor index 9c5c6e5..8624ad7 100644 --- a/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor +++ b/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor @@ -2,11 +2,13 @@ @using aoWebWallet.Models @inject TokenDataService tokenDataService @inject TokenClient tokenClient -

@ActionParam.Key = @ActionParam.Value | @ActionParam.ParamType

+@*

@ActionParam.Key = @ActionParam.Value | @ActionParam.ParamType

*@ + @if(Token == null) { Loading token data... + return; } @if (ActionParam.ParamType == ActionParamType.Balance && string.IsNullOrEmpty(Address)) @@ -17,27 +19,33 @@ @if (ActionParam.ParamType == ActionParamType.Balance && BalanceData == null) { Loading balance... + return; } @if (ReadOnly) { -

@ActionParam.Key = @BalanceHelper.FormatBalance(long.Parse(ActionParam.Value ?? "0"), Token?.TokenData?.Denomination ?? 0)

-} -else -{ - if (ActionParam.ParamType == ActionParamType.Quantity -|| ActionParam.ParamType == ActionParamType.Balance) + @ActionParam.Key @BalanceHelper.FormatBalance(long.Parse(ActionParam.Value ?? "0"), Token?.TokenData?.Denomination ?? 0) @Token?.TokenData?.Ticker + } + else { - + if (ActionParam.ParamType == ActionParamType.Quantity + || ActionParam.ParamType == ActionParamType.Balance) + { + var label = $"{ActionParam.Key} ({Token?.TokenData?.Ticker})"; + + + + @Token?.TokenData?.Ticker + if (ActionParam.ParamType == ActionParamType.Balance) { - Balance available: @BalanceHelper.FormatBalance(BalanceData?.Balance, Token?.TokenData?.Denomination ?? 1) + Balance available: @BalanceHelper.FormatBalance(BalanceData?.Balance, Token?.TokenData?.Denomination ?? 1) @Token?.TokenData?.Ticker } } } - +
@code { @@ -63,9 +71,9 @@ else protected override void OnAfterRender(bool firstRender) { - if (!(mudTextField?.ValidationErrors.Any() ?? false)) + if (!(mudTextField?.ValidationErrors.Any() ?? false) && Token?.TokenData?.Denomination != null) { - mudTextField?.SetText(ActionParam.Value); + mudTextField?.SetText(@BalanceHelper.FormatBalance(long.Parse(ActionParam.Value ?? "0"), Token.TokenData.Denomination.Value)); } base.OnAfterRender(firstRender); diff --git a/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs b/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs index df882c2..dc15de0 100644 --- a/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs +++ b/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs @@ -1,6 +1,7 @@ using aoWebWallet.Models; using aoWebWallet.Services; using ArweaveAO; +using ArweaveBlazor; using CommunityToolkit.Mvvm.ComponentModel; using MudBlazor; using System.Collections.ObjectModel; @@ -19,6 +20,7 @@ public partial class WalletDetailViewModel : ObservableObject private readonly TokenDataService dataService; private readonly TokenClient tokenClient; private readonly StorageService storageService; + private readonly ArweaveService arweaveService; private readonly ISnackbar snackbar; private readonly MemoryDataCache memoryDataCache; @@ -32,6 +34,12 @@ public partial class WalletDetailViewModel : ObservableObject [ObservableProperty] private bool canClaim3; + [ObservableProperty] + public string? activeArConnectAddress; + + [ObservableProperty] + public bool? hasArConnectExtension; + public int? SelectedWalletIndex { get; set; } @@ -50,6 +58,7 @@ public WalletDetailViewModel(MainViewModel mainViewModel, TokenDataService dataService, TokenClient tokenClient, StorageService storageService, + ArweaveService arweaveService, ISnackbar snackbar, MemoryDataCache memoryDataCache) { @@ -58,6 +67,7 @@ public WalletDetailViewModel(MainViewModel mainViewModel, this.dataService = dataService; this.tokenClient = tokenClient; this.storageService = storageService; + this.arweaveService = arweaveService; this.snackbar = snackbar; this.memoryDataCache = memoryDataCache; } @@ -72,13 +82,32 @@ public async Task Initialize(string address) await LoadSelectedWalletProcessData(address); await LoadSelectedWalletOwnerData(address); - mainViewModel.CheckHasArConnectExtension(); + CheckHasArConnectExtension(); SetClaims(); mainViewModel.AddToLog(ActivityLogType.ViewAddress, address); } + public async Task CheckHasArConnectExtension() + { + HasArConnectExtension = await arweaveService.HasArConnectAsync(); + await GetActiveArConnectAddress(); + } + + public async Task GetActiveArConnectAddress() + { + if (HasArConnectExtension.HasValue && HasArConnectExtension.Value) + { + ActiveArConnectAddress = await arweaveService.GetActiveAddress(); + + if (this.SelectedWallet != null) + { + this.SelectedWallet.IsConnected = SelectedWallet.Wallet.Address == ActiveArConnectAddress; + } + } + } + public async Task RefreshTokenTransferList() { if (selectedAddress != null) @@ -153,6 +182,11 @@ private async Task SelectWallet(string? address) this.LoadBalanceDataList(address); this.LoadTokenTransferList(address); + if (this.SelectedWallet != null) + { + this.SelectedWallet.IsConnected = SelectedWallet.Wallet.Address == ActiveArConnectAddress; + } + } else { From 1c039322beed4dddf0968b7ceda5ccf436abcd9c Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Fri, 26 Apr 2024 16:47:33 +0200 Subject: [PATCH 12/63] Sending transaction progress --- src/aoWebWallet/Models/ActionParam.cs | 2 - src/aoWebWallet/Pages/ActionPage.razor | 30 ++++++++- src/aoWebWallet/Pages/ActionPage.razor.cs | 3 + src/aoWebWallet/Pages/MvvmComponentBase.cs | 2 +- .../Services/TransactionService.cs | 65 ++++++++++--------- .../Shared/ReceiveTokenDialog.razor | 1 - src/aoWebWallet/Shared/SendTokenDialog.razor | 6 +- src/webvNext.DataLoader/DataLoader.cs | 3 - 8 files changed, 68 insertions(+), 44 deletions(-) diff --git a/src/aoWebWallet/Models/ActionParam.cs b/src/aoWebWallet/Models/ActionParam.cs index 822c03f..443b484 100644 --- a/src/aoWebWallet/Models/ActionParam.cs +++ b/src/aoWebWallet/Models/ActionParam.cs @@ -115,8 +115,6 @@ public static AoAction CreateFromQueryString(string qstring) actionValue = null; } - Console.WriteLine($"Val: {actionValue} args: {args.Count()}"); - action.Params.Add(new ActionParam { Key = actionKey, diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor index fc964cf..7632035 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -61,12 +61,29 @@ Preview @validation } - else + else if (!started && string.IsNullOrEmpty(transactionService.LastTransaction.Data?.Id)) { Cancel Submit } - + + @if (transactionService.LastTransaction.DataLoader != null) + { + + if (!string.IsNullOrEmpty(transactionService.LastTransaction.Data?.Id)) + { + + Transfer success! + TransactionId + + @transactionService.LastTransaction.Data?.Id + + + + Return to wallet + } + } +
@@ -75,9 +92,12 @@ private string? validation; private string? selectedWallet; private bool readOnly = false; + private bool started = false; private void Preview() { + //transactionService.LastTransaction.Data = new Transaction() { Id = "test" }; + validation = AoAction.IsValid(); readOnly = string.IsNullOrEmpty(validation); } @@ -86,6 +106,11 @@ readOnly = false; } + private void ReturnToWallet() + { + NavigationManager.NavigateTo($"/wallet/{selectedWallet}"); + } + private async Task Submit() { if (BindingContext.WalletList.Data == null) @@ -99,6 +124,7 @@ //Do we need the owner wallet? Wallet? ownerWallet = BindingContext.WalletList.Data.Where(x => x.Address == wallet.OwnerAddress).FirstOrDefault(); + started = true; await transactionService.SendAction(wallet, ownerWallet, AoAction); } diff --git a/src/aoWebWallet/Pages/ActionPage.razor.cs b/src/aoWebWallet/Pages/ActionPage.razor.cs index 03a4344..d129263 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor.cs +++ b/src/aoWebWallet/Pages/ActionPage.razor.cs @@ -10,9 +10,12 @@ public partial class ActionPage : MvvmComponentBase protected override void OnInitialized() { + transactionService.Reset(); + GetQueryStringValues(); //WatchDataLoaderVM(BindingContext.TokenList); WatchDataLoaderVM(BindingContext.WalletList); + WatchDataLoaderVM(transactionService.LastTransaction); //Auto select wallet if(!string.IsNullOrEmpty(WalletDetailViewModel.SelectedWallet?.Wallet.Address)) diff --git a/src/aoWebWallet/Pages/MvvmComponentBase.cs b/src/aoWebWallet/Pages/MvvmComponentBase.cs index 02abfff..58f8eeb 100644 --- a/src/aoWebWallet/Pages/MvvmComponentBase.cs +++ b/src/aoWebWallet/Pages/MvvmComponentBase.cs @@ -57,7 +57,7 @@ internal async void BindingContext_PropertyChanged(object? sender, System.Compon internal void ObjWatch_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { this.StateHasChanged(); - Console.WriteLine("Obj State changed"); + //Console.WriteLine("Obj State changed: " + sender?.ToString()); } private void Obj_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { diff --git a/src/aoWebWallet/Services/TransactionService.cs b/src/aoWebWallet/Services/TransactionService.cs index 8673c44..07c7e40 100644 --- a/src/aoWebWallet/Services/TransactionService.cs +++ b/src/aoWebWallet/Services/TransactionService.cs @@ -1,13 +1,18 @@ using aoWebWallet.Extensions; using aoWebWallet.Models; using ArweaveBlazor; +using CommunityToolkit.Mvvm.ComponentModel; using webvNext.DataLoader; namespace aoWebWallet.Services { - public class TransactionService(ArweaveService arweaveService) + public class TransactionService(ArweaveService arweaveService) : ObservableObject { - public DataLoaderViewModel LastTransactionId { get; set; } = new(); + public void Reset() + { + LastTransaction.Data = null; + } + public DataLoaderViewModel LastTransaction { get; set; } = new(); public async Task GetActiveArConnectAddress() { @@ -24,48 +29,58 @@ public class TransactionService(ArweaveService arweaveService) return null; } - public async Task SendAction(Wallet wallet, Wallet? ownerWallet, AoAction action) + public async Task SendAction(Wallet wallet, Wallet? ownerWallet, AoAction action) { if (wallet.Source == WalletTypes.ArConnect) { var activeAddress = await GetActiveArConnectAddress(); if(activeAddress == wallet.Address) - return await SendActionWithArConnect(action); + await SendActionWithArConnect(action); } if (ownerWallet?.Source == WalletTypes.ArConnect) { var activeAddress = await GetActiveArConnectAddress(); if (activeAddress == ownerWallet.Address) - return await SendActionWithEvalWithArConnect(wallet.Address, action); + await SendActionWithEvalWithArConnect(wallet.Address, action); } if (!string.IsNullOrEmpty(wallet.OwnerAddress) && ownerWallet?.Address == wallet.Address && !string.IsNullOrEmpty(ownerWallet?.Jwk)) { - return await SendActionWithEval(ownerWallet.Jwk, wallet.Address, action); + Console.WriteLine("eval"); + + await SendActionWithEval(ownerWallet.Jwk, wallet.Address, action); } if (!string.IsNullOrEmpty(wallet.Jwk)) - return await SendActionWithJwk(wallet.Jwk, action); + await SendActionWithJwk(wallet.Jwk, action); - return null; + Console.WriteLine("nothing"); + return; + } + + private async Task SendActionWithEvalWithArConnect(string processId, AoAction action) + { + var activeAddress = await GetActiveArConnectAddress(); + if (string.IsNullOrEmpty(activeAddress)) + return; + await SendActionWithEval(null, processId, action); } - private Task SendActionWithEvalWithArConnect(string processId, AoAction action) - => LastTransactionId.DataLoader.LoadAsync(async () => - { - var activeAddress = await GetActiveArConnectAddress(); - if (string.IsNullOrEmpty(activeAddress)) - return null; + private async Task SendActionWithArConnect(AoAction action) + { + var activeAddress = await GetActiveArConnectAddress(); + if (string.IsNullOrEmpty(activeAddress)) + return; - return await SendActionWithEval(null, processId, action); - }); + await SendActionWithJwk(null, action); + } private Task SendActionWithEval(string? jwk, string processId, AoAction action) - => LastTransactionId.DataLoader.LoadAsync(async () => + => LastTransaction.DataLoader.LoadAsync(async () => { var transferTags = action.ToEvalTags(); @@ -81,20 +96,10 @@ public class TransactionService(ArweaveService arweaveService) var idResult = await arweaveService.SendAsync(jwk, processId, null, data, evalTags); return new Transaction { Id = idResult }; - }); - - private Task SendActionWithArConnect(AoAction action) - => LastTransactionId.DataLoader.LoadAsync(async () => - { - var activeAddress = await GetActiveArConnectAddress(); - if (string.IsNullOrEmpty(activeAddress)) - return null; - - return await SendActionWithJwk(null, action); - }); + }, x => LastTransaction.Data = x); private Task SendActionWithJwk(string? jwk, AoAction action) - => LastTransactionId.DataLoader.LoadAsync(async () => + => LastTransaction.DataLoader.LoadAsync(async () => { if (action.Target?.Value == null) return null; @@ -105,7 +110,7 @@ public class TransactionService(ArweaveService arweaveService) var idResult = await arweaveService.SendAsync(jwk, action.Target.Value, null, null, transferTags); return new Transaction { Id = idResult }; - }); + }, x => LastTransaction.Data = x); diff --git a/src/aoWebWallet/Shared/ReceiveTokenDialog.razor b/src/aoWebWallet/Shared/ReceiveTokenDialog.razor index fd19fbf..e85e1bc 100644 --- a/src/aoWebWallet/Shared/ReceiveTokenDialog.razor +++ b/src/aoWebWallet/Shared/ReceiveTokenDialog.razor @@ -1,6 +1,5 @@ @using aoWebWallet.Models @using aoWebWallet.Shared -@inject ISnackbar Snackbar diff --git a/src/aoWebWallet/Shared/SendTokenDialog.razor b/src/aoWebWallet/Shared/SendTokenDialog.razor index 482c39e..f965e36 100644 --- a/src/aoWebWallet/Shared/SendTokenDialog.razor +++ b/src/aoWebWallet/Shared/SendTokenDialog.razor @@ -1,7 +1,6 @@ @using aoWebWallet.Models @using aoWebWallet.Shared @inject TokenClient TokenClient -@inject ISnackbar Snackbar @inject MainViewModel MainViewModel @inject WalletDetailViewModel WalletDetailViewModel @@ -66,10 +65,7 @@ { if (string.IsNullOrEmpty(TransactionId)) { - if (!MainViewModel.LastTransactionId.DataLoader.IsLoading) - { - Confirm - } + Confirm } else { diff --git a/src/webvNext.DataLoader/DataLoader.cs b/src/webvNext.DataLoader/DataLoader.cs index 5d6b29c..06ad6c7 100644 --- a/src/webvNext.DataLoader/DataLoader.cs +++ b/src/webvNext.DataLoader/DataLoader.cs @@ -36,9 +36,6 @@ public DataLoader(bool swallowExceptions = true) private bool _swallowExceptions; - [ObservableProperty] - private bool isLoading; - [ObservableProperty] private LoadingState loadingState; From 98b07e2f399a61bc0fcc4522733052ffc98f0fee Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Fri, 26 Apr 2024 17:02:10 +0200 Subject: [PATCH 13/63] Button styling and eval fix --- src/aoWebWallet/Pages/ActionPage.razor | 16 ++++++++++++---- src/aoWebWallet/Pages/ActionPage.razor.cs | 8 ++++++-- src/aoWebWallet/Services/TransactionService.cs | 7 ++----- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor index 7632035..6619958 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -58,13 +58,13 @@ @if (!readOnly) { - Preview + Preview @validation } else if (!started && string.IsNullOrEmpty(transactionService.LastTransaction.Data?.Id)) { - Cancel - Submit + Cancel + Submit } @if (transactionService.LastTransaction.DataLoader != null) @@ -80,7 +80,7 @@ - Return to wallet + Return to wallet } } @@ -91,6 +91,7 @@ { private string? validation; private string? selectedWallet; + private Wallet? selectedWalletObj; private bool readOnly = false; private bool started = false; @@ -117,6 +118,13 @@ return; var wallet = BindingContext.WalletList.Data.Where(x => x.Address == selectedWallet).FirstOrDefault(); + if(wallet == null) + { + if(selectedWalletObj?.Address == selectedWallet) + { + wallet = selectedWalletObj; + } + } if (wallet == null) return; diff --git a/src/aoWebWallet/Pages/ActionPage.razor.cs b/src/aoWebWallet/Pages/ActionPage.razor.cs index d129263..034d142 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor.cs +++ b/src/aoWebWallet/Pages/ActionPage.razor.cs @@ -18,8 +18,12 @@ protected override void OnInitialized() WatchDataLoaderVM(transactionService.LastTransaction); //Auto select wallet - if(!string.IsNullOrEmpty(WalletDetailViewModel.SelectedWallet?.Wallet.Address)) - selectedWallet = WalletDetailViewModel.SelectedWallet?.Wallet.Address; + if (!string.IsNullOrEmpty(WalletDetailViewModel.SelectedWallet?.Wallet.Address)) + { + selectedWalletObj = WalletDetailViewModel.SelectedWallet?.Wallet; + selectedWallet = selectedWalletObj?.Address; + + } NavigationManager.LocationChanged += NavigationManager_LocationChanged; diff --git a/src/aoWebWallet/Services/TransactionService.cs b/src/aoWebWallet/Services/TransactionService.cs index 07c7e40..47555b4 100644 --- a/src/aoWebWallet/Services/TransactionService.cs +++ b/src/aoWebWallet/Services/TransactionService.cs @@ -45,19 +45,16 @@ public async Task SendAction(Wallet wallet, Wallet? ownerWallet, AoAction action await SendActionWithEvalWithArConnect(wallet.Address, action); } - if (!string.IsNullOrEmpty(wallet.OwnerAddress) && ownerWallet?.Address == wallet.Address + if (!string.IsNullOrEmpty(wallet.OwnerAddress) && ownerWallet?.Address == wallet.OwnerAddress && !string.IsNullOrEmpty(ownerWallet?.Jwk)) { - Console.WriteLine("eval"); - await SendActionWithEval(ownerWallet.Jwk, wallet.Address, action); - } if (!string.IsNullOrEmpty(wallet.Jwk)) await SendActionWithJwk(wallet.Jwk, action); - Console.WriteLine("nothing"); + Console.WriteLine("No Wallet to send"); return; } From 8d229817fe8e453826a68abcc5e0a020adc1d517 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Fri, 26 Apr 2024 17:06:04 +0200 Subject: [PATCH 14/63] version update --- src/aoWebWallet/aoWebWallet.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aoWebWallet/aoWebWallet.csproj b/src/aoWebWallet/aoWebWallet.csproj index a1937b7..6063641 100644 --- a/src/aoWebWallet/aoWebWallet.csproj +++ b/src/aoWebWallet/aoWebWallet.csproj @@ -5,7 +5,7 @@ enable nullable enable - 0.2.0 + 0.3.0 false false true From 661af1c81b7926f2582fb43eb1efea288669e2b1 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Fri, 26 Apr 2024 17:26:13 +0200 Subject: [PATCH 15/63] Token loading fixes --- src/aoWebWallet/Pages/ActionPage.razor | 2 +- src/aoWebWallet/Pages/ActionPage.razor.cs | 2 +- src/aoWebWallet/Services/StorageService.cs | 5 ++++- src/aoWebWallet/Services/TokenDataService.cs | 3 +-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor index 6619958..1542f95 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -56,7 +56,7 @@ - @if (!readOnly) + @if (!readOnly && !string.IsNullOrEmpty(selectedWallet)) { Preview @validation diff --git a/src/aoWebWallet/Pages/ActionPage.razor.cs b/src/aoWebWallet/Pages/ActionPage.razor.cs index 034d142..806bcf1 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor.cs +++ b/src/aoWebWallet/Pages/ActionPage.razor.cs @@ -37,7 +37,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) await BindingContext.CheckHasArConnectExtension(); await BindingContext.LoadWalletList(); - await dataService.LoadTokenList(); + //await dataService.LoadTokenList(); } await base.OnAfterRenderAsync(firstRender); diff --git a/src/aoWebWallet/Services/StorageService.cs b/src/aoWebWallet/Services/StorageService.cs index 18bc2a9..c0f02a0 100644 --- a/src/aoWebWallet/Services/StorageService.cs +++ b/src/aoWebWallet/Services/StorageService.cs @@ -3,6 +3,7 @@ using ArweaveAO.Models.Token; using Blazored.LocalStorage; using System.Reflection.Metadata; +using static MudBlazor.CategoryTypes; namespace aoWebWallet.Services { @@ -94,7 +95,9 @@ public async ValueTask DeleteToken(string tokenId) public ValueTask SaveTokenList(List list) { - return localStorage.SetItemAsync(TOKEN_LIST_KEY, list); + var uniqueItems = list.GroupBy(i => i.TokenId).Select(g => g.First()); + + return localStorage.SetItemAsync(TOKEN_LIST_KEY, uniqueItems); } public async ValueTask> GetWallets() diff --git a/src/aoWebWallet/Services/TokenDataService.cs b/src/aoWebWallet/Services/TokenDataService.cs index b64770a..4938f3a 100644 --- a/src/aoWebWallet/Services/TokenDataService.cs +++ b/src/aoWebWallet/Services/TokenDataService.cs @@ -46,7 +46,7 @@ public async Task TryAddTokenIds(List allTokenIds) } } - LoadTokenList(); + await LoadTokenList(); } public async Task LoadTokenAsync(string tokenId) @@ -89,7 +89,6 @@ public async Task LoadTokenList(bool force = false) { if (!TokenList.Any() || force) { - TokenList.Clear(); await foreach (var item in LoadTokenDataAsync()) { From be545f63f518b76df09ba69ca9aa01b99579bab6 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Sun, 28 Apr 2024 15:01:07 +0200 Subject: [PATCH 16/63] refactor bugfixes --- src/aoWebWallet/Models/ActionParam.cs | 5 ++ src/aoWebWallet/Pages/ActionPage.razor | 1 + src/aoWebWallet/Pages/Tokens.razor.cs | 1 + src/aoWebWallet/Pages/WalletDetail.razor.cs | 1 + src/aoWebWallet/Services/StorageService.cs | 9 ++-- src/aoWebWallet/Services/TokenDataService.cs | 46 ++++++++----------- .../Services/TransactionService.cs | 16 ++++++- src/aoWebWallet/Shared/AddTokenDialog.razor | 2 - .../Components/ActionQuantityComponent.razor | 1 + 9 files changed, 47 insertions(+), 35 deletions(-) diff --git a/src/aoWebWallet/Models/ActionParam.cs b/src/aoWebWallet/Models/ActionParam.cs index 443b484..8394e15 100644 --- a/src/aoWebWallet/Models/ActionParam.cs +++ b/src/aoWebWallet/Models/ActionParam.cs @@ -38,6 +38,11 @@ public class AoAction return AllWithoutTarget.Select(x => new ArweaveBlazor.Models.Tag { Name = x.Key, Value = x.Value ?? string.Empty }).ToList(); } + public List ToDryRunTags() + { + return AllWithoutTarget.Select(x => new ArweaveAO.Models.Tag { Name = x.Key, Value = x.Value ?? string.Empty }).ToList(); + } + public string ToQueryString() { diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor index 1542f95..31413ec 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -133,6 +133,7 @@ Wallet? ownerWallet = BindingContext.WalletList.Data.Where(x => x.Address == wallet.OwnerAddress).FirstOrDefault(); started = true; + await transactionService.DryRunAction(wallet, ownerWallet, AoAction); await transactionService.SendAction(wallet, ownerWallet, AoAction); } diff --git a/src/aoWebWallet/Pages/Tokens.razor.cs b/src/aoWebWallet/Pages/Tokens.razor.cs index 9904aa6..ec675b1 100644 --- a/src/aoWebWallet/Pages/Tokens.razor.cs +++ b/src/aoWebWallet/Pages/Tokens.razor.cs @@ -7,6 +7,7 @@ public partial class Tokens : MvvmComponentBase protected override void OnInitialized() { WatchCollection(dataService.TokenList); + WatchObject(dataService.TokenDataLoader); base.OnInitialized(); } diff --git a/src/aoWebWallet/Pages/WalletDetail.razor.cs b/src/aoWebWallet/Pages/WalletDetail.razor.cs index f1c48b9..a5b1ba9 100644 --- a/src/aoWebWallet/Pages/WalletDetail.razor.cs +++ b/src/aoWebWallet/Pages/WalletDetail.razor.cs @@ -11,6 +11,7 @@ protected override void OnInitialized() { //WatchObject(dataService.TokenList); WatchObject(BindingContext.BalanceDataList); + WatchObject(dataService.TokenDataLoader); WatchCollection(dataService.TokenList); WatchCollection(BindingContext.BalanceDataList); WatchDataLoaderVM(MainViewModel.WalletList); diff --git a/src/aoWebWallet/Services/StorageService.cs b/src/aoWebWallet/Services/StorageService.cs index c0f02a0..32ba77b 100644 --- a/src/aoWebWallet/Services/StorageService.cs +++ b/src/aoWebWallet/Services/StorageService.cs @@ -36,7 +36,7 @@ private void AddSystemToken(List list, string tokenId) { var existing = list.Where(x => x.TokenId == tokenId).FirstOrDefault(); if (existing != null) - return; + existing.IsSystemToken = true; else list.Add(new Token { TokenId = tokenId, IsSystemToken = true }); } @@ -59,21 +59,22 @@ public async Task AddTokenId(string tokenId, bool isUserAdded = true, bool isVis await SaveTokenList(list); } - public async ValueTask AddToken(string tokenId, TokenData data, bool isUserAdded, bool isVisible = true) + public async ValueTask AddToken(string tokenId, TokenData data, bool isUserAdded, bool? isVisible) { var list = await GetTokenIds(); var existing = list.Where(x => x.TokenId == tokenId).FirstOrDefault(); if (existing != null) { - existing.IsVisible = true; + if(isVisible.HasValue) + existing.IsVisible = isVisible.Value; if(!existing.IsSystemToken) existing.IsUserAdded = true; } else { - existing = new Token { TokenId = tokenId, TokenData = data, IsUserAdded = isUserAdded, IsVisible = isVisible }; + existing = new Token { TokenId = tokenId, TokenData = data, IsUserAdded = isUserAdded, IsVisible = isVisible ?? true }; list.Add(existing); } diff --git a/src/aoWebWallet/Services/TokenDataService.cs b/src/aoWebWallet/Services/TokenDataService.cs index 4938f3a..9d240b1 100644 --- a/src/aoWebWallet/Services/TokenDataService.cs +++ b/src/aoWebWallet/Services/TokenDataService.cs @@ -38,30 +38,29 @@ public async Task TryAddTokenIds(List allTokenIds) { var data = await tokenClient.GetTokenMetaData(tokenId); return data; + }, async data => + { + if (data != null) + { + await storageService.AddToken(tokenId, data, isUserAdded: false, null); + + await LoadTokenList(); + } }); - if (data != null) - { - await storageService.AddToken(tokenId, data, isUserAdded: false); - } + } - - await LoadTokenList(); } public async Task LoadTokenAsync(string tokenId) { - var tokens = await storageService.GetTokenIds(); - - var token = tokens.Where(x => x.TokenId.Equals(tokenId, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); + var token = TokenList.Where(x => x.TokenId.Equals(tokenId, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); if(token == null) { token = new Token() { TokenId = tokenId, }; - - tokens.Add(token); } if (token.TokenData == null) @@ -77,7 +76,9 @@ public async Task LoadTokenAsync(string tokenId) token.TokenData = data; - await storageService.SaveTokenList(tokens); + TokenList.Add(token); + + await storageService.AddToken(tokenId, data, false, null); } } @@ -92,12 +93,15 @@ public async Task LoadTokenList(bool force = false) TokenList.Clear(); await foreach (var item in LoadTokenDataAsync()) { - TokenList.Add(item); + var existing = TokenList.Where(x => x.TokenId == item.TokenId).Any(); + + if(!existing) + TokenList.Add(item); } } } - public async IAsyncEnumerable LoadTokenDataAsync() + private async IAsyncEnumerable LoadTokenDataAsync() { var tokens = await storageService.GetTokenIds(); foreach (var token in tokens) @@ -144,20 +148,6 @@ public async Task TokenToggleVisibility(string tokenId) } } - public async Task AddToken(string tokenId, TokenData data, bool isUserAdded) - { - var newToken = await storageService.AddToken(tokenId, data, isUserAdded); - var existing = TokenList.Where(x => x.TokenId == newToken.TokenId).FirstOrDefault(); - if (existing == null) - { - TokenList.Add(newToken); - } - else - { - existing = newToken; - } - } - public async Task Clear() { await storageService.SaveTokenList(new()); diff --git a/src/aoWebWallet/Services/TransactionService.cs b/src/aoWebWallet/Services/TransactionService.cs index 47555b4..98d2456 100644 --- a/src/aoWebWallet/Services/TransactionService.cs +++ b/src/aoWebWallet/Services/TransactionService.cs @@ -1,12 +1,13 @@ using aoWebWallet.Extensions; using aoWebWallet.Models; +using ArweaveAO.Requests; using ArweaveBlazor; using CommunityToolkit.Mvvm.ComponentModel; using webvNext.DataLoader; namespace aoWebWallet.Services { - public class TransactionService(ArweaveService arweaveService) : ObservableObject + public class TransactionService(ArweaveService arweaveService, ArweaveAO.AODataClient aODataClient) : ObservableObject { public void Reset() { @@ -29,6 +30,19 @@ public void Reset() return null; } + public async Task DryRunAction(Wallet wallet, Wallet? ownerWallet, AoAction action) + { + var target = action.Target?.Value ?? string.Empty; + var druRunRequest = new DryRunRequest() + { + Target = target, + Tags = action.ToDryRunTags() + }; + + var result = aODataClient.DryRun(target, druRunRequest); + + } + public async Task SendAction(Wallet wallet, Wallet? ownerWallet, AoAction action) { if (wallet.Source == WalletTypes.ArConnect) diff --git a/src/aoWebWallet/Shared/AddTokenDialog.razor b/src/aoWebWallet/Shared/AddTokenDialog.razor index 4a538ea..9e741e2 100644 --- a/src/aoWebWallet/Shared/AddTokenDialog.razor +++ b/src/aoWebWallet/Shared/AddTokenDialog.razor @@ -40,8 +40,6 @@ var data = token.TokenData; if (data != null) { - await dataService.AddToken(TokenId, data, isUserAdded: true); - Snackbar.Add($"Token added ({data.Name})", Severity.Info); MudDialog.Close(DialogResult.Ok(true)); diff --git a/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor b/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor index 8624ad7..123d811 100644 --- a/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor +++ b/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor @@ -81,6 +81,7 @@ protected override async Task OnParametersSetAsync() { + BalanceData = null; var token = await tokenDataService.LoadTokenAsync(TokenId); if (token.TokenData?.Denomination != null) Token = token; From 0a94a5152e7fa488625dc83dc600ee0efff846f5 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Sun, 28 Apr 2024 15:24:55 +0200 Subject: [PATCH 17/63] working on dry run preview --- src/aoWebWallet/Pages/ActionPage.razor | 24 +++++++++++++++++-- src/aoWebWallet/Program.cs | 1 + .../Services/TransactionService.cs | 7 ++++-- .../Components/ActionQuantityComponent.razor | 6 +++-- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor index 31413ec..8ed798f 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -95,12 +95,33 @@ private bool readOnly = false; private bool started = false; - private void Preview() + private async void Preview() { //transactionService.LastTransaction.Data = new Transaction() { Id = "test" }; validation = AoAction.IsValid(); readOnly = string.IsNullOrEmpty(validation); + + if (BindingContext.WalletList.Data == null) + return; + + var wallet = BindingContext.WalletList.Data.Where(x => x.Address == selectedWallet).FirstOrDefault(); + if (wallet == null) + { + if (selectedWalletObj?.Address == selectedWallet) + { + wallet = selectedWalletObj; + } + } + + if (wallet == null) + return; + + //Do we need the owner wallet? + Wallet? ownerWallet = BindingContext.WalletList.Data.Where(x => x.Address == wallet.OwnerAddress).FirstOrDefault(); + + //await transactionService.DryRunAction(wallet, ownerWallet, AoAction); + } private void Cancel() { @@ -133,7 +154,6 @@ Wallet? ownerWallet = BindingContext.WalletList.Data.Where(x => x.Address == wallet.OwnerAddress).FirstOrDefault(); started = true; - await transactionService.DryRunAction(wallet, ownerWallet, AoAction); await transactionService.SendAction(wallet, ownerWallet, AoAction); } diff --git a/src/aoWebWallet/Program.cs b/src/aoWebWallet/Program.cs index 515a58d..382a683 100644 --- a/src/aoWebWallet/Program.cs +++ b/src/aoWebWallet/Program.cs @@ -99,6 +99,7 @@ private static void ConfigureServices(IServiceCollection services, string baseAd services.AddScoped(); services.AddArweaveBlazor(); + services.AddScoped(); //Register ViewModels diff --git a/src/aoWebWallet/Services/TransactionService.cs b/src/aoWebWallet/Services/TransactionService.cs index 98d2456..4e7e830 100644 --- a/src/aoWebWallet/Services/TransactionService.cs +++ b/src/aoWebWallet/Services/TransactionService.cs @@ -1,6 +1,7 @@ using aoWebWallet.Extensions; using aoWebWallet.Models; using ArweaveAO.Requests; +using ArweaveAO.Responses; using ArweaveBlazor; using CommunityToolkit.Mvvm.ComponentModel; using webvNext.DataLoader; @@ -30,17 +31,19 @@ public void Reset() return null; } - public async Task DryRunAction(Wallet wallet, Wallet? ownerWallet, AoAction action) + public async Task DryRunAction(Wallet wallet, Wallet? ownerWallet, AoAction action) { var target = action.Target?.Value ?? string.Empty; var druRunRequest = new DryRunRequest() { Target = target, + Owner = ownerWallet?.Address ?? wallet.Address, Tags = action.ToDryRunTags() }; - var result = aODataClient.DryRun(target, druRunRequest); + var result = await aODataClient.DryRun(target, druRunRequest); + return result; } public async Task SendAction(Wallet wallet, Wallet? ownerWallet, AoAction action) diff --git a/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor b/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor index 123d811..1ca1e11 100644 --- a/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor +++ b/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor @@ -16,7 +16,7 @@ Please select a wallet... return; } -@if (ActionParam.ParamType == ActionParamType.Balance && BalanceData == null) +@if (ActionParam.ParamType == ActionParamType.Balance && BalanceData == null && !ReadOnly) { Loading balance... @@ -86,7 +86,9 @@ if (token.TokenData?.Denomination != null) Token = token; - if (ActionParam.ParamType == ActionParamType.Balance && !string.IsNullOrEmpty(Address)) + if (ActionParam.ParamType == ActionParamType.Balance + && !string.IsNullOrEmpty(Address) + && !ReadOnly) { BalanceData = await tokenClient.GetBalance(token.TokenId, Address); } From 6d0641587d93f78c1264ca90ad07c9a87644010e Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Thu, 2 May 2024 10:08:03 +0200 Subject: [PATCH 18/63] showing dry run result --- src/aoWebWallet/Pages/ActionPage.razor | 36 ++++++++++++++++++- src/aoWebWallet/Pages/ActionPage.razor.cs | 1 + .../Services/TransactionService.cs | 31 ++++++++-------- 3 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor index 8ed798f..3976d0a 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -63,6 +63,27 @@ } else if (!started && string.IsNullOrEmpty(transactionService.LastTransaction.Data?.Id)) { + + if (transactionService.DryRunResult.Data != null) + { + + + Preview Result + @foreach (var msg in transactionService.DryRunResult.Data.Messages) + { + var error = msg.Tags.Where(x => x.Name == "Error").Select(x => x.Value).FirstOrDefault(); + + Message for: @msg.Target + @RemoveColorCodes(msg.Data) + @error + } + + @* + Learn More + *@ + + } + Cancel Submit } @@ -95,6 +116,19 @@ private bool readOnly = false; private bool started = false; + static string RemoveColorCodes(string? input) + { + if (input == null) + return string.Empty; + + // Define a regular expression pattern to match color codes + string pattern = @"\x1B\[[0-9;]*[mK]"; + + // Replace color codes with an empty string + string output = System.Text.RegularExpressions.Regex.Replace(input, pattern, ""); + return output; + } + private async void Preview() { //transactionService.LastTransaction.Data = new Transaction() { Id = "test" }; @@ -120,7 +154,7 @@ //Do we need the owner wallet? Wallet? ownerWallet = BindingContext.WalletList.Data.Where(x => x.Address == wallet.OwnerAddress).FirstOrDefault(); - //await transactionService.DryRunAction(wallet, ownerWallet, AoAction); + transactionService.DryRunAction(wallet, ownerWallet, AoAction); } private void Cancel() diff --git a/src/aoWebWallet/Pages/ActionPage.razor.cs b/src/aoWebWallet/Pages/ActionPage.razor.cs index 806bcf1..2b06e38 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor.cs +++ b/src/aoWebWallet/Pages/ActionPage.razor.cs @@ -16,6 +16,7 @@ protected override void OnInitialized() //WatchDataLoaderVM(BindingContext.TokenList); WatchDataLoaderVM(BindingContext.WalletList); WatchDataLoaderVM(transactionService.LastTransaction); + WatchDataLoaderVM(transactionService.DryRunResult); //Auto select wallet if (!string.IsNullOrEmpty(WalletDetailViewModel.SelectedWallet?.Wallet.Address)) diff --git a/src/aoWebWallet/Services/TransactionService.cs b/src/aoWebWallet/Services/TransactionService.cs index 4e7e830..c9816e5 100644 --- a/src/aoWebWallet/Services/TransactionService.cs +++ b/src/aoWebWallet/Services/TransactionService.cs @@ -13,8 +13,10 @@ public class TransactionService(ArweaveService arweaveService, ArweaveAO.AODataC public void Reset() { LastTransaction.Data = null; + DryRunResult.Data = null; } public DataLoaderViewModel LastTransaction { get; set; } = new(); + public DataLoaderViewModel DryRunResult { get; set; } = new(); public async Task GetActiveArConnectAddress() { @@ -31,20 +33,21 @@ public void Reset() return null; } - public async Task DryRunAction(Wallet wallet, Wallet? ownerWallet, AoAction action) - { - var target = action.Target?.Value ?? string.Empty; - var druRunRequest = new DryRunRequest() - { - Target = target, - Owner = ownerWallet?.Address ?? wallet.Address, - Tags = action.ToDryRunTags() - }; - - var result = await aODataClient.DryRun(target, druRunRequest); - - return result; - } + public Task DryRunAction(Wallet wallet, Wallet? ownerWallet, AoAction action) + => DryRunResult.DataLoader.LoadAsync(async () => + { + var target = action.Target?.Value ?? string.Empty; + var druRunRequest = new DryRunRequest() + { + Target = target, + Owner = ownerWallet?.Address ?? wallet.Address, + Tags = action.ToDryRunTags() + }; + + var result = await aODataClient.DryRun(target, druRunRequest); + + return result; + }, x => DryRunResult.Data = x); public async Task SendAction(Wallet wallet, Wallet? ownerWallet, AoAction action) { From 3c259395945c1b102840ae6ffe86e96cb26537bc Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Thu, 2 May 2024 11:56:01 +0200 Subject: [PATCH 19/63] Action dry run bugfix --- src/aoWebWallet/Pages/ActionPage.razor | 18 ++++++++++++++---- src/aoWebWallet/Services/TransactionService.cs | 4 ++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor index 3976d0a..0fb8819 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -95,13 +95,14 @@ { Transfer success! - TransactionId + Message Id @transactionService.LastTransaction.Data?.Id - Return to wallet + Return to wallet + @* View Transaction *@ } } @@ -152,9 +153,9 @@ return; //Do we need the owner wallet? - Wallet? ownerWallet = BindingContext.WalletList.Data.Where(x => x.Address == wallet.OwnerAddress).FirstOrDefault(); + //Wallet? ownerWallet = BindingContext.WalletList.Data.Where(x => x.Address == wallet.OwnerAddress).FirstOrDefault(); - transactionService.DryRunAction(wallet, ownerWallet, AoAction); + transactionService.DryRunAction(wallet, AoAction); } private void Cancel() @@ -167,6 +168,15 @@ NavigationManager.NavigateTo($"/wallet/{selectedWallet}"); } + private void ViewTransaction() + { + if (!string.IsNullOrEmpty(transactionService.LastTransaction.Data?.Id)) + NavigationManager.NavigateTo($"/transaction/{transactionService.LastTransaction.Data?.Id}"); + else + ReturnToWallet(); + + } + private async Task Submit() { if (BindingContext.WalletList.Data == null) diff --git a/src/aoWebWallet/Services/TransactionService.cs b/src/aoWebWallet/Services/TransactionService.cs index c9816e5..2804b1f 100644 --- a/src/aoWebWallet/Services/TransactionService.cs +++ b/src/aoWebWallet/Services/TransactionService.cs @@ -33,14 +33,14 @@ public void Reset() return null; } - public Task DryRunAction(Wallet wallet, Wallet? ownerWallet, AoAction action) + public Task DryRunAction(Wallet wallet, AoAction action) => DryRunResult.DataLoader.LoadAsync(async () => { var target = action.Target?.Value ?? string.Empty; var druRunRequest = new DryRunRequest() { Target = target, - Owner = ownerWallet?.Address ?? wallet.Address, + Owner = wallet.Address, Tags = action.ToDryRunTags() }; From 65dd007ee2a2773e6e2b098baa4fefb01bbd450a Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Thu, 2 May 2024 12:56:44 +0200 Subject: [PATCH 20/63] Setup for AddressBook --- src/aoWebWallet/Pages/AddressBook.razor | 111 ++++++++++++++++++ src/aoWebWallet/Pages/AddressBook.razor.cs | 27 +++++ src/aoWebWallet/Pages/Wallets.razor | 13 +- src/aoWebWallet/Pages/Wallets.razor.cs | 7 -- src/aoWebWallet/Services/StorageService.cs | 2 +- ...ponent.razor => AddContactComponent.razor} | 26 ++-- src/aoWebWallet/Shared/AddTokenDialog.razor | 2 +- src/aoWebWallet/Shared/AddWalletDialog.razor | 19 +-- .../Components/ActionInputComponent.razor | 2 +- src/aoWebWallet/Shared/NavMenu.razor | 2 + src/aoWebWallet/Shared/SendTokenDialog.razor | 2 +- 11 files changed, 164 insertions(+), 49 deletions(-) create mode 100644 src/aoWebWallet/Pages/AddressBook.razor create mode 100644 src/aoWebWallet/Pages/AddressBook.razor.cs rename src/aoWebWallet/Shared/{AddWalletComponent.razor => AddContactComponent.razor} (67%) diff --git a/src/aoWebWallet/Pages/AddressBook.razor b/src/aoWebWallet/Pages/AddressBook.razor new file mode 100644 index 0000000..979f80e --- /dev/null +++ b/src/aoWebWallet/Pages/AddressBook.razor @@ -0,0 +1,111 @@ +@page "/address-book" +@using aoWebWallet.Models +@inherits MvvmComponentBase +@inject IDialogService DialogService +@inject ISnackbar Snackbar +@inject ClipboardService ClipboardService + +Address Book - @Program.PageTitlePostFix + + + + + + Address Book + + + @if (BindingContext.WalletList.Data != null && BindingContext.WalletList.Data.Any()) + { + + + + } + + + + @if (BindingContext.WalletList.Data != null) + { + if(BindingContext.WalletList.Data.Where(x => x.IsReadOnly).Any()) + { + int logoCount = 1; + foreach (var wallet in BindingContext.WalletList.Data.Where(x => x.IsReadOnly)) + { + string logoUrl = $"images/account--{logoCount}.svg"; + string detailUrl = $"wallet/{wallet.Address}"; + + + + + +
+ + @wallet.Address + + +
+
+ @wallet.Name +
+
+ + @if(BindingContext.ProcessesDataList?.Data?.Where(x => x.Data?.Address == wallet.Address && (x.Data?.Processes?.Any() ?? false)).Any() ?? false) + { + AOS + } + + +
+
+ + logoCount++; + if (logoCount > 5) + logoCount = 1; + } + } + else + { + + + + + + + } + } + else + { + Loading address book... + + + } + +
+
+ + +@code +{ + private void OpenDialog() + { + var options = new DialogOptions { CloseOnEscapeKey = true }; + DialogService.Show("Add Contact", options); + } + + private async void DeleteWallet(Wallet wallet) + { + bool? result = await DialogService.ShowMessageBox( + "Warning", + $"Are you sure you want to delete this contact? {wallet.Address}?", + yesText: "Delete!", cancelText: "Cancel"); + + if (result != null) + { + await BindingContext.DeleteWallet(wallet); + + Snackbar.Add($"Contact deleted ({wallet.Address})", Severity.Info); + } + StateHasChanged(); + } + + +} diff --git a/src/aoWebWallet/Pages/AddressBook.razor.cs b/src/aoWebWallet/Pages/AddressBook.razor.cs new file mode 100644 index 0000000..8d519b8 --- /dev/null +++ b/src/aoWebWallet/Pages/AddressBook.razor.cs @@ -0,0 +1,27 @@ +using aoWebWallet.ViewModels; + +namespace aoWebWallet.Pages +{ + public partial class AddressBook : MvvmComponentBase + { + protected override void OnInitialized() + { + //WatchDataLoaderVM(BindingContext.TokenList); + WatchDataLoaderVM(BindingContext.WalletList); + WatchDataLoaderVM(BindingContext.ProcessesDataList); + + base.OnInitialized(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await BindingContext.LoadWalletList(); + } + + await base.OnAfterRenderAsync(firstRender); + } + + } +} diff --git a/src/aoWebWallet/Pages/Wallets.razor b/src/aoWebWallet/Pages/Wallets.razor index 89e60c5..e9f9afd 100644 --- a/src/aoWebWallet/Pages/Wallets.razor +++ b/src/aoWebWallet/Pages/Wallets.razor @@ -11,7 +11,7 @@ - @if (BindingContext.WalletList.Data == null || BindingContext.WalletList.Data.Count > 1) + @if (BindingContext.WalletList.Data == null || BindingContext.WalletList.Data.Where(x => !x.IsReadOnly).Count() > 1) { Wallets } @@ -34,10 +34,10 @@ @if (BindingContext.WalletList.Data != null) { - if(BindingContext.WalletList.Data.Any()) + if (BindingContext.WalletList.Data.Where(x => !x.IsReadOnly).Any()) { int logoCount = 1; - foreach (var wallet in BindingContext.WalletList.Data) + foreach (var wallet in BindingContext.WalletList.Data.Where(x => !x.IsReadOnly)) { string logoUrl = $"images/account--{logoCount}.svg"; string detailUrl = $"wallet/{wallet.Address}"; @@ -53,10 +53,6 @@
- @if (wallet.IsReadOnly) - { - read-only   - } @wallet.Name @if (wallet.NeedsBackup) @@ -98,9 +94,6 @@ - - - diff --git a/src/aoWebWallet/Pages/Wallets.razor.cs b/src/aoWebWallet/Pages/Wallets.razor.cs index 701ad89..ddce30b 100644 --- a/src/aoWebWallet/Pages/Wallets.razor.cs +++ b/src/aoWebWallet/Pages/Wallets.razor.cs @@ -26,12 +26,5 @@ protected override async Task OnAfterRenderAsync(bool firstRender) await base.OnAfterRenderAsync(firstRender); } - //protected override async Task LoadDataAsync() - //{ - - - // //BindingContext.LoadStats(); - //} - } } diff --git a/src/aoWebWallet/Services/StorageService.cs b/src/aoWebWallet/Services/StorageService.cs index 32ba77b..c65b8bd 100644 --- a/src/aoWebWallet/Services/StorageService.cs +++ b/src/aoWebWallet/Services/StorageService.cs @@ -111,7 +111,7 @@ public async ValueTask SaveWallet (Wallet wallet) { var list = await GetWallets(); - var existing = list.Where(x => x.Address == wallet.Address).FirstOrDefault(); + var existing = list.Where(x => x.Address == wallet.Address && x.IsReadOnly == wallet.IsReadOnly).FirstOrDefault(); if(existing != null) list.Remove(existing); diff --git a/src/aoWebWallet/Shared/AddWalletComponent.razor b/src/aoWebWallet/Shared/AddContactComponent.razor similarity index 67% rename from src/aoWebWallet/Shared/AddWalletComponent.razor rename to src/aoWebWallet/Shared/AddContactComponent.razor index 8111a1d..949c0db 100644 --- a/src/aoWebWallet/Shared/AddWalletComponent.razor +++ b/src/aoWebWallet/Shared/AddContactComponent.razor @@ -4,13 +4,13 @@ - + @Progress
- Add Custom Wallet + Add Contact
@@ -22,6 +22,8 @@ [Parameter] public bool HideAddButton { get; set; } + [CascadingParameter] MudDialogInstance? MudDialog { get; set; } + public string? Name { get; set; } public string? Address { get; set; } public string? Progress { get; set; } @@ -36,13 +38,14 @@ public async Task Submit() { - if(string.IsNullOrWhiteSpace(Address)) - { - Progress = "Input a wallet address."; - StateHasChanged(); - return false; - } - if (Address.Length != 43) + // if(string.IsNullOrWhiteSpace(Address)) + // { + // Progress = "Input a wallet address."; + // StateHasChanged(); + // return false; + // } + + if (string.IsNullOrWhiteSpace(Address) || Address.Length != 43) { Progress = "Length must be 43 characters."; StateHasChanged(); @@ -60,7 +63,10 @@ await BindingContext.SaveWallet(wallet); - Snackbar.Add($"Wallet added ({Address})", Severity.Info); + Snackbar.Add($"Contact added ({Address})", Severity.Info); + + MudDialog?.Close(true); + return true; } } diff --git a/src/aoWebWallet/Shared/AddTokenDialog.razor b/src/aoWebWallet/Shared/AddTokenDialog.razor index 9e741e2..76c873b 100644 --- a/src/aoWebWallet/Shared/AddTokenDialog.razor +++ b/src/aoWebWallet/Shared/AddTokenDialog.razor @@ -5,7 +5,7 @@ Add a token to view your balance. Provide a process-id that implements the token standard. - + @Progress diff --git a/src/aoWebWallet/Shared/AddWalletDialog.razor b/src/aoWebWallet/Shared/AddWalletDialog.razor index 7b1397a..48cf964 100644 --- a/src/aoWebWallet/Shared/AddWalletDialog.razor +++ b/src/aoWebWallet/Shared/AddWalletDialog.razor @@ -15,32 +15,15 @@ - - - @code { [CascadingParameter] MudDialogInstance MudDialog { get; set; } = default!; - private AddWalletComponent? addWalletRef; - public async Task Submit() { - // Call a function in AddWalletComponent - if (addWalletRef != null) - { - var result = await addWalletRef.Submit(); - if(result) - { - MudDialog.Close(DialogResult.Ok(true)); - } - } - else - { - MudDialog.Close(DialogResult.Ok(true)); - } + MudDialog.Close(DialogResult.Ok(true)); } //void Submit() => MudDialog.Close(DialogResult.Ok(true)); diff --git a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor index 4a3d20c..2a20332 100644 --- a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor +++ b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor @@ -14,7 +14,7 @@ else } else if (ActionParam.ParamType == ActionParamType.Process) { - + } else if (ActionParam.ParamType == ActionParamType.Integer) { diff --git a/src/aoWebWallet/Shared/NavMenu.razor b/src/aoWebWallet/Shared/NavMenu.razor index 2f506d6..634ad87 100644 --- a/src/aoWebWallet/Shared/NavMenu.razor +++ b/src/aoWebWallet/Shared/NavMenu.razor @@ -28,6 +28,8 @@ Wallets } + Address Book + Token Explorer
diff --git a/src/aoWebWallet/Shared/SendTokenDialog.razor b/src/aoWebWallet/Shared/SendTokenDialog.razor index f965e36..87b8150 100644 --- a/src/aoWebWallet/Shared/SendTokenDialog.razor +++ b/src/aoWebWallet/Shared/SendTokenDialog.razor @@ -18,7 +18,7 @@ @if (!isConfirm) { - + @Progress From 682fe48a8e8abf6e448debe6f92597edfe658cbd Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Thu, 2 May 2024 14:41:02 +0200 Subject: [PATCH 21/63] Edit wallets --- src/aoWebWallet/Pages/AddressBook.razor | 27 ++++++++++++++-- src/aoWebWallet/Pages/Wallets.razor | 11 +++++++ src/aoWebWallet/Services/StorageService.cs | 1 + src/aoWebWallet/Shared/AddTokenDialog.razor | 2 +- .../Components/ActionInputComponent.razor | 2 +- ...ponent.razor => EditWalletComponent.razor} | 31 ++++++++----------- src/aoWebWallet/Shared/NavMenu.razor | 4 +-- src/aoWebWallet/Shared/SendTokenDialog.razor | 2 +- .../ViewModels/WalletDetailViewModel.cs | 15 +++++++-- 9 files changed, 67 insertions(+), 28 deletions(-) rename src/aoWebWallet/Shared/{AddContactComponent.razor => EditWalletComponent.razor} (55%) diff --git a/src/aoWebWallet/Pages/AddressBook.razor b/src/aoWebWallet/Pages/AddressBook.razor index 979f80e..aa5bb68 100644 --- a/src/aoWebWallet/Pages/AddressBook.razor +++ b/src/aoWebWallet/Pages/AddressBook.razor @@ -52,7 +52,8 @@ { AOS } - + + @@ -87,8 +88,28 @@ { private void OpenDialog() { - var options = new DialogOptions { CloseOnEscapeKey = true }; - DialogService.Show("Add Contact", options); + var tempWallet = new Wallet() + { + Address = string.Empty, + IsReadOnly = true, + AddedDate = DateTimeOffset.UtcNow, + Source = WalletTypes.Manual + }; + + var parameters = new DialogParameters { { x => x.Wallet, tempWallet } }; + + var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true }; + DialogService.Show("Add Contact", parameters, options); + } + + private async void EditWallet(Wallet wallet) + { + var parameters = new DialogParameters { { x => x.Wallet, wallet } }; + + var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true }; + DialogService.Show("Edit Contact", parameters, options); + + StateHasChanged(); } private async void DeleteWallet(Wallet wallet) diff --git a/src/aoWebWallet/Pages/Wallets.razor b/src/aoWebWallet/Pages/Wallets.razor index e9f9afd..d948a7c 100644 --- a/src/aoWebWallet/Pages/Wallets.razor +++ b/src/aoWebWallet/Pages/Wallets.razor @@ -71,6 +71,7 @@ { } + @@ -119,6 +120,16 @@ DialogService.Show("Add Wallet", options); } + private async void EditWallet(Wallet wallet) + { + var parameters = new DialogParameters { { x => x.Wallet, wallet } }; + + var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true }; + DialogService.Show("Edit Wallet", parameters, options); + + StateHasChanged(); + } + private async void DeleteWallet(Wallet wallet) { bool? result = await DialogService.ShowMessageBox( diff --git a/src/aoWebWallet/Services/StorageService.cs b/src/aoWebWallet/Services/StorageService.cs index c65b8bd..81108a9 100644 --- a/src/aoWebWallet/Services/StorageService.cs +++ b/src/aoWebWallet/Services/StorageService.cs @@ -107,6 +107,7 @@ public async ValueTask> GetWallets() return result ?? new(); } + public async ValueTask SaveWallet (Wallet wallet) { var list = await GetWallets(); diff --git a/src/aoWebWallet/Shared/AddTokenDialog.razor b/src/aoWebWallet/Shared/AddTokenDialog.razor index 76c873b..4b25fb0 100644 --- a/src/aoWebWallet/Shared/AddTokenDialog.razor +++ b/src/aoWebWallet/Shared/AddTokenDialog.razor @@ -5,7 +5,7 @@ Add a token to view your balance. Provide a process-id that implements the token standard. - + @Progress diff --git a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor index 2a20332..b589f14 100644 --- a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor +++ b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor @@ -14,7 +14,7 @@ else } else if (ActionParam.ParamType == ActionParamType.Process) { - + } else if (ActionParam.ParamType == ActionParamType.Integer) { diff --git a/src/aoWebWallet/Shared/AddContactComponent.razor b/src/aoWebWallet/Shared/EditWalletComponent.razor similarity index 55% rename from src/aoWebWallet/Shared/AddContactComponent.razor rename to src/aoWebWallet/Shared/EditWalletComponent.razor index 949c0db..f5b6c1c 100644 --- a/src/aoWebWallet/Shared/AddContactComponent.razor +++ b/src/aoWebWallet/Shared/EditWalletComponent.razor @@ -4,13 +4,13 @@ - - + + @Progress
- Add Contact + Save
@@ -20,17 +20,21 @@ @code { [Parameter] - public bool HideAddButton { get; set; } + public Wallet Wallet { get; set; } = new() { Address = string.Empty }; [CascadingParameter] MudDialogInstance? MudDialog { get; set; } - public string? Name { get; set; } - public string? Address { get; set; } public string? Progress { get; set; } [Parameter] public bool IsExpanded { get; set; } + + protected override void OnInitialized() + { + base.OnInitialized(); + } + private void OnExpandCollapseClick() { IsExpanded = !IsExpanded; @@ -45,25 +49,16 @@ // return false; // } - if (string.IsNullOrWhiteSpace(Address) || Address.Length != 43) + if (string.IsNullOrWhiteSpace(Wallet.Address) || Wallet.Address.Length != 43) { Progress = "Length must be 43 characters."; StateHasChanged(); return false; } - var wallet = new Wallet - { - Address = Address, - Name = Name, - Source = WalletTypes.Manual, - IsReadOnly = true, - AddedDate = DateTimeOffset.UtcNow - }; - - await BindingContext.SaveWallet(wallet); + await BindingContext.SaveWallet(Wallet); - Snackbar.Add($"Contact added ({Address})", Severity.Info); + Snackbar.Add($"Address saved ({Wallet.Address})", Severity.Info); MudDialog?.Close(true); diff --git a/src/aoWebWallet/Shared/NavMenu.razor b/src/aoWebWallet/Shared/NavMenu.razor index 634ad87..030ffaf 100644 --- a/src/aoWebWallet/Shared/NavMenu.razor +++ b/src/aoWebWallet/Shared/NavMenu.razor @@ -2,14 +2,14 @@ - @if ((BindingContext.WalletList.Data ?? new()).Any()) + @if (BindingContext.WalletList.Data?.Where(x => !x.IsReadOnly).Any() ?? false) { Home @{ int logoCount = 1; } - @foreach (var wallet in BindingContext.WalletList.Data ?? new()) + @foreach (var wallet in BindingContext.WalletList.Data?.Where(x => !x.IsReadOnly).ToList() ?? new()) { string logoUrl = $"images/account--{logoCount}.svg"; string detailUrl = $"wallet/{wallet.Address}"; diff --git a/src/aoWebWallet/Shared/SendTokenDialog.razor b/src/aoWebWallet/Shared/SendTokenDialog.razor index 87b8150..db6f9df 100644 --- a/src/aoWebWallet/Shared/SendTokenDialog.razor +++ b/src/aoWebWallet/Shared/SendTokenDialog.razor @@ -18,7 +18,7 @@ @if (!isConfirm) { - + @Progress diff --git a/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs b/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs index dc15de0..3274963 100644 --- a/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs +++ b/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs @@ -296,8 +296,19 @@ public async Task AddWalletAsReadonly() if (SelectedWallet != null) { SelectedWallet.Wallet.Source = WalletTypes.Manual; - if (SelectedWallet.Wallet.OwnerAddress != null) - SelectedWallet.Wallet.Source = WalletTypes.AoProcess; + SelectedWallet.Wallet.IsReadOnly = true; + + var ownerAddress = SelectedWallet.Wallet.OwnerAddress; + if (ownerAddress != null) + { + var ownerWallet = mainViewModel.WalletList.Data?.Where(x => !x.IsReadOnly && x.Address == ownerAddress).FirstOrDefault(); + + if (ownerWallet != null) + { + SelectedWallet.Wallet.Source = WalletTypes.AoProcess; + SelectedWallet.Wallet.IsReadOnly = false; + } + } await storageService.SaveWallet(SelectedWallet.Wallet); await mainViewModel.LoadWalletList(force: true); From baf8a969b8906c6aae969b9cc3361636ff9b0cf8 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Thu, 2 May 2024 15:12:10 +0200 Subject: [PATCH 22/63] Star or edit --- src/aoWebWallet/Pages/WalletDetail.razor | 20 +++++++++++++++++-- .../ViewModels/WalletDetailViewModel.cs | 8 ++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/aoWebWallet/Pages/WalletDetail.razor b/src/aoWebWallet/Pages/WalletDetail.razor index 3eb4d69..2591903 100644 --- a/src/aoWebWallet/Pages/WalletDetail.razor +++ b/src/aoWebWallet/Pages/WalletDetail.razor @@ -51,8 +51,14 @@ @if (BindingContext.SelectedWallet?.Wallet.Source == WalletTypes.Explorer) { - - + + + + } + else if(BindingContext.SelectedWallet?.Wallet != null) + { + + } @@ -227,6 +233,16 @@ DialogService.Show("Add Token", options); } + private async void EditWallet(Wallet wallet) + { + var parameters = new DialogParameters { { x => x.Wallet, wallet } }; + + var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true }; + DialogService.Show("Edit Wallet", parameters, options); + + StateHasChanged(); + } + private void Receive(BalanceDataViewModel? balanceDataVM) { var parameters = new DialogParameters { { x => x.SelectedBalanceDataVM, balanceDataVM } }; diff --git a/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs b/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs index 3274963..f50d106 100644 --- a/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs +++ b/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs @@ -291,10 +291,14 @@ private void CheckCanOwnerOfSelectedWalletSend() } } - public async Task AddWalletAsReadonly() + public async Task SaveExplorerWallet() { - if (SelectedWallet != null) + if (SelectedWallet?.Wallet != null) { + var existing = mainViewModel.WalletList.Data?.Where(x => x.Address == SelectedWallet.Wallet.Address).Any() ?? false; + if (existing) + return; + SelectedWallet.Wallet.Source = WalletTypes.Manual; SelectedWallet.Wallet.IsReadOnly = true; From 4dd9cc0ae3f6408b7066bade0386a3c63c90cb10 Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Fri, 3 May 2024 15:14:14 +0100 Subject: [PATCH 23/63] setup page structure --- src/aoWebWallet/Pages/Start.razor | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/aoWebWallet/Pages/Start.razor diff --git a/src/aoWebWallet/Pages/Start.razor b/src/aoWebWallet/Pages/Start.razor new file mode 100644 index 0000000..2813180 --- /dev/null +++ b/src/aoWebWallet/Pages/Start.razor @@ -0,0 +1,29 @@ +@page "/start" +@inherits MvvmComponentBase + +Start - @Program.PageTitlePostFix + + + + + + Create AOWW Wallet + + + + + Connect to ArConnect + + + + + Load wallet from .json + + + + + + @code { + + +} From ed705598308142e48fe1be5f954b0da7f221e890 Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Fri, 3 May 2024 15:53:17 +0100 Subject: [PATCH 24/63] new style approach --- src/aoWebWallet/Pages/Start.razor | 6 +++--- src/aoWebWallet/wwwroot/css/app.css | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/aoWebWallet/Pages/Start.razor b/src/aoWebWallet/Pages/Start.razor index 2813180..4581a07 100644 --- a/src/aoWebWallet/Pages/Start.razor +++ b/src/aoWebWallet/Pages/Start.razor @@ -6,17 +6,17 @@ - + Create AOWW Wallet - + Connect to ArConnect - + Load wallet from .json diff --git a/src/aoWebWallet/wwwroot/css/app.css b/src/aoWebWallet/wwwroot/css/app.css index c058ba6..564cbd7 100644 --- a/src/aoWebWallet/wwwroot/css/app.css +++ b/src/aoWebWallet/wwwroot/css/app.css @@ -268,3 +268,30 @@ button.mud-button-root.mud-icon-button.mud-ripple.mud-ripple-icon.copy-clipboard display: flex; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.09); } + +.border-radius-25 { + border-radius: 25px; +} + +.mud-paper.border-radius-25 { + border-radius: 25px; +} + +.mud-paper.first-wallet { + background: rgb(48,20,82); + background: linear-gradient(180deg, rgba(48,20,82,1) 0%, rgba(69,20,84,1) 100%); +} + +.mud-navmenu { + background: rgb(48,20,82); + background: linear-gradient(180deg, rgba(48,20,82,1) 0%, rgba(69,20,84,1) 100%); +} + +.mud-appbar { + background-color: #101043 !important; +} + +body { + background: rgb(56,18,74); + background: linear-gradient(90deg, rgba(56,18,74,1) 0%, rgba(24,17,69,1) 100%, rgba(0,212,255,1) 100%); +} From 4dbebdc93f0bcb6e173df45d62fa083a5de27580 Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Fri, 3 May 2024 18:09:31 +0100 Subject: [PATCH 25/63] first wallet animation --- src/aoWebWallet/Pages/Start.razor | 1 + src/aoWebWallet/wwwroot/css/app.css | 6 + src/aoWebWallet/wwwroot/images/ww.svg | 3402 +++++++++++++++++++++++++ 3 files changed, 3409 insertions(+) create mode 100644 src/aoWebWallet/wwwroot/images/ww.svg diff --git a/src/aoWebWallet/Pages/Start.razor b/src/aoWebWallet/Pages/Start.razor index 4581a07..231decc 100644 --- a/src/aoWebWallet/Pages/Start.razor +++ b/src/aoWebWallet/Pages/Start.razor @@ -8,6 +8,7 @@ Create AOWW Wallet + diff --git a/src/aoWebWallet/wwwroot/css/app.css b/src/aoWebWallet/wwwroot/css/app.css index 564cbd7..0dbb108 100644 --- a/src/aoWebWallet/wwwroot/css/app.css +++ b/src/aoWebWallet/wwwroot/css/app.css @@ -280,6 +280,8 @@ button.mud-button-root.mud-icon-button.mud-ripple.mud-ripple-icon.copy-clipboard .mud-paper.first-wallet { background: rgb(48,20,82); background: linear-gradient(180deg, rgba(48,20,82,1) 0%, rgba(69,20,84,1) 100%); + height: 555px; + } .mud-navmenu { @@ -295,3 +297,7 @@ body { background: rgb(56,18,74); background: linear-gradient(90deg, rgba(56,18,74,1) 0%, rgba(24,17,69,1) 100%, rgba(0,212,255,1) 100%); } + +.ww-image-start { + width: 150px; +} diff --git a/src/aoWebWallet/wwwroot/images/ww.svg b/src/aoWebWallet/wwwroot/images/ww.svg new file mode 100644 index 0000000..dd7ccef --- /dev/null +++ b/src/aoWebWallet/wwwroot/images/ww.svg @@ -0,0 +1,3402 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From cbfa6734dc513d352094d4c47ed4ae7a5e2d0ed0 Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Fri, 3 May 2024 18:39:52 +0100 Subject: [PATCH 26/63] improved design --- src/aoWebWallet/Layout/MainLayout.razor | 2 +- src/aoWebWallet/Pages/Start.razor | 6 +++--- src/aoWebWallet/wwwroot/css/app.css | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/aoWebWallet/Layout/MainLayout.razor b/src/aoWebWallet/Layout/MainLayout.razor index 525dc6e..f932127 100644 --- a/src/aoWebWallet/Layout/MainLayout.razor +++ b/src/aoWebWallet/Layout/MainLayout.razor @@ -12,7 +12,7 @@ + Width="100" Class="pt-2" Alt="AOWW"/> diff --git a/src/aoWebWallet/Pages/Start.razor b/src/aoWebWallet/Pages/Start.razor index 231decc..3961c7b 100644 --- a/src/aoWebWallet/Pages/Start.razor +++ b/src/aoWebWallet/Pages/Start.razor @@ -4,11 +4,11 @@ Start - @Program.PageTitlePostFix - + Create AOWW Wallet - + @@ -21,7 +21,7 @@ Load wallet from .json
- + @code { diff --git a/src/aoWebWallet/wwwroot/css/app.css b/src/aoWebWallet/wwwroot/css/app.css index 0dbb108..98bf50b 100644 --- a/src/aoWebWallet/wwwroot/css/app.css +++ b/src/aoWebWallet/wwwroot/css/app.css @@ -280,7 +280,7 @@ button.mud-button-root.mud-icon-button.mud-ripple.mud-ripple-icon.copy-clipboard .mud-paper.first-wallet { background: rgb(48,20,82); background: linear-gradient(180deg, rgba(48,20,82,1) 0%, rgba(69,20,84,1) 100%); - height: 555px; + height: 333px; } @@ -299,5 +299,6 @@ body { } .ww-image-start { - width: 150px; + width: 120px; + border-radius: 333px !important; } From 29007c12546b9ad206734eb767dfe4f07eec6e3c Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Sat, 4 May 2024 01:10:37 +0100 Subject: [PATCH 27/63] add component structure --- src/aoWebWallet/Pages/Start.razor | 6 +- .../Shared/AddGenerateWalletComponent.razor | 8 +- src/aoWebWallet/wwwroot/images/aoww.svg | 268 + src/aoWebWallet/wwwroot/images/ths.svg | 15884 ++++++++++++++++ src/aoWebWallet/wwwroot/images/ww.svg | 3402 ---- 5 files changed, 16160 insertions(+), 3408 deletions(-) create mode 100644 src/aoWebWallet/wwwroot/images/aoww.svg create mode 100644 src/aoWebWallet/wwwroot/images/ths.svg delete mode 100644 src/aoWebWallet/wwwroot/images/ww.svg diff --git a/src/aoWebWallet/Pages/Start.razor b/src/aoWebWallet/Pages/Start.razor index 3961c7b..525c27f 100644 --- a/src/aoWebWallet/Pages/Start.razor +++ b/src/aoWebWallet/Pages/Start.razor @@ -1,5 +1,7 @@ @page "/start" @inherits MvvmComponentBase +@using aoWebWallet.Models +@using aoWebWallet.Shared Start - @Program.PageTitlePostFix @@ -7,8 +9,8 @@ - Create AOWW Wallet - + + diff --git a/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor b/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor index 83e00fb..121e38e 100644 --- a/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor +++ b/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor @@ -4,13 +4,13 @@ @inject ArweaveService ArweaveService @inject ISnackbar Snackbar - + - + @Progress -
+
- Create AOWW Wallet + Create
diff --git a/src/aoWebWallet/wwwroot/images/aoww.svg b/src/aoWebWallet/wwwroot/images/aoww.svg new file mode 100644 index 0000000..7af07a8 --- /dev/null +++ b/src/aoWebWallet/wwwroot/images/aoww.svg @@ -0,0 +1,268 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/aoWebWallet/wwwroot/images/ths.svg b/src/aoWebWallet/wwwroot/images/ths.svg new file mode 100644 index 0000000..a04a763 --- /dev/null +++ b/src/aoWebWallet/wwwroot/images/ths.svg @@ -0,0 +1,15884 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/aoWebWallet/wwwroot/images/ww.svg b/src/aoWebWallet/wwwroot/images/ww.svg deleted file mode 100644 index dd7ccef..0000000 --- a/src/aoWebWallet/wwwroot/images/ww.svg +++ /dev/null @@ -1,3402 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 873884e804a52cff4b648cb6ca785df7fee745a1 Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Sat, 4 May 2024 01:10:49 +0100 Subject: [PATCH 28/63] style --- src/aoWebWallet/Pages/Wallets.razor | 2 +- src/aoWebWallet/wwwroot/css/app.css | 11 ++++++++++- src/aoWebWallet/wwwroot/images/aoww.svg | 4 ++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/aoWebWallet/Pages/Wallets.razor b/src/aoWebWallet/Pages/Wallets.razor index 0f1a344..81b3f0a 100644 --- a/src/aoWebWallet/Pages/Wallets.razor +++ b/src/aoWebWallet/Pages/Wallets.razor @@ -39,7 +39,7 @@ { string logoUrl = $"images/account--{logoCount}.svg"; string detailUrl = $"wallet/{wallet.Address}"; - + diff --git a/src/aoWebWallet/wwwroot/css/app.css b/src/aoWebWallet/wwwroot/css/app.css index 98bf50b..ff1b5c0 100644 --- a/src/aoWebWallet/wwwroot/css/app.css +++ b/src/aoWebWallet/wwwroot/css/app.css @@ -299,6 +299,15 @@ body { } .ww-image-start { - width: 120px; + width: 100px; border-radius: 333px !important; + margin: 0 auto; +} + +.wallet-list.mud-paper { + background-color: rgba(222,222,222,0); +} + +.trigger-transparency.mud-paper { + background-color: rgba(222,222,222,0); } diff --git a/src/aoWebWallet/wwwroot/images/aoww.svg b/src/aoWebWallet/wwwroot/images/aoww.svg index 7af07a8..dc710cf 100644 --- a/src/aoWebWallet/wwwroot/images/aoww.svg +++ b/src/aoWebWallet/wwwroot/images/aoww.svg @@ -7,11 +7,11 @@ } .cls-2 { - opacity: .44; + opacity: .22; } /*************************************************** - * Generated by SVG Artista on 5/4/2024, 12:14:24 AM + * Generated by SVG Artista on 5/4/2024, 12:24:29 AM * MIT license (https://opensource.org/licenses/MIT) * W. https://svgartista.net **************************************************/ From c68ec6145adb6980b4eb7a3300a89f12501c3e4b Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Sat, 4 May 2024 12:32:31 +0100 Subject: [PATCH 29/63] create aoww wallet --- src/aoWebWallet/Pages/Start.razor | 2 +- .../Shared/AddGenerateWalletComponent.razor | 2 +- .../wwwroot/images/origin-icon-base.png | Bin 0 -> 83948 bytes src/aoWebWallet/wwwroot/images/ths.svg | 16147 +--------------- 4 files changed, 289 insertions(+), 15862 deletions(-) create mode 100644 src/aoWebWallet/wwwroot/images/origin-icon-base.png diff --git a/src/aoWebWallet/Pages/Start.razor b/src/aoWebWallet/Pages/Start.razor index 525c27f..5b5a8df 100644 --- a/src/aoWebWallet/Pages/Start.razor +++ b/src/aoWebWallet/Pages/Start.razor @@ -9,7 +9,7 @@ - + diff --git a/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor b/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor index 121e38e..0aca3c2 100644 --- a/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor +++ b/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor @@ -6,7 +6,7 @@ - + @Progress
diff --git a/src/aoWebWallet/wwwroot/images/origin-icon-base.png b/src/aoWebWallet/wwwroot/images/origin-icon-base.png new file mode 100644 index 0000000000000000000000000000000000000000..8cbb28edc726f09524484afc9fd20f3c4de7ce1a GIT binary patch literal 83948 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 - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + - \ No newline at end of file + From 2cccab4496379ed649df7917ac159a9bae4010ab Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Sat, 4 May 2024 19:00:14 +0100 Subject: [PATCH 30/63] create new aoww wallet ui --- src/aoWebWallet/Pages/Start.razor | 6 +++- .../Shared/AddGenerateWalletComponent.razor | 16 ++++++--- src/aoWebWallet/wwwroot/css/app.css | 34 ++++++++++++++++--- src/aoWebWallet/wwwroot/images/ths.svg | 10 +----- 4 files changed, 47 insertions(+), 19 deletions(-) diff --git a/src/aoWebWallet/Pages/Start.razor b/src/aoWebWallet/Pages/Start.razor index 5b5a8df..cac0d8e 100644 --- a/src/aoWebWallet/Pages/Start.razor +++ b/src/aoWebWallet/Pages/Start.razor @@ -9,7 +9,11 @@ - +

+
+ +
+
diff --git a/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor b/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor index 0aca3c2..cb8b76e 100644 --- a/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor +++ b/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor @@ -6,17 +6,25 @@ - - @Progress + + + + + + @Progress
- - Create + + Create AOWW Wallet
@code { + + bool Disabled { get; set; } = false; + DefaultFocus DefaultFocus { get; set; } = DefaultFocus.FirstChild; + [Parameter] public bool HideAddButton { get; set; } diff --git a/src/aoWebWallet/wwwroot/css/app.css b/src/aoWebWallet/wwwroot/css/app.css index ff1b5c0..44247cb 100644 --- a/src/aoWebWallet/wwwroot/css/app.css +++ b/src/aoWebWallet/wwwroot/css/app.css @@ -298,11 +298,7 @@ body { background: linear-gradient(90deg, rgba(56,18,74,1) 0%, rgba(24,17,69,1) 100%, rgba(0,212,255,1) 100%); } -.ww-image-start { - width: 100px; - border-radius: 333px !important; - margin: 0 auto; -} + .wallet-list.mud-paper { background-color: rgba(222,222,222,0); @@ -311,3 +307,31 @@ body { .trigger-transparency.mud-paper { background-color: rgba(222,222,222,0); } + +.background-x { + background-image: url("../images/origin-icon-base.png"); + width: 100px; + background-size: 100px; + animation:spin 111s linear infinite; +} + +.ww-image-start { + width: 100px; + border-radius: 333px !important; + animation:spin-clockwise 111s linear infinite; +} + +@keyframes spin{ + from{transform:rotate(0deg)} + to{transform:rotate(360deg)} +} + +@keyframes spin-clockwise{ + from{transform:rotate(360deg)} + to{transform:rotate(0deg)} +} + +.button-dark { + background-color: #170d35 !important; + text-transform: none !important; +} diff --git a/src/aoWebWallet/wwwroot/images/ths.svg b/src/aoWebWallet/wwwroot/images/ths.svg index d79655e..052fbfd 100644 --- a/src/aoWebWallet/wwwroot/images/ths.svg +++ b/src/aoWebWallet/wwwroot/images/ths.svg @@ -1,5 +1,5 @@ - + - - From bbc5aa506cf3f90d85e63935612ab3334ffe91a6 Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Sat, 4 May 2024 19:42:13 +0100 Subject: [PATCH 31/63] add arconnect component to first wallet page --- src/aoWebWallet/Pages/Start.razor | 2 +- src/aoWebWallet/Shared/AddArConnectComponent.razor | 2 +- src/aoWebWallet/wwwroot/css/app.css | 9 ++++----- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/aoWebWallet/Pages/Start.razor b/src/aoWebWallet/Pages/Start.razor index cac0d8e..a5195e6 100644 --- a/src/aoWebWallet/Pages/Start.razor +++ b/src/aoWebWallet/Pages/Start.razor @@ -19,7 +19,7 @@ - Connect to ArConnect + diff --git a/src/aoWebWallet/Shared/AddArConnectComponent.razor b/src/aoWebWallet/Shared/AddArConnectComponent.razor index 3c9b6f1..ccfe005 100644 --- a/src/aoWebWallet/Shared/AddArConnectComponent.razor +++ b/src/aoWebWallet/Shared/AddArConnectComponent.razor @@ -4,7 +4,7 @@ @inject ISnackbar Snackbar @inject ArweaveService ArweaveService - + Connect Wallet diff --git a/src/aoWebWallet/wwwroot/css/app.css b/src/aoWebWallet/wwwroot/css/app.css index 44247cb..828ebe5 100644 --- a/src/aoWebWallet/wwwroot/css/app.css +++ b/src/aoWebWallet/wwwroot/css/app.css @@ -308,6 +308,10 @@ body { background-color: rgba(222,222,222,0); } +.trigger-transparency { + background-color: rgba(222,222,222,0) !important; +} + .background-x { background-image: url("../images/origin-icon-base.png"); width: 100px; @@ -330,8 +334,3 @@ body { from{transform:rotate(360deg)} to{transform:rotate(0deg)} } - -.button-dark { - background-color: #170d35 !important; - text-transform: none !important; -} From 566644d170567cb9f06872557d844e88f83f0220 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Sun, 5 May 2024 12:55:26 +0200 Subject: [PATCH 32/63] autocomplete on new action page --- .../Extensions/WalletExtensions.cs | 17 +++++ .../Components/ActionInputComponent.razor | 75 +++++++++++++++++-- src/aoWebWallet/aoWebWallet.csproj | 2 +- 3 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 src/aoWebWallet/Extensions/WalletExtensions.cs diff --git a/src/aoWebWallet/Extensions/WalletExtensions.cs b/src/aoWebWallet/Extensions/WalletExtensions.cs new file mode 100644 index 0000000..946e7cf --- /dev/null +++ b/src/aoWebWallet/Extensions/WalletExtensions.cs @@ -0,0 +1,17 @@ +using aoWebWallet.Models; + +namespace aoWebWallet.Extensions +{ + public static class WalletExtensions + { + public static string ToAutocompleteDisplay(this Wallet wallet) + { + if (string.IsNullOrWhiteSpace(wallet.Name)) + { + return wallet.Address; + } + + return $"{wallet.Name} ({wallet.Address})"; + } + } +} diff --git a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor index 4a3d20c..1cac3ae 100644 --- a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor +++ b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor @@ -1,4 +1,5 @@ @using aoWebWallet.Models +@inject MainViewModel MainViewModel @*

@ActionParam.Key = @ActionParam.Value | @ActionParam.ParamType

*@ @@ -14,7 +15,29 @@ else } else if (ActionParam.ParamType == ActionParamType.Process) { - + + + + @e.ToAutocompleteDisplay() + + + + + @e.ToAutocompleteDisplay() + + + + @(ActionParam.Value ?? "Not selected") + + @* *@ } else if (ActionParam.ParamType == ActionParamType.Integer) { @@ -34,7 +57,8 @@ else private string? textValue; MudTextField? mudTextField; - MudTextField? mudProcessField; + //MudTextField? mudProcessField; + MudAutocomplete? mudProcessField; MudTextField? mudIntField; // protected override void OnParametersSet() @@ -62,7 +86,13 @@ else if (!(mudProcessField?.ValidationErrors.Any() ?? false)) { - mudProcessField?.SetText(ActionParam.Value); + // if (ActionParam.Value != null && mudProcessField != null) + // mudProcessField.Text = ActionParam.Value; + + //mudProcessField?.ForceUpdate(); + + // if(mudProcessField != null) + // mudProcessField.Value = ActionParam.Value; } if (!(mudIntField?.ValidationErrors.Any() ?? false)) @@ -91,9 +121,24 @@ else StateHasChanged(); } - public IEnumerable ValidateProcess(string? input) + public async void UpdateWalletValue(Wallet? e) + { + if (mudProcessField != null) + await mudProcessField.Validate(); + + if (!(mudProcessField?.ValidationErrors.Any() ?? false)) + { + ActionParam.Value = e?.Address; + } + else + ActionParam.Value = null; + + StateHasChanged(); + } + + public IEnumerable ValidateProcess(Wallet? input) { - if (input == null || input.Length != 43) + if (input == null || input.Address.Length != 43) { yield return "Address must have length of 43 characters."; } @@ -112,4 +157,24 @@ else StateHasChanged(); } + private async Task> WalletSearch(string value) + { + // if text is null or empty, don't return values (drop-down will not open) + if (string.IsNullOrEmpty(value)) + return new Wallet[0]; + + var contacts = MainViewModel.WalletList.Data?.Where(x => + x.Address.Contains(value, StringComparison.InvariantCultureIgnoreCase) + || x.ToAutocompleteDisplay().Equals(value, StringComparison.InvariantCultureIgnoreCase) + || (x.Name?.Contains(value, StringComparison.InvariantCultureIgnoreCase) ?? false) + ).Select(x => x).ToList() ?? new(); + + if (contacts.Any()) + return contacts; + else if(value.Length == 43) + return new Wallet[1] { new Wallet() { Address = value } }; + else + return new Wallet[0]; + } + } diff --git a/src/aoWebWallet/aoWebWallet.csproj b/src/aoWebWallet/aoWebWallet.csproj index 6063641..7f435ab 100644 --- a/src/aoWebWallet/aoWebWallet.csproj +++ b/src/aoWebWallet/aoWebWallet.csproj @@ -13,7 +13,7 @@ - + From 45941a3cf1bab406de3305b52b69b494e6a413ce Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Sun, 5 May 2024 18:34:17 +0100 Subject: [PATCH 33/63] multiple design and user experience tasks --- src/aoWebWallet/Pages/Start.razor | 7 ++++- .../Shared/AddArConnectComponent.razor | 2 -- .../Shared/AddGenerateWalletComponent.razor | 2 +- .../Shared/AddUploadWalletComponent.razor | 4 +-- src/aoWebWallet/wwwroot/css/app.css | 27 ++++++++++++++++++- .../wwwroot/images/arconnect-logo.svg | 10 +++++++ src/aoWebWallet/wwwroot/index.html | 21 +++++++++------ 7 files changed, 58 insertions(+), 15 deletions(-) create mode 100644 src/aoWebWallet/wwwroot/images/arconnect-logo.svg diff --git a/src/aoWebWallet/Pages/Start.razor b/src/aoWebWallet/Pages/Start.razor index a5195e6..1b8abae 100644 --- a/src/aoWebWallet/Pages/Start.razor +++ b/src/aoWebWallet/Pages/Start.razor @@ -19,12 +19,17 @@ +
+
+ +
+
- Load wallet from .json + diff --git a/src/aoWebWallet/Shared/AddArConnectComponent.razor b/src/aoWebWallet/Shared/AddArConnectComponent.razor index ccfe005..ae675bb 100644 --- a/src/aoWebWallet/Shared/AddArConnectComponent.razor +++ b/src/aoWebWallet/Shared/AddArConnectComponent.razor @@ -6,8 +6,6 @@ - Connect Wallet - @if (!BindingContext.HasArConnectExtension.HasValue) diff --git a/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor b/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor index cb8b76e..c48be21 100644 --- a/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor +++ b/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor @@ -13,7 +13,7 @@ @Progress
- + Create AOWW Wallet
diff --git a/src/aoWebWallet/Shared/AddUploadWalletComponent.razor b/src/aoWebWallet/Shared/AddUploadWalletComponent.razor index 3f9ec11..5d3d39a 100644 --- a/src/aoWebWallet/Shared/AddUploadWalletComponent.razor +++ b/src/aoWebWallet/Shared/AddUploadWalletComponent.razor @@ -4,7 +4,7 @@ @inject ArweaveService ArweaveService @inject ISnackbar Snackbar - + @* Load .json wallet *@ @@ -19,7 +19,7 @@ @ondragleave="@ClearDragClass" @ondragend="@ClearDragClass"> - diff --git a/src/aoWebWallet/wwwroot/css/app.css b/src/aoWebWallet/wwwroot/css/app.css index 828ebe5..a4df131 100644 --- a/src/aoWebWallet/wwwroot/css/app.css +++ b/src/aoWebWallet/wwwroot/css/app.css @@ -111,9 +111,10 @@ a, .btn-link { justify-content: center; } .loading-progress-text { - position: absolute; + position: relative; text-align: center; font-weight: bold; + color: white; } @@ -325,6 +326,26 @@ body { animation:spin-clockwise 111s linear infinite; } +.background-xs { + background-image: url("../images/origin-icon-base.png"); + width: 333px; + background-size: 333px; + animation:spin 111s linear infinite; +} + +.ww-image-start-xs { + width: 333px; + border-radius: 333px !important; + animation:spin-clockwise 111s linear infinite; +} + +.ar-image-start { + width: 55px; + border-radius: 333px !important; + opacity: 0.55; + padding: 1.72rem 0; +} + @keyframes spin{ from{transform:rotate(0deg)} to{transform:rotate(360deg)} @@ -334,3 +355,7 @@ body { from{transform:rotate(360deg)} to{transform:rotate(0deg)} } + +.text-transform-none { + text-transform: none !important; +} diff --git a/src/aoWebWallet/wwwroot/images/arconnect-logo.svg b/src/aoWebWallet/wwwroot/images/arconnect-logo.svg new file mode 100644 index 0000000..41491ee --- /dev/null +++ b/src/aoWebWallet/wwwroot/images/arconnect-logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/aoWebWallet/wwwroot/index.html b/src/aoWebWallet/wwwroot/index.html index 8ff86ed..b061bb3 100644 --- a/src/aoWebWallet/wwwroot/index.html +++ b/src/aoWebWallet/wwwroot/index.html @@ -59,15 +59,20 @@
-
-
- - - - -
+
+
+
+ AoWW +
+

loading...

+
-
From ac63aa6e16ecf57297f63bccf71f9adb9b263cb9 Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Sun, 5 May 2024 19:43:58 +0100 Subject: [PATCH 34/63] create wallet interfaces v1 --- src/aoWebWallet/Pages/Start.razor | 11 +++-- .../Shared/AddUploadWalletComponent.razor | 19 ++++---- src/aoWebWallet/wwwroot/css/app.css | 48 ++++++++++++++----- .../wwwroot/images/arconnect-logo.svg | 23 +++++---- src/aoWebWallet/wwwroot/images/json-logo.svg | 35 ++++++++++++++ 5 files changed, 104 insertions(+), 32 deletions(-) create mode 100644 src/aoWebWallet/wwwroot/images/json-logo.svg diff --git a/src/aoWebWallet/Pages/Start.razor b/src/aoWebWallet/Pages/Start.razor index 1b8abae..26eadda 100644 --- a/src/aoWebWallet/Pages/Start.razor +++ b/src/aoWebWallet/Pages/Start.razor @@ -20,15 +20,20 @@
-
- +
+
- + +
+
+ +
+
diff --git a/src/aoWebWallet/Shared/AddUploadWalletComponent.razor b/src/aoWebWallet/Shared/AddUploadWalletComponent.razor index 5d3d39a..f75855f 100644 --- a/src/aoWebWallet/Shared/AddUploadWalletComponent.razor +++ b/src/aoWebWallet/Shared/AddUploadWalletComponent.razor @@ -13,7 +13,7 @@ Accept=".json" OnFilesChanged="OnInputFileChanged" Hidden="@false" - InputClass="absolute mud-width-full mud-height-full overflow-hidden z-20" + InputClass="absolute mud-width-full mud-height-full overflow-hidden z-20 trigger-transparency cursor-pointer" InputStyle="opacity:0" @ondragenter="@SetDragClass" @ondragleave="@ClearDragClass" @@ -26,7 +26,7 @@ Drag and drop wallet file or click here. - Your files won't be uploaded and are only read by the local app. + Your .JSON files won't be uploaded and are only read by the local app. @foreach (var file in _fileNames) { @@ -34,13 +34,14 @@ } - - Open file picker - + Class="relative d-flex justify-center gap-4 z-30"> + + Load .json wallet + @* - - - - - - - - + + + + + + + + + + + + diff --git a/src/aoWebWallet/wwwroot/images/json-logo.svg b/src/aoWebWallet/wwwroot/images/json-logo.svg new file mode 100644 index 0000000..d3e8231 --- /dev/null +++ b/src/aoWebWallet/wwwroot/images/json-logo.svg @@ -0,0 +1,35 @@ + + + + + + + From af22b2c1645798b58d774cc42524e05cc1b68b13 Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Sun, 5 May 2024 23:10:42 +0100 Subject: [PATCH 35/63] add social refs --- src/aoWebWallet/wwwroot/images/discord.svg | 34 +++++++++++++++++++++ src/aoWebWallet/wwwroot/images/twitter.svg | 35 ++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 src/aoWebWallet/wwwroot/images/discord.svg create mode 100644 src/aoWebWallet/wwwroot/images/twitter.svg diff --git a/src/aoWebWallet/wwwroot/images/discord.svg b/src/aoWebWallet/wwwroot/images/discord.svg new file mode 100644 index 0000000..5905b05 --- /dev/null +++ b/src/aoWebWallet/wwwroot/images/discord.svg @@ -0,0 +1,34 @@ + + + + + + diff --git a/src/aoWebWallet/wwwroot/images/twitter.svg b/src/aoWebWallet/wwwroot/images/twitter.svg new file mode 100644 index 0000000..d3e8231 --- /dev/null +++ b/src/aoWebWallet/wwwroot/images/twitter.svg @@ -0,0 +1,35 @@ + + + + + + + From ac5c17e5e59ec665e19683c79f4a01e3ea6d0fea Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Sun, 5 May 2024 23:10:54 +0100 Subject: [PATCH 36/63] add social refs --- src/aoWebWallet/Shared/NavMenu.razor | 28 +++++++++++-- src/aoWebWallet/wwwroot/css/app.css | 16 ++++++++ src/aoWebWallet/wwwroot/images/discord.svg | 46 ++++++---------------- src/aoWebWallet/wwwroot/images/twitter.svg | 5 +-- 4 files changed, 56 insertions(+), 39 deletions(-) diff --git a/src/aoWebWallet/Shared/NavMenu.razor b/src/aoWebWallet/Shared/NavMenu.razor index 2f506d6..4ac90aa 100644 --- a/src/aoWebWallet/Shared/NavMenu.razor +++ b/src/aoWebWallet/Shared/NavMenu.razor @@ -35,8 +35,8 @@ Settings About
-
- @if (BindingContext.UserSettings?.IsDarkMode ?? true) +
+ @* @if (BindingContext.UserSettings?.IsDarkMode ?? true) { } @@ -44,7 +44,29 @@ { } - Theme + Theme *@ + + + + + + + + + Twitter + + + + + + + + + + + Discord + +
diff --git a/src/aoWebWallet/wwwroot/css/app.css b/src/aoWebWallet/wwwroot/css/app.css index c9d4645..d7e60e0 100644 --- a/src/aoWebWallet/wwwroot/css/app.css +++ b/src/aoWebWallet/wwwroot/css/app.css @@ -385,3 +385,19 @@ body { .text-transform-none { text-transform: none !important; } + +.twitter-image { + margin-top: 10px; + margin-left: 10px; + object-fit: fill !important; +} + +.discord-image { + padding: 5px; + object-fit: fill !important; +} + +.mud-chip.mud-chip-size-medium .mud-avatar.custom-avatar-size { + width: 24px !important; + height: 24px !important; +} diff --git a/src/aoWebWallet/wwwroot/images/discord.svg b/src/aoWebWallet/wwwroot/images/discord.svg index 5905b05..cded9c1 100644 --- a/src/aoWebWallet/wwwroot/images/discord.svg +++ b/src/aoWebWallet/wwwroot/images/discord.svg @@ -1,34 +1,14 @@ - - - - - + + + + + + + + diff --git a/src/aoWebWallet/wwwroot/images/twitter.svg b/src/aoWebWallet/wwwroot/images/twitter.svg index d3e8231..5905b05 100644 --- a/src/aoWebWallet/wwwroot/images/twitter.svg +++ b/src/aoWebWallet/wwwroot/images/twitter.svg @@ -1,5 +1,4 @@ - - + - + From c4cd51e059ecc45c14c26230e00c1d6b9f01b409 Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Sun, 5 May 2024 23:29:14 +0100 Subject: [PATCH 37/63] /start --- src/aoWebWallet/Layout/MainLayout.razor | 14 ++++++++------ src/aoWebWallet/Pages/About.razor | 7 +++---- src/aoWebWallet/Shared/NavMenu.razor | 11 +++-------- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/aoWebWallet/Layout/MainLayout.razor b/src/aoWebWallet/Layout/MainLayout.razor index f932127..6a0dd4c 100644 --- a/src/aoWebWallet/Layout/MainLayout.razor +++ b/src/aoWebWallet/Layout/MainLayout.razor @@ -27,12 +27,13 @@ @Body -
+
- - - - +
+ + +
Version: @Program.GetVersionWithoutHash() @if (!string.IsNullOrEmpty(versionHash)) @@ -40,7 +41,8 @@ -@versionHash } - zsXSvJtHVSK4QyPch4Uf0JMiZi9uEhgVvyz6qeEJcfY + +
diff --git a/src/aoWebWallet/Pages/About.razor b/src/aoWebWallet/Pages/About.razor index 91875c6..354b01f 100644 --- a/src/aoWebWallet/Pages/About.razor +++ b/src/aoWebWallet/Pages/About.razor @@ -47,10 +47,9 @@ - - - - + + Contribute to developers address
+ zsXSvJtHVSK4QyPch4Uf0JMiZi9uEhgVvyz6qeEJcfY
diff --git a/src/aoWebWallet/Shared/NavMenu.razor b/src/aoWebWallet/Shared/NavMenu.razor index 4ac90aa..b7bc29e 100644 --- a/src/aoWebWallet/Shared/NavMenu.razor +++ b/src/aoWebWallet/Shared/NavMenu.razor @@ -30,12 +30,12 @@ Token Explorer -
+
Settings About
-
+
@* @if (BindingContext.UserSettings?.IsDarkMode ?? true) { @@ -70,12 +70,7 @@
-
- - - Copyright @DateTimeOffset.UtcNow.Year -
+ @code { From ce73dacd07be4c1fce91753b3f3288b77956f77f Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Tue, 7 May 2024 19:31:50 +0200 Subject: [PATCH 38/63] Improved flow for empty address book --- src/aoWebWallet/Pages/AddressBook.razor | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/aoWebWallet/Pages/AddressBook.razor b/src/aoWebWallet/Pages/AddressBook.razor index aa5bb68..78f3f88 100644 --- a/src/aoWebWallet/Pages/AddressBook.razor +++ b/src/aoWebWallet/Pages/AddressBook.razor @@ -14,7 +14,7 @@ Address Book - @if (BindingContext.WalletList.Data != null && BindingContext.WalletList.Data.Any()) + @if (BindingContext.WalletList.Data != null) { @@ -67,7 +67,8 @@ { - + Your Address Book is currently empty. + Add your first contact From 3a1eaa121a5ccf0fc89d07aa10eb9461ae2e89ef Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Wed, 8 May 2024 23:44:06 +0100 Subject: [PATCH 39/63] route to start page for first and new wallets --- src/aoWebWallet/Pages/ActionPage.razor | 4 ++-- src/aoWebWallet/Pages/Wallets.razor | 20 +++----------------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor index 0fb8819..3d17eea 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -87,7 +87,7 @@ Cancel Submit } - + @if (transactionService.LastTransaction.DataLoader != null) { @@ -105,7 +105,7 @@ @* View Transaction *@ } } - + diff --git a/src/aoWebWallet/Pages/Wallets.razor b/src/aoWebWallet/Pages/Wallets.razor index 2ad09e9..3a76f85 100644 --- a/src/aoWebWallet/Pages/Wallets.razor +++ b/src/aoWebWallet/Pages/Wallets.razor @@ -5,6 +5,7 @@ @inject ISnackbar Snackbar @inject TokenDataService dataService @inject ClipboardService ClipboardService +@inject NavigationManager Navigation @Program.PageTitlePostFix @@ -26,7 +27,7 @@ @if (BindingContext.WalletList.Data != null && BindingContext.WalletList.Data.Any()) { - + } @@ -83,22 +84,7 @@ } else { - - - - - - - - - - - - - - - - + Navigation.NavigateTo("/start"); } } else From 37fdc72a9200154c959a2970e1c214718e8b23cb Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Wed, 8 May 2024 23:56:25 +0100 Subject: [PATCH 40/63] improve style --- src/aoWebWallet/wwwroot/css/app.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/aoWebWallet/wwwroot/css/app.css b/src/aoWebWallet/wwwroot/css/app.css index d7e60e0..21ed512 100644 --- a/src/aoWebWallet/wwwroot/css/app.css +++ b/src/aoWebWallet/wwwroot/css/app.css @@ -401,3 +401,7 @@ body { width: 24px !important; height: 24px !important; } + +.mud-paper, .mud-tabs-toolbar { + background-color: rgba(16,16,67,0.33) !important; +} From 8c9154c495a2128ed4a0d345054e6ae6d3d2e473 Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Thu, 9 May 2024 00:17:14 +0100 Subject: [PATCH 41/63] improve user experience to send and receive tokens. --- src/aoWebWallet/Pages/WalletDetail.razor | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/aoWebWallet/Pages/WalletDetail.razor b/src/aoWebWallet/Pages/WalletDetail.razor index 2591903..69d857e 100644 --- a/src/aoWebWallet/Pages/WalletDetail.razor +++ b/src/aoWebWallet/Pages/WalletDetail.razor @@ -145,15 +145,15 @@ - - + + Receive @if ((BindingContext.SelectedWallet?.CanSend ?? false)) { var hasBalance = balance.BalanceDataLoader.Data?.Balance ?? 0; - - + + Send } @@ -259,7 +259,7 @@ NavigationManager.NavigateTo($"/action?{aoAction.ToQueryString()}"); - // var parameters = new DialogParameters { + // var parameters = new DialogParameters { // { x => x.SelectedBalanceDataVM, balanceDataVM }, // { x => x.SelectedWallet, BindingContext.SelectedWallet } // }; From 5314ed3b7006d05b9e59c3b1b1fee3b1cb9dfe44 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Fri, 10 May 2024 13:09:32 +0200 Subject: [PATCH 42/63] Do not add token multiple times --- src/aoWebWallet/Pages/ActionPage.razor | 5 +++-- src/aoWebWallet/Services/StorageService.cs | 18 ------------------ src/aoWebWallet/Services/TokenDataService.cs | 7 +++++-- src/aoWebWallet/Services/TransactionService.cs | 4 +++- .../aoww.Services.Tests.csproj | 11 +++++++---- 5 files changed, 18 insertions(+), 27 deletions(-) diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor index 3d17eea..adf2e92 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -17,14 +17,15 @@ @if (BindingContext.WalletList.Data != null) { - if (!BindingContext.WalletList.Data.Any()) + var sendWallets = BindingContext.WalletList.Data.Where(x => !x.IsReadOnly).ToList(); + if (!sendWallets.Any()) { Add Wallet } else { - @foreach (var wallet in BindingContext.WalletList.Data ?? new()) + @foreach (var wallet in sendWallets ?? new()) { diff --git a/src/aoWebWallet/Services/StorageService.cs b/src/aoWebWallet/Services/StorageService.cs index 81108a9..268c51d 100644 --- a/src/aoWebWallet/Services/StorageService.cs +++ b/src/aoWebWallet/Services/StorageService.cs @@ -41,24 +41,6 @@ private void AddSystemToken(List list, string tokenId) list.Add(new Token { TokenId = tokenId, IsSystemToken = true }); } - public async Task AddTokenId(string tokenId, bool isUserAdded = true, bool isVisible = false) - { - if(tokenId.Length != 43) - return; - - var list = await GetTokenIds(); - - var existing = list.Where(x => x.TokenId == tokenId).FirstOrDefault(); - if (existing != null) - return; - - - existing = new Token { TokenId = tokenId, IsUserAdded = isUserAdded, IsVisible = isVisible }; - list.Add(existing); - - await SaveTokenList(list); - } - public async ValueTask AddToken(string tokenId, TokenData data, bool isUserAdded, bool? isVisible) { var list = await GetTokenIds(); diff --git a/src/aoWebWallet/Services/TokenDataService.cs b/src/aoWebWallet/Services/TokenDataService.cs index 9d240b1..bbea531 100644 --- a/src/aoWebWallet/Services/TokenDataService.cs +++ b/src/aoWebWallet/Services/TokenDataService.cs @@ -25,6 +25,8 @@ public TokenDataService(StorageService storageService, TokenClient tokenClient) public async Task TryAddTokenIds(List allTokenIds) { + allTokenIds = allTokenIds.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + foreach (var tokenId in allTokenIds) { if (string.IsNullOrEmpty(tokenId) || tokenId.Length != 43) @@ -76,7 +78,9 @@ public async Task LoadTokenAsync(string tokenId) token.TokenData = data; - TokenList.Add(token); + var existing = TokenList.Where(x => x.TokenId.Equals(tokenId, StringComparison.OrdinalIgnoreCase)).Any(); + if(!existing) + TokenList.Add(token); await storageService.AddToken(tokenId, data, false, null); } @@ -127,7 +131,6 @@ private async IAsyncEnumerable LoadTokenDataAsync() } await storageService.SaveTokenList(tokens); - } public async Task DeleteToken(string tokenId) diff --git a/src/aoWebWallet/Services/TransactionService.cs b/src/aoWebWallet/Services/TransactionService.cs index 2804b1f..dbf3556 100644 --- a/src/aoWebWallet/Services/TransactionService.cs +++ b/src/aoWebWallet/Services/TransactionService.cs @@ -36,6 +36,8 @@ public void Reset() public Task DryRunAction(Wallet wallet, AoAction action) => DryRunResult.DataLoader.LoadAsync(async () => { + DryRunResult.Data = null; + var target = action.Target?.Value ?? string.Empty; var druRunRequest = new DryRunRequest() { @@ -74,7 +76,7 @@ public async Task SendAction(Wallet wallet, Wallet? ownerWallet, AoAction action if (!string.IsNullOrEmpty(wallet.Jwk)) await SendActionWithJwk(wallet.Jwk, action); - Console.WriteLine("No Wallet to send"); + //Console.WriteLine("No Wallet to send"); return; } diff --git a/src/aoww.Services.Tests/aoww.Services.Tests.csproj b/src/aoww.Services.Tests/aoww.Services.Tests.csproj index 45f184c..efa497d 100644 --- a/src/aoww.Services.Tests/aoww.Services.Tests.csproj +++ b/src/aoww.Services.Tests/aoww.Services.Tests.csproj @@ -10,10 +10,13 @@ - - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + From 645d51ad55cd0afefe88cb759c1ad22dbe57a119 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Fri, 10 May 2024 14:21:28 +0200 Subject: [PATCH 43/63] Load more transactions for wallet or token --- src/aoWebWallet/Pages/TokenDetail.razor | 11 +++++ src/aoWebWallet/Pages/WalletDetail.razor | 12 +++++ .../ViewModels/TokenDetailViewModel.cs | 29 ++++++++++-- .../ViewModels/WalletDetailViewModel.cs | 44 ++++++++++++++----- src/aoww.Services/GraphqlClient.cs | 23 +++++++--- src/aoww.Services/Models/GraphqlResponse.cs | 3 ++ src/aoww.Services/Models/TokenTransfer.cs | 1 + 7 files changed, 100 insertions(+), 23 deletions(-) diff --git a/src/aoWebWallet/Pages/TokenDetail.razor b/src/aoWebWallet/Pages/TokenDetail.razor index f83ee85..4d6e1a1 100644 --- a/src/aoWebWallet/Pages/TokenDetail.razor +++ b/src/aoWebWallet/Pages/TokenDetail.razor @@ -46,6 +46,13 @@ } + + @if (BindingContext.TokenTransferList.DataLoader.LoadingState == LoadingState.Finished && BindingContext.CanLoadMoreTransactions) + { + Load More + } + + } @@ -56,5 +63,9 @@ [Parameter] public string? TokenId { get; set; } + private Task LoadMoreTransactions() + { + return BindingContext.LoadMoreTransactions(); + } } diff --git a/src/aoWebWallet/Pages/WalletDetail.razor b/src/aoWebWallet/Pages/WalletDetail.razor index 69d857e..ee937a8 100644 --- a/src/aoWebWallet/Pages/WalletDetail.razor +++ b/src/aoWebWallet/Pages/WalletDetail.razor @@ -185,6 +185,13 @@ } + + @if (BindingContext.TokenTransferList.DataLoader.LoadingState == LoadingState.Finished && BindingContext.CanLoadMoreTransactions) + { + Load More + } + + } @@ -292,4 +299,9 @@ await BindingContext.Claim3(); } + private Task LoadMoreTransactions() + { + return BindingContext.LoadMoreTransactions(); + } + } diff --git a/src/aoWebWallet/ViewModels/TokenDetailViewModel.cs b/src/aoWebWallet/ViewModels/TokenDetailViewModel.cs index 0da07bb..81246ab 100644 --- a/src/aoWebWallet/ViewModels/TokenDetailViewModel.cs +++ b/src/aoWebWallet/ViewModels/TokenDetailViewModel.cs @@ -13,6 +13,11 @@ public class TokenDetailViewModel : ObservableObject private readonly MainViewModel mainViewModel; private readonly GraphqlClient graphqlClient; private readonly TokenDataService dataService; + private string? _tokenId; + + private List tokenTransactions = new(); + + public bool CanLoadMoreTransactions { get; set; } = true; public DataLoaderViewModel Token { get; set; } = new(); @@ -28,6 +33,9 @@ public TokenDetailViewModel(MainViewModel mainViewModel, GraphqlClient graphqlCl public async Task Initialize(string tokenId) { + _tokenId = tokenId; + TokenTransferList.Data = new(); + await this.LoadTokenData(tokenId); this.LoadTokenTransferListForToken(tokenId); @@ -47,18 +55,31 @@ public Task LoadTokenData(string tokenId) => Token.DataLoader.LoadAsync(async () public Task LoadTokenTransferListForToken(string tokenId) => TokenTransferList.DataLoader.LoadAsync(async () => { - TokenTransferList.Data = new(); + tokenTransactions = await graphqlClient.GetTransactionsForToken(tokenId, GetCursor(tokenTransactions)); + CanLoadMoreTransactions = tokenTransactions.Any(); - var all = await graphqlClient.GetTransactionsForToken(tokenId); + var existing = TokenTransferList.Data ?? new(); - TokenTransferList.Data = all.ToList(); + TokenTransferList.Data = existing.Concat(tokenTransactions).OrderByDescending(x => x.Timestamp).ToList(); - var allTokenIds = all.Select(x => x.TokenId).Distinct().ToList(); + var allTokenIds = tokenTransactions.Select(x => x.TokenId).Distinct().ToList(); dataService.TryAddTokenIds(allTokenIds); return TokenTransferList.Data; }); + public async Task LoadMoreTransactions() + { + if (_tokenId != null) + { + await LoadTokenTransferListForToken(_tokenId); + } + } + + private static string? GetCursor(List transactions) + { + return transactions.Select(x => x.Cursor).LastOrDefault(); + } } } diff --git a/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs b/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs index 10fba42..359d7d6 100644 --- a/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs +++ b/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs @@ -7,11 +7,8 @@ using CommunityToolkit.Mvvm.ComponentModel; using MudBlazor; using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Net; using webvNext.DataLoader; using webvNext.DataLoader.Cache; -using static MudBlazor.Colors; namespace aoWebWallet.ViewModels { @@ -26,8 +23,14 @@ public partial class WalletDetailViewModel : ObservableObject private readonly ISnackbar snackbar; private readonly MemoryDataCache memoryDataCache; + private List incoming = new(); + private List outgoing = new(); + private List outgoingProcess = new(); + + private string? selectedAddress = null; + public bool CanLoadMoreTransactions { get; set; } = true; [ObservableProperty] private bool canClaim1; @@ -111,6 +114,19 @@ public async Task GetActiveArConnectAddress() } public async Task RefreshTokenTransferList() + { + if (selectedAddress != null) + { + incoming = new(); + outgoing = new(); + outgoingProcess = new(); + TokenTransferList.Data = new(); + + await LoadTokenTransferList(selectedAddress); + } + } + + public async Task LoadMoreTransactions() { if (selectedAddress != null) { @@ -130,7 +146,7 @@ public async Task RefreshBalance() { if (selectedAddress != null) { - await LoadTokenTransferList(selectedAddress); + await RefreshTokenTransferList(); await LoadBalanceDataList(selectedAddress); } } @@ -199,23 +215,27 @@ private async Task SelectWallet(string? address) public Task LoadTokenTransferList(string address) => TokenTransferList.DataLoader.LoadAsync(async () => { - TokenTransferList.Data = new(); + incoming = await graphqlClient.GetTransactionsIn(address, GetCursor(incoming)); + outgoing = await graphqlClient.GetTransactionsOut(address, GetCursor(outgoing)); + outgoingProcess = await graphqlClient.GetTransactionsOutFromProcess(address, GetCursor(outgoingProcess)); - var incoming = await graphqlClient.GetTransactionsIn(address); - var outgoing = await graphqlClient.GetTransactionsOut(address); - var outgoingProcess = await graphqlClient.GetTransactionsOutFromProcess(address); + var allNew = incoming.Concat(outgoing).Concat(outgoingProcess).OrderByDescending(x => x.Timestamp).ToList(); + CanLoadMoreTransactions = allNew.Any(); - var all = incoming.Concat(outgoing).Concat(outgoingProcess); + var existing = TokenTransferList.Data ?? new(); - TokenTransferList.Data = all.OrderByDescending(x => x.Timestamp).ToList(); + TokenTransferList.Data = existing.Concat(allNew).OrderByDescending(x => x.Timestamp).ToList(); - var allTokenIds = all.Select(x => x.TokenId).Distinct().ToList(); + var allTokenIds = allNew.Select(x => x.TokenId).Distinct().ToList(); dataService.TryAddTokenIds(allTokenIds); return TokenTransferList.Data; }); - + private static string? GetCursor(List transactions) + { + return transactions.Select(x => x.Cursor).LastOrDefault(); + } public async Task LoadBalanceDataList(string address, bool onlyNew = false) { diff --git a/src/aoww.Services/GraphqlClient.cs b/src/aoww.Services/GraphqlClient.cs index 642b30b..34e6fe4 100644 --- a/src/aoww.Services/GraphqlClient.cs +++ b/src/aoww.Services/GraphqlClient.cs @@ -15,12 +15,13 @@ public GraphqlClient(HttpClient httpClient) this.httpClient = httpClient; } - public async Task> GetTransactionsIn(string adddress, string? fromTxId = null) + public async Task> GetTransactionsIn(string adddress, string? cursor = null) { string query = $$""" query { transactions( - first: 100 + first: 50 + after: "{{cursor}}" sort: HEIGHT_DESC tags: [ { name: "Data-Protocol", values: ["ao"] } @@ -29,6 +30,7 @@ public async Task> GetTransactionsIn(string adddress, string ] ) { edges { + cursor node { id recipient @@ -77,6 +79,7 @@ public async Task> GetTransactionsIn(string adddress, string var transaction = new TokenTransfer() { Id = edge.Node.Id, + Cursor = edge.Cursor, From = edge.Node.Owner?.Address ?? string.Empty, TokenTransferType = Enums.TokenTransferType.Transfer }; @@ -132,12 +135,13 @@ public async Task> GetTransactionsIn(string adddress, string return processInfo; } - public async Task> GetTransactionsOut(string address, string? fromTxId = null) + public async Task> GetTransactionsOut(string address, string? cursor = null) { string query = $$""" query { transactions( - first: 100 + first: 50 + after: "{{cursor}}" sort: HEIGHT_DESC owners: ["{{address}}"] tags: [ @@ -146,6 +150,7 @@ public async Task> GetTransactionsOut(string address, string ] ) { edges { + cursor node { id recipient @@ -180,12 +185,13 @@ public async Task> GetTransactionsOut(string address, string return result; } - public async Task> GetTransactionsOutFromProcess(string address, string? fromTxId = null) + public async Task> GetTransactionsOutFromProcess(string address, string? cursor = null) { string query = $$""" query { transactions( - first: 100 + first: 50 + after: "{{cursor}}" sort: HEIGHT_DESC tags: [ { name: "From-Process", values: ["{{address}}"] } @@ -194,6 +200,7 @@ public async Task> GetTransactionsOutFromProcess(string addr ] ) { edges { + cursor node { id recipient @@ -277,12 +284,13 @@ public async Task> GetTransactionsOutFromProcess(string addr return result.FirstOrDefault(); } - public async Task> GetTransactionsForToken(string tokenId, string? fromTxId = null) + public async Task> GetTransactionsForToken(string tokenId, string? cursor = null) { string query = $$""" query { transactions( first: 50 + after: "{{cursor}}" sort: HEIGHT_DESC recipients: ["{{tokenId}}"] tags: [ @@ -291,6 +299,7 @@ public async Task> GetTransactionsForToken(string tokenId, s ] ) { edges { + cursor node { id recipient diff --git a/src/aoww.Services/Models/GraphqlResponse.cs b/src/aoww.Services/Models/GraphqlResponse.cs index 315901a..4e8282f 100644 --- a/src/aoww.Services/Models/GraphqlResponse.cs +++ b/src/aoww.Services/Models/GraphqlResponse.cs @@ -26,6 +26,9 @@ public class Data public class Edge { + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } + [JsonPropertyName("node")] public Node? Node { get; set; } } diff --git a/src/aoww.Services/Models/TokenTransfer.cs b/src/aoww.Services/Models/TokenTransfer.cs index 31a2800..6e3cb7b 100644 --- a/src/aoww.Services/Models/TokenTransfer.cs +++ b/src/aoww.Services/Models/TokenTransfer.cs @@ -18,5 +18,6 @@ public class TokenTransfer public long Quantity { get; set; } public TokenTransferType TokenTransferType { get; set; } + public string? Cursor { get; set; } } } From 980c7566ed2e21740bc2617a51c3e5ad7afff5a0 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Fri, 10 May 2024 15:32:14 +0200 Subject: [PATCH 44/63] Removed old theme switch code and old SendTokenDialog.razor --- src/aoWebWallet/Layout/MainLayout.razor | 2 +- src/aoWebWallet/Layout/MainLayout.razor.cs | 11 -- src/aoWebWallet/Models/UserSettings.cs | 2 +- src/aoWebWallet/Pages/WalletDetail.razor | 7 - src/aoWebWallet/Shared/NavMenu.razor | 31 +-- src/aoWebWallet/Shared/SendTokenDialog.razor | 194 ------------------- src/aoWebWallet/ViewModels/MainViewModel.cs | 15 +- 7 files changed, 4 insertions(+), 258 deletions(-) delete mode 100644 src/aoWebWallet/Shared/SendTokenDialog.razor diff --git a/src/aoWebWallet/Layout/MainLayout.razor b/src/aoWebWallet/Layout/MainLayout.razor index 5f53704..2c20bf9 100644 --- a/src/aoWebWallet/Layout/MainLayout.razor +++ b/src/aoWebWallet/Layout/MainLayout.razor @@ -4,7 +4,7 @@ string? versionHash = Program.GetVersionHash(); } - + diff --git a/src/aoWebWallet/Layout/MainLayout.razor.cs b/src/aoWebWallet/Layout/MainLayout.razor.cs index 6e274a8..cd38a56 100644 --- a/src/aoWebWallet/Layout/MainLayout.razor.cs +++ b/src/aoWebWallet/Layout/MainLayout.razor.cs @@ -11,8 +11,6 @@ public partial class MainLayout protected override void OnInitialized() { - BindingContext.PropertyChanged += BindingContext_PropertyChanged; - base.OnInitialized(); } @@ -27,17 +25,8 @@ protected override async Task OnAfterRenderAsync(bool firstRender) await base.OnAfterRenderAsync(firstRender); } - private void BindingContext_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(MainViewModel.IsDarkMode)) - { - this.StateHasChanged(); - } - } - public virtual void Dispose() { - BindingContext.PropertyChanged -= BindingContext_PropertyChanged; } } } diff --git a/src/aoWebWallet/Models/UserSettings.cs b/src/aoWebWallet/Models/UserSettings.cs index 6446d8d..fd7d708 100644 --- a/src/aoWebWallet/Models/UserSettings.cs +++ b/src/aoWebWallet/Models/UserSettings.cs @@ -2,7 +2,7 @@ { public class UserSettings { - public bool? IsDarkMode { get; set; } = true; + //public bool? IsDarkMode { get; set; } = true; public string? ComputeUnitUrl { get; set; } public string? GraphqlApiUrl { get; set; } diff --git a/src/aoWebWallet/Pages/WalletDetail.razor b/src/aoWebWallet/Pages/WalletDetail.razor index ee937a8..07b46fb 100644 --- a/src/aoWebWallet/Pages/WalletDetail.razor +++ b/src/aoWebWallet/Pages/WalletDetail.razor @@ -265,13 +265,6 @@ var aoAction = AoAction.CreateForTokenTransaction(balanceDataVM.Token.TokenId); NavigationManager.NavigateTo($"/action?{aoAction.ToQueryString()}"); - - // var parameters = new DialogParameters { - // { x => x.SelectedBalanceDataVM, balanceDataVM }, - // { x => x.SelectedWallet, BindingContext.SelectedWallet } - // }; - // var options = new DialogOptions { CloseOnEscapeKey = true }; - // DialogService.Show("Transfer Token", parameters, options); } private async Task RefreshBalances() diff --git a/src/aoWebWallet/Shared/NavMenu.razor b/src/aoWebWallet/Shared/NavMenu.razor index 864c319..2e95140 100644 --- a/src/aoWebWallet/Shared/NavMenu.razor +++ b/src/aoWebWallet/Shared/NavMenu.razor @@ -38,16 +38,6 @@ About
- @* @if (BindingContext.UserSettings?.IsDarkMode ?? true) - { - - } - else - { - - } - Theme *@ - @@ -83,26 +73,7 @@ { WatchDataLoaderVM(BindingContext.WalletList); - BindingContext.PropertyChanged += BindingContext_PropertyChanged; - base.OnInitialized(); } - - private void BindingContext_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(MainViewModel.IsDarkMode)) - { - this.StateHasChanged(); - } - } - - public virtual void Dispose() - { - BindingContext.PropertyChanged -= BindingContext_PropertyChanged; - } - - private Task ToggleTheme() - { - return BindingContext.SetIsDarkMode(!BindingContext.IsDarkMode); - } + } diff --git a/src/aoWebWallet/Shared/SendTokenDialog.razor b/src/aoWebWallet/Shared/SendTokenDialog.razor deleted file mode 100644 index 7ae278a..0000000 --- a/src/aoWebWallet/Shared/SendTokenDialog.razor +++ /dev/null @@ -1,194 +0,0 @@ -@using aoWebWallet.Models -@using aoWebWallet.Shared -@inject TokenClient TokenClient -@inject MainViewModel MainViewModel -@inject WalletDetailViewModel WalletDetailViewModel - - - - - - - @SelectedBalanceDataVM?.Token?.TokenData?.Name - @SelectedBalanceDataVM?.Token?.TokenData?.Ticker - - - Available balance
@BalanceHelper.FormatBalance(SelectedBalanceDataVM?.BalanceDataLoader.Data?.Balance, SelectedBalanceDataVM?.Token?.TokenData?.Denomination ?? 0)
- - @if (!isConfirm) - { - - - - - @Progress - } - - @if (isConfirm) - { - if (string.IsNullOrEmpty(TransactionId)) - { - - Are you sure? - You are about to transfer: - - } - - Amount: @Amount @SelectedBalanceDataVM?.Token?.TokenData?.Ticker - Receiver: - - @Address - - - - - - if (!string.IsNullOrEmpty(TransactionId)) - { - - Transfer success! - TransactionId - - @TransactionId - - - } - else if (!string.IsNullOrEmpty(Error)) - { - - Transfer Error! - TransactionId - - @Error - - - } - } -
- - Cancel - @if (!isConfirm) - { - Next - } - else - { - if (string.IsNullOrEmpty(TransactionId)) - { - Confirm - } - else - { - Close - } - } - -
-@code { - [CascadingParameter] MudDialogInstance MudDialog { get; set; } = default!; - - [Parameter] - public BalanceDataViewModel? SelectedBalanceDataVM { get; set; } - - [Parameter] - public WalletDetailsViewModel? SelectedWallet { get; set; } - - public string? Progress { get; set; } - public string? Address { get; set; } - public decimal Amount { get; set; } - public string? TransactionId { get; set; } - public string? Error { get; set; } - - public bool isConfirm = false; - public bool showLoader = false; - - public MudButton? confButtonRef; - - //public int Denomination => BindingContext.SelectedBalanceDataVM?.Token?.TokenData?.Denomination ?? 0; - public string DenominationFormat => "F" + (SelectedBalanceDataVM?.Token?.TokenData?.Denomination ?? 1).ToString(); - - public async Task Submit() - { - if (string.IsNullOrWhiteSpace(Address)) - { - Progress = "Input a wallet address."; - StateHasChanged(); - return; - } - if (Address.Length != 43) - { - Progress = "Address length must be 43 characters."; - StateHasChanged(); - return; - } - - if (string.IsNullOrEmpty(SelectedBalanceDataVM?.BalanceDataLoader.Data?.TokenId) - || SelectedBalanceDataVM.Token?.TokenData?.Denomination == null - || !SelectedBalanceDataVM.Token.TokenData.Denomination.HasValue) - return; - - long amountLong = BalanceHelper.DecimalToTokenAmount(Amount, SelectedBalanceDataVM.Token.TokenData.Denomination!.Value); - if (amountLong <= 0) - { - Progress = "Amount has to be higher than 0."; - StateHasChanged(); - return; - } - if (amountLong > SelectedBalanceDataVM?.BalanceDataLoader.Data?.Balance) - { - Progress = "Not enough balance available."; - StateHasChanged(); - return; - } - - isConfirm = true; - } - - public async Task Confirm() - { - Error = null; - - if (string.IsNullOrEmpty(Address)) - return; - if (Address.Length != 43) - { - Progress = "Address length must be 43 characters."; - StateHasChanged(); - return; - } - - if (string.IsNullOrEmpty(SelectedBalanceDataVM?.BalanceDataLoader.Data?.TokenId) - || SelectedBalanceDataVM.Token?.TokenData?.Denomination == null - || !SelectedBalanceDataVM.Token.TokenData.Denomination.HasValue) - return; - - if (confButtonRef != null) - confButtonRef.Disabled = true; - - this.StateHasChanged(); - - if (SelectedWallet == null) - { - throw new Exception("SelectedWallet is null"); - } - - long amountLong = BalanceHelper.DecimalToTokenAmount(Amount, SelectedBalanceDataVM.Token.TokenData.Denomination!.Value); - var result = await MainViewModel.SendToken(SelectedWallet.Wallet, SelectedBalanceDataVM.Token.TokenId, Address, amountLong); - TransactionId = result?.Id; - - await WalletDetailViewModel.RefreshBalance(); - - if (TransactionId != null) - { - await MainViewModel.AddToLog(ActivityLogType.SendTransaction, TransactionId); - } - } - - public void Close() - { - MudDialog.Close(DialogResult.Ok(true)); - - } - - void Cancel() => MudDialog.Cancel(); -} diff --git a/src/aoWebWallet/ViewModels/MainViewModel.cs b/src/aoWebWallet/ViewModels/MainViewModel.cs index 5b806fe..e49c9e3 100644 --- a/src/aoWebWallet/ViewModels/MainViewModel.cs +++ b/src/aoWebWallet/ViewModels/MainViewModel.cs @@ -28,9 +28,6 @@ public partial class MainViewModel : ObservableRecipient private readonly GraphqlClient graphqlClient; private readonly MemoryDataCache memoryDataCache; - [ObservableProperty] - private bool isDarkMode = true; - [ObservableProperty] public bool? hasArConnectExtension; @@ -192,7 +189,7 @@ public async Task LoadUserSettings() UserSettings = await storageService.GetUserSettings(); if (UserSettings != null) { - IsDarkMode = UserSettings.IsDarkMode ?? true; + } } @@ -201,7 +198,6 @@ public async Task SaveUserSettings() if (UserSettings != null) { await storageService.SaveUserSettings(UserSettings); - IsDarkMode = UserSettings.IsDarkMode ?? true; } } @@ -319,14 +315,5 @@ public async Task GetActiveArConnectAddress() return new Transaction { Id = idResult }; }); - public async Task SetIsDarkMode(bool isDarkMode) - { - if (UserSettings != null) - { - UserSettings.IsDarkMode = isDarkMode; - await SaveUserSettings(); - } - } - } } From 356b2d84e6e59f00e4b6b7448efe1e4f6a928a1a Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Fri, 10 May 2024 16:35:56 +0200 Subject: [PATCH 45/63] First setup changing urls in settings --- src/aoWebWallet.sln | 5 ++- src/aoWebWallet/Models/GatewayConfig.cs | 7 +++ src/aoWebWallet/Models/UserSettings.cs | 7 ++- src/aoWebWallet/Pages/MvvmComponentBase.cs | 13 ------ src/aoWebWallet/Pages/Settings.razor | 33 +++++++------- src/aoWebWallet/Pages/TokenDetail.razor | 1 + src/aoWebWallet/Pages/TransactionDetail.razor | 1 + src/aoWebWallet/Pages/WalletDetail.razor | 1 + src/aoWebWallet/Program.cs | 7 +++ src/aoWebWallet/Services/UrlHelper.cs | 19 ++++++-- .../Components/ApiConnectionDisplay.razor | 14 ------ .../Shared/Components/ChangeAPIDialog.razor | 44 ------------------- .../Components/TokenListComponent.razor | 1 + .../Components/TransactionComponent.razor | 2 +- .../Shared/ReceiveTokenDialog.razor | 1 + src/aoWebWallet/ViewModels/MainViewModel.cs | 37 +++++++--------- src/aoww.Services.Tests/GraphqlTests.cs | 7 ++- src/aoww.Services/GraphqlClient.cs | 7 ++- src/aoww.Services/Models/GraphqlConfig.cs | 13 ++++++ src/aoww.Services/aoww.Services.csproj | 4 ++ 20 files changed, 103 insertions(+), 121 deletions(-) create mode 100644 src/aoWebWallet/Models/GatewayConfig.cs delete mode 100644 src/aoWebWallet/Shared/Components/ApiConnectionDisplay.razor delete mode 100644 src/aoWebWallet/Shared/Components/ChangeAPIDialog.razor create mode 100644 src/aoww.Services/Models/GraphqlConfig.cs diff --git a/src/aoWebWallet.sln b/src/aoWebWallet.sln index a90b2ae..8de578f 100644 --- a/src/aoWebWallet.sln +++ b/src/aoWebWallet.sln @@ -9,11 +9,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libs", "Libs", "{06E5BC39-7 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "webvNext.DataLoader", "webvNext.DataLoader\webvNext.DataLoader.csproj", "{17CA4374-64D0-4618-852F-8A76D0A57166}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "aoww.Services", "aoww.Services\aoww.Services.csproj", "{178C3213-D574-4B39-A2DA-1FB1D2806242}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "aoww.Services", "aoww.Services\aoww.Services.csproj", "{178C3213-D574-4B39-A2DA-1FB1D2806242}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{89AC47DF-65AD-4870-AA1D-74ABF1F3D8FE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "aoww.Services.Tests", "aoww.Services.Tests\aoww.Services.Tests.csproj", "{322F4807-05CF-431D-B400-7420E1B29936}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "aoww.Services.Tests", "aoww.Services.Tests\aoww.Services.Tests.csproj", "{322F4807-05CF-431D-B400-7420E1B29936}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -43,6 +43,7 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {17CA4374-64D0-4618-852F-8A76D0A57166} = {06E5BC39-764A-48B9-B4F9-F48387A2C965} + {178C3213-D574-4B39-A2DA-1FB1D2806242} = {06E5BC39-764A-48B9-B4F9-F48387A2C965} {322F4807-05CF-431D-B400-7420E1B29936} = {89AC47DF-65AD-4870-AA1D-74ABF1F3D8FE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/aoWebWallet/Models/GatewayConfig.cs b/src/aoWebWallet/Models/GatewayConfig.cs new file mode 100644 index 0000000..9b75fb1 --- /dev/null +++ b/src/aoWebWallet/Models/GatewayConfig.cs @@ -0,0 +1,7 @@ +namespace aoWebWallet.Models +{ + public class GatewayConfig + { + public string GatewayUrl { get; set; } = "https://arweave.net"; + } +} diff --git a/src/aoWebWallet/Models/UserSettings.cs b/src/aoWebWallet/Models/UserSettings.cs index fd7d708..29d4430 100644 --- a/src/aoWebWallet/Models/UserSettings.cs +++ b/src/aoWebWallet/Models/UserSettings.cs @@ -3,8 +3,11 @@ public class UserSettings { //public bool? IsDarkMode { get; set; } = true; - public string? ComputeUnitUrl { get; set; } - public string? GraphqlApiUrl { get; set; } + public string GatewayUrl { get; set; } = "https://arweave.net"; + public string GraphqlUrl { get; set; } = "https://arweave.net/graphql"; + public string ComputeUnitUrl { get; set; } = "https://cu.ao-testnet.xyz"; + public string MessengerUnitUrl { get; set; } = "https://mu.ao-testnet.xyz"; + public bool Claimed1 { get; set; } public bool Claimed2 { get; set; } diff --git a/src/aoWebWallet/Pages/MvvmComponentBase.cs b/src/aoWebWallet/Pages/MvvmComponentBase.cs index 58f8eeb..1fce93d 100644 --- a/src/aoWebWallet/Pages/MvvmComponentBase.cs +++ b/src/aoWebWallet/Pages/MvvmComponentBase.cs @@ -18,8 +18,6 @@ public abstract class MvvmComponentBase : ComponentBase, IDisposable where T protected override void OnInitialized() { - BindingContext.PropertyChanged += BindingContext_PropertyChanged; - foreach(var obj in ObjWatch) { obj.PropertyChanged += ObjWatch_PropertyChanged; @@ -45,15 +43,6 @@ protected override async Task OnInitializedAsync() // await base.OnParametersSetAsync(); //} - internal async void BindingContext_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(MainViewModel.ComputeUnitUrl)) - { - await LoadDataAsync(); - this.StateHasChanged(); - } - } - internal void ObjWatch_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { this.StateHasChanged(); @@ -87,8 +76,6 @@ protected void WatchDataLoaderVM(DataLoaderViewModel vm) where D : class public virtual void Dispose() { - BindingContext.PropertyChanged -= BindingContext_PropertyChanged; - foreach (var obj in ObjWatch) { obj.PropertyChanged -= ObjWatch_PropertyChanged; diff --git a/src/aoWebWallet/Pages/Settings.razor b/src/aoWebWallet/Pages/Settings.razor index d3ad79e..7beafaa 100644 --- a/src/aoWebWallet/Pages/Settings.razor +++ b/src/aoWebWallet/Pages/Settings.razor @@ -24,18 +24,19 @@ } + + + + + + + - @* - - - + Save - - - + + - Save - *@ @@ -44,18 +45,16 @@ private string? newUrl { get; set; } private string? customUrl { get; set; } - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { - newUrl = MainViewModel.ComputeUnitUrl; - base.OnInitialized(); + await BindingContext.LoadUserSettings(); + + await base.OnInitializedAsync(); } - void Submit() + async void Submit() { - if (!string.IsNullOrEmpty(newUrl)) - MainViewModel.ComputeUnitUrl = newUrl; - if (!string.IsNullOrEmpty(customUrl)) - MainViewModel.ComputeUnitUrl = customUrl; + await BindingContext.SaveUserSettings(); Snackbar.Add("Settings saved, reloading data...", Severity.Info); } diff --git a/src/aoWebWallet/Pages/TokenDetail.razor b/src/aoWebWallet/Pages/TokenDetail.razor index 4d6e1a1..b181293 100644 --- a/src/aoWebWallet/Pages/TokenDetail.razor +++ b/src/aoWebWallet/Pages/TokenDetail.razor @@ -5,6 +5,7 @@ @inject ISnackbar Snackbar @inject NavigationManager NavigationManager; @inject TokenDataService dataService; +@inject GatewayUrlHelper UrlHelper; @Program.PageTitlePostFix diff --git a/src/aoWebWallet/Pages/TransactionDetail.razor b/src/aoWebWallet/Pages/TransactionDetail.razor index 2ad64e4..d5f677f 100644 --- a/src/aoWebWallet/Pages/TransactionDetail.razor +++ b/src/aoWebWallet/Pages/TransactionDetail.razor @@ -3,6 +3,7 @@ @inherits MvvmComponentBase @inject NavigationManager NavigationManager; @inject TokenDataService dataService +@inject GatewayUrlHelper UrlHelper; @TxId - @Program.PageTitlePostFix diff --git a/src/aoWebWallet/Pages/WalletDetail.razor b/src/aoWebWallet/Pages/WalletDetail.razor index 07b46fb..6a0a919 100644 --- a/src/aoWebWallet/Pages/WalletDetail.razor +++ b/src/aoWebWallet/Pages/WalletDetail.razor @@ -7,6 +7,7 @@ @inject TokenDataService dataService @inject MainViewModel MainViewModel @inject ClipboardService ClipboardService +@inject GatewayUrlHelper UrlHelper; @Address - @Program.PageTitlePostFix diff --git a/src/aoWebWallet/Program.cs b/src/aoWebWallet/Program.cs index eeee165..392bdcc 100644 --- a/src/aoWebWallet/Program.cs +++ b/src/aoWebWallet/Program.cs @@ -12,6 +12,8 @@ using System.Globalization; using ClipLazor.Extention; using aoww.Services; +using aoww.Services.Models; +using aoWebWallet.Models; namespace aoWebWallet { @@ -93,6 +95,7 @@ private static void ConfigureServices(IServiceCollection services, string baseAd services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddSingleton(); @@ -112,6 +115,10 @@ private static void ConfigureServices(IServiceCollection services, string baseAd services.AddBlazoredLocalStorage(); services.AddClipboard(); + + //Options + services.AddSingleton(new GraphqlConfig()); + services.AddSingleton(new GatewayConfig()); } } } diff --git a/src/aoWebWallet/Services/UrlHelper.cs b/src/aoWebWallet/Services/UrlHelper.cs index 2416720..ec3fb17 100644 --- a/src/aoWebWallet/Services/UrlHelper.cs +++ b/src/aoWebWallet/Services/UrlHelper.cs @@ -1,13 +1,24 @@ -namespace aoWebWallet.Services +using aoWebWallet.Models; +using Microsoft.Extensions.Options; + +namespace aoWebWallet.Services { - public static class UrlHelper + public class GatewayUrlHelper { - public static string? GetArweaveUrl(string? id) + private readonly GatewayConfig config; + + public GatewayUrlHelper(IOptions config) + { + this.config = config.Value; + } + + public string? GetArweaveUrl(string? id) { if (string.IsNullOrWhiteSpace(id)) return null; - return $"https://arweave.net/{id}"; + Uri combinedUri = new Uri(new Uri(config.GatewayUrl), id); + return combinedUri.ToString(); } } } diff --git a/src/aoWebWallet/Shared/Components/ApiConnectionDisplay.razor b/src/aoWebWallet/Shared/Components/ApiConnectionDisplay.razor deleted file mode 100644 index 1fc93db..0000000 --- a/src/aoWebWallet/Shared/Components/ApiConnectionDisplay.razor +++ /dev/null @@ -1,14 +0,0 @@ -@inherits MvvmComponentBase -@inject IDialogService DialogService - -@BindingContext.ComputeUnitUrl - - - -@code { - private void OpenDialog() - { - var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Medium, FullWidth = true }; - DialogService.Show("Change Compute Unit Url", options); - } -} diff --git a/src/aoWebWallet/Shared/Components/ChangeAPIDialog.razor b/src/aoWebWallet/Shared/Components/ChangeAPIDialog.razor deleted file mode 100644 index 229ba97..0000000 --- a/src/aoWebWallet/Shared/Components/ChangeAPIDialog.razor +++ /dev/null @@ -1,44 +0,0 @@ -@inject MainViewModel MainViewModel - - - - - - - - - - - - - - - - Cancel - Ok - - -@code { - [CascadingParameter] - MudDialogInstance? MudDialog { get; set; } - - private string? newUrl { get; set; } - private string? customUrl { get; set; } - - protected override void OnInitialized() - { - newUrl = MainViewModel.ComputeUnitUrl; - base.OnInitialized(); - } - - void Submit() { - if(!string.IsNullOrEmpty(newUrl)) - MainViewModel.ComputeUnitUrl = newUrl; - if (!string.IsNullOrEmpty(customUrl)) - MainViewModel.ComputeUnitUrl = customUrl; - - MudDialog?.Close(DialogResult.Ok(true)); - } - - void Cancel() => MudDialog?.Cancel(); -} \ No newline at end of file diff --git a/src/aoWebWallet/Shared/Components/TokenListComponent.razor b/src/aoWebWallet/Shared/Components/TokenListComponent.razor index 4aee823..3d7a1f0 100644 --- a/src/aoWebWallet/Shared/Components/TokenListComponent.razor +++ b/src/aoWebWallet/Shared/Components/TokenListComponent.razor @@ -1,5 +1,6 @@ @using aoWebWallet.Models @inherits MvvmComponentBase +@inject GatewayUrlHelper UrlHelper; @if (token != null) { diff --git a/src/aoWebWallet/Shared/Components/TransactionComponent.razor b/src/aoWebWallet/Shared/Components/TransactionComponent.razor index e7aeb10..3f8bc88 100644 --- a/src/aoWebWallet/Shared/Components/TransactionComponent.razor +++ b/src/aoWebWallet/Shared/Components/TransactionComponent.razor @@ -1,7 +1,7 @@ @using aoWebWallet.Models - @inherits MvvmComponentBase @inject TokenDataService dataService +@inject GatewayUrlHelper UrlHelper; @if (transfer != null) { diff --git a/src/aoWebWallet/Shared/ReceiveTokenDialog.razor b/src/aoWebWallet/Shared/ReceiveTokenDialog.razor index e85e1bc..6f744df 100644 --- a/src/aoWebWallet/Shared/ReceiveTokenDialog.razor +++ b/src/aoWebWallet/Shared/ReceiveTokenDialog.razor @@ -1,5 +1,6 @@ @using aoWebWallet.Models @using aoWebWallet.Shared +@inject GatewayUrlHelper UrlHelper; diff --git a/src/aoWebWallet/ViewModels/MainViewModel.cs b/src/aoWebWallet/ViewModels/MainViewModel.cs index e49c9e3..ff7a52f 100644 --- a/src/aoWebWallet/ViewModels/MainViewModel.cs +++ b/src/aoWebWallet/ViewModels/MainViewModel.cs @@ -11,6 +11,7 @@ using ClipLazor.Enums; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using Microsoft.Extensions.Options; using MudBlazor; using System.Text.Json; using webvNext.DataLoader; @@ -27,6 +28,8 @@ public partial class MainViewModel : ObservableRecipient private readonly ArweaveService arweaveService; private readonly GraphqlClient graphqlClient; private readonly MemoryDataCache memoryDataCache; + private readonly GatewayConfig gatewayConfig; + private readonly GraphqlConfig graphqlConfig; [ObservableProperty] public bool? hasArConnectExtension; @@ -34,10 +37,6 @@ public partial class MainViewModel : ObservableRecipient [ObservableProperty] public string? activeArConnectAddress; - [ObservableProperty] - [NotifyPropertyChangedRecipients] - private string? computeUnitUrl; - [ObservableProperty] private UserSettings? userSettings; @@ -54,13 +53,17 @@ public MainViewModel(TokenDataService dataService, StorageService storageService, ArweaveService arweaveService, GraphqlClient graphqlClient, - MemoryDataCache memoryDataCache) : base() + MemoryDataCache memoryDataCache, + IOptions graphqlConfig, + IOptions gatewayConfig) : base() { this.dataService = dataService; this.storageService = storageService; this.arweaveService = arweaveService; this.graphqlClient = graphqlClient; this.memoryDataCache = memoryDataCache; + this.gatewayConfig = gatewayConfig.Value; + this.graphqlConfig = graphqlConfig.Value; } public async Task AddToLog(ActivityLogType type, string id) @@ -170,26 +173,13 @@ public async Task ClearUserData() WalletList = new(); //BalanceDataList.Data = null; } - - partial void OnComputeUnitUrlChanged(string? value) - { - //ClearUserData(); - - if (!string.IsNullOrEmpty(value)) - { - //storageService.SetApiUrl(value); - - } - - } - public async Task LoadUserSettings() { UserSettings = await storageService.GetUserSettings(); if (UserSettings != null) { - + UpdateUserSettings(UserSettings); } } @@ -198,9 +188,16 @@ public async Task SaveUserSettings() if (UserSettings != null) { await storageService.SaveUserSettings(UserSettings); + + UpdateUserSettings(UserSettings); } } - + + private void UpdateUserSettings(UserSettings userSettings) + { + graphqlConfig.ApiUrl = userSettings.GraphqlUrl; + gatewayConfig.GatewayUrl = userSettings.GatewayUrl; + } public async Task DisconnectArWallet() { diff --git a/src/aoww.Services.Tests/GraphqlTests.cs b/src/aoww.Services.Tests/GraphqlTests.cs index cb6482a..59abfa0 100644 --- a/src/aoww.Services.Tests/GraphqlTests.cs +++ b/src/aoww.Services.Tests/GraphqlTests.cs @@ -1,3 +1,6 @@ +using aoww.Services.Models; +using Microsoft.Extensions.Options; + namespace aoww.Services.Tests { [TestClass] @@ -6,7 +9,7 @@ public class GraphqlTests [TestMethod] public async Task GetTransactionsTest() { - var graph = new GraphqlClient(new HttpClient()); + var graph = new GraphqlClient(new HttpClient(), Options.Create(new())); var result = await graph.GetTransactionsIn("4NdFkWsgFQIEmJnzFSYrO88UmRPf0ABfVh_fRc2u130"); @@ -16,7 +19,7 @@ public async Task GetTransactionsTest() [TestMethod] public async Task GetMintTest() { - var graph = new GraphqlClient(new HttpClient()); + var graph = new GraphqlClient(new HttpClient(), Options.Create(new())); var result = await graph.GetTransactionsIn("CeiYr2VjUVAFXmPJvfj-Pfk6zmprBzeqNeRWAbImbOo"); diff --git a/src/aoww.Services/GraphqlClient.cs b/src/aoww.Services/GraphqlClient.cs index 34e6fe4..cac74c2 100644 --- a/src/aoww.Services/GraphqlClient.cs +++ b/src/aoww.Services/GraphqlClient.cs @@ -1,4 +1,5 @@ using aoww.Services.Models; +using Microsoft.Extensions.Options; using System.Net.Http.Json; namespace aoww.Services @@ -9,10 +10,12 @@ namespace aoww.Services public class GraphqlClient { private readonly HttpClient httpClient; + private readonly GraphqlConfig config; - public GraphqlClient(HttpClient httpClient) + public GraphqlClient(HttpClient httpClient, IOptions config) { this.httpClient = httpClient; + this.config = config.Value; } public async Task> GetTransactionsIn(string adddress, string? cursor = null) @@ -421,7 +424,7 @@ public async Task> GetAoProcessesForAddress(string address) { var request = new GraphqlRequest { Query = query }; - HttpResponseMessage res = await httpClient.PostAsJsonAsync("https://arweave.net/graphql", request); + HttpResponseMessage res = await httpClient.PostAsJsonAsync(config.ApiUrl, request); if (res.IsSuccessStatusCode) { return await res.Content.ReadFromJsonAsync(); diff --git a/src/aoww.Services/Models/GraphqlConfig.cs b/src/aoww.Services/Models/GraphqlConfig.cs new file mode 100644 index 0000000..3adb6ed --- /dev/null +++ b/src/aoww.Services/Models/GraphqlConfig.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace aoww.Services.Models +{ + public class GraphqlConfig + { + public string ApiUrl { get; set; } = "https://arweave.net/graphql"; + } +} diff --git a/src/aoww.Services/aoww.Services.csproj b/src/aoww.Services/aoww.Services.csproj index fa71b7a..cc6a561 100644 --- a/src/aoww.Services/aoww.Services.csproj +++ b/src/aoww.Services/aoww.Services.csproj @@ -6,4 +6,8 @@ enable + + + + From 4f8e44004704bef926373470ac7ff2a9785b6630 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Fri, 10 May 2024 16:47:08 +0200 Subject: [PATCH 46/63] loading in afterrender --- src/aoWebWallet/Pages/Settings.razor | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/aoWebWallet/Pages/Settings.razor b/src/aoWebWallet/Pages/Settings.razor index 7beafaa..2fc67c6 100644 --- a/src/aoWebWallet/Pages/Settings.razor +++ b/src/aoWebWallet/Pages/Settings.razor @@ -42,14 +42,15 @@ @code { - private string? newUrl { get; set; } - private string? customUrl { get; set; } - protected override async Task OnInitializedAsync() + protected override async Task OnAfterRenderAsync(bool firstRender) { - await BindingContext.LoadUserSettings(); + //if (firstRender) + //{ + await BindingContext.LoadUserSettings(); + //} - await base.OnInitializedAsync(); + await base.OnAfterRenderAsync(firstRender); } async void Submit() From bd44d555526654b4cc3d1395d2984eb992783839 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Fri, 10 May 2024 16:52:13 +0200 Subject: [PATCH 47/63] only on first render --- src/aoWebWallet/Pages/Settings.razor | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/aoWebWallet/Pages/Settings.razor b/src/aoWebWallet/Pages/Settings.razor index 2fc67c6..fa7629a 100644 --- a/src/aoWebWallet/Pages/Settings.razor +++ b/src/aoWebWallet/Pages/Settings.razor @@ -45,10 +45,10 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { - //if (firstRender) - //{ + if (firstRender) + { await BindingContext.LoadUserSettings(); - //} + } await base.OnAfterRenderAsync(firstRender); } From f4a56d37cbe742c1922cc600a89cccfe8e0fae93 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Fri, 10 May 2024 16:57:10 +0200 Subject: [PATCH 48/63] userSettings never null --- src/aoWebWallet/Pages/Settings.razor | 10 ---------- src/aoWebWallet/ViewModels/MainViewModel.cs | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/aoWebWallet/Pages/Settings.razor b/src/aoWebWallet/Pages/Settings.razor index fa7629a..c6e7b29 100644 --- a/src/aoWebWallet/Pages/Settings.razor +++ b/src/aoWebWallet/Pages/Settings.razor @@ -43,16 +43,6 @@ @code { - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) - { - await BindingContext.LoadUserSettings(); - } - - await base.OnAfterRenderAsync(firstRender); - } - async void Submit() { await BindingContext.SaveUserSettings(); diff --git a/src/aoWebWallet/ViewModels/MainViewModel.cs b/src/aoWebWallet/ViewModels/MainViewModel.cs index ff7a52f..105cb3c 100644 --- a/src/aoWebWallet/ViewModels/MainViewModel.cs +++ b/src/aoWebWallet/ViewModels/MainViewModel.cs @@ -38,7 +38,7 @@ public partial class MainViewModel : ObservableRecipient public string? activeArConnectAddress; [ObservableProperty] - private UserSettings? userSettings; + private UserSettings userSettings = new(); public DataLoaderViewModel LastTransactionId { get; set; } = new(); From 11686cc9523b5e9803ba6215c178199e0650fb7b Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Sat, 11 May 2024 11:50:16 +0200 Subject: [PATCH 49/63] Add wallet flow improvements --- src/aoWebWallet/Pages/ActionPage.razor | 7 ++-- src/aoWebWallet/Pages/Start.razor | 41 +------------------ src/aoWebWallet/Pages/Wallets.razor | 5 --- .../Shared/AddArConnectComponent.razor | 10 ++++- .../Shared/AddGenerateWalletComponent.razor | 8 +++- .../Shared/AddUploadWalletComponent.razor | 10 +++++ .../Shared/AddWalletComponent.razor | 34 +++++++++++++++ src/aoWebWallet/Shared/AddWalletDialog.razor | 13 +----- .../Shared/EditWalletComponent.razor | 11 ++--- src/aoWebWallet/aoWebWallet.csproj | 6 +++ 10 files changed, 76 insertions(+), 69 deletions(-) create mode 100644 src/aoWebWallet/Shared/AddWalletComponent.razor diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor index adf2e92..d529cdf 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -62,7 +62,7 @@ Preview @validation } - else if (!started && string.IsNullOrEmpty(transactionService.LastTransaction.Data?.Id)) + else if (!started && !string.IsNullOrEmpty(selectedWallet) && string.IsNullOrEmpty(transactionService.LastTransaction.Data?.Id)) { if (transactionService.DryRunResult.Data != null) @@ -204,8 +204,9 @@ private void OpenDialog() { - var options = new DialogOptions { CloseOnEscapeKey = true }; - DialogService.Show("Add Wallet", options); + NavigationManager.NavigateTo("/start"); + // var options = new DialogOptions { CloseOnEscapeKey = true }; + // DialogService.Show("Add Wallet", options); } } diff --git a/src/aoWebWallet/Pages/Start.razor b/src/aoWebWallet/Pages/Start.razor index 26eadda..620994a 100644 --- a/src/aoWebWallet/Pages/Start.razor +++ b/src/aoWebWallet/Pages/Start.razor @@ -3,44 +3,7 @@ @using aoWebWallet.Models @using aoWebWallet.Shared -Start - @Program.PageTitlePostFix +Add a wallet - @Program.PageTitlePostFix - - - - -
-
- -
-
- -
-
- - -
-
- -
-
- -
-
- - -
-
- -
-
- -
-
-
-
+ - @code { - - -} diff --git a/src/aoWebWallet/Pages/Wallets.razor b/src/aoWebWallet/Pages/Wallets.razor index 3a76f85..7f8f2d1 100644 --- a/src/aoWebWallet/Pages/Wallets.razor +++ b/src/aoWebWallet/Pages/Wallets.razor @@ -100,11 +100,6 @@ @code { - private void OpenDialog() - { - var options = new DialogOptions { CloseOnEscapeKey = true }; - DialogService.Show("Add Wallet", options); - } private async void EditWallet(Wallet wallet) { diff --git a/src/aoWebWallet/Shared/AddArConnectComponent.razor b/src/aoWebWallet/Shared/AddArConnectComponent.razor index 135099f..f6fc840 100644 --- a/src/aoWebWallet/Shared/AddArConnectComponent.razor +++ b/src/aoWebWallet/Shared/AddArConnectComponent.razor @@ -2,6 +2,7 @@ @inherits MvvmComponentBase @inject ISnackbar Snackbar @inject ArweaveService ArweaveService +@inject NavigationManager NavigationManager @@ -125,7 +126,14 @@ Snackbar.Add($"Wallet added ({address})", Severity.Info); - MudDialog?.Close(true); + if (MudDialog != null) + { + MudDialog.Close(); + } + else + { + NavigationManager.NavigateTo($"/wallet/{wallet.Address}"); + } } } diff --git a/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor b/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor index 366df46..f30882b 100644 --- a/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor +++ b/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor @@ -2,6 +2,7 @@ @inherits MvvmComponentBase @inject ArweaveService ArweaveService @inject ISnackbar Snackbar +@inject NavigationManager NavigationManager @@ -13,7 +14,7 @@ @Progress
- Create AOWW Wallet + Create aoWW Wallet
@@ -65,6 +66,11 @@ { MudDialog.Close(); } + else + { + NavigationManager.NavigateTo($"/wallet/{wallet.Address}"); + } + return true; } } diff --git a/src/aoWebWallet/Shared/AddUploadWalletComponent.razor b/src/aoWebWallet/Shared/AddUploadWalletComponent.razor index cb1c90e..8d4f7ba 100644 --- a/src/aoWebWallet/Shared/AddUploadWalletComponent.razor +++ b/src/aoWebWallet/Shared/AddUploadWalletComponent.razor @@ -2,6 +2,8 @@ @inherits MvvmComponentBase @inject ArweaveService ArweaveService @inject ISnackbar Snackbar +@inject NavigationManager NavigationManager + @@ -154,6 +156,14 @@ { MudDialog.Close(); } + else if(_fileNames.Count == 1) + { + NavigationManager.NavigateTo($"/wallet/{_fileNames.First().Address}"); + } + else + { + NavigationManager.NavigateTo($"/"); + } } private void SetDragClass() diff --git a/src/aoWebWallet/Shared/AddWalletComponent.razor b/src/aoWebWallet/Shared/AddWalletComponent.razor new file mode 100644 index 0000000..84bd04f --- /dev/null +++ b/src/aoWebWallet/Shared/AddWalletComponent.razor @@ -0,0 +1,34 @@ + + + + +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+
+
diff --git a/src/aoWebWallet/Shared/AddWalletDialog.razor b/src/aoWebWallet/Shared/AddWalletDialog.razor index 48cf964..926ef1e 100644 --- a/src/aoWebWallet/Shared/AddWalletDialog.razor +++ b/src/aoWebWallet/Shared/AddWalletDialog.razor @@ -4,18 +4,7 @@ - - - - - - - - - - - - + @code { diff --git a/src/aoWebWallet/Shared/EditWalletComponent.razor b/src/aoWebWallet/Shared/EditWalletComponent.razor index f5b6c1c..b74f1c7 100644 --- a/src/aoWebWallet/Shared/EditWalletComponent.razor +++ b/src/aoWebWallet/Shared/EditWalletComponent.razor @@ -4,9 +4,9 @@ - - + @Progress +
@@ -26,8 +26,7 @@ public string? Progress { get; set; } - [Parameter] - public bool IsExpanded { get; set; } + public bool IsReadOnly => !Wallet.IsReadOnly; protected override void OnInitialized() @@ -35,10 +34,6 @@ base.OnInitialized(); } - private void OnExpandCollapseClick() - { - IsExpanded = !IsExpanded; - } public async Task Submit() { diff --git a/src/aoWebWallet/aoWebWallet.csproj b/src/aoWebWallet/aoWebWallet.csproj index b4872a5..db18bf9 100644 --- a/src/aoWebWallet/aoWebWallet.csproj +++ b/src/aoWebWallet/aoWebWallet.csproj @@ -28,4 +28,10 @@ + + + true + + + From 499483353dc167e1bd2434b2b2339640e67bf739 Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Sat, 11 May 2024 11:52:12 +0100 Subject: [PATCH 50/63] add wallet background opacity at start page --- src/aoWebWallet/wwwroot/css/app.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aoWebWallet/wwwroot/css/app.css b/src/aoWebWallet/wwwroot/css/app.css index 21ed512..67da73b 100644 --- a/src/aoWebWallet/wwwroot/css/app.css +++ b/src/aoWebWallet/wwwroot/css/app.css @@ -317,7 +317,7 @@ body { } .trigger-transparency.mud-paper { - background-color: rgba(222,222,222,0); + background-color: rgba(222,222,222,0) !important; } .trigger-transparency { From cd49f8ff0ba5f5fe8d47d816524b1368e0e66d87 Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Sat, 11 May 2024 14:08:45 +0100 Subject: [PATCH 51/63] style wallet component --- src/aoWebWallet/Pages/ActionPage.razor | 8 ++--- .../Components/ActionInputComponent.razor | 31 ++++++++++--------- .../Components/ActionQuantityComponent.razor | 6 ++-- .../Components/TransactionComponent.razor | 4 +-- src/aoWebWallet/wwwroot/css/app.css | 12 +++++++ 5 files changed, 36 insertions(+), 25 deletions(-) diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor index d529cdf..5081f8a 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -24,10 +24,10 @@ } else { - + @foreach (var wallet in sendWallets ?? new()) { - + @* *@ @@ -59,7 +59,7 @@ @if (!readOnly && !string.IsNullOrEmpty(selectedWallet)) { - Preview + Preview @validation } else if (!started && !string.IsNullOrEmpty(selectedWallet) && string.IsNullOrEmpty(transactionService.LastTransaction.Data?.Id)) @@ -67,7 +67,7 @@ if (transactionService.DryRunResult.Data != null) { - + Preview Result @foreach (var msg in transactionService.DryRunResult.Data.Messages) diff --git a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor index 1cac3ae..50cc0c9 100644 --- a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor +++ b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor @@ -17,25 +17,26 @@ else { - - - @e.ToAutocompleteDisplay() - - - - - @e.ToAutocompleteDisplay() - - - - @(ActionParam.Value ?? "Not selected") + AdornmentIcon="@Icons.Material.Filled.Search" + AdornmentColor="Color.Primary" + Class="my-4"> + + + @e.ToAutocompleteDisplay() + + + + + @e.ToAutocompleteDisplay() + + + + @(ActionParam.Value ?? "Not selected") @* *@ } diff --git a/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor b/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor index 1ca1e11..0a6456e 100644 --- a/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor +++ b/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor @@ -41,7 +41,7 @@ if (ActionParam.ParamType == ActionParamType.Balance) { - Balance available: @BalanceHelper.FormatBalance(BalanceData?.Balance, Token?.TokenData?.Denomination ?? 1) @Token?.TokenData?.Ticker + Balance available: @BalanceHelper.FormatBalance(BalanceData?.Balance, Token?.TokenData?.Denomination ?? 1) @Token?.TokenData?.Ticker } } } @@ -86,7 +86,7 @@ if (token.TokenData?.Denomination != null) Token = token; - if (ActionParam.ParamType == ActionParamType.Balance + if (ActionParam.ParamType == ActionParamType.Balance && !string.IsNullOrEmpty(Address) && !ReadOnly) { @@ -140,7 +140,7 @@ if (!(mudTextField?.ValidationErrors.Any() ?? false)) { long amountLong = BalanceHelper.DecimalToTokenAmount(e, Token.TokenData.Denomination.Value); - + ActionParam.Value = amountLong.ToString(); } else diff --git a/src/aoWebWallet/Shared/Components/TransactionComponent.razor b/src/aoWebWallet/Shared/Components/TransactionComponent.razor index 3f8bc88..5fb7cfe 100644 --- a/src/aoWebWallet/Shared/Components/TransactionComponent.razor +++ b/src/aoWebWallet/Shared/Components/TransactionComponent.razor @@ -48,7 +48,7 @@ else if(isReceive) { + @BalanceHelper.FormatBalance(transfer.Quantity, tokenData?.Denomination ?? 0) - + @if(transfer.TokenTransferType == aoww.Services.Enums.TokenTransferType.Mint) { MINT @@ -66,9 +66,7 @@ @transfer.From -
-
@transfer.To diff --git a/src/aoWebWallet/wwwroot/css/app.css b/src/aoWebWallet/wwwroot/css/app.css index 67da73b..43a01cc 100644 --- a/src/aoWebWallet/wwwroot/css/app.css +++ b/src/aoWebWallet/wwwroot/css/app.css @@ -405,3 +405,15 @@ body { .mud-paper, .mud-tabs-toolbar { background-color: rgba(16,16,67,0.33) !important; } + +.wallet-item-background { + background-color: rgba(16,16,67,1) !important; +} + +.mud-primary-hover.wallet-item-background { + background-color: rgba(16,16,67,1) !important; +} + +.mud-list.mud-list-padding { + background-color: rgba(16,16,67,1) !important; +} From 98d0612b91256a897d5e9047b8e5728893196cce Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Sat, 11 May 2024 14:33:19 +0100 Subject: [PATCH 52/63] style consistency --- src/aoWebWallet/Pages/ActionPage.razor | 8 ++++---- src/aoWebWallet/Shared/ActionEditor.razor | 4 ++-- .../Shared/Components/ActionInputComponent.razor | 2 +- .../Shared/Components/ActionQuantityComponent.razor | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor index 5081f8a..f2e6430 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -36,7 +36,7 @@ @wallet.Address
-
+
@wallet.Name
@@ -59,7 +59,7 @@ @if (!readOnly && !string.IsNullOrEmpty(selectedWallet)) { - Preview + Preview @validation } else if (!started && !string.IsNullOrEmpty(selectedWallet) && string.IsNullOrEmpty(transactionService.LastTransaction.Data?.Id)) @@ -85,8 +85,8 @@ } - Cancel - Submit + Cancel + Submit } @if (transactionService.LastTransaction.DataLoader != null) diff --git a/src/aoWebWallet/Shared/ActionEditor.razor b/src/aoWebWallet/Shared/ActionEditor.razor index 6eccb07..2a9a111 100644 --- a/src/aoWebWallet/Shared/ActionEditor.razor +++ b/src/aoWebWallet/Shared/ActionEditor.razor @@ -1,14 +1,14 @@ @using aoWebWallet.Models - Target @AoAction.Target?.Value + Target @AoAction.Target?.Value @foreach (var ActionParam in AoAction.Filled) { - @ActionParam.Key @ActionParam.Value + @ActionParam.Key @ActionParam.Value } diff --git a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor index 50cc0c9..9c5833c 100644 --- a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor +++ b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor @@ -5,7 +5,7 @@ @if(ReadOnly) { - @ActionParam.Key @ActionParam.Value + @ActionParam.Key @ActionParam.Value } else { diff --git a/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor b/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor index 0a6456e..8706f56 100644 --- a/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor +++ b/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor @@ -36,7 +36,7 @@ - @Token?.TokenData?.Ticker + @*@Token?.TokenData?.Ticker*@ if (ActionParam.ParamType == ActionParamType.Balance) From bf00ab1345daa690f64c6cd8eea0331239f1ce17 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Sat, 11 May 2024 15:35:21 +0200 Subject: [PATCH 53/63] Bugfix dynamic loading of tokens based on transactions when viewing a wallet --- src/aoWebWallet/Services/TokenDataService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aoWebWallet/Services/TokenDataService.cs b/src/aoWebWallet/Services/TokenDataService.cs index bbea531..0759fdb 100644 --- a/src/aoWebWallet/Services/TokenDataService.cs +++ b/src/aoWebWallet/Services/TokenDataService.cs @@ -46,7 +46,7 @@ public async Task TryAddTokenIds(List allTokenIds) { await storageService.AddToken(tokenId, data, isUserAdded: false, null); - await LoadTokenList(); + await LoadTokenList(force: true); } }); From 4c5b19cf024d1030e38d0e7740c06384b4cf53bc Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Sat, 11 May 2024 16:07:47 +0200 Subject: [PATCH 54/63] Replace text in preview message --- src/aoWebWallet/Pages/ActionPage.razor | 15 +----- src/aoWebWallet/Services/BalanceHelper.cs | 5 +- .../Services/TransactionService.cs | 46 ++++++++++++++++++- 3 files changed, 48 insertions(+), 18 deletions(-) diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor index f2e6430..92b840f 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -75,8 +75,9 @@ var error = msg.Tags.Where(x => x.Name == "Error").Select(x => x.Value).FirstOrDefault(); Message for: @msg.Target - @RemoveColorCodes(msg.Data) + @msg.Data @error +
} @* @@ -118,18 +119,6 @@ private bool readOnly = false; private bool started = false; - static string RemoveColorCodes(string? input) - { - if (input == null) - return string.Empty; - - // Define a regular expression pattern to match color codes - string pattern = @"\x1B\[[0-9;]*[mK]"; - - // Replace color codes with an empty string - string output = System.Text.RegularExpressions.Regex.Replace(input, pattern, ""); - return output; - } private async void Preview() { diff --git a/src/aoWebWallet/Services/BalanceHelper.cs b/src/aoWebWallet/Services/BalanceHelper.cs index 57193c7..018dd39 100644 --- a/src/aoWebWallet/Services/BalanceHelper.cs +++ b/src/aoWebWallet/Services/BalanceHelper.cs @@ -1,7 +1,4 @@ -using Microsoft.AspNetCore.Components; -using System.Numerics; - -namespace aoWebWallet.Services +namespace aoWebWallet.Services { public class BalanceHelper { diff --git a/src/aoWebWallet/Services/TransactionService.cs b/src/aoWebWallet/Services/TransactionService.cs index dbf3556..73e599e 100644 --- a/src/aoWebWallet/Services/TransactionService.cs +++ b/src/aoWebWallet/Services/TransactionService.cs @@ -8,7 +8,9 @@ namespace aoWebWallet.Services { - public class TransactionService(ArweaveService arweaveService, ArweaveAO.AODataClient aODataClient) : ObservableObject + public class TransactionService(ArweaveService arweaveService, + ArweaveAO.AODataClient aODataClient, + TokenDataService tokenDataService) : ObservableObject { public void Reset() { @@ -48,9 +50,51 @@ public void Reset() var result = await aODataClient.DryRun(target, druRunRequest); + var balanceInputs = action.AllInputs.Where(x => x.ParamType == ActionParamType.Balance); + foreach (var balanceInput in balanceInputs) + { + if (balanceInput.Value == null) + continue; + + var token = tokenDataService.TokenList.Where(x => x.TokenId == balanceInput.Args.FirstOrDefault()).FirstOrDefault(); + + if(token?.TokenData?.Denomination != null) + { + string original1 = $"You received {balanceInput.Value}"; + string original2 = $"You transferred {balanceInput.Value}"; + + long longValue = long.Parse(balanceInput.Value); + var formatValue = BalanceHelper.FormatBalance(longValue, token.TokenData.Denomination.Value); + + string replace1 = $"You received {formatValue} {token.TokenData.Ticker}"; + string replace2 = $"You transferred {formatValue} {token.TokenData.Ticker}"; + + foreach(var msg in result?.Messages ?? new()) + { + msg.Data = RemoveColorCodes(msg.Data); + msg.Data = msg.Data.Replace(original1, replace1); + msg.Data = msg.Data.Replace(original2, replace2); + } + + } + } + return result; }, x => DryRunResult.Data = x); + static string RemoveColorCodes(string? input) + { + if (input == null) + return string.Empty; + + // Define a regular expression pattern to match color codes + string pattern = @"\x1B\[[0-9;]*[mK]"; + + // Replace color codes with an empty string + string output = System.Text.RegularExpressions.Regex.Replace(input, pattern, ""); + return output; + } + public async Task SendAction(Wallet wallet, Wallet? ownerWallet, AoAction action) { if (wallet.Source == WalletTypes.ArConnect) From 910895ee4d5c0b14e9910f7e35386abe4b4807a9 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Sat, 11 May 2024 18:05:02 +0200 Subject: [PATCH 55/63] dynamic token list --- src/aoWebWallet/Pages/ActionPage.razor.cs | 4 +- src/aoWebWallet/Pages/MvvmComponentBase.cs | 7 +- src/aoWebWallet/Pages/WalletDetail.razor.cs | 17 ++-- src/aoWebWallet/Services/StorageService.cs | 81 +++++++++++++++++-- src/aoWebWallet/Services/TokenDataService.cs | 2 +- .../ViewModels/BalanceDataViewModel.cs | 13 +++ .../ViewModels/TokenDetailViewModel.cs | 2 +- .../ViewModels/TransactionDetailViewModel.cs | 4 +- .../ViewModels/WalletDetailViewModel.cs | 51 +++++++++--- 9 files changed, 150 insertions(+), 31 deletions(-) diff --git a/src/aoWebWallet/Pages/ActionPage.razor.cs b/src/aoWebWallet/Pages/ActionPage.razor.cs index 2b06e38..a9e0dc0 100644 --- a/src/aoWebWallet/Pages/ActionPage.razor.cs +++ b/src/aoWebWallet/Pages/ActionPage.razor.cs @@ -62,8 +62,10 @@ private async void GetQueryStringValues() .AllInputs .Where(x => x.ParamType == ActionParamType.Balance || x.ParamType == ActionParamType.Quantity) .Select(x => x.Args.FirstOrDefault()) + .Where(x => x != null) .Distinct() - .ToList(); + .Select(x => x!) + .ToList(); await dataService.TryAddTokenIds(tokens); diff --git a/src/aoWebWallet/Pages/MvvmComponentBase.cs b/src/aoWebWallet/Pages/MvvmComponentBase.cs index 1fce93d..a78d574 100644 --- a/src/aoWebWallet/Pages/MvvmComponentBase.cs +++ b/src/aoWebWallet/Pages/MvvmComponentBase.cs @@ -1,9 +1,6 @@ -using aoWebWallet.ViewModels; -using CommunityToolkit.Mvvm.ComponentModel; -using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components; using System.Collections.Specialized; using System.ComponentModel; -using System.Diagnostics; using webvNext.DataLoader; namespace aoWebWallet.Pages @@ -51,6 +48,8 @@ internal void ObjWatch_PropertyChanged(object? sender, System.ComponentModel.Pro private void Obj_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { this.StateHasChanged(); + //Console.WriteLine("Obj Collection changed: " + sender?.ToString()); + } protected virtual Task LoadDataAsync() diff --git a/src/aoWebWallet/Pages/WalletDetail.razor.cs b/src/aoWebWallet/Pages/WalletDetail.razor.cs index a5b1ba9..4c2ab9c 100644 --- a/src/aoWebWallet/Pages/WalletDetail.razor.cs +++ b/src/aoWebWallet/Pages/WalletDetail.razor.cs @@ -10,8 +10,9 @@ public partial class WalletDetail : MvvmComponentBase protected override void OnInitialized() { //WatchObject(dataService.TokenList); - WatchObject(BindingContext.BalanceDataList); + //WatchObject(BindingContext.BalanceDataList); WatchObject(dataService.TokenDataLoader); + WatchCollection(dataService.TokenList); WatchCollection(BindingContext.BalanceDataList); WatchDataLoaderVM(MainViewModel.WalletList); @@ -19,10 +20,19 @@ protected override void OnInitialized() WatchDataLoaderVM(BindingContext.SelectedProcessData); dataService.TokenList.CollectionChanged += TokenList_CollectionChanged; + BindingContext.PropertyChanged += BindingContext_PropertyChanged; base.OnInitialized(); } + private void BindingContext_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(BindingContext.VisibleTokenList)) + { + BindingContext.TokenAddedRefresh(); + } + } + private void TokenList_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { BindingContext.TokenAddedRefresh(); @@ -45,11 +55,6 @@ protected override async Task LoadDataAsync() { dataService.LoadTokenList(); - //if (!string.IsNullOrEmpty(Address)) - //{ - // BindingContext.LoadBalanceDataList(Address); - //} - await base.LoadDataAsync(); } diff --git a/src/aoWebWallet/Services/StorageService.cs b/src/aoWebWallet/Services/StorageService.cs index 268c51d..d14f011 100644 --- a/src/aoWebWallet/Services/StorageService.cs +++ b/src/aoWebWallet/Services/StorageService.cs @@ -24,21 +24,90 @@ public async ValueTask> GetTokenIds() var result = await localStorage.GetItemAsync>(TOKEN_LIST_KEY); result = result ?? new(); - AddSystemToken(result, "Sa0iBLPNyJQrwpTTG-tWLQU-1QeUAJA73DdxGGiKoJc"); //CRED - AddSystemToken(result, "8p7ApPZxC_37M06QHVejCQrKsHbcJEerd3jWNkDUWPQ"); //BARK - AddSystemToken(result, "OT9qTE2467gcozb2g8R6D6N3nQS94ENcaAIJfUzHCww"); //TRUNK - AddSystemToken(result, "BUhZLMwQ6yZHguLtJYA5lLUa9LQzLXMXRfaq9FVcPJc"); //0rbit + AddSystemToken(result, "Sa0iBLPNyJQrwpTTG-tWLQU-1QeUAJA73DdxGGiKoJc", + new TokenData { + Denomination = 3, + Logo = "eIOOJiqtJucxvB4k8a-sEKcKpKTh9qQgOV3Au7jlGYc", + Name = "AOCRED", + Ticker = "testnet-AOCRED" + }); //CRED + + + AddSystemToken(result, "8p7ApPZxC_37M06QHVejCQrKsHbcJEerd3jWNkDUWPQ", + new TokenData + { + Denomination = 3, + Logo = "AdFxCN1eEPboxNpCNL23WZRNhIhiamOeS-TUwx_Nr3Q", + Name = "Bark", + Ticker = "BRKTST" + }); //BARK + + AddSystemToken(result, "OT9qTE2467gcozb2g8R6D6N3nQS94ENcaAIJfUzHCww", + new TokenData + { + Denomination = 3, + Logo = "4eTBOaxZSSyGbpKlHyilxNKhXbocuZdiMBYIORjS4f0", + Name = "TRUNK", + Ticker = "TRUNK" + }); //TRUNK + + AddSystemToken(result, "BUhZLMwQ6yZHguLtJYA5lLUa9LQzLXMXRfaq9FVcPJc", + new TokenData + { + Denomination = 12, + Logo = "nvx7DgTR8ws_k6VNCSe8vhwbZLx5jNbfNLJS0IKTTHA", + Name = "0rbit Points", + Ticker = "0RBIT" + }); //0rbit + + AddSystemToken(result, "PBg5TSJPQp9xgXGfjN27GA28Mg5bQmNEdXH2TXY4t-A", + new TokenData + { + Denomination = 12, + Logo = "VzvP24VxdNt1kf3E-EXxxrihaNBnXpEI-5ymwWddJRk", + Name = "Earth", + Ticker = "EARTH" + }); + + AddSystemToken(result, "KmGmJieqSRJpbW6JJUFQrH3sQPEG9F6DQETlXNt4GpM", + new TokenData + { + Denomination = 12, + Logo = "jayAVj1wgIcmin0bjG_DIGxq3_qANSp5EV7PcfUAvdQ", + Name = "Fire", + Ticker = "FIRE" + }); + + AddSystemToken(result, "2nfFJb8LIA69gwuLNcFQezSuw4CXPE4--U-j-7cxKOU", + new TokenData + { + Denomination = 12, + Logo = "7WqV5FWdDcbQzQNxNvfpr093yLHDtjeO7qPM9HQskWE", + Name = "Air", + Ticker = "AIR" + }); + + AddSystemToken(result, "NkXX3uZ4oGkQ3DPAWtjLb2sTA-yxmZKdlOlEHqMfWLQ", + new TokenData + { + Denomination = 12, + Logo = "ioI2_z6qkzGBrvZXbojjf6Q5uVZumx4rDDdHm-Jfyt0", + Name = "Lava", + Ticker = "FIRE-EARTH" + }); + + return result; } - private void AddSystemToken(List list, string tokenId) + private void AddSystemToken(List list, string tokenId, TokenData tokenData) { var existing = list.Where(x => x.TokenId == tokenId).FirstOrDefault(); if (existing != null) existing.IsSystemToken = true; else - list.Add(new Token { TokenId = tokenId, IsSystemToken = true }); + list.Add(new Token { TokenId = tokenId, IsSystemToken = true, TokenData = tokenData }); } public async ValueTask AddToken(string tokenId, TokenData data, bool isUserAdded, bool? isVisible) diff --git a/src/aoWebWallet/Services/TokenDataService.cs b/src/aoWebWallet/Services/TokenDataService.cs index 0759fdb..75a36fd 100644 --- a/src/aoWebWallet/Services/TokenDataService.cs +++ b/src/aoWebWallet/Services/TokenDataService.cs @@ -23,7 +23,7 @@ public TokenDataService(StorageService storageService, TokenClient tokenClient) this.tokenClient = tokenClient; } - public async Task TryAddTokenIds(List allTokenIds) + public async Task TryAddTokenIds(List allTokenIds) { allTokenIds = allTokenIds.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); diff --git a/src/aoWebWallet/ViewModels/BalanceDataViewModel.cs b/src/aoWebWallet/ViewModels/BalanceDataViewModel.cs index 14c599c..7d129ac 100644 --- a/src/aoWebWallet/ViewModels/BalanceDataViewModel.cs +++ b/src/aoWebWallet/ViewModels/BalanceDataViewModel.cs @@ -8,5 +8,18 @@ public class BalanceDataViewModel { public DataLoaderViewModel BalanceDataLoader { get; set; } = new DataLoaderViewModel(); public Token? Token { get; set; } + + //public void Load() + //{ + // BalanceDataLoader.DataLoader.LoadAsync(async () => + // { + // var balanceData = await tokenClient.GetBalance(token.TokenId, address); + // return balanceData; + // }, (x) => + // { + // balanceData.BalanceDataLoader.Data = x; + // TokenTransferList.ForcePropertyChanged(); + // }); + //} } } diff --git a/src/aoWebWallet/ViewModels/TokenDetailViewModel.cs b/src/aoWebWallet/ViewModels/TokenDetailViewModel.cs index 81246ab..12f1cb0 100644 --- a/src/aoWebWallet/ViewModels/TokenDetailViewModel.cs +++ b/src/aoWebWallet/ViewModels/TokenDetailViewModel.cs @@ -62,7 +62,7 @@ public Task LoadTokenTransferListForToken(string tokenId) => TokenTransferList.D TokenTransferList.Data = existing.Concat(tokenTransactions).OrderByDescending(x => x.Timestamp).ToList(); - var allTokenIds = tokenTransactions.Select(x => x.TokenId).Distinct().ToList(); + var allTokenIds = tokenTransactions.Where(x => x.TokenId != null).Select(x => x.TokenId!).Distinct().ToList(); dataService.TryAddTokenIds(allTokenIds); return TokenTransferList.Data; diff --git a/src/aoWebWallet/ViewModels/TransactionDetailViewModel.cs b/src/aoWebWallet/ViewModels/TransactionDetailViewModel.cs index 8a20885..f9e07c5 100644 --- a/src/aoWebWallet/ViewModels/TransactionDetailViewModel.cs +++ b/src/aoWebWallet/ViewModels/TransactionDetailViewModel.cs @@ -41,8 +41,8 @@ public Task LoadSelectedTokenTransfer(string txId) => SelectedTransaction.DataLo SelectedTransaction.Data = result; - if (result != null) - dataService.TryAddTokenIds(new List() { result.TokenId }); + if (result?.TokenId != null) + dataService.TryAddTokenIds(new List() { result.TokenId }); return result; }, (x) => SelectedTransaction.Data = x); diff --git a/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs b/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs index 359d7d6..4f16a02 100644 --- a/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs +++ b/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs @@ -28,6 +28,11 @@ public partial class WalletDetailViewModel : ObservableObject private List outgoingProcess = new(); + [ObservableProperty] + public List visibleTokenList = new(); + + + private string? selectedAddress = null; public bool CanLoadMoreTransactions { get; set; } = true; @@ -80,6 +85,11 @@ public WalletDetailViewModel(MainViewModel mainViewModel, public async Task Initialize(string address) { + VisibleTokenList = new(); + VisibleTokenList.Add("Sa0iBLPNyJQrwpTTG-tWLQU-1QeUAJA73DdxGGiKoJc"); + + ResetTokenTransferlist(); + selectedAddress = address; await SelectWallet(address); @@ -117,15 +127,20 @@ public async Task RefreshTokenTransferList() { if (selectedAddress != null) { - incoming = new(); - outgoing = new(); - outgoingProcess = new(); - TokenTransferList.Data = new(); + ResetTokenTransferlist(); await LoadTokenTransferList(selectedAddress); } } + private void ResetTokenTransferlist() + { + incoming = new(); + outgoing = new(); + outgoingProcess = new(); + TokenTransferList.Data = new(); + } + public async Task LoadMoreTransactions() { if (selectedAddress != null) @@ -226,9 +241,23 @@ public Task LoadTokenTransferList(string address) => TokenTransferList.DataLoade TokenTransferList.Data = existing.Concat(allNew).OrderByDescending(x => x.Timestamp).ToList(); - var allTokenIds = allNew.Select(x => x.TokenId).Distinct().ToList(); + List allTokenIds = allNew.Where(x => x.TokenId != null).Select(x => x.TokenId!).Distinct().ToList(); dataService.TryAddTokenIds(allTokenIds); + bool hasNew = false; + foreach(var token in allTokenIds) + { + var exist = VisibleTokenList.Where(x => x == token).Any(); + if (!exist) + { + VisibleTokenList.Add(token); + hasNew = true; + } + } + if (hasNew) + OnPropertyChanged(nameof(VisibleTokenList)); + + return TokenTransferList.Data; }); @@ -237,15 +266,13 @@ public Task LoadTokenTransferList(string address) => TokenTransferList.DataLoade return transactions.Select(x => x.Cursor).LastOrDefault(); } - public async Task LoadBalanceDataList(string address, bool onlyNew = false) + private async Task LoadBalanceDataList(string address, bool onlyNew = false) { //First clear if (!onlyNew) BalanceDataList.Clear(); - var result = new List(); - - foreach (var token in dataService.TokenList.Where(x => x.IsVisible)) + foreach (var token in dataService.TokenList.Where(x => VisibleTokenList.Contains(x.TokenId) && x.IsVisible)) { if (onlyNew) { @@ -254,6 +281,9 @@ public async Task LoadBalanceDataList(string address, bool onlyNew = false) } var balanceData = new BalanceDataViewModel { Token = token }; + BalanceDataList.Add(balanceData); + + await Task.Delay(60); balanceData.BalanceDataLoader.DataLoader.LoadAsync(async () => { @@ -265,7 +295,8 @@ public async Task LoadBalanceDataList(string address, bool onlyNew = false) TokenTransferList.ForcePropertyChanged(); }); - BalanceDataList.Add(balanceData); + + } } From c321a452eb59851bc02169b371e1a83b1573769a Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Sun, 12 May 2024 11:57:02 +0200 Subject: [PATCH 56/63] Moved balances to BalanceDataComponent.razor --- src/aoWebWallet/Pages/WalletDetail.razor | 55 +-------------- .../Shared/BalanceDataComponent.razor | 70 +++++++++++++++++++ .../ViewModels/WalletDetailViewModel.cs | 2 +- 3 files changed, 73 insertions(+), 54 deletions(-) create mode 100644 src/aoWebWallet/Shared/BalanceDataComponent.razor diff --git a/src/aoWebWallet/Pages/WalletDetail.razor b/src/aoWebWallet/Pages/WalletDetail.razor index 6a0a919..3cff8fa 100644 --- a/src/aoWebWallet/Pages/WalletDetail.razor +++ b/src/aoWebWallet/Pages/WalletDetail.razor @@ -123,43 +123,7 @@ { @foreach (var balance in BindingContext.BalanceDataList) { - if (balance.Token?.TokenData == null) - continue; - - - -
- - - @balance.Token.TokenData?.Name - @balance.Token.TokenData?.Ticker - -
- - - - @if (balance.BalanceDataLoader.Data != null) - { - @BalanceHelper.FormatBalance(balance.BalanceDataLoader.Data?.Balance, balance.Token?.TokenData?.Denomination ?? 0) - } - - - - - - Receive - - - @if ((BindingContext.SelectedWallet?.CanSend ?? false)) - { - var hasBalance = balance.BalanceDataLoader.Data?.Balance ?? 0; - - Send - - } - -
-
+ } } @@ -251,22 +215,7 @@ StateHasChanged(); } - private void Receive(BalanceDataViewModel? balanceDataVM) - { - var parameters = new DialogParameters { { x => x.SelectedBalanceDataVM, balanceDataVM } }; - var options = new DialogOptions { CloseOnEscapeKey = true }; - DialogService.Show("Receive Token", parameters, options); - } - - private void Send(BalanceDataViewModel? balanceDataVM) - { - if(balanceDataVM?.Token == null) - return; - - var aoAction = AoAction.CreateForTokenTransaction(balanceDataVM.Token.TokenId); - - NavigationManager.NavigateTo($"/action?{aoAction.ToQueryString()}"); - } + private async Task RefreshBalances() { diff --git a/src/aoWebWallet/Shared/BalanceDataComponent.razor b/src/aoWebWallet/Shared/BalanceDataComponent.razor new file mode 100644 index 0000000..b8b7801 --- /dev/null +++ b/src/aoWebWallet/Shared/BalanceDataComponent.razor @@ -0,0 +1,70 @@ +@using aoWebWallet.Models +@inject GatewayUrlHelper UrlHelper +@inject IDialogService DialogService +@inject NavigationManager NavigationManager + +@if (BalanceDataVM?.Token?.TokenData == null) +{ + return; +} + + + +
+ + + @BalanceDataVM.Token.TokenData?.Name + @BalanceDataVM.Token.TokenData?.Ticker + +
+ + + + @if (BalanceDataVM.BalanceDataLoader.Data != null) + { + @BalanceHelper.FormatBalance(BalanceDataVM.BalanceDataLoader.Data?.Balance, BalanceDataVM.Token?.TokenData?.Denomination ?? 0) + } + + + + + + Receive + + + @if (CanSend) + { + var hasBalance = BalanceDataVM.BalanceDataLoader.Data?.Balance ?? 0; + + Send + + } + +
+
+ +@code{ + [Parameter] + public BalanceDataViewModel? BalanceDataVM { get; set; } + + [Parameter] + public bool CanSend { get; set; } + + private void Receive(BalanceDataViewModel? balanceDataVM) + { + var parameters = new DialogParameters { { x => x.SelectedBalanceDataVM, balanceDataVM } }; + var options = new DialogOptions { CloseOnEscapeKey = true }; + DialogService.Show("Receive Token", parameters, options); + } + + private void Send(BalanceDataViewModel? balanceDataVM) + { + if (balanceDataVM?.Token == null) + return; + + var aoAction = AoAction.CreateForTokenTransaction(balanceDataVM.Token.TokenId); + + NavigationManager.NavigateTo($"/action?{aoAction.ToQueryString()}"); + } +} + \ No newline at end of file diff --git a/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs b/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs index 4f16a02..9b0ed97 100644 --- a/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs +++ b/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs @@ -283,7 +283,7 @@ private async Task LoadBalanceDataList(string address, bool onlyNew = false) var balanceData = new BalanceDataViewModel { Token = token }; BalanceDataList.Add(balanceData); - await Task.Delay(60); + await Task.Delay(30); balanceData.BalanceDataLoader.DataLoader.LoadAsync(async () => { From db3c2468866631efc5c85940767a76bfdcd11ba1 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Sun, 12 May 2024 12:09:27 +0200 Subject: [PATCH 57/63] Added unit test for build in token data --- src/aoWebWallet.Tests/StorageServiceTests.cs | 37 +++++++++++++++++++ .../aoWebWallet.Tests.csproj | 27 ++++++++++++++ src/aoWebWallet.sln | 7 ++++ src/aoWebWallet/Services/StorageService.cs | 33 +++++++++++------ 4 files changed, 92 insertions(+), 12 deletions(-) create mode 100644 src/aoWebWallet.Tests/StorageServiceTests.cs create mode 100644 src/aoWebWallet.Tests/aoWebWallet.Tests.csproj diff --git a/src/aoWebWallet.Tests/StorageServiceTests.cs b/src/aoWebWallet.Tests/StorageServiceTests.cs new file mode 100644 index 0000000..280d839 --- /dev/null +++ b/src/aoWebWallet.Tests/StorageServiceTests.cs @@ -0,0 +1,37 @@ +using aoWebWallet.Models; +using aoWebWallet.Services; +using ArweaveAO; + +namespace aoWebWallet.Tests +{ + [TestClass] + public class StorageServiceTests + { + [TestMethod] + public async Task TestBuildInTokenData() + { + List result = new(); + + StorageService.AddSystemTokens(result); + + TokenClient tokenClient = new TokenClient(new HttpClient()); + + foreach(var token in result) + { + //Get live data + var data = await tokenClient.GetTokenMetaData(token.TokenId); + + Assert.IsNotNull(token.TokenData); + Assert.IsNotNull(data); + + Assert.AreEqual(token.TokenId, data.TokenId); + Assert.AreEqual(token.TokenData.TokenId, data.TokenId); + Assert.AreEqual(token.TokenData.Name, data.Name); + Assert.AreEqual(token.TokenData.Ticker, data.Ticker); + Assert.AreEqual(token.TokenData.Denomination, data.Denomination); + Assert.AreEqual(token.TokenData.Logo, data.Logo); + + } + } + } +} \ No newline at end of file diff --git a/src/aoWebWallet.Tests/aoWebWallet.Tests.csproj b/src/aoWebWallet.Tests/aoWebWallet.Tests.csproj new file mode 100644 index 0000000..9cc222a --- /dev/null +++ b/src/aoWebWallet.Tests/aoWebWallet.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/src/aoWebWallet.sln b/src/aoWebWallet.sln index 8de578f..b9bea89 100644 --- a/src/aoWebWallet.sln +++ b/src/aoWebWallet.sln @@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{89AC47DF EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "aoww.Services.Tests", "aoww.Services.Tests\aoww.Services.Tests.csproj", "{322F4807-05CF-431D-B400-7420E1B29936}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "aoWebWallet.Tests", "aoWebWallet.Tests\aoWebWallet.Tests.csproj", "{12E9E40E-96D1-4501-A9A4-EBE4D4F43D8D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -37,6 +39,10 @@ Global {322F4807-05CF-431D-B400-7420E1B29936}.Debug|Any CPU.Build.0 = Debug|Any CPU {322F4807-05CF-431D-B400-7420E1B29936}.Release|Any CPU.ActiveCfg = Release|Any CPU {322F4807-05CF-431D-B400-7420E1B29936}.Release|Any CPU.Build.0 = Release|Any CPU + {12E9E40E-96D1-4501-A9A4-EBE4D4F43D8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {12E9E40E-96D1-4501-A9A4-EBE4D4F43D8D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {12E9E40E-96D1-4501-A9A4-EBE4D4F43D8D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {12E9E40E-96D1-4501-A9A4-EBE4D4F43D8D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -45,6 +51,7 @@ Global {17CA4374-64D0-4618-852F-8A76D0A57166} = {06E5BC39-764A-48B9-B4F9-F48387A2C965} {178C3213-D574-4B39-A2DA-1FB1D2806242} = {06E5BC39-764A-48B9-B4F9-F48387A2C965} {322F4807-05CF-431D-B400-7420E1B29936} = {89AC47DF-65AD-4870-AA1D-74ABF1F3D8FE} + {12E9E40E-96D1-4501-A9A4-EBE4D4F43D8D} = {89AC47DF-65AD-4870-AA1D-74ABF1F3D8FE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {432E3F8E-53FF-4D9C-869D-48449BD3B8B4} diff --git a/src/aoWebWallet/Services/StorageService.cs b/src/aoWebWallet/Services/StorageService.cs index d14f011..d690f67 100644 --- a/src/aoWebWallet/Services/StorageService.cs +++ b/src/aoWebWallet/Services/StorageService.cs @@ -1,9 +1,6 @@ using aoWebWallet.Models; -using aoWebWallet.Pages; using ArweaveAO.Models.Token; using Blazored.LocalStorage; -using System.Reflection.Metadata; -using static MudBlazor.CategoryTypes; namespace aoWebWallet.Services { @@ -24,9 +21,18 @@ public async ValueTask> GetTokenIds() var result = await localStorage.GetItemAsync>(TOKEN_LIST_KEY); result = result ?? new(); - AddSystemToken(result, "Sa0iBLPNyJQrwpTTG-tWLQU-1QeUAJA73DdxGGiKoJc", - new TokenData { - Denomination = 3, + AddSystemTokens(result); + + return result; + } + + public static void AddSystemTokens(List result) + { + AddSystemToken(result, "Sa0iBLPNyJQrwpTTG-tWLQU-1QeUAJA73DdxGGiKoJc", + new TokenData + { + TokenId = "Sa0iBLPNyJQrwpTTG-tWLQU-1QeUAJA73DdxGGiKoJc", + Denomination = 3, Logo = "eIOOJiqtJucxvB4k8a-sEKcKpKTh9qQgOV3Au7jlGYc", Name = "AOCRED", Ticker = "testnet-AOCRED" @@ -36,6 +42,7 @@ public async ValueTask> GetTokenIds() AddSystemToken(result, "8p7ApPZxC_37M06QHVejCQrKsHbcJEerd3jWNkDUWPQ", new TokenData { + TokenId = "8p7ApPZxC_37M06QHVejCQrKsHbcJEerd3jWNkDUWPQ", Denomination = 3, Logo = "AdFxCN1eEPboxNpCNL23WZRNhIhiamOeS-TUwx_Nr3Q", Name = "Bark", @@ -45,6 +52,7 @@ public async ValueTask> GetTokenIds() AddSystemToken(result, "OT9qTE2467gcozb2g8R6D6N3nQS94ENcaAIJfUzHCww", new TokenData { + TokenId = "OT9qTE2467gcozb2g8R6D6N3nQS94ENcaAIJfUzHCww", Denomination = 3, Logo = "4eTBOaxZSSyGbpKlHyilxNKhXbocuZdiMBYIORjS4f0", Name = "TRUNK", @@ -54,15 +62,17 @@ public async ValueTask> GetTokenIds() AddSystemToken(result, "BUhZLMwQ6yZHguLtJYA5lLUa9LQzLXMXRfaq9FVcPJc", new TokenData { + TokenId = "BUhZLMwQ6yZHguLtJYA5lLUa9LQzLXMXRfaq9FVcPJc", Denomination = 12, Logo = "nvx7DgTR8ws_k6VNCSe8vhwbZLx5jNbfNLJS0IKTTHA", Name = "0rbit Points", - Ticker = "0RBIT" + Ticker = "0RBT" }); //0rbit AddSystemToken(result, "PBg5TSJPQp9xgXGfjN27GA28Mg5bQmNEdXH2TXY4t-A", new TokenData { + TokenId = "PBg5TSJPQp9xgXGfjN27GA28Mg5bQmNEdXH2TXY4t-A", Denomination = 12, Logo = "VzvP24VxdNt1kf3E-EXxxrihaNBnXpEI-5ymwWddJRk", Name = "Earth", @@ -72,6 +82,7 @@ public async ValueTask> GetTokenIds() AddSystemToken(result, "KmGmJieqSRJpbW6JJUFQrH3sQPEG9F6DQETlXNt4GpM", new TokenData { + TokenId = "KmGmJieqSRJpbW6JJUFQrH3sQPEG9F6DQETlXNt4GpM", Denomination = 12, Logo = "jayAVj1wgIcmin0bjG_DIGxq3_qANSp5EV7PcfUAvdQ", Name = "Fire", @@ -81,6 +92,7 @@ public async ValueTask> GetTokenIds() AddSystemToken(result, "2nfFJb8LIA69gwuLNcFQezSuw4CXPE4--U-j-7cxKOU", new TokenData { + TokenId = "2nfFJb8LIA69gwuLNcFQezSuw4CXPE4--U-j-7cxKOU", Denomination = 12, Logo = "7WqV5FWdDcbQzQNxNvfpr093yLHDtjeO7qPM9HQskWE", Name = "Air", @@ -90,18 +102,15 @@ public async ValueTask> GetTokenIds() AddSystemToken(result, "NkXX3uZ4oGkQ3DPAWtjLb2sTA-yxmZKdlOlEHqMfWLQ", new TokenData { + TokenId = "NkXX3uZ4oGkQ3DPAWtjLb2sTA-yxmZKdlOlEHqMfWLQ", Denomination = 12, Logo = "ioI2_z6qkzGBrvZXbojjf6Q5uVZumx4rDDdHm-Jfyt0", Name = "Lava", Ticker = "FIRE-EARTH" }); - - - - return result; } - private void AddSystemToken(List list, string tokenId, TokenData tokenData) + private static void AddSystemToken(List list, string tokenId, TokenData tokenData) { var existing = list.Where(x => x.TokenId == tokenId).FirstOrDefault(); if (existing != null) From 8f3153487181ee94700ad87a5c4ee880a108f7ec Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Sun, 12 May 2024 12:21:19 +0200 Subject: [PATCH 58/63] update test dependencies --- src/aoWebWallet.Tests/aoWebWallet.Tests.csproj | 11 +++++++---- src/aoWebWallet/ViewModels/WalletDetailViewModel.cs | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/aoWebWallet.Tests/aoWebWallet.Tests.csproj b/src/aoWebWallet.Tests/aoWebWallet.Tests.csproj index 9cc222a..02f6a5e 100644 --- a/src/aoWebWallet.Tests/aoWebWallet.Tests.csproj +++ b/src/aoWebWallet.Tests/aoWebWallet.Tests.csproj @@ -10,10 +10,13 @@ - - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs b/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs index 9b0ed97..64b386c 100644 --- a/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs +++ b/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs @@ -283,7 +283,7 @@ private async Task LoadBalanceDataList(string address, bool onlyNew = false) var balanceData = new BalanceDataViewModel { Token = token }; BalanceDataList.Add(balanceData); - await Task.Delay(30); + await Task.Delay(50); balanceData.BalanceDataLoader.DataLoader.LoadAsync(async () => { From 4391f829f71b27d861be3f6f10df30aa91a0d042 Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Sun, 12 May 2024 15:01:44 +0100 Subject: [PATCH 59/63] background color consistency across ui elements --- src/aoWebWallet/wwwroot/css/app.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aoWebWallet/wwwroot/css/app.css b/src/aoWebWallet/wwwroot/css/app.css index 43a01cc..d89c204 100644 --- a/src/aoWebWallet/wwwroot/css/app.css +++ b/src/aoWebWallet/wwwroot/css/app.css @@ -414,6 +414,6 @@ body { background-color: rgba(16,16,67,1) !important; } -.mud-list.mud-list-padding { +.mud-list.mud-list-padding, .mud-dialog { background-color: rgba(16,16,67,1) !important; } From 3b48522eee52f54f4ff5e7734728a965f1c61685 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Tue, 14 May 2024 12:54:46 +0200 Subject: [PATCH 60/63] Change gateways --- src/aoWebWallet.Tests/StorageServiceTests.cs | 4 +++- src/aoWebWallet/Program.cs | 2 ++ src/aoWebWallet/Shared/AddWalletComponent.razor | 6 +++--- src/aoWebWallet/ViewModels/MainViewModel.cs | 9 ++++++++- src/aoWebWallet/aoWebWallet.csproj | 4 ++-- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/aoWebWallet.Tests/StorageServiceTests.cs b/src/aoWebWallet.Tests/StorageServiceTests.cs index 280d839..49955d9 100644 --- a/src/aoWebWallet.Tests/StorageServiceTests.cs +++ b/src/aoWebWallet.Tests/StorageServiceTests.cs @@ -1,6 +1,8 @@ using aoWebWallet.Models; using aoWebWallet.Services; using ArweaveAO; +using ArweaveAO.Models; +using Microsoft.Extensions.Options; namespace aoWebWallet.Tests { @@ -14,7 +16,7 @@ public async Task TestBuildInTokenData() StorageService.AddSystemTokens(result); - TokenClient tokenClient = new TokenClient(new HttpClient()); + TokenClient tokenClient = new TokenClient(Options.Create(new ArweaveConfig()), new HttpClient()); foreach(var token in result) { diff --git a/src/aoWebWallet/Program.cs b/src/aoWebWallet/Program.cs index 392bdcc..ee61b05 100644 --- a/src/aoWebWallet/Program.cs +++ b/src/aoWebWallet/Program.cs @@ -14,6 +14,7 @@ using aoww.Services; using aoww.Services.Models; using aoWebWallet.Models; +using ArweaveAO.Models; namespace aoWebWallet { @@ -119,6 +120,7 @@ private static void ConfigureServices(IServiceCollection services, string baseAd //Options services.AddSingleton(new GraphqlConfig()); services.AddSingleton(new GatewayConfig()); + services.AddSingleton(new ArweaveConfig()); } } } diff --git a/src/aoWebWallet/Shared/AddWalletComponent.razor b/src/aoWebWallet/Shared/AddWalletComponent.razor index 84bd04f..fe033f1 100644 --- a/src/aoWebWallet/Shared/AddWalletComponent.razor +++ b/src/aoWebWallet/Shared/AddWalletComponent.razor @@ -1,6 +1,6 @@  - +
@@ -10,7 +10,7 @@ - +
@@ -20,7 +20,7 @@ - +
diff --git a/src/aoWebWallet/ViewModels/MainViewModel.cs b/src/aoWebWallet/ViewModels/MainViewModel.cs index 105cb3c..9d974fe 100644 --- a/src/aoWebWallet/ViewModels/MainViewModel.cs +++ b/src/aoWebWallet/ViewModels/MainViewModel.cs @@ -5,6 +5,7 @@ using aoww.Services; using aoww.Services.Models; using ArweaveAO; +using ArweaveAO.Models; using ArweaveAO.Models.Token; using ArweaveBlazor; using ClipLazor.Components; @@ -28,6 +29,7 @@ public partial class MainViewModel : ObservableRecipient private readonly ArweaveService arweaveService; private readonly GraphqlClient graphqlClient; private readonly MemoryDataCache memoryDataCache; + private readonly ArweaveConfig arweaveConfig; private readonly GatewayConfig gatewayConfig; private readonly GraphqlConfig graphqlConfig; @@ -55,13 +57,15 @@ public MainViewModel(TokenDataService dataService, GraphqlClient graphqlClient, MemoryDataCache memoryDataCache, IOptions graphqlConfig, - IOptions gatewayConfig) : base() + IOptions gatewayConfig, + IOptions arweaveConfig) : base() { this.dataService = dataService; this.storageService = storageService; this.arweaveService = arweaveService; this.graphqlClient = graphqlClient; this.memoryDataCache = memoryDataCache; + this.arweaveConfig = arweaveConfig.Value; this.gatewayConfig = gatewayConfig.Value; this.graphqlConfig = graphqlConfig.Value; } @@ -197,6 +201,9 @@ private void UpdateUserSettings(UserSettings userSettings) { graphqlConfig.ApiUrl = userSettings.GraphqlUrl; gatewayConfig.GatewayUrl = userSettings.GatewayUrl; + arweaveConfig.ComputeUnitUrl = userSettings.ComputeUnitUrl; + + arweaveService.SetConnection(userSettings.GatewayUrl, userSettings.GraphqlUrl, userSettings.MessengerUnitUrl, userSettings.ComputeUnitUrl); } public async Task DisconnectArWallet() diff --git a/src/aoWebWallet/aoWebWallet.csproj b/src/aoWebWallet/aoWebWallet.csproj index db18bf9..6668c35 100644 --- a/src/aoWebWallet/aoWebWallet.csproj +++ b/src/aoWebWallet/aoWebWallet.csproj @@ -12,8 +12,8 @@ - - + + From 3db77e21dc1395714338d9f9b2085d6ab175c359 Mon Sep 17 00:00:00 2001 From: Nuno Lopes Date: Tue, 14 May 2024 15:44:50 +0100 Subject: [PATCH 61/63] optimise responsiveness on create wallets page --- src/aoWebWallet/Shared/AddUploadWalletComponent.razor | 2 +- src/aoWebWallet/Shared/AddWalletComponent.razor | 6 +++--- src/aoWebWallet/wwwroot/css/app.css | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/aoWebWallet/Shared/AddUploadWalletComponent.razor b/src/aoWebWallet/Shared/AddUploadWalletComponent.razor index 8d4f7ba..3684acf 100644 --- a/src/aoWebWallet/Shared/AddUploadWalletComponent.razor +++ b/src/aoWebWallet/Shared/AddUploadWalletComponent.razor @@ -20,7 +20,7 @@ @ondragleave="@ClearDragClass" @ondragend="@ClearDragClass"> - diff --git a/src/aoWebWallet/Shared/AddWalletComponent.razor b/src/aoWebWallet/Shared/AddWalletComponent.razor index fe033f1..e73e3ea 100644 --- a/src/aoWebWallet/Shared/AddWalletComponent.razor +++ b/src/aoWebWallet/Shared/AddWalletComponent.razor @@ -1,6 +1,6 @@  - +
@@ -10,7 +10,7 @@ - +
@@ -20,7 +20,7 @@ - +
diff --git a/src/aoWebWallet/wwwroot/css/app.css b/src/aoWebWallet/wwwroot/css/app.css index d89c204..9de43bb 100644 --- a/src/aoWebWallet/wwwroot/css/app.css +++ b/src/aoWebWallet/wwwroot/css/app.css @@ -281,14 +281,15 @@ button.mud-button-root.mud-icon-button.mud-ripple.mud-ripple-icon.copy-clipboard .mud-paper.first-wallet { background: rgb(48,20,82); background: linear-gradient(180deg, rgba(48,20,82,1) 0%, rgba(69,20,84,1) 100%); - height: 333px; + height: auto; + min-height: 333px; } .mud-paper.first-wallet-upload { background: rgb(48,20,82); background: linear-gradient(180deg, rgba(48,20,82,1) 0%, rgba(69,20,84,1) 100%); - height: 444px; + height: auto; } From c5e869f100f8467750d5311cd9f868378bf77bd4 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Wed, 15 May 2024 10:59:29 +0200 Subject: [PATCH 62/63] Fix edit wallet name --- src/aoWebWallet/Services/StorageService.cs | 2 +- .../Shared/EditWalletComponent.razor | 25 +++++++++++++++---- src/aoWebWallet/aoWebWallet.csproj | 4 +-- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/aoWebWallet/Services/StorageService.cs b/src/aoWebWallet/Services/StorageService.cs index d690f67..95463e8 100644 --- a/src/aoWebWallet/Services/StorageService.cs +++ b/src/aoWebWallet/Services/StorageService.cs @@ -176,7 +176,7 @@ public async ValueTask SaveWallet (Wallet wallet) if(existing != null) list.Remove(existing); - list.Add(wallet); + list.Insert(0,wallet); await SaveWalletList(list); } diff --git a/src/aoWebWallet/Shared/EditWalletComponent.razor b/src/aoWebWallet/Shared/EditWalletComponent.razor index b74f1c7..c8c070f 100644 --- a/src/aoWebWallet/Shared/EditWalletComponent.razor +++ b/src/aoWebWallet/Shared/EditWalletComponent.razor @@ -4,9 +4,9 @@ - + @Progress - +
@@ -25,10 +25,20 @@ [CascadingParameter] MudDialogInstance? MudDialog { get; set; } public string? Progress { get; set; } + public string Address { get; set; } = string.Empty; + public string? Name { get; set; } - public bool IsReadOnly => !Wallet.IsReadOnly; + public bool IsReadOnly => !string.IsNullOrEmpty(Wallet.Address); + protected override void OnParametersSet() + { + Address = Wallet.Address; + Name = Wallet.Name; + + base.OnParametersSet(); + } + protected override void OnInitialized() { base.OnInitialized(); @@ -44,16 +54,21 @@ // return false; // } - if (string.IsNullOrWhiteSpace(Wallet.Address) || Wallet.Address.Length != 43) + if (string.IsNullOrWhiteSpace(Address) || Address.Length != 43) { Progress = "Length must be 43 characters."; StateHasChanged(); return false; } + Wallet.Name = Name; + Wallet.Address = Address; + await BindingContext.SaveWallet(Wallet); - Snackbar.Add($"Address saved ({Wallet.Address})", Severity.Info); + StateHasChanged(); + + Snackbar.Add($"Address saved {Wallet.Name} ({Wallet.Address})", Severity.Info); MudDialog?.Close(true); diff --git a/src/aoWebWallet/aoWebWallet.csproj b/src/aoWebWallet/aoWebWallet.csproj index 6668c35..96bd239 100644 --- a/src/aoWebWallet/aoWebWallet.csproj +++ b/src/aoWebWallet/aoWebWallet.csproj @@ -19,8 +19,8 @@ - - + + From 538d70e9ce85def910c1429d293695e59571a852 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Wed, 15 May 2024 11:32:56 +0200 Subject: [PATCH 63/63] add token on wallet details page fix --- src/aoWebWallet/Pages/WalletDetail.razor | 2 +- src/aoWebWallet/Pages/WalletDetail.razor.cs | 4 +- .../Shared/AddTokenToWalletDialog.razor | 64 +++++++++++++++++++ src/aoWebWallet/aoWebWallet.csproj | 2 +- 4 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 src/aoWebWallet/Shared/AddTokenToWalletDialog.razor diff --git a/src/aoWebWallet/Pages/WalletDetail.razor b/src/aoWebWallet/Pages/WalletDetail.razor index 3cff8fa..714219a 100644 --- a/src/aoWebWallet/Pages/WalletDetail.razor +++ b/src/aoWebWallet/Pages/WalletDetail.razor @@ -202,7 +202,7 @@ private void OpenAddTokenDialog() { var options = new DialogOptions { CloseOnEscapeKey = true }; - DialogService.Show("Add Token", options); + DialogService.Show("Add Token", options); } private async void EditWallet(Wallet wallet) diff --git a/src/aoWebWallet/Pages/WalletDetail.razor.cs b/src/aoWebWallet/Pages/WalletDetail.razor.cs index 4c2ab9c..86a72f1 100644 --- a/src/aoWebWallet/Pages/WalletDetail.razor.cs +++ b/src/aoWebWallet/Pages/WalletDetail.razor.cs @@ -33,9 +33,9 @@ private void BindingContext_PropertyChanged(object? sender, System.ComponentMode } } - private void TokenList_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + private async void TokenList_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { - BindingContext.TokenAddedRefresh(); + await BindingContext.TokenAddedRefresh(); } protected override async Task OnParametersSetAsync() diff --git a/src/aoWebWallet/Shared/AddTokenToWalletDialog.razor b/src/aoWebWallet/Shared/AddTokenToWalletDialog.razor new file mode 100644 index 0000000..9facebe --- /dev/null +++ b/src/aoWebWallet/Shared/AddTokenToWalletDialog.razor @@ -0,0 +1,64 @@ +@inject TokenDataService dataService +@inherits MvvmComponentBase +@inject ISnackbar Snackbar + + + + Add a token to view your balance. Provide a process-id that implements the token standard. + + + + @Progress + + + Cancel + Ok + + +@code { + [CascadingParameter] MudDialogInstance MudDialog { get; set; } = default!; + + public string? TokenId { get; set; } + public string? Progress { get; set; } + + public async Task Submit() + { + if (string.IsNullOrWhiteSpace(TokenId)) + { + Progress = "Input the process-id of an ao-process implementing the token standard."; + return; + } + if(TokenId.Length != 43) + { + Progress = "Length must be 43 characters."; + return; + } + + Progress = "Checking metadata..."; + try + { + var token = await dataService.LoadTokenAsync(TokenId); + var data = token.TokenData; + if (data != null) + { + BindingContext.VisibleTokenList.Add(TokenId); + BindingContext.TokenAddedRefresh(); + + Snackbar.Add($"Token added ({data.Name})", Severity.Info); + + MudDialog.Close(DialogResult.Ok(true)); + } + else + { + Progress = "Could not find token metadata."; + } + } + catch + { + Progress = "Could not find token metadata."; + } + } + + //void Submit() => MudDialog.Close(DialogResult.Ok(true)); + void Cancel() => MudDialog.Cancel(); +} \ No newline at end of file diff --git a/src/aoWebWallet/aoWebWallet.csproj b/src/aoWebWallet/aoWebWallet.csproj index 96bd239..5a32973 100644 --- a/src/aoWebWallet/aoWebWallet.csproj +++ b/src/aoWebWallet/aoWebWallet.csproj @@ -13,7 +13,7 @@ - +