diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2c69dcf..6db6156 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: env: APP_VERSION: ${{ env.GITHUB_REF_SLUG }}-$(git rev-parse --short "$GITHUB_SHA") - name: Setup dotnet - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x - name: Build with dotnet diff --git a/README.md b/README.md index 65ba118..ae54c5d 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ ao Web Wallet is a wallet for the [ao network](https://ao.arweave.dev) running o Features: - Add ArConnect Wallets -- Add read-only wallets +- Create new wallets and import wallet.json files +- View your ao processes +- Send tokens for your owned ao processes - View balances of a wallet - View all transactions for a wallet -- Send tokens for ArConnect Wallets -- Receive instructions for all wallets - Add custom tokens - Dark and Light theme diff --git a/assets/aoww-logo-3840.png b/assets/aoww-logo-3840.png new file mode 100644 index 0000000..328b3eb Binary files /dev/null and b/assets/aoww-logo-3840.png differ diff --git a/assets/aowwdotnet.png b/assets/aowwdotnet.png new file mode 100644 index 0000000..6dcec72 Binary files /dev/null and b/assets/aowwdotnet.png differ diff --git a/assets/spiral.png b/assets/spiral.png new file mode 100644 index 0000000..62692fb Binary files /dev/null and b/assets/spiral.png differ diff --git a/assets/spiral2.png b/assets/spiral2.png new file mode 100644 index 0000000..e8181c1 Binary files /dev/null and b/assets/spiral2.png differ diff --git a/src/aoWebWallet.Tests/StorageServiceTests.cs b/src/aoWebWallet.Tests/StorageServiceTests.cs new file mode 100644 index 0000000..49955d9 --- /dev/null +++ b/src/aoWebWallet.Tests/StorageServiceTests.cs @@ -0,0 +1,39 @@ +using aoWebWallet.Models; +using aoWebWallet.Services; +using ArweaveAO; +using ArweaveAO.Models; +using Microsoft.Extensions.Options; + +namespace aoWebWallet.Tests +{ + [TestClass] + public class StorageServiceTests + { + [TestMethod] + public async Task TestBuildInTokenData() + { + List result = new(); + + StorageService.AddSystemTokens(result); + + TokenClient tokenClient = new TokenClient(Options.Create(new ArweaveConfig()), 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..efbd122 --- /dev/null +++ b/src/aoWebWallet.Tests/aoWebWallet.Tests.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/src/aoWebWallet.sln b/src/aoWebWallet.sln index c0ab0cd..3cac0de 100644 --- a/src/aoWebWallet.sln +++ b/src/aoWebWallet.sln @@ -9,6 +9,14 @@ 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("{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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "aoww.Services.Tests", "aoww.Services.Tests\aoww.Services.Tests.csproj", "{322F4807-05CF-431D-B400-7420E1B29936}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "aoWebWallet.Tests", "aoWebWallet.Tests\aoWebWallet.Tests.csproj", "{12E9E40E-96D1-4501-A9A4-EBE4D4F43D8D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -23,12 +31,27 @@ Global {17CA4374-64D0-4618-852F-8A76D0A57166}.Debug|Any CPU.Build.0 = Debug|Any CPU {17CA4374-64D0-4618-852F-8A76D0A57166}.Release|Any CPU.ActiveCfg = Release|Any CPU {17CA4374-64D0-4618-852F-8A76D0A57166}.Release|Any CPU.Build.0 = Release|Any CPU + {178C3213-D574-4B39-A2DA-1FB1D2806242}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {178C3213-D574-4B39-A2DA-1FB1D2806242}.Debug|Any CPU.Build.0 = Debug|Any CPU + {178C3213-D574-4B39-A2DA-1FB1D2806242}.Release|Any CPU.ActiveCfg = Release|Any CPU + {178C3213-D574-4B39-A2DA-1FB1D2806242}.Release|Any CPU.Build.0 = Release|Any CPU + {322F4807-05CF-431D-B400-7420E1B29936}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 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} + {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/Extensions/TagExtensions.cs b/src/aoWebWallet/Extensions/TagExtensions.cs new file mode 100644 index 0000000..243732b --- /dev/null +++ b/src/aoWebWallet/Extensions/TagExtensions.cs @@ -0,0 +1,18 @@ +using ArweaveBlazor.Models; + +namespace aoWebWallet.Extensions +{ + public static class TagExtensions + { + public static string ToSendCommand(this List tagList) + { + var tagListString = string.Join(", ", tagList.Select(x => x.ToSendCommand())); + return "{" + tagListString + "}"; + } + + public static string ToSendCommand(this ArweaveBlazor.Models.Tag tag) + { + return $"{tag.Name} = \"{tag.Value}\""; + } + } +} 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/Layout/MainLayout.razor b/src/aoWebWallet/Layout/MainLayout.razor index 6f46874..2c20bf9 100644 --- a/src/aoWebWallet/Layout/MainLayout.razor +++ b/src/aoWebWallet/Layout/MainLayout.razor @@ -4,18 +4,18 @@ string? versionHash = Program.GetVersionHash(); } - - + + - + + Width="100" Class="pt-2" Alt="AOWW"/> - + @*aoWebWallet*@ @**@ @* *@ @@ -27,22 +27,22 @@ @Body -
+
- - - - - - - +
+ + +
+ Version: @Program.GetVersionWithoutHash() @if (!string.IsNullOrEmpty(versionHash)) { -@versionHash } - zsXSvJtHVSK4QyPch4Uf0JMiZi9uEhgVvyz6qeEJcfY + +
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/ActionParam.cs b/src/aoWebWallet/Models/ActionParam.cs new file mode 100644 index 0000000..666a5ec --- /dev/null +++ b/src/aoWebWallet/Models/ActionParam.cs @@ -0,0 +1,193 @@ + +using System.Text; + +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 + && x.ParamType != ActionParamType.Target); + + public string? IsValid() + { + 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; + } + + 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 List ToDryRunTags() + { + return AllWithoutTarget.Select(x => new ArweaveAO.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; + } + + action.Params.Add(new ActionParam + { + Key = actionKey, + Value = actionValue, + Args = args, + ParamType = actionParamType + }); + + } + } + + return action; + } + + public static AoAction CreateForTokenTransaction(string recipient, 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.Filled, Value = recipient }, + new ActionParam { Key= "Quantity", ParamType = ActionParamType.Balance, Args = new List { tokenId } } + } + + }; + } + + 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 + { + public required string Key { get; set; } + public string? Value { get; set; } + + /// + /// Arguments (like TokenId) + /// + public List Args { get; set; } = new(); + + public ActionParamType ParamType { get; set; } + + } + + public enum ActionParamType + { + None = 0, + Target, + Filled, + Input, + Integer, + Process, + Balance, //Arg1: TokenId //Must have balance + Quantity, //Arg1: TokenId //Does not care about balance + } +} 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/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/Models/UserSettings.cs b/src/aoWebWallet/Models/UserSettings.cs index 6446d8d..86b8e5e 100644 --- a/src/aoWebWallet/Models/UserSettings.cs +++ b/src/aoWebWallet/Models/UserSettings.cs @@ -2,9 +2,9 @@ { public class UserSettings { - public bool? IsDarkMode { get; set; } = true; - public string? ComputeUnitUrl { get; set; } - public string? GraphqlApiUrl { get; set; } + //public bool? IsDarkMode { get; set; } = true; + + public GatewayUrlConfig GatewayUrlConfig { get; set; } = new(); public bool Claimed1 { get; set; } public bool Claimed2 { get; set; } @@ -12,5 +12,13 @@ public class UserSettings } - + public class GatewayUrlConfig + { + 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"; + } + + } diff --git a/src/aoWebWallet/Models/Wallet.cs b/src/aoWebWallet/Models/Wallet.cs index 5baff19..d2ba0ea 100644 --- a/src/aoWebWallet/Models/Wallet.cs +++ b/src/aoWebWallet/Models/Wallet.cs @@ -6,10 +6,11 @@ namespace aoWebWallet.Models public class Wallet { public required string Address { get; set; } + public string? OwnerAddress { get; set; } public string? Name { get; set; } public string? Jwk { get; set; } public WalletTypes Source { get; set; } - public bool IsConnected { get; set; } + public bool IsReadOnly { get; set; } public DateTimeOffset? LastBackedUpDate { get; set; } @@ -18,29 +19,6 @@ public class Wallet public bool NeedsBackup => Source == WalletTypes.Generated && !string.IsNullOrEmpty(Jwk) && !LastBackedUpDate.HasValue; - public bool CanSend - { - get - { - if (IsReadOnly) - return false; - - var result = Source switch - { - WalletTypes.Manual => false, - WalletTypes.None => false, - WalletTypes.ArConnect => IsConnected, - WalletTypes.Explorer => false, - WalletTypes.Generated => !string.IsNullOrEmpty(Jwk), - WalletTypes.Imported => !string.IsNullOrEmpty(Jwk), - _ => false - }; - - return result; - - } - } - } public enum WalletTypes @@ -50,6 +28,7 @@ public enum WalletTypes ArConnect, Explorer, Generated, - Imported + Imported, + AoProcess } } diff --git a/src/aoWebWallet/Pages/About.razor b/src/aoWebWallet/Pages/About.razor index 2565d1d..9801c51 100644 --- a/src/aoWebWallet/Pages/About.razor +++ b/src/aoWebWallet/Pages/About.razor @@ -3,8 +3,12 @@ About - @Program.PageTitlePostFix - - ao Web Wallet + + + + + ao Web Wallet + @@ -13,26 +17,7 @@ Created for the Hack the Weave hackathon by Michiel Post and Nuno Lopes - - Features - - - - - - - - - - - - - - - - - - + @@ -51,16 +36,22 @@ - - - - + + Contribute to developers address
+ zsXSvJtHVSK4QyPch4Uf0JMiZi9uEhgVvyz6qeEJcfY
@code { + + private List _items = new List + { + new BreadcrumbItem("Home", href: "/"), + new BreadcrumbItem("About", href: null, disabled: true) + }; + string? versionHash = Program.GetVersionHash(); private string result = DateTimeOffset.UtcNow.ToString(); diff --git a/src/aoWebWallet/Pages/ActionPage.razor b/src/aoWebWallet/Pages/ActionPage.razor new file mode 100644 index 0000000..97b0169 --- /dev/null +++ b/src/aoWebWallet/Pages/ActionPage.razor @@ -0,0 +1,207 @@ +@page "/action" +@using aoWebWallet.Models +@inherits MvvmComponentBase +@inject IDialogService DialogService +@inject ISnackbar Snackbar +@inject NavigationManager NavigationManager +@inject TokenDataService dataService +@inject TransactionService transactionService; +@inject WalletDetailViewModel WalletDetailViewModel + +@Program.PageTitlePostFix + + + + + + + @if (BindingContext.WalletList.Data != null) + { + var sendWallets = BindingContext.WalletList.Data.Where(x => !x.IsReadOnly).ToList(); + if (!sendWallets.Any()) + { + Add Wallet + } + else + { + + @foreach (var wallet in sendWallets ?? new()) + { + + + @* *@ + +
+ + @wallet.Address + +
+
+ @wallet.Name +
+
+
+ +
+ } +
+ } + } + +
+ + @if(readOnly) + { + Please review your transaction: + } + + + + @if (!readOnly && !string.IsNullOrEmpty(selectedWallet)) + { + Preview + @validation + } + else if (!started && !string.IsNullOrEmpty(selectedWallet) && 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 + @msg.Data + @error +
+ } +
+ @* + Learn More + *@ +
+ } + + Cancel + Submit + } + + @if (transactionService.LastTransaction.DataLoader != null) + { + + if (!string.IsNullOrEmpty(transactionService.LastTransaction.Data?.Id)) + { + + Transfer success! + Message Id + + @transactionService.LastTransaction.Data?.Id + + + + Return to wallet + @* View Transaction *@ + } + } + + +
+ +@code +{ + private string? validation; + private string? selectedWallet; + private Wallet? selectedWalletObj; + private bool readOnly = false; + private bool started = false; + + private List _items = new List + { + new BreadcrumbItem("Home", href: "/"), + new BreadcrumbItem("New transaction", href: null, disabled: true) + }; + + + 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(); + + transactionService.DryRunAction(wallet, AoAction); + + } + private void Cancel() + { + readOnly = false; + } + + private void ReturnToWallet() + { + 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) + 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(); + + started = true; + await transactionService.SendAction(wallet, ownerWallet, AoAction); + } + + private void OpenDialog() + { + NavigationManager.NavigateTo("/start"); + // 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 new file mode 100644 index 0000000..a9e0dc0 --- /dev/null +++ b/src/aoWebWallet/Pages/ActionPage.razor.cs @@ -0,0 +1,93 @@ +using aoWebWallet.Models; +using aoWebWallet.ViewModels; +using Microsoft.AspNetCore.Components.Routing; + +namespace aoWebWallet.Pages +{ + public partial class ActionPage : MvvmComponentBase + { + public AoAction AoAction { get; set; } = new(); + + protected override void OnInitialized() + { + transactionService.Reset(); + + GetQueryStringValues(); + //WatchDataLoaderVM(BindingContext.TokenList); + WatchDataLoaderVM(BindingContext.WalletList); + WatchDataLoaderVM(transactionService.LastTransaction); + WatchDataLoaderVM(transactionService.DryRunResult); + + //Auto select wallet + if (!string.IsNullOrEmpty(WalletDetailViewModel.SelectedWallet?.Wallet.Address)) + { + selectedWalletObj = WalletDetailViewModel.SelectedWallet?.Wallet; + selectedWallet = selectedWalletObj?.Address; + + } + + NavigationManager.LocationChanged += NavigationManager_LocationChanged; + + base.OnInitialized(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await BindingContext.CheckHasArConnectExtension(); + + await BindingContext.LoadWalletList(); + //await dataService.LoadTokenList(); + } + + await base.OnAfterRenderAsync(firstRender); + } + + private void NavigationManager_LocationChanged(object? sender, LocationChangedEventArgs e) + { + GetQueryStringValues(); + StateHasChanged(); + } + + private async void GetQueryStringValues() + { + var uri = new Uri(NavigationManager.Uri); + var query = uri.Query; + + AoAction = AoAction.CreateFromQueryString(query); + + //Add and load tokens + var tokens = AoAction + .AllInputs + .Where(x => x.ParamType == ActionParamType.Balance || x.ParamType == ActionParamType.Quantity) + .Select(x => x.Args.FirstOrDefault()) + .Where(x => x != null) + .Distinct() + .Select(x => x!) + .ToList(); + + await dataService.TryAddTokenIds(tokens); + + StateHasChanged(); + } + + + + public override void Dispose() + { + NavigationManager.LocationChanged -= NavigationManager_LocationChanged; + + base.Dispose(); + } + + //protected override async Task LoadDataAsync() + //{ + // await BindingContext.LoadTokenList(); + + // await base.LoadDataAsync(); + + //} + + } +} diff --git a/src/aoWebWallet/Pages/AddressBook.razor b/src/aoWebWallet/Pages/AddressBook.razor new file mode 100644 index 0000000..d7aae6f --- /dev/null +++ b/src/aoWebWallet/Pages/AddressBook.razor @@ -0,0 +1,139 @@ +@page "/address-book" +@using aoWebWallet.Models +@inherits MvvmComponentBase +@inject IDialogService DialogService +@inject ISnackbar Snackbar +@inject ClipboardService ClipboardService + +Address Book - @Program.PageTitlePostFix + + + + + + + @if (BindingContext.WalletList.Data != null) + { + + + + } + + + + @if (BindingContext.WalletList.Data != null) + { + if(BindingContext.WalletList.Data.Where(x => x.IsReadOnly).Any()) + { + foreach (var wallet in BindingContext.WalletList.Data.Where(x => x.IsReadOnly)) + { + string detailUrl = $"/wallet/{wallet.Address}"; + + +
+ + @if(BindingContext.ProcessesDataList?.Data?.Where(x => x.Data?.Address == wallet.Address && (x.Data?.Processes?.Any() ?? false)).Any() ?? false) + { + AOS + } +
+ +
+ + @wallet.Address + + +
+
+ @wallet.Name +
+
+ + + + + + Edit + + + Delete + + +
+
+ } + } + else + { + + + Your Address Book is currently empty. + Add your first contact + + + + } + } + else + { + Loading address book... + + + } + +
+
+ + +@code +{ + private List _items = new List + { + new BreadcrumbItem("Home", href: "/"), + new BreadcrumbItem("Address book", href: null, disabled: true) + }; + + private void OpenDialog() + { + 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) + { + 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/Apps.razor b/src/aoWebWallet/Pages/Apps.razor new file mode 100644 index 0000000..0f762b0 --- /dev/null +++ b/src/aoWebWallet/Pages/Apps.razor @@ -0,0 +1,55 @@ +@page "/apps" +@inject GatewayUrlHelper UrlHelper; +@using aoWebWallet.Models +@using aoWebWallet.Shared + +Friends of aoWebWallet - @Program.PageTitlePostFix + + + + + + A list of friends of aoWebWallet. Use your tokens and explore the ao ecosystem. + + + + + + + + + + + + + + + + + + +@code { + private List _items = new List + { + new BreadcrumbItem("Home", href: "/"), + new BreadcrumbItem("Apps", href: null, disabled: true) + }; +} diff --git a/src/aoWebWallet/Pages/MvvmComponentBase.cs b/src/aoWebWallet/Pages/MvvmComponentBase.cs index 0687f07..68f0c45 100644 --- a/src/aoWebWallet/Pages/MvvmComponentBase.cs +++ b/src/aoWebWallet/Pages/MvvmComponentBase.cs @@ -1,8 +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 @@ -13,19 +11,31 @@ 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() { - BindingContext.PropertyChanged += BindingContext_PropertyChanged; + //BindingContext.PropertyChanged += BindingContext_PropertyChanged; - foreach(var obj in ObjWatch) + foreach (var obj in ObjWatch) { obj.PropertyChanged += ObjWatch_PropertyChanged; } + foreach (var obj in CollectionWatch) + { + obj.CollectionChanged += Obj_CollectionChanged; + } + base.OnInitialized(); } + //private void BindingContext_PropertyChanged(object? sender, PropertyChangedEventArgs e) + //{ + // Console.WriteLine("Changed! " + e.PropertyName); + // this.StateHasChanged(); + //} + protected override async Task OnInitializedAsync() { await LoadDataAsync(); @@ -38,24 +48,16 @@ protected override async Task OnInitializedAsync() // await base.OnParametersSetAsync(); //} - internal async void BindingContext_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + internal void ObjWatch_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { - if (e.PropertyName == nameof(MainViewModel.ComputeUnitUrl)) - { - await LoadDataAsync(); - this.StateHasChanged(); - } + this.StateHasChanged(); + //Console.WriteLine("Obj State changed: " + sender?.ToString()); } - - internal async void ObjWatch_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + private void Obj_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { this.StateHasChanged(); - await ChartRenderAsync(); - } + //Console.WriteLine("Obj Collection changed: " + sender?.ToString()); - protected virtual Task ChartRenderAsync() - { - return Task.CompletedTask; } protected virtual Task LoadDataAsync() @@ -63,11 +65,16 @@ 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,12 +83,17 @@ protected void WatchDataLoaderVM(DataLoaderViewModel vm) where D : class public virtual void Dispose() { - BindingContext.PropertyChanged -= BindingContext_PropertyChanged; + //BindingContext.PropertyChanged -= BindingContext_PropertyChanged; foreach (var obj in ObjWatch) { obj.PropertyChanged -= ObjWatch_PropertyChanged; } + + foreach (var obj in CollectionWatch) + { + obj.CollectionChanged -= Obj_CollectionChanged; + } } } } diff --git a/src/aoWebWallet/Pages/ReceivePage.razor b/src/aoWebWallet/Pages/ReceivePage.razor new file mode 100644 index 0000000..b220e0a --- /dev/null +++ b/src/aoWebWallet/Pages/ReceivePage.razor @@ -0,0 +1,182 @@ +@page "/receive/{address}" +@using ZXing; +@using aoWebWallet.Models +@using MudExtensions +@inherits MvvmComponentBase +@inject GatewayUrlHelper UrlHelper; +@inject IDialogService DialogService +@inject ISnackbar Snackbar +@inject NavigationManager NavigationManager +@inject TokenDataService dataService +@inject TransactionService transactionService; +@inject WalletDetailViewModel WalletDetailViewModel +@inject ClipboardService ClipboardService + + +Receive Tokens - @Program.PageTitlePostFix + + + + + + + + + Scan QR + + + + + + + + + @if (BindingContext.Token?.TokenData != null) + { + + + + @BindingContext.Token.TokenData?.Name + @BindingContext.Token.TokenData?.Ticker + + + } + + +
+
+ Address + + + @Address + + + +
+
+ + +
+
+ + + + + + + + Pay using aoWebWallet + + + + + + Share this page + + + + + + + @if (BindingContext.Token != null) + { + + + + + aos command + + Send({ Target = "@BindingContext.Token.TokenId", Action = "Transfer", Recipient = "@Address", Quantity = "TOKEN_AMOUNT"}) + + + + + } + + +
+ + + + + + + + + @if (BindingContext.TokenTransferList.Data?.Any() ?? false) + { + + Incoming Transactions + + + + @foreach (var transfer in BindingContext.TokenTransferList.Data) + { + + } + + + } + else + { + + Waiting for incoming transactions... + + } + + + + + +
+ +@code { + private List _items = new List + { + new BreadcrumbItem("Home", href: "/"), + new BreadcrumbItem("Receive tokens", href: null, disabled: true) + }; + + [Parameter] + public string? Address { get; set; } + + [SupplyParameterFromQuery(Name = "tokenId")] + public string? TokenId { get; set; } + + private Timer? _timer; + public string detailUrl => $"/wallet/{Address}"; + public string shareUrl => NavigationManager.ToAbsoluteUri(BindingContext.ShareLink).ToString(); + + private void Callback(object? state) + { + InvokeAsync(() => + { + Console.WriteLine($"{Address}: Updated at: {DateTime.Now}"); + + BindingContext.LoadTokenTransferList(); + + StateHasChanged(); + }); + } + + + + protected override async Task OnParametersSetAsync() + { + if (Address != null && Address.Length != 43) + { + NavigationManager.NavigateTo(""); + } + + if (Address != null) + await BindingContext.Initialize(Address, TokenId); + + await base.OnParametersSetAsync(); + } + + public override void Dispose() + { + _timer?.Dispose(); + } + +} diff --git a/src/aoWebWallet/Pages/ReceivePage.razor.cs b/src/aoWebWallet/Pages/ReceivePage.razor.cs new file mode 100644 index 0000000..9b6c883 --- /dev/null +++ b/src/aoWebWallet/Pages/ReceivePage.razor.cs @@ -0,0 +1,22 @@ +using aoWebWallet.ViewModels; + +namespace aoWebWallet.Pages +{ + public partial class ReceivePage : MvvmComponentBase + { + protected override void OnInitialized() + { + _timer = new Timer(Callback, null, 0, 10000); + + //WatchObject(BindingContext.Token); + WatchDataLoaderVM(BindingContext.TokenTransferList); + WatchObject(dataService.TokenDataLoader); + + WatchCollection(dataService.TokenList); + + + base.OnInitialized(); + } + + } +} diff --git a/src/aoWebWallet/Pages/ScanQrPage.razor b/src/aoWebWallet/Pages/ScanQrPage.razor new file mode 100644 index 0000000..44b719b --- /dev/null +++ b/src/aoWebWallet/Pages/ScanQrPage.razor @@ -0,0 +1,75 @@ +@page "/scan-qr" +@using ReactorBlazorQRCodeScanner +@using aoWebWallet.Models +@inherits MvvmComponentBase +@inject GatewayUrlHelper UrlHelper; +@inject ISnackbar Snackbar +@inject NavigationManager NavigationManager +@inject IJSRuntime JS + +Scan QR - @Program.PageTitlePostFix + + + + + + + + + + + + + + +@code { + + private List _items = new List + { + new BreadcrumbItem("Home", href: "/"), + new BreadcrumbItem("QR Scanner", href: null, disabled: true) + }; + + private QRCodeScannerJsInterop? _qrCodeScannerJsInterop; + private Action? _onQrCodeScanAction; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + _onQrCodeScanAction = (code) => OnQrCodeScan(code); + + _qrCodeScannerJsInterop = new QRCodeScannerJsInterop(JS); + await _qrCodeScannerJsInterop.Init(_onQrCodeScanAction); + } + } + + private void OnQrCodeScan(string code) + { + if(!code.StartsWith("ao:")) + { + Snackbar.Add("QR Code not recognized", Severity.Error); + return; + } + + var address = code.Substring(3, 43); + + int tokenStart = code.IndexOf("?tokenId="); + if (tokenStart > 0) + { + string tokenId = code.Substring(tokenStart + 9, 43); + + var aoAction = AoAction.CreateForTokenTransaction(address, tokenId); + + NavigationManager.NavigateTo($"/action?{aoAction.ToQueryString()}"); + } + else + { + NavigationManager.NavigateTo($"/wallet/{address}"); + } + + Snackbar.Add("Token type not recognized", Severity.Error); + return; + + } +} diff --git a/src/aoWebWallet/Pages/ScanQrPage.razor.cs b/src/aoWebWallet/Pages/ScanQrPage.razor.cs new file mode 100644 index 0000000..1362735 --- /dev/null +++ b/src/aoWebWallet/Pages/ScanQrPage.razor.cs @@ -0,0 +1,10 @@ +using aoWebWallet.ViewModels; + +namespace aoWebWallet.Pages +{ + public partial class ScanQrPage : MvvmComponentBase + { + + + } +} diff --git a/src/aoWebWallet/Pages/Settings.razor b/src/aoWebWallet/Pages/Settings.razor index d3ad79e..080e630 100644 --- a/src/aoWebWallet/Pages/Settings.razor +++ b/src/aoWebWallet/Pages/Settings.razor @@ -5,8 +5,8 @@ Settings - @Program.PageTitlePostFix - - Settings + + @@ -24,38 +24,34 @@ } + + - @* - - - + + + + - - - + Save + + + - Save - *@ @code { - private string? newUrl { get; set; } - private string? customUrl { get; set; } - protected override void OnInitialized() - { - newUrl = MainViewModel.ComputeUnitUrl; - base.OnInitialized(); - } + private List _items = new List + { + new BreadcrumbItem("Home", href: "/"), + new BreadcrumbItem("Settings", href: null, disabled: true) + }; - 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/Start.razor b/src/aoWebWallet/Pages/Start.razor new file mode 100644 index 0000000..8d550af --- /dev/null +++ b/src/aoWebWallet/Pages/Start.razor @@ -0,0 +1,8 @@ +@page "/start" +@using aoWebWallet.Models +@using aoWebWallet.Shared + +Add a wallet - @Program.PageTitlePostFix + + + diff --git a/src/aoWebWallet/Pages/TokenDetail.razor b/src/aoWebWallet/Pages/TokenDetail.razor index 9adb983..be980ca 100644 --- a/src/aoWebWallet/Pages/TokenDetail.razor +++ b/src/aoWebWallet/Pages/TokenDetail.razor @@ -1,35 +1,35 @@ @page "/token/{tokenId}" @using aoWebWallet.Models -@inherits MvvmComponentBase +@inherits MvvmComponentBase @inject IDialogService DialogService @inject ISnackbar Snackbar @inject NavigationManager NavigationManager; +@inject TokenDataService dataService; +@inject GatewayUrlHelper UrlHelper; @Program.PageTitlePostFix - - Token Explorer + + - + - @if (BindingContext.TokenList.Data != null) + @if (BindingContext.Token.Data != null) { - var token = BindingContext.TokenList.Data.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 - - } + + } @@ -42,11 +42,18 @@ Transactions - @foreach (var transfer in BindingContext.TokenTransferList.Data) + @foreach (var transfer in BindingContext.TokenTransferList.Data) + { + + } + + + @if (BindingContext.TokenTransferList.DataLoader.LoadingState == LoadingState.Finished && BindingContext.CanLoadMoreTransactions) { - + Load More } - + + } @@ -54,8 +61,18 @@ @code { + private List _items = new List + { + new BreadcrumbItem("Home", href: "/"), + new BreadcrumbItem("Token details", href: null, disabled: true) + }; + [Parameter] public string? TokenId { get; set; } + private Task LoadMoreTransactions() + { + return BindingContext.LoadMoreTransactions(); + } } diff --git a/src/aoWebWallet/Pages/TokenDetail.razor.cs b/src/aoWebWallet/Pages/TokenDetail.razor.cs index 406530a..28ff237 100644 --- a/src/aoWebWallet/Pages/TokenDetail.razor.cs +++ b/src/aoWebWallet/Pages/TokenDetail.razor.cs @@ -3,31 +3,33 @@ namespace aoWebWallet.Pages { - public partial class TokenDetail : MvvmComponentBase + public partial class TokenDetail : MvvmComponentBase { protected override void OnInitialized() { - WatchDataLoaderVM(BindingContext.TokenList); + WatchCollection(dataService.TokenList); + WatchDataLoaderVM(BindingContext.Token); 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..4ee523e 100644 --- a/src/aoWebWallet/Pages/Tokens.razor +++ b/src/aoWebWallet/Pages/Tokens.razor @@ -2,28 +2,29 @@ @using aoWebWallet.Models @inherits MvvmComponentBase @inject IDialogService DialogService +@inject TokenDataService dataService @inject ISnackbar Snackbar @Program.PageTitlePostFix - - 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,23 +36,18 @@ } - @* else - { - foreach (var token in BindingContext.TokenList.Data) - { - - } - } *@ - - - - } @code { + private List _items = new List + { + new BreadcrumbItem("Home", href: "/"), + new BreadcrumbItem("Token explorer", href: null, disabled: true) + }; + private void OpenAddTokenDialog() { @@ -61,8 +57,7 @@ private async void ToggleVisibility(Token token) { - - await BindingContext.TokenToggleVisibility(token.TokenId); + await dataService.TokenToggleVisibility(token.TokenId); StateHasChanged(); } @@ -76,7 +71,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..ec675b1 100644 --- a/src/aoWebWallet/Pages/Tokens.razor.cs +++ b/src/aoWebWallet/Pages/Tokens.razor.cs @@ -6,17 +6,20 @@ public partial class Tokens : MvvmComponentBase { protected override void OnInitialized() { - WatchDataLoaderVM(BindingContext.TokenList); + WatchCollection(dataService.TokenList); + WatchObject(dataService.TokenDataLoader); base.OnInitialized(); } - protected override async Task LoadDataAsync() + protected override async Task OnAfterRenderAsync(bool firstRender) { - await BindingContext.LoadTokenList(); - - await base.LoadDataAsync(); + if (firstRender) + { + await dataService.LoadTokenList(); + } + await base.OnAfterRenderAsync(firstRender); } } diff --git a/src/aoWebWallet/Pages/TransactionDetail.razor b/src/aoWebWallet/Pages/TransactionDetail.razor index e591eeb..cae7be1 100644 --- a/src/aoWebWallet/Pages/TransactionDetail.razor +++ b/src/aoWebWallet/Pages/TransactionDetail.razor @@ -1,7 +1,9 @@ @page "/transaction/{txid}" @using aoWebWallet.Models -@inherits MvvmComponentBase +@inherits MvvmComponentBase @inject NavigationManager NavigationManager; +@inject TokenDataService dataService +@inject GatewayUrlHelper UrlHelper; @TxId - @Program.PageTitlePostFix @@ -9,61 +11,66 @@ - + + @if (BindingContext.SelectedTransaction.Data != null) { var transfer = BindingContext.SelectedTransaction.Data; Transaction - + @transfer.Id - var tokenData = BindingContext.TokenList.Data?.Where(x => x.TokenId == transfer.TokenId).Select(x => x.TokenData).FirstOrDefault(); - - - - @if (transfer.BlockHeight.HasValue) - { - @transfer.Timestamp.ToString("s") - } - else - { - unconfirmed - } - - @if (tokenData != null) - { - - - @tokenData.Name - @tokenData.Ticker - - } - - @{ - string detailUrlFrom = $"wallet/{transfer.From}"; - string detailUrlTo = $"wallet/{transfer.To}"; - } - - - @transfer.From - - - - - @transfer.To - - - @if (tokenData != null) - { - @BalanceHelper.FormatBalance(transfer.Quantity, tokenData.Denomination ?? 0) - } - - - + var tokenData = dataService.TokenList.Where(x => x.TokenId == transfer.TokenId).Select(x => x.TokenData).FirstOrDefault(); + + + + + + + + @if (transfer.BlockHeight.HasValue) + { + @transfer.Timestamp.ToString("s") + } + else + { + unconfirmed + } + + @if (tokenData != null) + { + + @tokenData.Name + @tokenData.Ticker + + } + + @{ + string detailUrlFrom = $"wallet/{transfer.From}"; + string detailUrlTo = $"wallet/{transfer.To}"; + } + + + @transfer.From + + + + + + @transfer.To + + + @if (tokenData != null) + { + @BalanceHelper.FormatBalance(transfer.Quantity, tokenData.Denomination ?? 0) + } + + + } diff --git a/src/aoWebWallet/Pages/TransactionDetail.razor.cs b/src/aoWebWallet/Pages/TransactionDetail.razor.cs index b96d054..a68298e 100644 --- a/src/aoWebWallet/Pages/TransactionDetail.razor.cs +++ b/src/aoWebWallet/Pages/TransactionDetail.razor.cs @@ -1,40 +1,41 @@ -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; } protected override void OnInitialized() { + WatchCollection(dataService.TokenList); WatchDataLoaderVM(BindingContext.TokenTransferList); WatchDataLoaderVM(BindingContext.SelectedTransaction); 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 21e3852..deaef49 100644 --- a/src/aoWebWallet/Pages/WalletDetail.razor +++ b/src/aoWebWallet/Pages/WalletDetail.razor @@ -1,58 +1,86 @@ @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 +@inject GatewayUrlHelper UrlHelper; @Address - @Program.PageTitlePostFix + + + + + + + + - - + @Address - @BindingContext.SelectedWallet?.Name - @if (BindingContext.SelectedWallet?.NeedsBackup ?? false) + + @if (BindingContext.SelectedWallet?.IsConnected ?? false) + { + + } + @BindingContext.SelectedWallet?.Wallet.Name + @if (BindingContext.SelectedWallet?.Wallet.OwnerAddress != null) + { + owner: @BindingContext.SelectedWallet?.Wallet.OwnerAddress + } + + @if (BindingContext.SelectedWallet?.Wallet.NeedsBackup ?? false) { - + Wallet not backed up yet! } - + - @if (!string.IsNullOrEmpty(BindingContext.SelectedWallet?.Jwk)) - { - - - - } - - - - @if (BindingContext.SelectedWallet?.Source == WalletTypes.Explorer) - { - - - - } + + + + @if (!string.IsNullOrEmpty(BindingContext.SelectedWallet?.Wallet.Jwk)) + { + Save + } + + + + + + @if (BindingContext.SelectedWallet?.Wallet.Source == WalletTypes.Explorer) + { + + } + else if(BindingContext.SelectedWallet?.Wallet != null) + { + + } + + - @if (!(BindingContext.SelectedWallet?.IsReadOnly ?? true) && (BindingContext.SelectedWallet?.IsConnected ?? false)) + @if (BindingContext.SelectedWallet?.IsConnected ?? false) { - - @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) @@ -60,25 +88,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) } } @@ -87,61 +115,14 @@ } - - @* - - *@ - - - - - - - + - @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) - continue; - - - - -
- - - @balance.Data?.Token?.TokenData?.Name - @balance.Data?.Token?.TokenData?.Ticker - -
- - - - @if (balance.Data?.BalanceData != null) - { - @BalanceHelper.FormatBalance(balance.Data.BalanceData.Balance, balance.Data.Token?.TokenData?.Denomination ?? 0) - } - - - - - - - - - @if ((BindingContext.SelectedWallet?.CanSend ?? false)) - { - var hasBalance = balance.Data?.BalanceData?.Balance ?? 0; - - - - } - -
-
+ } } @@ -165,9 +146,16 @@ @foreach (var transfer in BindingContext.TokenTransferList.Data) { - + } + + @if (BindingContext.TokenTransferList.DataLoader.LoadingState == LoadingState.Finished && BindingContext.CanLoadMoreTransactions) + { + Load More + } + + }
@@ -207,44 +195,41 @@ @code { + private List _items = new List + { + new BreadcrumbItem("Home", href: "/"), + new BreadcrumbItem("Wallet", href: null, disabled: true) + }; + [Parameter] public string? Address { get; set; } private void OpenAddTokenDialog() { var options = new DialogOptions { CloseOnEscapeKey = true }; - DialogService.Show("Add Token", options); + DialogService.Show("Add Token", options); } - private void Receive(BalanceDataViewModel? balanceDataVM) + private async void EditWallet(Wallet wallet) { - BindingContext.SelectedBalanceDataVM = balanceDataVM; - var options = new DialogOptions { CloseOnEscapeKey = true }; - DialogService.Show("Receive Token", options); - } + var parameters = new DialogParameters { { x => x.Wallet, wallet } }; - private void Send(BalanceDataViewModel? balanceDataVM) - { - BindingContext.SelectedBalanceDataVM = balanceDataVM; - var options = new DialogOptions { CloseOnEscapeKey = true }; - DialogService.Show("Transfer Token", options); + var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true }; + DialogService.Show("Edit Wallet", parameters, options); + + StateHasChanged(); } + + 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() @@ -262,4 +247,9 @@ await BindingContext.Claim3(); } + private Task LoadMoreTransactions() + { + return BindingContext.LoadMoreTransactions(); + } + } diff --git a/src/aoWebWallet/Pages/WalletDetail.razor.cs b/src/aoWebWallet/Pages/WalletDetail.razor.cs index 9ddfe91..32e7dce 100644 --- a/src/aoWebWallet/Pages/WalletDetail.razor.cs +++ b/src/aoWebWallet/Pages/WalletDetail.razor.cs @@ -5,51 +5,71 @@ namespace aoWebWallet.Pages { - public partial class WalletDetail : MvvmComponentBase + public partial class WalletDetail : MvvmComponentBase { protected override void OnInitialized() { - WatchDataLoaderVM(BindingContext.TokenList); - WatchDataLoaderVM(BindingContext.WalletList); - WatchDataLoaderVM(BindingContext.BalanceDataList); + //WatchObject(dataService.TokenList); + //WatchObject(BindingContext.BalanceDataList); + WatchObject(dataService.TokenDataLoader); + + WatchCollection(dataService.TokenList); + WatchCollection(BindingContext.BalanceDataList); + WatchDataLoaderVM(MainViewModel.WalletList); WatchDataLoaderVM(BindingContext.TokenTransferList); WatchDataLoaderVM(BindingContext.SelectedProcessData); + dataService.TokenList.CollectionChanged += TokenList_CollectionChanged; + BindingContext.PropertyChanged += BindingContext_PropertyChanged; + base.OnInitialized(); } - protected override void OnParametersSet() + private void BindingContext_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(BindingContext.VisibleTokenList)) + { + BindingContext.TokenAddedRefresh(); + } + } + + private async void TokenList_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { - BindingContext.SelectedWallet = null; - //BindingContext.SelectedAddress = null; + BindingContext.TokenAddedRefresh(); + } - if(Address != null && Address.Length != 43) + protected override async Task OnParametersSetAsync() + { + if (Address != null && Address.Length != 43) { NavigationManager.NavigateTo(""); } - BindingContext.SelectedAddress = Address; + if (Address != null) + await BindingContext.Initialize(Address); - base.OnParametersSet(); + await base.OnParametersSetAsync(); } protected override async Task LoadDataAsync() { - BindingContext.LoadTokenList(); - - //if (!string.IsNullOrEmpty(Address)) - //{ - // BindingContext.LoadBalanceDataList(Address); - //} + dataService.LoadTokenList(); await base.LoadDataAsync(); } private async void DownloadWallet(Wallet wallet) { - await BindingContext.DownloadWallet(wallet); + await MainViewModel.DownloadWallet(wallet); StateHasChanged(); } + public override void Dispose() + { + dataService.TokenList.CollectionChanged -= TokenList_CollectionChanged; + + base.Dispose(); + } + } } diff --git a/src/aoWebWallet/Pages/Wallets.razor b/src/aoWebWallet/Pages/Wallets.razor index 5b66410..3b726c5 100644 --- a/src/aoWebWallet/Pages/Wallets.razor +++ b/src/aoWebWallet/Pages/Wallets.razor @@ -3,80 +3,78 @@ @inherits MvvmComponentBase @inject IDialogService DialogService @inject ISnackbar Snackbar +@inject TokenDataService dataService +@inject ClipboardService ClipboardService +@inject NavigationManager Navigation @Program.PageTitlePostFix - + - @if (BindingContext.WalletList.Data == null || BindingContext.WalletList.Data.Count > 1) - { - Wallets - } - else - { - Wallet - } - - + - - @if (BindingContext.WalletList.Data != null && BindingContext.WalletList.Data.Any()) - { - - - - } + + @if (BindingContext.WalletList.Data != null && BindingContext.WalletList.Data.Any()) + { + + + + } + @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}"; - + - +
+ + @if(BindingContext.ProcessesDataList?.Data?.Where(x => x.Data?.Address == wallet.Address && (x.Data?.Processes?.Any() ?? false)).Any() ?? false) + { + AOS + } +
- -
- + +
+ @wallet.Address - +
-
- @if (wallet.IsReadOnly) - { - read-only   - } +
@wallet.Name - @if (wallet.IsConnected) - { - - } + @if (wallet.NeedsBackup) { - + Wallet not backed up yet! }
- @if(BindingContext.ProcessesDataList?.Data?.Where(x => x.Data?.Address == wallet.Address && (x.Data?.Processes?.Any() ?? false)).Any() ?? false) - { - AOS - } - @if(!string.IsNullOrEmpty(wallet.Jwk)) - { - - } - + + @if(!string.IsNullOrEmpty(wallet.Jwk)) + { + + Backup + + } + + Edit + + + Delete + + @@ -87,15 +85,7 @@ } else { - - - - - - - - - + Navigation.NavigateTo("/start"); } } else @@ -111,10 +101,20 @@ @code { - private void OpenDialog() + private List _items = new List + { + new BreadcrumbItem("Home", href: "#"), + new BreadcrumbItem("", href: "#") + }; + + private async void EditWallet(Wallet wallet) { - var options = new DialogOptions { CloseOnEscapeKey = true }; - DialogService.Show("Add Wallet", options); + 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) diff --git a/src/aoWebWallet/Pages/Wallets.razor.cs b/src/aoWebWallet/Pages/Wallets.razor.cs index 17980e8..3232d95 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); @@ -17,21 +17,14 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { - await BindingContext.CheckHasArConnectExtension(); - await BindingContext.LoadWalletList(); - await BindingContext.LoadTokenList(); + await dataService.LoadTokenList(); + + await BindingContext.CheckHasArConnectExtension(); } await base.OnAfterRenderAsync(firstRender); } - //protected override async Task LoadDataAsync() - //{ - - - // //BindingContext.LoadStats(); - //} - } } diff --git a/src/aoWebWallet/Program.cs b/src/aoWebWallet/Program.cs index e69cd66..b2bf742 100644 --- a/src/aoWebWallet/Program.cs +++ b/src/aoWebWallet/Program.cs @@ -11,6 +11,11 @@ using ArweaveBlazor; using System.Globalization; using ClipLazor.Extention; +using aoww.Services; +using aoww.Services.Models; +using aoWebWallet.Models; +using ArweaveAO.Models; +using MudExtensions.Services; namespace aoWebWallet { @@ -87,9 +92,15 @@ private static void ConfigureServices(IServiceCollection services, string baseAd services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(baseAddress) }); + services.AddMudExtensions(); + + //Services - services.AddScoped(); + services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddSingleton(); @@ -97,14 +108,24 @@ private static void ConfigureServices(IServiceCollection services, string baseAd services.AddScoped(); services.AddArweaveBlazor(); + services.AddScoped(); //Register ViewModels services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddBlazoredLocalStorage(); services.AddClipboard(); + + //Options + services.AddSingleton(new GraphqlConfig()); + services.AddSingleton(new GatewayConfig()); + services.AddSingleton(new ArweaveConfig()); } } } 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/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/GraphqlClient.cs b/src/aoWebWallet/Services/GraphqlClient.cs deleted file mode 100644 index 1a48a1e..0000000 --- a/src/aoWebWallet/Services/GraphqlClient.cs +++ /dev/null @@ -1,188 +0,0 @@ -using aoWebWallet.Models; -using ArweaveAO.Models.Token; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http.Json; -using System.Text; -using System.Threading.Tasks; -using static System.Net.WebRequestMethods; - -namespace aoWebWallet.Services -{ - public class GraphqlClient - { - private readonly HttpClient httpClient; - - public GraphqlClient(HttpClient httpClient) - { - this.httpClient = httpClient; - } - - public async Task> GetTransactionsIn(string adddress, string? fromTxId = null) - { - string query = "query {\r\n transactions(\r\n first: 100\r\n sort: HEIGHT_DESC\r\n tags: [\r\n { name: \"Data-Protocol\", values: [\"ao\"] }\r\n { name: \"Action\", values: [\"Transfer\"] }\r\n { name: \"Recipient\", values: [\"" + adddress + "\"] }\r\n ]\r\n ) {\r\n edges {\r\n node {\r\n id\r\n recipient\r\n owner {\r\n address\r\n }\r\n block {\r\n timestamp\r\n height\r\n }\r\n tags {\r\n name\r\n value\r\n }\r\n }\r\n }\r\n }\r\n}\r\n"; - var queryResult = await PostQueryAsync(query); - - var result = new List(); - - foreach (var edge in queryResult?.Data?.Transactions?.Edges ?? new()) - { - TokenTransfer? transaction = GetTransaction(edge); - - if(transaction != null) - result.Add(transaction); - } - - return result; - } - - private static TokenTransfer? GetTransaction(Edge edge) - { - if (edge == null || edge.Node == null) - return null; - - var isTransfer = edge.Node.Tags.Where(x => x.Name == "Action" && x.Value == "Transfer").Any(); - if (!isTransfer) - return null; - - var transaction = new TokenTransfer() - { - Id = edge.Node.Id, - From = edge.Node.Owner?.Address ?? string.Empty - }; - - if (edge.Node.Block != null) - { - transaction.Timestamp = DateTimeOffset.FromUnixTimeSeconds(edge.Node.Block.Timestamp); - transaction.BlockHeight = edge.Node.Block.Height; - } - else - transaction.Timestamp = DateTimeOffset.UtcNow; - - transaction.TokenId = edge.Node.Recipient; - transaction.To = edge.Node.Tags.Where(x => x.Name == "Recipient").Select(x => x.Value).FirstOrDefault(); - - string? quantity = edge.Node.Tags.Where(x => x.Name == "Quantity").Select(x => x.Value).FirstOrDefault(); - if (!string.IsNullOrWhiteSpace(quantity) && long.TryParse(quantity, out long quantityLong)) - transaction.Quantity = quantityLong; - return transaction; - } - - private static AoProcessInfo? GetAoProcessInfo(Edge edge) - { - if (edge == null || edge.Node == null) - return null; - - - var name = edge.Node.Tags.Where(x => x.Name == "Name").Select(x => x.Value).FirstOrDefault(); - if (string.IsNullOrEmpty(name)) - return null; - - var processInfo = new AoProcessInfo() - { - Id = edge.Node.Id, - Name = name, - }; - - processInfo.Version = edge.Node.Tags.Where(x => x.Name == "Version").Select(x => x.Value).FirstOrDefault(); - - return processInfo; - } - - public async Task> GetTransactionsOut(string adddress, string? fromTxId = null) - { - string query = "query {\r\n transactions(\r\n first: 100\r\n sort: HEIGHT_DESC\r\n owners: [\"" + adddress + "\"]\r\n tags: [\r\n { name: \"Data-Protocol\", values: [\"ao\"] }\r\n { name: \"Action\", values: [\"Transfer\"] }\r\n ]\r\n ) {\r\n edges {\r\n node {\r\n id\r\n recipient\r\n owner {\r\n address\r\n }\r\n block {\r\n timestamp\r\n height\r\n }\r\n tags {\r\n name\r\n value\r\n }\r\n }\r\n }\r\n }\r\n}\r\n"; - var queryResult = await PostQueryAsync(query); - - var result = new List(); - - foreach (var edge in queryResult?.Data?.Transactions?.Edges ?? new()) - { - TokenTransfer? transaction = GetTransaction(edge); - - if (transaction != null) - result.Add(transaction); - } - - return result; - } - - public async Task GetTransactionsById(string txId) - { - string query = "query {\r\n transactions(\r\n first: 1\r\n sort: HEIGHT_DESC\r\n ids: [\"" + txId + "\"]\r\n tags: [\r\n { name: \"Data-Protocol\", values: [\"ao\"] }\r\n { name: \"Action\", values: [\"Transfer\"] }\r\n ]\r\n ) {\r\n edges {\r\n node {\r\n id\r\n recipient\r\n owner {\r\n address\r\n }\r\n block {\r\n timestamp\r\n height\r\n }\r\n tags {\r\n name\r\n value\r\n }\r\n }\r\n }\r\n }\r\n}\r\n"; - var queryResult = await PostQueryAsync(query); - - var result = new List(); - - foreach (var edge in queryResult?.Data?.Transactions?.Edges ?? new()) - { - TokenTransfer? transaction = GetTransaction(edge); - - if (transaction != null) - result.Add(transaction); - } - - return result.FirstOrDefault(); - } - - public async Task> GetTransactionsForToken(string tokenId, string? fromTxId = null) - { - string query = "query {\r\n transactions(\r\n first: 50\r\n sort: HEIGHT_DESC\r\n recipients: [\"" + tokenId + "\"]\r\n tags: [\r\n { name: \"Data-Protocol\", values: [\"ao\"] }\r\n { name: \"Action\", values: [\"Transfer\"] }\r\n ]\r\n ) {\r\n edges {\r\n node {\r\n id\r\n recipient\r\n owner {\r\n address\r\n }\r\n block {\r\n timestamp\r\n height\r\n }\r\n tags {\r\n name\r\n value\r\n }\r\n }\r\n }\r\n }\r\n}\r\n"; - var queryResult = await PostQueryAsync(query); - - var result = new List(); - - foreach (var edge in queryResult?.Data?.Transactions?.Edges ?? new()) - { - TokenTransfer? transaction = GetTransaction(edge); - - if (transaction != null) - result.Add(transaction); - } - - return result; - } - - public async Task> GetAoProcessesForAddress(string address) - { - string query = "query {\r\n transactions(\r\n first: 100,\r\n owners: [\"" + address + "\"],\r\n tags: [\r\n { name: \"Data-Protocol\", values: [\"ao\"] },\r\n { name: \"Type\", values: [\"Process\"]},\r\n { name: \"App-Name\", values: [\"aos\"]},\r\n \r\n ]\r\n ) {\r\n edges {\r\n node {\r\n id\r\n tags {\r\n name\r\n value\r\n }\r\n }\r\n }\r\n }\r\n }"; - var queryResult = await PostQueryAsync(query); - - var result = new List(); - - foreach (var edge in queryResult?.Data?.Transactions?.Edges ?? new()) - { - AoProcessInfo? processInfo = GetAoProcessInfo(edge); - - if (processInfo != null) - result.Add(processInfo); - } - - return result; - } - - //public async Task GetTransactionsForToken(string tokenId, string fromTxId) - //{ - - //} - - protected async Task PostQueryAsync(string query) - { - var request = new GraphqlRequest { Query = query}; - - HttpResponseMessage res = await httpClient.PostAsJsonAsync("https://arweave.net/graphql", request); - if (res.IsSuccessStatusCode) - { - return await res.Content.ReadFromJsonAsync(); - } - else - { - string msg = await res.Content.ReadAsStringAsync(); - Console.WriteLine(msg); - throw new Exception(msg); - } - } - } -} diff --git a/src/aoWebWallet/Services/StorageService.cs b/src/aoWebWallet/Services/StorageService.cs index 2f01b0b..4497480 100644 --- a/src/aoWebWallet/Services/StorageService.cs +++ b/src/aoWebWallet/Services/StorageService.cs @@ -1,8 +1,6 @@ using aoWebWallet.Models; -using aoWebWallet.Pages; using ArweaveAO.Models.Token; using Blazored.LocalStorage; -using System.Reflection.Metadata; namespace aoWebWallet.Services { @@ -23,38 +21,130 @@ 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 + AddSystemTokens(result); return result; } - private void AddSystemToken(List list, string tokenId) + 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" + }); //CRED + + + AddSystemToken(result, "8p7ApPZxC_37M06QHVejCQrKsHbcJEerd3jWNkDUWPQ", + new TokenData + { + TokenId = "8p7ApPZxC_37M06QHVejCQrKsHbcJEerd3jWNkDUWPQ", + Denomination = 3, + Logo = "AdFxCN1eEPboxNpCNL23WZRNhIhiamOeS-TUwx_Nr3Q", + Name = "Bark", + Ticker = "BRKTST" + }); //BARK + + AddSystemToken(result, "OT9qTE2467gcozb2g8R6D6N3nQS94ENcaAIJfUzHCww", + new TokenData + { + TokenId = "OT9qTE2467gcozb2g8R6D6N3nQS94ENcaAIJfUzHCww", + Denomination = 3, + Logo = "4eTBOaxZSSyGbpKlHyilxNKhXbocuZdiMBYIORjS4f0", + Name = "TRUNK", + Ticker = "TRUNK" + }); //TRUNK + + AddSystemToken(result, "aYrCboXVSl1AXL9gPFe3tfRxRf0ZmkOXH65mKT0HHZw", + new TokenData + { + TokenId = "aYrCboXVSl1AXL9gPFe3tfRxRf0ZmkOXH65mKT0HHZw", + Denomination = 6, + Logo = "wfI-5PlYXL66_BqquCXm7kq-ic1keu0b2CqRjw82yrU", + Name = "AR.IO EXP", + Ticker = "EXP" + }); + + AddSystemToken(result, "BUhZLMwQ6yZHguLtJYA5lLUa9LQzLXMXRfaq9FVcPJc", + new TokenData + { + TokenId = "BUhZLMwQ6yZHguLtJYA5lLUa9LQzLXMXRfaq9FVcPJc", + Denomination = 12, + Logo = "nvx7DgTR8ws_k6VNCSe8vhwbZLx5jNbfNLJS0IKTTHA", + Name = "0rbit Points", + Ticker = "0RBT" + }); //0rbit + + AddSystemToken(result, "PBg5TSJPQp9xgXGfjN27GA28Mg5bQmNEdXH2TXY4t-A", + new TokenData + { + TokenId = "PBg5TSJPQp9xgXGfjN27GA28Mg5bQmNEdXH2TXY4t-A", + Denomination = 12, + Logo = "VzvP24VxdNt1kf3E-EXxxrihaNBnXpEI-5ymwWddJRk", + Name = "Earth", + Ticker = "EARTH" + }); + + AddSystemToken(result, "KmGmJieqSRJpbW6JJUFQrH3sQPEG9F6DQETlXNt4GpM", + new TokenData + { + TokenId = "KmGmJieqSRJpbW6JJUFQrH3sQPEG9F6DQETlXNt4GpM", + Denomination = 12, + Logo = "jayAVj1wgIcmin0bjG_DIGxq3_qANSp5EV7PcfUAvdQ", + Name = "Fire", + Ticker = "FIRE" + }); + + AddSystemToken(result, "2nfFJb8LIA69gwuLNcFQezSuw4CXPE4--U-j-7cxKOU", + new TokenData + { + TokenId = "2nfFJb8LIA69gwuLNcFQezSuw4CXPE4--U-j-7cxKOU", + Denomination = 12, + Logo = "7WqV5FWdDcbQzQNxNvfpr093yLHDtjeO7qPM9HQskWE", + Name = "Air", + Ticker = "AIR" + }); + + AddSystemToken(result, "NkXX3uZ4oGkQ3DPAWtjLb2sTA-yxmZKdlOlEHqMfWLQ", + new TokenData + { + TokenId = "NkXX3uZ4oGkQ3DPAWtjLb2sTA-yxmZKdlOlEHqMfWLQ", + Denomination = 12, + Logo = "ioI2_z6qkzGBrvZXbojjf6Q5uVZumx4rDDdHm-Jfyt0", + Name = "Lava", + Ticker = "FIRE-EARTH" + }); + } + + private static void AddSystemToken(List list, string tokenId, TokenData tokenData) { 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 }); + list.Add(new Token { TokenId = tokenId, IsSystemToken = true, TokenData = tokenData }); } - public async ValueTask AddToken(string tokenId, TokenData data, bool isUserAdded) + 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 }; + existing = new Token { TokenId = tokenId, TokenData = data, IsUserAdded = isUserAdded, IsVisible = isVisible ?? true }; list.Add(existing); } @@ -76,7 +166,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() @@ -85,15 +177,16 @@ public async ValueTask> GetWallets() return result ?? new(); } + 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); - list.Add(wallet); + list.Insert(0,wallet); await SaveWalletList(list); } diff --git a/src/aoWebWallet/Services/TokenDataService.cs b/src/aoWebWallet/Services/TokenDataService.cs new file mode 100644 index 0000000..1cb5dea --- /dev/null +++ b/src/aoWebWallet/Services/TokenDataService.cs @@ -0,0 +1,162 @@ + +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; } = 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) + { + allTokenIds = allTokenIds.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + + 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; + }, async data => + { + if (data != null) + { + await storageService.AddToken(tokenId, data, isUserAdded: false, null); + + await LoadTokenList(force: true); + } + }); + + + } + } + + public async Task LoadTokenAsync(string tokenId) + { + await LoadTokenList(force: false); + + var token = TokenList.Where(x => x.TokenId.Equals(tokenId, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); + if(token == null) + { + token = new Token() + { + TokenId = tokenId, + }; + } + + if (token.TokenData == null) + { + var data = await TokenDataLoader.LoadAsync(async () => + { + var data = await tokenClient.GetTokenMetaData(tokenId); + return data; + }); + + if (data != null) + { + + token.TokenData = data; + + var existing = TokenList.Where(x => x.TokenId.Equals(tokenId, StringComparison.OrdinalIgnoreCase)).Any(); + if(!existing) + TokenList.Add(token); + + await storageService.AddToken(tokenId, data, false, null); + } + + } + + return token; + } + + public async Task LoadTokenList(bool force = false) + { + if (!TokenList.Any() || force) + { + TokenList.Clear(); + await foreach (var item in LoadTokenDataAsync()) + { + var existing = TokenList.Where(x => x.TokenId == item.TokenId).Any(); + + if(!existing) + TokenList.Add(item); + } + } + } + + private 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 Clear() + { + await storageService.SaveTokenList(new()); + TokenList.Clear(); + } + } +} diff --git a/src/aoWebWallet/Services/TransactionService.cs b/src/aoWebWallet/Services/TransactionService.cs new file mode 100644 index 0000000..73e599e --- /dev/null +++ b/src/aoWebWallet/Services/TransactionService.cs @@ -0,0 +1,182 @@ +using aoWebWallet.Extensions; +using aoWebWallet.Models; +using ArweaveAO.Requests; +using ArweaveAO.Responses; +using ArweaveBlazor; +using CommunityToolkit.Mvvm.ComponentModel; +using webvNext.DataLoader; + +namespace aoWebWallet.Services +{ + public class TransactionService(ArweaveService arweaveService, + ArweaveAO.AODataClient aODataClient, + TokenDataService tokenDataService) : ObservableObject + { + 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() + { + bool hasArConnectExtension = await arweaveService.HasArConnectAsync(); + + if (hasArConnectExtension) + { + var address = await arweaveService.GetActiveAddress(); + + return address; + + } + + return null; + } + + 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() + { + Target = target, + Owner = wallet.Address, + Tags = action.ToDryRunTags() + }; + + 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) + { + var activeAddress = await GetActiveArConnectAddress(); + if(activeAddress == wallet.Address) + await SendActionWithArConnect(action); + } + + if (ownerWallet?.Source == WalletTypes.ArConnect) + { + var activeAddress = await GetActiveArConnectAddress(); + if (activeAddress == ownerWallet.Address) + await SendActionWithEvalWithArConnect(wallet.Address, action); + } + + if (!string.IsNullOrEmpty(wallet.OwnerAddress) && ownerWallet?.Address == wallet.OwnerAddress + && !string.IsNullOrEmpty(ownerWallet?.Jwk)) + { + await SendActionWithEval(ownerWallet.Jwk, wallet.Address, action); + } + + if (!string.IsNullOrEmpty(wallet.Jwk)) + await SendActionWithJwk(wallet.Jwk, action); + + //Console.WriteLine("No Wallet to send"); + return; + } + + private async Task SendActionWithEvalWithArConnect(string processId, AoAction action) + { + var activeAddress = await GetActiveArConnectAddress(); + if (string.IsNullOrEmpty(activeAddress)) + return; + + await SendActionWithEval(null, processId, action); + } + + private async Task SendActionWithArConnect(AoAction action) + { + var activeAddress = await GetActiveArConnectAddress(); + if (string.IsNullOrEmpty(activeAddress)) + return; + + await SendActionWithJwk(null, action); + } + + private Task SendActionWithEval(string? jwk, string processId, AoAction action) + => LastTransaction.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 }; + }, x => LastTransaction.Data = x); + + private Task SendActionWithJwk(string? jwk, AoAction action) + => LastTransaction.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 }; + }, x => LastTransaction.Data = x); + + + + + } +} 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/ActionEditor.razor b/src/aoWebWallet/Shared/ActionEditor.razor new file mode 100644 index 0000000..8279355 --- /dev/null +++ b/src/aoWebWallet/Shared/ActionEditor.razor @@ -0,0 +1,60 @@ +@using aoWebWallet.Models + + + + + Token ID @AoAction.Target?.Value?.ToShortAddress() + + + + + Token ID @AoAction.Target?.Value + + + + + @foreach (var ActionParam in AoAction.Filled) + { + + @ActionParam.Key @ActionParam.Value + + } + + + + + @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/AddArConnectComponent.razor b/src/aoWebWallet/Shared/AddArConnectComponent.razor index 8a5bf8a..ad01e02 100644 --- a/src/aoWebWallet/Shared/AddArConnectComponent.razor +++ b/src/aoWebWallet/Shared/AddArConnectComponent.razor @@ -1,14 +1,12 @@ @using aoWebWallet.Models @inherits MvvmComponentBase -@inject TokenClient TokenClient @inject ISnackbar Snackbar @inject ArweaveService ArweaveService +@inject NavigationManager NavigationManager - + - @(IsExpanded ? "Connect wallet" : "Connect wallet") - - + @if (!BindingContext.HasArConnectExtension.HasValue) { @@ -17,9 +15,9 @@ else if (!BindingContext.HasArConnectExtension.Value) { -
- - Download ArConnect +
+ + Download ArConnect
} @@ -27,10 +25,10 @@ { if (string.IsNullOrEmpty(BindingContext.ActiveArConnectAddress)) { - -
- - Connect with ArConnect + +
+ + Connect with ArConnect
@@ -40,17 +38,12 @@ var wallet = BindingContext.WalletList.Data?.Where(x => x.Source == WalletTypes.ArConnect && x.Address == BindingContext.ActiveArConnectAddress).FirstOrDefault(); if (wallet == null) { - - - -
- - Connect with ArConnect + +
+ + Connect with ArConnect
- - - } else { @@ -64,7 +57,7 @@ { Add } *@ - + @@ -133,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 4dc145c..a70adf3 100644 --- a/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor +++ b/src/aoWebWallet/Shared/AddGenerateWalletComponent.razor @@ -1,26 +1,31 @@ @using aoWebWallet.Models @inherits MvvmComponentBase -@inject TokenClient TokenClient @inject ArweaveService ArweaveService @inject ISnackbar Snackbar +@inject NavigationManager NavigationManager - + - @(IsExpanded ? "Create a new wallet" : "Create a new wallet") - - - - @Progress -
- Create + + + + + + @Progress +
+ + Create aoWW Wallet +
- +@code { + bool Disabled { get; set; } = false; + bool ButtonDisabled { get; set; } = false; + DefaultFocus DefaultFocus { get; set; } = DefaultFocus.FirstChild; -@code { [Parameter] public bool HideAddButton { get; set; } @@ -40,6 +45,10 @@ public async Task Submit() { + if (ButtonDisabled) + return false; + + ButtonDisabled = true; var jwk = await ArweaveService.GenerateWallet(); var address = await ArweaveService.GetAddress(jwk); @@ -58,10 +67,17 @@ Snackbar.Add($"Wallet added ({address})", Severity.Info); + ButtonDisabled = false; + if (MudDialog != null) { MudDialog.Close(); } + else + { + NavigationManager.NavigateTo($"/wallet/{wallet.Address}"); + } + return true; } } diff --git a/src/aoWebWallet/Shared/AddTokenDialog.razor b/src/aoWebWallet/Shared/AddTokenDialog.razor index 5d5c47d..4b25fb0 100644 --- a/src/aoWebWallet/Shared/AddTokenDialog.razor +++ b/src/aoWebWallet/Shared/AddTokenDialog.razor @@ -1,12 +1,11 @@ -@inherits MvvmComponentBase -@inject TokenClient TokenClient +@inject TokenDataService dataService @inject ISnackbar Snackbar Add a token to view your balance. Provide a process-id that implements the token standard. - + @Progress @@ -37,11 +36,10 @@ Progress = "Checking metadata..."; try { - var data = await TokenClient.GetTokenMetaData(TokenId); + var token = await dataService.LoadTokenAsync(TokenId); + var data = token.TokenData; if (data != null) { - await BindingContext.AddToken(TokenId, data, isUserAdded: true); - Snackbar.Add($"Token added ({data.Name})", Severity.Info); MudDialog.Close(DialogResult.Ok(true)); 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/Shared/AddUploadWalletComponent.razor b/src/aoWebWallet/Shared/AddUploadWalletComponent.razor index 68f84d0..1ff7b4a 100644 --- a/src/aoWebWallet/Shared/AddUploadWalletComponent.razor +++ b/src/aoWebWallet/Shared/AddUploadWalletComponent.razor @@ -1,34 +1,33 @@ @using aoWebWallet.Models @inherits MvvmComponentBase -@inject TokenClient TokenClient @inject ArweaveService ArweaveService @inject ISnackbar Snackbar +@inject NavigationManager NavigationManager - + + - @(IsExpanded ? "Load .json wallet" : "Load .json wallet") - - - - + @* Load .json wallet + *@ + - - @@ -158,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 index 9a681d1..c2a5627 100644 --- a/src/aoWebWallet/Shared/AddWalletComponent.razor +++ b/src/aoWebWallet/Shared/AddWalletComponent.razor @@ -1,71 +1,34 @@ -@using aoWebWallet.Models -@inherits MvvmComponentBase -@inject TokenClient TokenClient -@inject ISnackbar Snackbar - - - - @(IsExpanded ? "Add read-only wallet" : "Add read-only wallet") - - - - - @Progress - @if (!HideAddButton) - { -
- Add + + + + +
+
+ +
- } - - -
- - - -@code { - [Parameter] - public bool HideAddButton { get; set; } - - public string? Name { get; set; } - public string? Address { get; set; } - public string? Progress { get; set; } - - [Parameter] - public bool IsExpanded { get; set; } - - private void OnExpandCollapseClick() - { - IsExpanded = !IsExpanded; - } - - public async Task Submit() - { - if(string.IsNullOrWhiteSpace(Address)) - { - Progress = "Input a wallet address."; - StateHasChanged(); - return false; - } - if (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); - - Snackbar.Add($"Wallet added ({Address})", Severity.Info); - return true; - } -} + + +
+ + +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+
+
diff --git a/src/aoWebWallet/Shared/AddWalletDialog.razor b/src/aoWebWallet/Shared/AddWalletDialog.razor index 47e4575..926ef1e 100644 --- a/src/aoWebWallet/Shared/AddWalletDialog.razor +++ b/src/aoWebWallet/Shared/AddWalletDialog.razor @@ -1,43 +1,20 @@ @using aoWebWallet.Models @using aoWebWallet.Shared -@inherits MvvmComponentBase -@inject TokenClient TokenClient @inject ISnackbar Snackbar - - - - + - - Cancel - Ok - @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)); void Cancel() => MudDialog.Cancel(); -} \ No newline at end of file +} diff --git a/src/aoWebWallet/Shared/BalanceDataComponent.razor b/src/aoWebWallet/Shared/BalanceDataComponent.razor new file mode 100644 index 0000000..7dd0fa5 --- /dev/null +++ b/src/aoWebWallet/Shared/BalanceDataComponent.razor @@ -0,0 +1,67 @@ +@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) + { + NavigationManager.NavigateTo($"/receive/{balanceDataVM.Address}?tokenId={balanceDataVM.Token.TokenId}"); + } + + private void Send(BalanceDataViewModel? balanceDataVM) + { + if (balanceDataVM?.Token == null) + return; + + var aoAction = AoAction.CreateForTokenTransaction(balanceDataVM.Token.TokenId); + + NavigationManager.NavigateTo($"/action?{aoAction.ToQueryString()}"); + } +} diff --git a/src/aoWebWallet/Shared/Components/ActionInputComponent.razor b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor new file mode 100644 index 0000000..3a8a064 --- /dev/null +++ b/src/aoWebWallet/Shared/Components/ActionInputComponent.razor @@ -0,0 +1,188 @@ +@using aoWebWallet.Models +@inject MainViewModel MainViewModel +@*

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

*@ + + +@if(ReadOnly) +{ + @ActionParam.Key @ActionParam.Value +} +else +{ + if (ActionParam.ParamType == ActionParamType.Input) + { + + } + else if (ActionParam.ParamType == ActionParamType.Process) + { + + + + @e.ToAutocompleteDisplay() + + + + + @e.ToAutocompleteDisplay() + + + + + + + + @(ActionParam.Value?.ToShortAddress() ?? "Not selected") + + + @(ActionParam.Value ?? "Not selected") + + @* *@ + } + else if (ActionParam.ParamType == ActionParamType.Integer) + { + + } +} + + +@code { + + [Parameter] + public required ActionParam ActionParam { get; set; } + + [Parameter] + public bool ReadOnly { get; set; } + + private string? textValue; + + MudTextField? mudTextField; + //MudTextField? mudProcessField; + MudAutocomplete? 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) + { + if (!(mudTextField?.ValidationErrors.Any() ?? false)) + { + mudTextField?.SetText(ActionParam.Value); + } + + if (!(mudProcessField?.ValidationErrors.Any() ?? false)) + { + // if (ActionParam.Value != null && mudProcessField != null) + // mudProcessField.Text = ActionParam.Value; + + //mudProcessField?.ForceUpdate(); + + // if(mudProcessField != null) + // mudProcessField.Value = ActionParam.Value; + } + + if (!(mudIntField?.ValidationErrors.Any() ?? false)) + { + mudIntField?.SetText(ActionParam.Value); + } + + base.OnAfterRender(firstRender); + } + + 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 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.Address.Length != 43) + { + 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(); + } + + 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/Shared/Components/ActionQuantityComponent.razor b/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor new file mode 100644 index 0000000..2540f7e --- /dev/null +++ b/src/aoWebWallet/Shared/Components/ActionQuantityComponent.razor @@ -0,0 +1,152 @@ +@using ArweaveAO.Models.Token +@using aoWebWallet.Models +@inject TokenDataService tokenDataService +@inject TokenClient tokenClient +@*

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

*@ + + +@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 && !ReadOnly) +{ + Loading balance... + + return; +} + +@if (ReadOnly) +{ + @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) @Token?.TokenData?.Ticker
+ } + } +} +
+ +@code { + + [Parameter] + public required ActionParam ActionParam { get; set; } + + [Parameter] + 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(); + + MudTextField? mudTextField; + + protected override void OnAfterRender(bool firstRender) + { + if (!(mudTextField?.ValidationErrors.Any() ?? false) && Token?.TokenData?.Denomination != null) + { + mudTextField?.SetText(@BalanceHelper.FormatBalance(long.Parse(ActionParam.Value ?? "0"), Token.TokenData.Denomination.Value)); + } + + base.OnAfterRender(firstRender); + } + + protected override async Task OnParametersSetAsync() + { + BalanceData = null; + var token = await tokenDataService.LoadTokenAsync(TokenId); + if (token.TokenData?.Denomination != null) + Token = token; + + if (ActionParam.ParamType == ActionParamType.Balance + && !string.IsNullOrEmpty(Address) + && !ReadOnly) + { + BalanceData = await tokenClient.GetBalance(token.TokenId, Address); + } + + base.OnParametersSetAsync(); + } + + public IEnumerable ValidateBalance(decimal e) + { + if (e < 0) + { + yield return "Must be greater or equal than 0."; + } + + 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) + { + 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/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/AppListItem.razor b/src/aoWebWallet/Shared/Components/AppListItem.razor new file mode 100644 index 0000000..2754775 --- /dev/null +++ b/src/aoWebWallet/Shared/Components/AppListItem.razor @@ -0,0 +1,34 @@ + + + + + + + + + + + @Name - @Description + + + + + +@code { + + [Parameter] + [EditorRequired] + public required string Name { get; set; } + + [Parameter] + [EditorRequired] + public required string Link { get; set; } + + [Parameter] + [EditorRequired] + public required string Logo { get; set; } + + [Parameter] + [EditorRequired] + public required string Description { get; set; } +} 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..04a6176 100644 --- a/src/aoWebWallet/Shared/Components/TokenListComponent.razor +++ b/src/aoWebWallet/Shared/Components/TokenListComponent.razor @@ -1,16 +1,17 @@ @using aoWebWallet.Models @inherits MvvmComponentBase +@inject GatewayUrlHelper UrlHelper; @if (token != null) { -
+
@token.TokenData?.Name @token.TokenData?.Ticker - + @token.TokenId diff --git a/src/aoWebWallet/Shared/Components/TransactionComponent.razor b/src/aoWebWallet/Shared/Components/TransactionComponent.razor index 10082a4..5fb7cfe 100644 --- a/src/aoWebWallet/Shared/Components/TransactionComponent.razor +++ b/src/aoWebWallet/Shared/Components/TransactionComponent.razor @@ -1,9 +1,11 @@ @using aoWebWallet.Models @inherits MvvmComponentBase +@inject TokenDataService dataService +@inject GatewayUrlHelper UrlHelper; @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}"; @@ -46,7 +48,15 @@ else if(isReceive) { + @BalanceHelper.FormatBalance(transfer.Quantity, tokenData?.Denomination ?? 0) - + + @if(transfer.TokenTransferType == aoww.Services.Enums.TokenTransferType.Mint) + { + MINT + } + else + { + + } @transfer.From @@ -56,9 +66,7 @@ @transfer.From -
-
@transfer.To diff --git a/src/aoWebWallet/Shared/Components/WalletAvatar.razor b/src/aoWebWallet/Shared/Components/WalletAvatar.razor new file mode 100644 index 0000000..5ad7046 --- /dev/null +++ b/src/aoWebWallet/Shared/Components/WalletAvatar.razor @@ -0,0 +1,9 @@ +
+ +
+ +@code { + + [Parameter] + public string? Address { get; set; } +} diff --git a/src/aoWebWallet/Shared/EditWalletComponent.razor b/src/aoWebWallet/Shared/EditWalletComponent.razor new file mode 100644 index 0000000..c8c070f --- /dev/null +++ b/src/aoWebWallet/Shared/EditWalletComponent.razor @@ -0,0 +1,77 @@ +@using aoWebWallet.Models +@inherits MvvmComponentBase +@inject ISnackbar Snackbar + + + + + @Progress + + +
+ + Save + +
+
+
+ + + +@code { + [Parameter] + public Wallet Wallet { get; set; } = new() { Address = string.Empty }; + + [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 => !string.IsNullOrEmpty(Wallet.Address); + + + protected override void OnParametersSet() + { + Address = Wallet.Address; + Name = Wallet.Name; + + base.OnParametersSet(); + } + + protected override void OnInitialized() + { + base.OnInitialized(); + } + + + public async Task Submit() + { + // 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(); + return false; + } + + Wallet.Name = Name; + Wallet.Address = Address; + + await BindingContext.SaveWallet(Wallet); + + StateHasChanged(); + + Snackbar.Add($"Address saved {Wallet.Name} ({Wallet.Address})", Severity.Info); + + MudDialog?.Close(true); + + return true; + } +} diff --git a/src/aoWebWallet/Shared/NavMenu.razor b/src/aoWebWallet/Shared/NavMenu.razor index 94caeb9..ffed6f1 100644 --- a/src/aoWebWallet/Shared/NavMenu.razor +++ b/src/aoWebWallet/Shared/NavMenu.razor @@ -2,24 +2,18 @@ - @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}"; + string detailUrl = $"wallet/{wallet.Address}"; - +
@wallet.Address.ToShortAddress()
- - logoCount++; }
} @@ -28,33 +22,43 @@ Wallets } - Token Explorer + Address Book + Token Explorer + QR Scanner + Apps Meme Frames -
+
Settings About
-
- @if (BindingContext.UserSettings?.IsDarkMode ?? true) - { - - } - else - { - - } - Theme +
+ + + + + + + + Twitter + + + + + + + + + + + Discord + +
-
- - - Copyright @DateTimeOffset.UtcNow.Year -
+ @code { @@ -65,26 +69,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/ReceiveTokenDialog.razor b/src/aoWebWallet/Shared/ReceiveTokenDialog.razor deleted file mode 100644 index 7e473aa..0000000 --- a/src/aoWebWallet/Shared/ReceiveTokenDialog.razor +++ /dev/null @@ -1,49 +0,0 @@ -@using aoWebWallet.Models -@using aoWebWallet.Shared -@inherits MvvmComponentBase -@inject TokenClient TokenClient -@inject ISnackbar Snackbar - - - - - - - @BindingContext.SelectedBalanceDataVM?.Token?.TokenData?.Name - @BindingContext.SelectedBalanceDataVM?.Token?.TokenData?.Ticker - - -
- - How to receive tokens? - Send tokens to this address: - - @BindingContext.SelectedBalanceDataVM?.BalanceData?.Account - - - -
- - From aos: - Command: - - Send({ Target = "@BindingContext.SelectedBalanceDataVM?.Token?.TokenId", Action = "Transfer", Recipient = "@BindingContext.SelectedBalanceDataVM?.BalanceData?.Account", Quantity = "TOKEN_AMOUNT"}) - - -
- - Cancel - Ok - -
-@code { - [CascadingParameter] MudDialogInstance MudDialog { get; set; } = default!; - - public async Task Submit() - { - MudDialog.Close(DialogResult.Ok(true)); - } - - //void Submit() => MudDialog.Close(DialogResult.Ok(true)); - void Cancel() => MudDialog.Cancel(); -} \ No newline at end of file diff --git a/src/aoWebWallet/Shared/SendTokenDialog.razor b/src/aoWebWallet/Shared/SendTokenDialog.razor deleted file mode 100644 index 49bab38..0000000 --- a/src/aoWebWallet/Shared/SendTokenDialog.razor +++ /dev/null @@ -1,182 +0,0 @@ -@using aoWebWallet.Models -@using aoWebWallet.Shared -@inherits MvvmComponentBase -@inject TokenClient TokenClient -@inject ISnackbar Snackbar - - - - - - - @BindingContext.SelectedBalanceDataVM?.Token?.TokenData?.Name - @BindingContext.SelectedBalanceDataVM?.Token?.TokenData?.Ticker - - - Available balance
@BalanceHelper.FormatBalance(BindingContext.SelectedBalanceDataVM?.BalanceData?.Balance, BindingContext.SelectedBalanceDataVM?.Token?.TokenData?.Denomination ?? 0)
- - @if (!isConfirm) - { - - - - - @Progress - } - - @if (isConfirm) - { - if (string.IsNullOrEmpty(TransactionId)) - { - - Are you sure? - You are about to transfer: - - } - - Amount: @Amount @BindingContext.SelectedBalanceDataVM?.Token?.TokenData?.Ticker - Receiver: - - @Address - - - - - - if (!string.IsNullOrEmpty(TransactionId)) - { - - Transfer success! - TransactionId - - @TransactionId - - - } - } -
- - Cancel - @if (!isConfirm) - { - Next - } - else - { - if (string.IsNullOrEmpty(TransactionId)) - { - if (!BindingContext.LastTransactionId.DataLoader.IsLoading) - { - Confirm - } - } - else - { - Close - } - } - -
-@code { - [CascadingParameter] MudDialogInstance MudDialog { get; set; } = default!; - - public string? Progress { get; set; } - public string? Address { get; set; } - public decimal Amount { get; set; } - public string? TransactionId { 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" + (BindingContext.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(BindingContext.SelectedBalanceDataVM?.BalanceData?.TokenId) - || BindingContext.SelectedBalanceDataVM.Token?.TokenData?.Denomination == null - || !BindingContext.SelectedBalanceDataVM.Token.TokenData.Denomination.HasValue) - return; - - long amountLong = BalanceHelper.DecimalToTokenAmount(Amount, BindingContext.SelectedBalanceDataVM.Token.TokenData.Denomination!.Value); - if (amountLong <= 0) - { - Progress = "Amount has to be higher than 0."; - StateHasChanged(); - return; - } - if (amountLong > BindingContext.SelectedBalanceDataVM?.BalanceData?.Balance) - { - Progress = "Not enough balance available."; - StateHasChanged(); - return; - } - - isConfirm = true; - } - - public async Task Confirm() - { - if (string.IsNullOrEmpty(Address)) - return; - if (Address.Length != 43) - { - Progress = "Address length must be 43 characters."; - StateHasChanged(); - return; - } - - if (string.IsNullOrEmpty(BindingContext.SelectedBalanceDataVM?.BalanceData?.TokenId) - || BindingContext.SelectedBalanceDataVM.Token?.TokenData?.Denomination == null - || !BindingContext.SelectedBalanceDataVM.Token.TokenData.Denomination.HasValue) - return; - - if (confButtonRef != null) - confButtonRef.Disabled = true; - - this.StateHasChanged(); - - if (BindingContext.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, BindingContext.SelectedBalanceDataVM.Token.TokenId, Address, amountLong); - TransactionId = result?.Id; - - if (!string.IsNullOrEmpty(BindingContext.SelectedAddress)) - { - await BindingContext.LoadBalanceDataList(BindingContext.SelectedAddress); - await BindingContext.LoadTokenTransferList(BindingContext.SelectedAddress); - } - - if (TransactionId != null) - { - await BindingContext.AddToLog(ActivityLogType.SendTransaction, TransactionId); - } - } - - public void Close() - { - MudDialog.Close(DialogResult.Ok(true)); - - } - - void Cancel() => MudDialog.Cancel(); -} diff --git a/src/aoWebWallet/ViewModels/BalanceDataViewModel.cs b/src/aoWebWallet/ViewModels/BalanceDataViewModel.cs index d33c121..5c9d469 100644 --- a/src/aoWebWallet/ViewModels/BalanceDataViewModel.cs +++ b/src/aoWebWallet/ViewModels/BalanceDataViewModel.cs @@ -1,11 +1,27 @@ using aoWebWallet.Models; using ArweaveAO.Models.Token; +using webvNext.DataLoader; namespace aoWebWallet.ViewModels { public class BalanceDataViewModel { - public BalanceData? BalanceData { get; set; } - public Token? Token { get; set; } + public DataLoaderViewModel BalanceDataLoader { get; set; } = new DataLoaderViewModel(); + public required Token Token { get; set; } + + public required string Address { 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/MainViewModel.cs b/src/aoWebWallet/ViewModels/MainViewModel.cs index bd4ada6..b4d238c 100644 --- a/src/aoWebWallet/ViewModels/MainViewModel.cs +++ b/src/aoWebWallet/ViewModels/MainViewModel.cs @@ -1,15 +1,20 @@ -using aoWebWallet.Models; +using aoWebWallet.Extensions; +using aoWebWallet.Models; using aoWebWallet.Pages; using aoWebWallet.Services; +using aoww.Services; +using aoww.Services.Models; using ArweaveAO; +using ArweaveAO.Models; using ArweaveAO.Models.Token; using ArweaveBlazor; using ClipLazor.Components; using ClipLazor.Enums; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using Microsoft.Extensions.Options; using MudBlazor; -using System.Net; +using System.Text.Json; using webvNext.DataLoader; using webvNext.DataLoader.Cache; @@ -19,17 +24,14 @@ public partial class MainViewModel : ObservableRecipient { private const string CLAIM_PROCESS_ID = "5Mv1TBYZvKjNlWUpH78hWORIhqj1uqn_wdkJrA7emfU"; - private readonly DataService dataService; - private readonly TokenClient tokenClient; + private readonly TokenDataService dataService; 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] - private bool isDarkMode = true; + private readonly ArweaveConfig arweaveConfig; + private readonly GatewayConfig gatewayConfig; + private readonly GraphqlConfig graphqlConfig; [ObservableProperty] public bool? hasArConnectExtension; @@ -38,181 +40,42 @@ public partial class MainViewModel : ObservableRecipient public string? activeArConnectAddress; [ObservableProperty] - [NotifyPropertyChangedRecipients] - private string? computeUnitUrl; - - [ObservableProperty] - private string? selectedAddress; - - [ObservableProperty] - private Wallet? selectedWallet; - - [ObservableProperty] - private int? selectedWalletIndex; + private UserSettings userSettings = new(); - [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, - TokenClient tokenClient, + public MainViewModel(TokenDataService dataService, StorageService storageService, ArweaveService arweaveService, GraphqlClient graphqlClient, - ISnackbar snackbar, - IClipLazor clipboard, - MemoryDataCache memoryDataCache) : base() + MemoryDataCache memoryDataCache, + IOptions graphqlConfig, + IOptions gatewayConfig, + IOptions arweaveConfig) : base() { this.dataService = dataService; - this.tokenClient = tokenClient; this.storageService = storageService; this.arweaveService = arweaveService; this.graphqlClient = graphqlClient; - this.snackbar = snackbar; - this.clipboard = clipboard; this.memoryDataCache = memoryDataCache; + this.arweaveConfig = arweaveConfig.Value; + this.gatewayConfig = gatewayConfig.Value; + this.graphqlConfig = graphqlConfig.Value; } 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 all = incoming.Concat(outgoing); - - 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; @@ -233,7 +96,7 @@ public Task LoadProcessesDataList() => ProcessesDataList.DataLoader.LoadAsync(as return new WalletProcessDataViewModel() { Address = address, Processes = data }; }, TimeSpan.FromMinutes(1)); - + }, (x) => { processData.Data = x; ProcessesDataList.ForcePropertyChanged(); }); result.Add(processData); } @@ -244,87 +107,27 @@ 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 LoadWalletList(bool force = false) { if (WalletList.Data == null || force) { var list = await storageService.GetWallets(); - WalletList.Data = list; - - await LoadProcessesDataList(); - } - } - - 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); - } + //foreach (var wallet in list) + //{ + // if(wallet.Source == WalletTypes.AoProcess && !string.IsNullOrEmpty(wallet.OwnerAddress)) + // { + // var owner = list.Where(x => x.Address == wallet.OwnerAddress).FirstOrDefault(); + // var canOwnerSend = owner?.CanSend ?? false; + // wallet.OwnerCanSend = canOwnerSend; + // } + //} - 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); - } + WalletList.Data = list; - 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); + await LoadProcessesDataList(); } } @@ -332,7 +135,6 @@ public async Task SaveWallet(Wallet wallet) { await storageService.SaveWallet(wallet); await LoadWalletList(force: true); - await SetClaims(); } public async Task DeleteWallet(Wallet wallet) @@ -353,7 +155,7 @@ public async Task DownloadWallet(Wallet wallet) if (this.WalletList.Data != null) { var selected = this.WalletList.Data.Where(x => x.Address == address).FirstOrDefault(); - if(selected != null) + if (selected != null) { selected.LastBackedUpDate = DateTimeOffset.UtcNow; await storageService.SaveWalletList(this.WalletList.Data); @@ -366,125 +168,22 @@ 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; - } - - private async Task TryAddTokenIds(List allTokenIds) - { - foreach(var tokenId in allTokenIds) - { - if (string.IsNullOrEmpty(tokenId)) - 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(); - - if (value != null) - this.AddToLog(ActivityLogType.ViewAddress, value); + //BalanceDataList.Data = null; } - - 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 = current; - var indexOf = all.IndexOf(current); - SelectedWalletIndex = (indexOf % 5) + 1; - - } - else - { - var tempWallet = new Wallet - { - Address = address, - AddedDate = DateTimeOffset.Now, - IsConnected = false, - IsReadOnly = true, - LastUsedDate = DateTimeOffset.UtcNow, - Name = null, - Source = WalletTypes.Explorer - }; - SelectedWallet = tempWallet; - SelectedWalletIndex = 5; - } - - this.LoadBalanceDataList(address); - this.LoadTokenTransferList(address); - } - else - { - SelectedWallet = null; - SelectedWalletIndex = 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) - { - //ClearUserData(); - - if (!string.IsNullOrEmpty(value)) - { - //storageService.SetApiUrl(value); - - dataService.Init(value); - } - - } - + public async Task LoadUserSettings() { UserSettings = await storageService.GetUserSettings(); if (UserSettings != null) { - IsDarkMode = UserSettings.IsDarkMode ?? true; + UpdateUserSettings(UserSettings.GatewayUrlConfig); } } @@ -493,103 +192,18 @@ public async Task SaveUserSettings() if (UserSettings != null) { await storageService.SaveUserSettings(UserSettings); - IsDarkMode = UserSettings.IsDarkMode ?? true; - } - } - - public async Task AddWalletAsReadonly() - { - if(SelectedWallet != null) - { - SelectedWallet.Source = WalletTypes.Manual; - await storageService.SaveWallet(SelectedWallet); - await LoadWalletList(force: true); - - snackbar.Add("Wallet added to list as read-only wallet.", Severity.Info); + UpdateUserSettings(UserSettings.GatewayUrlConfig); } } - 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() + private void UpdateUserSettings(GatewayUrlConfig userSettings) { - 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); - } - } + graphqlConfig.ApiUrl = userSettings.GraphqlUrl; + gatewayConfig.GatewayUrl = userSettings.GatewayUrl; + arweaveConfig.ComputeUnitUrl = userSettings.ComputeUnitUrl; - } - 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); - } - } + arweaveService.SetConnection(userSettings.GatewayUrl, userSettings.GraphqlUrl, userSettings.MessengerUnitUrl, userSettings.ComputeUnitUrl); } public async Task DisconnectArWallet() @@ -607,50 +221,14 @@ 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.WalletList.Data != null) - { - var wallets = this.WalletList.Data.Where(x => !x.IsConnected - && x.Source == WalletTypes.ArConnect - && x.Address == ActiveArConnectAddress); - foreach (var wallet in wallets) - { - wallet.IsConnected = true; - } - this.WalletList.ForcePropertyChanged(); - await storageService.SaveWalletList(this.WalletList.Data); - } - } - } - - 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); - } - } + //if (this.SelectedWallet != null) + //{ + // this.SelectedWallet.IsConnected = SelectedWallet.Wallet.Address == ActiveArConnectAddress; + //} } } @@ -659,6 +237,13 @@ public async Task CopyToClipboard(string? text) if (wallet.Source == WalletTypes.ArConnect) return SendTokenWithArConnect(tokenId, address, amount); + if (!string.IsNullOrEmpty(wallet.OwnerAddress)) + { + var ownerWallet = WalletList.Data!.Where(x => x.Address == wallet.OwnerAddress).FirstOrDefault(); + return SendTokenWithEval(ownerWallet?.Jwk, wallet.Address, tokenId, address, amount); + + } + if (!string.IsNullOrEmpty(wallet.Jwk)) return SendTokenWithJwk(wallet.Jwk, tokenId, address, amount); @@ -666,10 +251,38 @@ public async Task CopyToClipboard(string? text) } - public Task SendTokenWithJwk(string jwk, string tokenId, string address, long amount) + public Task SendTokenWithEval(string? jwk, string processId, string tokenId, string address, long amount) + => LastTransactionId.DataLoader.LoadAsync(async () => + { + + var transferTags = new List + { + new ArweaveBlazor.Models.Tag() { Name = "Target", Value = tokenId}, + new ArweaveBlazor.Models.Tag() { Name = "Action", Value = "Transfer"}, + new ArweaveBlazor.Models.Tag() { Name = "Wallet", Value = "aoww"}, + new ArweaveBlazor.Models.Tag() { Name = "Recipient", Value = address}, + new ArweaveBlazor.Models.Tag() { Name = "Quantity", Value = amount.ToString()}, + }; + + + + var data = $"Send({transferTags.ToSendCommand()})"; + + var evalTags = new List + { + new ArweaveBlazor.Models.Tag() { Name = "Action", Value = "Eval"}, + new ArweaveBlazor.Models.Tag() { Name = "Wallet", Value = "aoww"}, + }; + + var idResult = await arweaveService.SendAsync(jwk, processId, null, data, evalTags); + + return new Transaction { Id = idResult }; + }); + + public Task SendTokenWithJwk(string? jwk, string tokenId, string address, long amount) => LastTransactionId.DataLoader.LoadAsync(async () => { - var idResult = await arweaveService.SendAsync(jwk, tokenId, null, new List + var idResult = await arweaveService.SendAsync(jwk, tokenId, null, null, new List { new ArweaveBlazor.Models.Tag() { Name = "Action", Value = "Transfer"}, new ArweaveBlazor.Models.Tag() { Name = "Wallet", Value = "aoww"}, @@ -687,15 +300,7 @@ public async Task CopyToClipboard(string? text) if (string.IsNullOrEmpty(ActiveArConnectAddress)) return null; - var idResult = await arweaveService.SendAsync(null, tokenId, null, new List - { - new ArweaveBlazor.Models.Tag() { Name = "Action", Value = "Transfer"}, - new ArweaveBlazor.Models.Tag() { Name = "Wallet", Value = "aoww"}, - new ArweaveBlazor.Models.Tag() { Name = "Recipient", Value = address}, - new ArweaveBlazor.Models.Tag() { Name = "Quantity", Value = amount.ToString()}, - }); - - return new Transaction { Id = idResult }; + return await SendTokenWithJwk(null, tokenId, address, amount); }); public Task Claim(int claim) @@ -705,7 +310,7 @@ public async Task CopyToClipboard(string? text) if (string.IsNullOrEmpty(ActiveArConnectAddress)) return null; - var idResult = await arweaveService.SendAsync(null, CLAIM_PROCESS_ID, null, new List + var idResult = await arweaveService.SendAsync(null, CLAIM_PROCESS_ID, null, null, new List { new ArweaveBlazor.Models.Tag() { Name = "Action", Value = "claim" + claim}, new ArweaveBlazor.Models.Tag() { Name = "Wallet", Value = "aoww"}, @@ -714,13 +319,5 @@ public async Task CopyToClipboard(string? text) return new Transaction { Id = idResult }; }); - public async Task SetIsDarkMode(bool isDarkMode) - { - if(UserSettings != null) - { - UserSettings.IsDarkMode = isDarkMode; - await SaveUserSettings(); - } - } } } diff --git a/src/aoWebWallet/ViewModels/ReceiveViewModel.cs b/src/aoWebWallet/ViewModels/ReceiveViewModel.cs new file mode 100644 index 0000000..367ce27 --- /dev/null +++ b/src/aoWebWallet/ViewModels/ReceiveViewModel.cs @@ -0,0 +1,86 @@ +using aoWebWallet.Models; +using aoWebWallet.Services; +using aoww.Services; +using aoww.Services.Models; +using ArweaveAO; +using CommunityToolkit.Mvvm.ComponentModel; +using System.Net; +using System.Text; +using webvNext.DataLoader; + +namespace aoWebWallet.ViewModels +{ + public partial class ReceiveViewModel(TokenDataService tokenDataService, + GraphqlClient graphqlClient + ) : ObservableObject + { + [ObservableProperty] + private Token? _token; + + public DataLoaderViewModel> TokenTransferList { get; set; } = new(); + + public string? TokenId { get; set; } + public DateTimeOffset StartTime { get; set; } = DateTimeOffset.Now; + public required string Address { get; set; } + public string QrCode + { + get + { + //return $"{Address}"; + + if (string.IsNullOrEmpty(TokenId)) + return $"ao:{Address}"; + else + return $"ao:{Address}?tokenId={TokenId}"; + } + } + + public string ShareLink + { + get + { + if (!string.IsNullOrWhiteSpace(TokenId)) + { + return "/action?" + AoAction.CreateForTokenTransaction(Address, TokenId).ToQueryString(); + } + else + return $"/wallet/{Address}"; + } + } + + public async Task Initialize(string address, string? tokenId) + { + Address = address; + Token = null; + TokenId = tokenId; + StartTime = DateTimeOffset.UtcNow; + TokenTransferList.Data?.Clear(); + + if (!string.IsNullOrWhiteSpace(tokenId)) + { + Token = await tokenDataService.LoadTokenAsync(tokenId); + } + + await LoadTokenTransferList(); + } + + public Task LoadTokenTransferList() => TokenTransferList.DataLoader.LoadAsync(async () => + { + var address = Address; + + var incoming = await graphqlClient.GetTokenTransfersIn(address); + + incoming = incoming.Where(x => x.Timestamp > StartTime).OrderByDescending(x => x.Timestamp).ToList(); + + if(!string.IsNullOrEmpty(TokenId)) + incoming = incoming.Where(x => x.TokenId == TokenId).ToList(); + + TokenTransferList.Data = incoming.OrderByDescending(x => x.Timestamp).ToList(); + + List allTokenIds = incoming.Where(x => x.TokenId != null).Select(x => x.TokenId!).Distinct().ToList(); + tokenDataService.TryAddTokenIds(allTokenIds); + + return TokenTransferList.Data; + }); + } +} diff --git a/src/aoWebWallet/ViewModels/TokenDetailViewModel.cs b/src/aoWebWallet/ViewModels/TokenDetailViewModel.cs new file mode 100644 index 0000000..12f1cb0 --- /dev/null +++ b/src/aoWebWallet/ViewModels/TokenDetailViewModel.cs @@ -0,0 +1,85 @@ +using aoWebWallet.Models; +using aoWebWallet.Services; +using aoww.Services; +using aoww.Services.Models; +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; + private string? _tokenId; + + private List tokenTransactions = new(); + + public bool CanLoadMoreTransactions { get; set; } = true; + + 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) + { + _tokenId = tokenId; + TokenTransferList.Data = new(); + + await this.LoadTokenData(tokenId); + + this.LoadTokenTransferListForToken(tokenId); + + mainViewModel.AddToLog(ActivityLogType.ViewToken, tokenId); + } + + public Task LoadTokenData(string tokenId) => Token.DataLoader.LoadAsync(async () => + { + Token.Data = null; + + var result = await dataService.LoadTokenAsync(tokenId); + + return result; + + }, (x) => Token.Data = x); + + public Task LoadTokenTransferListForToken(string tokenId) => TokenTransferList.DataLoader.LoadAsync(async () => + { + tokenTransactions = await graphqlClient.GetTransactionsForToken(tokenId, GetCursor(tokenTransactions)); + CanLoadMoreTransactions = tokenTransactions.Any(); + + var existing = TokenTransferList.Data ?? new(); + + TokenTransferList.Data = existing.Concat(tokenTransactions).OrderByDescending(x => x.Timestamp).ToList(); + + var allTokenIds = tokenTransactions.Where(x => x.TokenId != null).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/TransactionDetailViewModel.cs b/src/aoWebWallet/ViewModels/TransactionDetailViewModel.cs new file mode 100644 index 0000000..f9e07c5 --- /dev/null +++ b/src/aoWebWallet/ViewModels/TransactionDetailViewModel.cs @@ -0,0 +1,51 @@ +using aoWebWallet.Models; +using aoWebWallet.Services; +using aoww.Services; +using aoww.Services.Models; +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?.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 new file mode 100644 index 0000000..6dea6da --- /dev/null +++ b/src/aoWebWallet/ViewModels/WalletDetailViewModel.cs @@ -0,0 +1,456 @@ +using aoWebWallet.Models; +using aoWebWallet.Services; +using aoww.Services; +using aoww.Services.Models; +using ArweaveAO; +using ArweaveBlazor; +using CommunityToolkit.Mvvm.ComponentModel; +using MudBlazor; +using System.Collections.ObjectModel; +using webvNext.DataLoader; +using webvNext.DataLoader.Cache; + +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 ArweaveService arweaveService; + private readonly ISnackbar snackbar; + private readonly MemoryDataCache memoryDataCache; + + private List incoming = new(); + private List outgoing = new(); + private List outgoingProcess = new(); + + + [ObservableProperty] + public List visibleTokenList = new(); + + + + private string? selectedAddress = null; + + public bool CanLoadMoreTransactions { get; set; } = true; + + [ObservableProperty] + private bool canClaim1; + [ObservableProperty] + private bool canClaim2; + [ObservableProperty] + private bool canClaim3; + + [ObservableProperty] + public string? activeArConnectAddress; + + [ObservableProperty] + public bool? hasArConnectExtension; + + public WalletDetailsViewModel? SelectedWallet { get; set; } + + + public DataLoaderViewModel SelectedProcessData { get; set; } = new(); + + public ObservableCollection BalanceDataList { get; } = new(); + + public DataLoaderViewModel> TokenTransferList { get; set; } = new(); + + + public WalletDetailViewModel(MainViewModel mainViewModel, + GraphqlClient graphqlClient, + TokenDataService dataService, + TokenClient tokenClient, + StorageService storageService, + ArweaveService arweaveService, + ISnackbar snackbar, + MemoryDataCache memoryDataCache) + { + this.mainViewModel = mainViewModel; + this.graphqlClient = graphqlClient; + this.dataService = dataService; + this.tokenClient = tokenClient; + this.storageService = storageService; + this.arweaveService = arweaveService; + this.snackbar = snackbar; + this.memoryDataCache = memoryDataCache; + } + + + public async Task Initialize(string address) + { + VisibleTokenList = new(); + VisibleTokenList.Add("Sa0iBLPNyJQrwpTTG-tWLQU-1QeUAJA73DdxGGiKoJc"); + + ResetTokenTransferlist(); + + selectedAddress = address; + + await SelectWallet(address); + + await LoadSelectedWalletProcessData(address); + await LoadSelectedWalletOwnerData(address); + + 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) + { + ResetTokenTransferlist(); + + await LoadTokenTransferList(selectedAddress); + } + } + + private void ResetTokenTransferlist() + { + incoming = new(); + outgoing = new(); + outgoingProcess = new(); + TokenTransferList.Data = new(); + } + + public async Task LoadMoreTransactions() + { + if (selectedAddress != null) + { + await LoadTokenTransferList(selectedAddress); + } + } + + public async Task RefreshBalanceDataList() + { + if (selectedAddress != null) + { + await LoadBalanceDataList(selectedAddress); + } + } + + public async Task RefreshBalance() + { + if (selectedAddress != null) + { + await RefreshTokenTransferList(); + await LoadBalanceDataList(selectedAddress); + } + } + + public async Task TokenAddedRefresh() + { + if (selectedAddress != null) + { + await LoadBalanceDataList(selectedAddress, onlyNew: true); + } + } + + //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); + } + else + { + var tempWallet = new Wallet + { + Address = address, + AddedDate = DateTimeOffset.Now, + LastUsedDate = DateTimeOffset.UtcNow, + Name = null, + Source = WalletTypes.Explorer + }; + SelectedWallet = new WalletDetailsViewModel(tempWallet); + } + + this.LoadBalanceDataList(address); + this.LoadTokenTransferList(address); + + if (this.SelectedWallet != null) + { + this.SelectedWallet.IsConnected = SelectedWallet.Wallet.Address == ActiveArConnectAddress; + } + + } + else + { + SelectedWallet = null; + } + } + + public Task LoadTokenTransferList(string address) => TokenTransferList.DataLoader.LoadAsync(async () => + { + incoming = await graphqlClient.GetTokenTransfersIn(address, GetCursor(incoming)); + outgoing = await graphqlClient.GetTransactionsOut(address, GetCursor(outgoing)); + outgoingProcess = await graphqlClient.GetTransactionsOutFromProcess(address, GetCursor(outgoingProcess)); + + var allNew = incoming.Concat(outgoing).Concat(outgoingProcess).OrderByDescending(x => x.Timestamp).ToList(); + CanLoadMoreTransactions = allNew.Any(); + + var existing = TokenTransferList.Data ?? new(); + + TokenTransferList.Data = existing.Concat(allNew).OrderByDescending(x => x.Timestamp).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; + }); + + private static string? GetCursor(List transactions) + { + return transactions.Select(x => x.Cursor).LastOrDefault(); + } + + private async Task LoadBalanceDataList(string address, bool onlyNew = false) + { + //First clear + if (!onlyNew) + BalanceDataList.Clear(); + + foreach (var token in dataService.TokenList.Where(x => VisibleTokenList.Contains(x.TokenId) && x.IsVisible)) + { + if (onlyNew) + { + if (BalanceDataList.Where(x => x.Token?.TokenId == token.TokenId).Any()) + continue; + + } + var balanceData = new BalanceDataViewModel { Token = token, Address = address }; + BalanceDataList.Add(balanceData); + + await Task.Delay(50); + + balanceData.BalanceDataLoader.DataLoader.LoadAsync(async () => + { + var balanceData = await tokenClient.GetBalance(token.TokenId, address); + return balanceData; + }, (x) => + { + balanceData.BalanceDataLoader.Data = x; + TokenTransferList.ForcePropertyChanged(); + }); + + + + } + } + + 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 SaveExplorerWallet() + { + 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; + + 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); + + 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; + + } + + 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); + } + } + } + + + } +} diff --git a/src/aoWebWallet/ViewModels/WalletDetailsViewModel.cs b/src/aoWebWallet/ViewModels/WalletDetailsViewModel.cs new file mode 100644 index 0000000..e849a3c --- /dev/null +++ b/src/aoWebWallet/ViewModels/WalletDetailsViewModel.cs @@ -0,0 +1,53 @@ +using aoWebWallet.Models; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace aoWebWallet.ViewModels +{ + public partial class WalletDetailsViewModel : ObservableObject + { + public WalletDetailsViewModel(Wallet wallet) + { + Wallet = wallet; + SetCanSend(); + } + + public Wallet Wallet { get; set; } + + [ObservableProperty] + private bool isConnected; + + [ObservableProperty] + private bool ownerCanSend; + + [ObservableProperty] + private bool canSend; + + partial void OnIsConnectedChanged(bool value) + { + SetCanSend(); + } + + partial void OnOwnerCanSendChanged(bool value) + { + SetCanSend(); + } + + private void SetCanSend() + { + var result = Wallet.Source switch + { + WalletTypes.None => false, + WalletTypes.ArConnect => IsConnected, + WalletTypes.Manual => !string.IsNullOrEmpty(Wallet.OwnerAddress) && OwnerCanSend, + WalletTypes.Explorer => !string.IsNullOrEmpty(Wallet.OwnerAddress) && OwnerCanSend, + WalletTypes.AoProcess => !string.IsNullOrEmpty(Wallet.OwnerAddress) && OwnerCanSend, + WalletTypes.Generated => !string.IsNullOrEmpty(Wallet.Jwk), + WalletTypes.Imported => !string.IsNullOrEmpty(Wallet.Jwk), + _ => false + }; + + CanSend = result; + + } + } +} diff --git a/src/aoWebWallet/ViewModels/WalletProcessDataViewModel.cs b/src/aoWebWallet/ViewModels/WalletProcessDataViewModel.cs index 677b1d0..ce7912a 100644 --- a/src/aoWebWallet/ViewModels/WalletProcessDataViewModel.cs +++ b/src/aoWebWallet/ViewModels/WalletProcessDataViewModel.cs @@ -1,4 +1,4 @@ -using aoWebWallet.Models; +using aoww.Services.Models; using ArweaveAO.Models.Token; namespace aoWebWallet.ViewModels diff --git a/src/aoWebWallet/_Imports.razor b/src/aoWebWallet/_Imports.razor index f47c765..ad39343 100644 --- a/src/aoWebWallet/_Imports.razor +++ b/src/aoWebWallet/_Imports.razor @@ -18,4 +18,5 @@ @using webvNext.DataLoader @using ArweaveAO @using ArweaveBlazor -@using aoWebWallet.Extensions \ No newline at end of file +@using aoWebWallet.Extensions +@using aoww.Services.Models \ No newline at end of file diff --git a/src/aoWebWallet/aoWebWallet.csproj b/src/aoWebWallet/aoWebWallet.csproj index 1aec625..d01605b 100644 --- a/src/aoWebWallet/aoWebWallet.csproj +++ b/src/aoWebWallet/aoWebWallet.csproj @@ -5,26 +5,43 @@ enable nullable enable - 0.2.0 + 0.4.0 false false true - - + + + - + - - + + + + + + + true + + + + + + + + + <_ContentIncludedByDefault Remove="Pages\ReceivePage.razor" /> + + diff --git a/src/aoWebWallet/wwwroot/css/app.css b/src/aoWebWallet/wwwroot/css/app.css index d53e212..6aa9bdb 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; } @@ -121,15 +122,25 @@ a, .btn-link { content: var(--blazor-load-percentage-text, "Loading"); } + + .aoww-main-menu { display: flex; flex-direction: column; - min-height: calc(100vh - 64px); + +} + +@media (min-width: 960px) { + .aoww-main-menu { + min-height: calc(100vh - 64px); + max-height: calc(100vh - 64px); + } } -@media (max-width: 960px) { +@media (max-width: 959px) { .aoww-main-menu { min-height: 100vh; + max-height: calc(100vh - 64px); } } @@ -203,4 +214,249 @@ button.mud-button-root.mud-icon-button.mud-ripple.mud-ripple-icon.copy-clipboard object-fit: contain; } +.d-min-h-vh-100 { + min-height: 100vh; +} + +.d-max-w-100 { + max-width: 100%; +} + +.d-w-100 { + width: 100%; +} + +.d-icon-wallet { + margin-left:2px; + width:20px; + padding-bottom:4px; +} + +.d-font-weight-500 { + font-weight: 500 !important; +} + +.d-overflow-hidden { + overflow: hidden; +} + +.d-custom-1 { + width:222px; + display:flex; + flex-direction: row; + align-items:center; + max-height: 50px; +} + +.d-custom-2 { + display:flex; + white-space: nowrap; +} + +.d-custom-3 { + margin-top:10px; + display:flex; + margin-left:auto; +} + +.d-custom-4 { + width:100%; + margin-left:5px; + display: flex; + align-items:center; + padding:8px; +} + +.d-custom-5 { + width:31px; + height:31px; +} + +.d-custom-6 { + border-radius:22px; + width:100%; + margin-top:5px; + 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%); + 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: auto; + +} + +.cursor-pointer { + cursor: pointer; +} + +.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%); +} + + + +.wallet-list.mud-paper { + background-color: rgba(222,222,222,0); +} + +.trigger-transparency.mud-paper { + background-color: rgba(222,222,222,0) !important; +} + +.trigger-transparency { + background-color: rgba(222,222,222,0) !important; +} + +.trigger-transparency .mud-input-control-input-container .mud-paper{ + background-color: rgba(222,222,222,0) !important; +} + + +.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; +} + +.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; + +} + +.ar-image-start { + width: 33px; + border-radius: 333px !important; + opacity: 0.55; + padding: 1.72rem 0; + animation:spin-clockwise 111s linear infinite; +} + +.ar-logo-setup { + padding: 2.4321rem; + opacity: 0.67; + +} + +@keyframes spin{ + from{transform:rotate(0deg)} + to{transform:rotate(360deg)} +} + +@keyframes spin-clockwise{ + from{transform:rotate(360deg)} + to{transform:rotate(0deg)} +} + +.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; +} + +.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, .mud-dialog { + background-color: rgba(16,16,67,1) !important; +} + +.float-right { + float:right; +} + + +.hover-here:hover > svg.float-right { + background: white; +} + +.mud-chip.aos-chip, .mud-chip.aos-chip:hover { + top:-15px; right:-15px; scale:0.67; background:white; font-weight: 900; padding: 5px; + position: absolute; +} + +.mud-breadcrumbs.breadcrumbs-aoww { + padding-bottom: 0; +} + + +@media only screen and (max-width: 600px) { + .flex-row.send-receive-buttons-mobile { + flex-direction: column-reverse !important; + + + + } + + .flex-row.send-receive-buttons-mobile button{ + margin-left:0 !important; + margin-bottom: 4px; + } +} diff --git a/src/aoWebWallet/wwwroot/images/account--1.svg b/src/aoWebWallet/wwwroot/images/account--1.svg deleted file mode 100644 index 0f5eabf..0000000 --- a/src/aoWebWallet/wwwroot/images/account--1.svg +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/aoWebWallet/wwwroot/images/account--2.svg b/src/aoWebWallet/wwwroot/images/account--2.svg deleted file mode 100644 index 3b973e4..0000000 --- a/src/aoWebWallet/wwwroot/images/account--2.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/src/aoWebWallet/wwwroot/images/account--3.svg b/src/aoWebWallet/wwwroot/images/account--3.svg deleted file mode 100644 index 03fd0b7..0000000 --- a/src/aoWebWallet/wwwroot/images/account--3.svg +++ /dev/null @@ -1,184 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/aoWebWallet/wwwroot/images/account--4.svg b/src/aoWebWallet/wwwroot/images/account--4.svg deleted file mode 100644 index cc36913..0000000 --- a/src/aoWebWallet/wwwroot/images/account--4.svg +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/aoWebWallet/wwwroot/images/account--5.svg b/src/aoWebWallet/wwwroot/images/account--5.svg deleted file mode 100644 index ee02298..0000000 --- a/src/aoWebWallet/wwwroot/images/account--5.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - diff --git a/src/aoWebWallet/wwwroot/images/aoww.svg b/src/aoWebWallet/wwwroot/images/aoww.svg new file mode 100644 index 0000000..dc710cf --- /dev/null +++ b/src/aoWebWallet/wwwroot/images/aoww.svg @@ -0,0 +1,268 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/aoWebWallet/wwwroot/images/arconnect-logo.svg b/src/aoWebWallet/wwwroot/images/arconnect-logo.svg new file mode 100644 index 0000000..eae3d78 --- /dev/null +++ b/src/aoWebWallet/wwwroot/images/arconnect-logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/src/aoWebWallet/wwwroot/images/discord.svg b/src/aoWebWallet/wwwroot/images/discord.svg new file mode 100644 index 0000000..cded9c1 --- /dev/null +++ b/src/aoWebWallet/wwwroot/images/discord.svg @@ -0,0 +1,14 @@ + + + + + + + + + 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 @@ + + + + + + + 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 0000000..8cbb28e Binary files /dev/null and b/src/aoWebWallet/wwwroot/images/origin-icon-base.png differ diff --git a/src/aoWebWallet/wwwroot/images/social.jpg b/src/aoWebWallet/wwwroot/images/social.jpg index fa1a9f2..9763020 100644 Binary files a/src/aoWebWallet/wwwroot/images/social.jpg and b/src/aoWebWallet/wwwroot/images/social.jpg differ diff --git a/src/aoWebWallet/wwwroot/images/ths.svg b/src/aoWebWallet/wwwroot/images/ths.svg new file mode 100644 index 0000000..052fbfd --- /dev/null +++ b/src/aoWebWallet/wwwroot/images/ths.svg @@ -0,0 +1,303 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/aoWebWallet/wwwroot/images/twitter.svg b/src/aoWebWallet/wwwroot/images/twitter.svg new file mode 100644 index 0000000..5905b05 --- /dev/null +++ b/src/aoWebWallet/wwwroot/images/twitter.svg @@ -0,0 +1,34 @@ + + + + + + diff --git a/src/aoWebWallet/wwwroot/index.html b/src/aoWebWallet/wwwroot/index.html index 8ff86ed..adc78e6 100644 --- a/src/aoWebWallet/wwwroot/index.html +++ b/src/aoWebWallet/wwwroot/index.html @@ -19,6 +19,7 @@ + + + diff --git a/src/aoww.Services.Tests/GraphqlTests.cs b/src/aoww.Services.Tests/GraphqlTests.cs new file mode 100644 index 0000000..7ea69b9 --- /dev/null +++ b/src/aoww.Services.Tests/GraphqlTests.cs @@ -0,0 +1,29 @@ +using aoww.Services.Models; +using Microsoft.Extensions.Options; + +namespace aoww.Services.Tests +{ + [TestClass] + public class GraphqlTests + { + [TestMethod] + public async Task GetTransactionsTest() + { + var graph = new GraphqlClient(new HttpClient(), Options.Create(new())); + + var result = await graph.GetTokenTransfersIn("4NdFkWsgFQIEmJnzFSYrO88UmRPf0ABfVh_fRc2u130"); + + Assert.IsNotNull(result); + } + + [TestMethod] + public async Task GetMintTest() + { + var graph = new GraphqlClient(new HttpClient(), Options.Create(new())); + + var result = await graph.GetTokenTransfersIn("CeiYr2VjUVAFXmPJvfj-Pfk6zmprBzeqNeRWAbImbOo"); + + Assert.IsNotNull(result); + } + } +} \ No newline at end of file diff --git a/src/aoww.Services.Tests/aoww.Services.Tests.csproj b/src/aoww.Services.Tests/aoww.Services.Tests.csproj new file mode 100644 index 0000000..68052eb --- /dev/null +++ b/src/aoww.Services.Tests/aoww.Services.Tests.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/src/aoww.Services/Enums/TokenTransferType.cs b/src/aoww.Services/Enums/TokenTransferType.cs new file mode 100644 index 0000000..f4305b0 --- /dev/null +++ b/src/aoww.Services/Enums/TokenTransferType.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace aoww.Services.Enums +{ + public enum TokenTransferType + { + Transfer, + Mint + } +} diff --git a/src/aoww.Services/GraphqlClient.cs b/src/aoww.Services/GraphqlClient.cs new file mode 100644 index 0000000..184f9ef --- /dev/null +++ b/src/aoww.Services/GraphqlClient.cs @@ -0,0 +1,521 @@ +using aoww.Services.Models; +using Microsoft.Extensions.Options; +using System.Net.Http.Json; + +namespace aoww.Services +{ + /// + /// https://arweave.net/graphql + /// + public class GraphqlClient + { + private readonly HttpClient httpClient; + private readonly GraphqlConfig config; + + public GraphqlClient(HttpClient httpClient, IOptions config) + { + this.httpClient = httpClient; + this.config = config.Value; + } + + public async Task> GetTransactionsIn(string address, string? cursor = null) + { + string query = $$""" + query { + transactions( + first: 50 + after: "{{cursor}}" + sort: HEIGHT_DESC + tags: [ + { name: "Data-Protocol", values: ["ao"] } + { name: "Recipient", values: ["{{address}}"] } + ] + ) { + edges { + cursor + node { + id + recipient + owner { + address + } + block { + timestamp + height + } + tags { + name + value + } + } + } + } + } + + """; + var queryResult = await PostQueryAsync(query); + + var result = new List(); + + foreach (var edge in queryResult?.Data?.Transactions?.Edges ?? new()) + { + AoTransaction? transaction = GetAoTransaction(edge); + + if (transaction != null) + result.Add(transaction); + } + + return result; + } + + + public async Task> GetTokenTransfersIn(string address, string? cursor = null) + { + string query = $$""" + query { + transactions( + first: 50 + after: "{{cursor}}" + sort: HEIGHT_DESC + tags: [ + { name: "Data-Protocol", values: ["ao"] } + { name: "Action", values: ["Transfer", "Mint-Token"] } + { name: "Recipient", values: ["{{address}}"] } + ] + ) { + edges { + cursor + node { + id + recipient + owner { + address + } + block { + timestamp + height + } + tags { + name + value + } + } + } + } + } + + """; + var queryResult = await PostQueryAsync(query); + + var result = new List(); + + foreach (var edge in queryResult?.Data?.Transactions?.Edges ?? new()) + { + TokenTransfer? transaction = GetTokenTransfer(edge); + + if (transaction != null) + result.Add(transaction); + } + + return result; + } + + private static AoTransaction? GetAoTransaction(Edge edge) + { + if (edge == null || edge.Node == null) + return null; + + var transaction = new AoTransaction() + { + Id = edge.Node.Id, + Cursor = edge.Cursor, + From = edge.Node.Owner?.Address ?? string.Empty, + }; + + if (edge.Node.Block != null) + { + transaction.Timestamp = DateTimeOffset.FromUnixTimeSeconds(edge.Node.Block.Timestamp); + transaction.BlockHeight = edge.Node.Block.Height; + } + else + transaction.Timestamp = DateTimeOffset.UtcNow; + + var fromProcess = edge.Node.Tags.Where(x => x.Name == "From-Process").Select(x => x.Value).FirstOrDefault(); + if (!string.IsNullOrEmpty(fromProcess)) + transaction.From = fromProcess; + + + transaction.To = edge.Node.Tags.Where(x => x.Name == "Recipient").Select(x => x.Value).FirstOrDefault(); + + return transaction; + } + + private static TokenTransfer? GetTokenTransfer(Edge edge) + { + if (edge == null || edge.Node == null) + return null; + + var isTransfer = edge.Node.Tags.Where(x => x.Name == "Action" && x.Value == "Transfer").Any(); + var isMint = edge.Node.Tags.Where(x => x.Name == "Action" && x.Value == "Mint-Token").Any(); + if (!isTransfer && !isMint) + return null; + + var transaction = new TokenTransfer() + { + Id = edge.Node.Id, + Cursor = edge.Cursor, + From = edge.Node.Owner?.Address ?? string.Empty, + TokenTransferType = Enums.TokenTransferType.Transfer + }; + + if (isMint) + transaction.TokenTransferType = Enums.TokenTransferType.Mint; + + + if (edge.Node.Block != null) + { + transaction.Timestamp = DateTimeOffset.FromUnixTimeSeconds(edge.Node.Block.Timestamp); + transaction.BlockHeight = edge.Node.Block.Height; + } + else + transaction.Timestamp = DateTimeOffset.UtcNow; + + var fromProcess = edge.Node.Tags.Where(x => x.Name == "From-Process").Select(x => x.Value).FirstOrDefault(); + if (!string.IsNullOrEmpty(fromProcess)) + transaction.From = fromProcess; + + if (isMint) + transaction.TokenId = edge.Node.Tags.Where(x => x.Name == "TokenId").Select(x => x.Value).FirstOrDefault(); + else + transaction.TokenId = edge.Node.Recipient; + + transaction.To = edge.Node.Tags.Where(x => x.Name == "Recipient").Select(x => x.Value).FirstOrDefault(); + + string? quantity = edge.Node.Tags.Where(x => x.Name == "Quantity").Select(x => x.Value).FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(quantity) && long.TryParse(quantity, out long quantityLong)) + transaction.Quantity = quantityLong; + return transaction; + } + + private static AoProcessInfo? GetAoProcessInfo(Edge edge) + { + if (edge == null || edge.Node == null) + return null; + + + var name = edge.Node.Tags.Where(x => x.Name == "Name").Select(x => x.Value).FirstOrDefault(); + if (string.IsNullOrEmpty(name)) + return null; + + var processInfo = new AoProcessInfo() + { + Id = edge.Node.Id, + Owner = edge.Node.Owner?.Address, + Name = name, + }; + + processInfo.Version = edge.Node.Tags.Where(x => x.Name == "Version").Select(x => x.Value).FirstOrDefault(); + + return processInfo; + } + + public async Task> GetTransactionsOut(string address, string? cursor = null) + { + string query = $$""" + query { + transactions( + first: 50 + after: "{{cursor}}" + sort: HEIGHT_DESC + owners: ["{{address}}"] + tags: [ + { name: "Data-Protocol", values: ["ao"] } + { name: "Action", values: ["Transfer"] } + ] + ) { + edges { + cursor + node { + id + recipient + owner { + address + } + block { + timestamp + height + } + tags { + name + value + } + } + } + } + } + """; + var queryResult = await PostQueryAsync(query); + + var result = new List(); + + foreach (var edge in queryResult?.Data?.Transactions?.Edges ?? new()) + { + TokenTransfer? transaction = GetTokenTransfer(edge); + + if (transaction != null) + result.Add(transaction); + } + + return result; + } + + public async Task> GetTransactionsOutFromProcess(string address, string? cursor = null) + { + string query = $$""" + query { + transactions( + first: 50 + after: "{{cursor}}" + sort: HEIGHT_DESC + tags: [ + { name: "From-Process", values: ["{{address}}"] } + { name: "Data-Protocol", values: ["ao"] } + { name: "Action", values: ["Transfer"] } + ] + ) { + edges { + cursor + node { + id + recipient + owner { + address + } + block { + timestamp + height + } + tags { + name + value + } + } + } + } + } + + """; + var queryResult = await PostQueryAsync(query); + + var result = new List(); + + foreach (var edge in queryResult?.Data?.Transactions?.Edges ?? new()) + { + TokenTransfer? transaction = GetTokenTransfer(edge); + + if (transaction != null) + result.Add(transaction); + } + + return result; + } + + public async Task GetTransactionsById(string txId) + { + string query = $$""" + query { + transactions( + first: 1 + sort: HEIGHT_DESC + ids: ["{{txId}}"] + tags: [ + { name: "Data-Protocol", values: ["ao"] } + { name: "Action", values: ["Transfer"] } + ] + ) { + edges { + node { + id + recipient + owner { + address + } + block { + timestamp + height + } + tags { + name + value + } + } + } + } + } + """; + var queryResult = await PostQueryAsync(query); + + var result = new List(); + + foreach (var edge in queryResult?.Data?.Transactions?.Edges ?? new()) + { + TokenTransfer? transaction = GetTokenTransfer(edge); + + if (transaction != null) + result.Add(transaction); + } + + return result.FirstOrDefault(); + } + + public async Task> GetTransactionsForToken(string tokenId, string? cursor = null) + { + string query = $$""" + query { + transactions( + first: 50 + after: "{{cursor}}" + sort: HEIGHT_DESC + recipients: ["{{tokenId}}"] + tags: [ + { name: "Data-Protocol", values: ["ao"] } + { name: "Action", values: ["Transfer"] } + ] + ) { + edges { + cursor + node { + id + recipient + owner { + address + } + block { + timestamp + height + } + tags { + name + value + } + } + } + } + } + """; + var queryResult = await PostQueryAsync(query); + + var result = new List(); + + foreach (var edge in queryResult?.Data?.Transactions?.Edges ?? new()) + { + TokenTransfer? transaction = GetTokenTransfer(edge); + + if (transaction != null) + result.Add(transaction); + } + + return result; + } + + public async Task> GetAoProcessesForAddress(string address) + { + string query = $$""" + query { + transactions( + first: 100 + owners: ["{{address}}"] + tags: [ + { name: "Data-Protocol", values: ["ao"] } + { name: "Type", values: ["Process"] } + { name: "App-Name", values: ["aos"] } + ] + ) { + edges { + node { + id + tags { + name + value + } + } + } + } + } + """; + var queryResult = await PostQueryAsync(query); + + var result = new List(); + + foreach (var edge in queryResult?.Data?.Transactions?.Edges ?? new()) + { + AoProcessInfo? processInfo = GetAoProcessInfo(edge); + + if (processInfo != null) + result.Add(processInfo); + } + + return result; + } + + public async Task GetOwnerForAoProcessAddress(string address) + { + string query = $$""" + query { + transactions( + first: 50 + ids: ["{{address}}"] + tags: [ + { name: "Data-Protocol", values: ["ao"] } + { name: "Type", values: ["Process"] } + { name: "App-Name", values: ["aos"] } + ] + ) { + edges { + node { + id + owner { + address + } + tags { + name + value + } + } + } + } + } + """; + var queryResult = await PostQueryAsync(query); + + var result = new List(); + + foreach (var edge in queryResult?.Data?.Transactions?.Edges ?? new()) + { + AoProcessInfo? processInfo = GetAoProcessInfo(edge); + + if (processInfo != null) + result.Add(processInfo); + } + + return result.FirstOrDefault(); + } + + protected async Task PostQueryAsync(string query) + { + var request = new GraphqlRequest { Query = query }; + + HttpResponseMessage res = await httpClient.PostAsJsonAsync(config.ApiUrl, request); + if (res.IsSuccessStatusCode) + { + return await res.Content.ReadFromJsonAsync(); + } + else + { + string msg = await res.Content.ReadAsStringAsync(); + Console.WriteLine(msg); + throw new Exception(msg); + } + } + } +} diff --git a/src/aoWebWallet/Models/AoProcessInfo.cs b/src/aoww.Services/Models/AoProcessInfo.cs similarity index 71% rename from src/aoWebWallet/Models/AoProcessInfo.cs rename to src/aoww.Services/Models/AoProcessInfo.cs index ce9ed6c..5dae333 100644 --- a/src/aoWebWallet/Models/AoProcessInfo.cs +++ b/src/aoww.Services/Models/AoProcessInfo.cs @@ -1,9 +1,10 @@ -namespace aoWebWallet.Models +namespace aoww.Services.Models { public class AoProcessInfo { public required string Id { get; set; } public required string Name { get; set; } public string? Version { get; set; } + public string? Owner { get; set; } } } diff --git a/src/aoWebWallet/Models/TokenTransfer.cs b/src/aoww.Services/Models/AoTransaction.cs similarity index 67% rename from src/aoWebWallet/Models/TokenTransfer.cs rename to src/aoww.Services/Models/AoTransaction.cs index f669241..f15646d 100644 --- a/src/aoWebWallet/Models/TokenTransfer.cs +++ b/src/aoww.Services/Models/AoTransaction.cs @@ -3,17 +3,21 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using aoww.Services.Enums; -namespace aoWebWallet.Models +namespace aoww.Services.Models { - public class TokenTransfer + public class AoTransaction { public required string Id { get; set; } public DateTimeOffset Timestamp { get; set; } public int? BlockHeight { get; set; } - public string? TokenId { get; set; } public required string From { get; set; } public string? To { get; set; } - public long Quantity { get; set; } + + public string? Cursor { get; set; } + + //public string? Data { get; set; } + } } 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/aoWebWallet/Models/GraphqlRequest.cs b/src/aoww.Services/Models/GraphqlRequest.cs similarity index 74% rename from src/aoWebWallet/Models/GraphqlRequest.cs rename to src/aoww.Services/Models/GraphqlRequest.cs index 597b592..2698548 100644 --- a/src/aoWebWallet/Models/GraphqlRequest.cs +++ b/src/aoww.Services/Models/GraphqlRequest.cs @@ -1,4 +1,4 @@ -namespace aoWebWallet.Models +namespace aoww.Services.Models { public class GraphqlRequest { diff --git a/src/aoWebWallet/Models/GraphqlResponse.cs b/src/aoww.Services/Models/GraphqlResponse.cs similarity index 88% rename from src/aoWebWallet/Models/GraphqlResponse.cs rename to src/aoww.Services/Models/GraphqlResponse.cs index 85f42d8..4e8282f 100644 --- a/src/aoWebWallet/Models/GraphqlResponse.cs +++ b/src/aoww.Services/Models/GraphqlResponse.cs @@ -1,9 +1,6 @@ -using ArweaveAO.Models; -using MudBlazor; -using System.Text.Json.Serialization; -using static System.Runtime.InteropServices.JavaScript.JSType; +using System.Text.Json.Serialization; -namespace aoWebWallet.Models +namespace aoww.Services.Models { public class GraphqlResponse { @@ -29,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/Tag.cs b/src/aoww.Services/Models/Tag.cs new file mode 100644 index 0000000..583be53 --- /dev/null +++ b/src/aoww.Services/Models/Tag.cs @@ -0,0 +1,15 @@ +using System.Diagnostics; +using System.Text.Json.Serialization; + +namespace aoww.Services.Models +{ + [DebuggerDisplay("{Name}: {Value}")] + public class Tag + { + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("value")] + public required string Value { get; set; } + } +} diff --git a/src/aoww.Services/Models/TokenTransfer.cs b/src/aoww.Services/Models/TokenTransfer.cs new file mode 100644 index 0000000..c2eb091 --- /dev/null +++ b/src/aoww.Services/Models/TokenTransfer.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using aoww.Services.Enums; + +namespace aoww.Services.Models +{ + public class TokenTransfer : AoTransaction + { + public string? TokenId { get; set; } + public long Quantity { get; set; } + + public TokenTransferType TokenTransferType { get; set; } + } +} diff --git a/src/aoww.Services/aoww.Services.csproj b/src/aoww.Services/aoww.Services.csproj new file mode 100644 index 0000000..cc6a561 --- /dev/null +++ b/src/aoww.Services/aoww.Services.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/src/webvNext.DataLoader/DataLoader.cs b/src/webvNext.DataLoader/DataLoader.cs index 0d58d22..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; @@ -57,36 +54,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 //{