diff --git a/.github/workflows/gui.yaml b/.github/workflows/gui.yaml
new file mode 100644
index 0000000..0cec6a9
--- /dev/null
+++ b/.github/workflows/gui.yaml
@@ -0,0 +1,69 @@
+name: gui
+
+on: [push, pull_request]
+
+jobs:
+
+ linux:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v1
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v2
+ with:
+ dotnet-version: 6.0.x
+ - name: Install dependencies
+ working-directory: ./gui
+ run: dotnet restore
+ - name: Publish
+ working-directory: ./gui
+ run: dotnet publish -c Release -r linux-x64 --self-contained true /p:PublishSingleFile=true /p:EnableCompressionInSingleFile=true /p:IncludeNativeLibrariesForSelfExtract=true /p:PublishTrimmed=true
+ - name: Archive publish artifacts
+ uses: actions/upload-artifact@v3
+ with:
+ name: maskcore_linux-x64
+ path: gui/bin/Release/net6.0/linux-x64/publish
+
+ win:
+ runs-on: windows-latest
+
+ steps:
+ - uses: actions/checkout@v1
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v2
+ with:
+ dotnet-version: 6.0.x
+ - name: Install dependencies
+ working-directory: ./gui
+ run: dotnet restore
+ - name: Publish
+ working-directory: ./gui
+ run: dotnet publish -c Release -r win-x64 --self-contained true /p:PublishSingleFile=true /p:EnableCompressionInSingleFile=true /p:IncludeNativeLibrariesForSelfExtract=true /p:PublishTrimmed=true
+ - name: Archive publish artifacts
+ uses: actions/upload-artifact@v3
+ with:
+ name: maskcore_win-x64
+ path: gui/bin/Release/net6.0/win-x64/publish
+
+ osx:
+ runs-on: macos-latest
+
+ steps:
+ - uses: actions/checkout@v1
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v2
+ with:
+ dotnet-version: 6.0.x
+ - name: Install dependencies
+ working-directory: ./gui
+ run: dotnet restore -r osx-x64
+ - name: Publish
+ working-directory: ./gui
+ run: dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-x64 -property:Configuration=Release -p:UseAppHost=true -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishTrimmed=true -p:SelfContained=true
+ - name: Archive publish artifacts
+ uses: actions/upload-artifact@v3
+ with:
+ name: maskcore_osx-x64
+ path: gui/bin/Release/net6.0/osx-x64/publish
diff --git a/gui/.gitignore b/gui/.gitignore
new file mode 100644
index 0000000..8afdcb6
--- /dev/null
+++ b/gui/.gitignore
@@ -0,0 +1,454 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# Tye
+.tye/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+##
+## Visual studio for Mac
+##
+
+
+# globs
+Makefile.in
+*.userprefs
+*.usertasks
+config.make
+config.status
+aclocal.m4
+install-sh
+autom4te.cache/
+*.tar.gz
+tarballs/
+test-results/
+
+# Mac bundle stuff
+*.dmg
+*.app
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+##
+## Visual Studio Code
+##
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/gui/App.axaml b/gui/App.axaml
new file mode 100644
index 0000000..93f1a15
--- /dev/null
+++ b/gui/App.axaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gui/App.axaml.cs b/gui/App.axaml.cs
new file mode 100644
index 0000000..d938542
--- /dev/null
+++ b/gui/App.axaml.cs
@@ -0,0 +1,61 @@
+using System;
+using System.IO;
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using Dimension.MaskCore.Common;
+using Dimension.MaskCore.Data.Repository;
+using Dimension.MaskCore.UI.Shell;
+using Microsoft.Extensions.DependencyInjection;
+using Realms;
+
+namespace Dimension.MaskCore;
+
+internal class App : Application
+{
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+ EnsureWorkingDirectory();
+ Ioc.Default.ConfigureServices(ConfigureServices());
+ }
+
+ private void EnsureWorkingDirectory()
+ {
+ if (!Directory.Exists(Consts.ConfigDirectory))
+ {
+ Directory.CreateDirectory(Consts.ConfigDirectory);
+ }
+ }
+
+ private static IServiceProvider ConfigureServices()
+ {
+ var services = new ServiceCollection();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton(_ =>
+ Realm.GetInstance(new RealmConfiguration(Path.Combine(Consts.ConfigDirectory, ".realm"))
+ { ShouldDeleteIfMigrationNeeded = true }));
+ return services.BuildServiceProvider();
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ switch (ApplicationLifetime)
+ {
+ case IClassicDesktopStyleApplicationLifetime classicDesktopStyleApplicationLifetime:
+ classicDesktopStyleApplicationLifetime.MainWindow = new RootWindow();
+ classicDesktopStyleApplicationLifetime.Exit += (_, _) =>
+ {
+ Ioc.Default.GetRequiredService().Dispose();
+ };
+ break;
+ case ISingleViewApplicationLifetime singleViewApplicationLifetime:
+ singleViewApplicationLifetime.MainView = new RootShell();
+ break;
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+}
\ No newline at end of file
diff --git a/gui/Assets/favicon.ico b/gui/Assets/favicon.ico
new file mode 100644
index 0000000..6c09952
Binary files /dev/null and b/gui/Assets/favicon.ico differ
diff --git a/gui/Common/Consts.cs b/gui/Common/Consts.cs
new file mode 100644
index 0000000..0b59e54
--- /dev/null
+++ b/gui/Common/Consts.cs
@@ -0,0 +1,25 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace Dimension.MaskCore.Common;
+
+internal class Consts
+{
+ public static string ConfigDirectory
+ {
+ get
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
+ "Mask");
+ }
+
+ return Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
+ ".mask");
+ }
+ }
+}
\ No newline at end of file
diff --git a/gui/Common/Extension/EnumerabsleExtension.cs b/gui/Common/Extension/EnumerabsleExtension.cs
new file mode 100644
index 0000000..a6af59e
--- /dev/null
+++ b/gui/Common/Extension/EnumerabsleExtension.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Dimension.MaskCore.Common.Extension;
+
+internal static class EnumerabsleExtension
+{
+ public static IEnumerable Randomize(this IEnumerable source)
+ {
+ var rnd = new Random();
+ return source.OrderBy(item => rnd.Next());
+ }
+}
\ No newline at end of file
diff --git a/gui/Common/Extension/RxRealmExtension.cs b/gui/Common/Extension/RxRealmExtension.cs
new file mode 100644
index 0000000..a088596
--- /dev/null
+++ b/gui/Common/Extension/RxRealmExtension.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Linq;
+using Realms;
+
+namespace Dimension.MaskCore.Common.Extension;
+
+public static class RxRealmExtension
+{
+ public static IObservable> AsObservable(this IQueryable query) where T : RealmObjectBase
+ {
+ return Observable.Create>(emitter => query.SubscribeForNotifications((sender, _, error) =>
+ {
+ if (error != null)
+ {
+ emitter.OnError(error);
+ }
+ else
+ {
+ emitter.OnNext(sender);
+ }
+ }));
+ }
+}
\ No newline at end of file
diff --git a/gui/Common/Helpers/MnemonicHelper.cs b/gui/Common/Helpers/MnemonicHelper.cs
new file mode 100644
index 0000000..49cc583
--- /dev/null
+++ b/gui/Common/Helpers/MnemonicHelper.cs
@@ -0,0 +1,11 @@
+using Dimension.MaskWalletCore;
+
+namespace Dimension.MaskCore.Common.Helpers;
+
+internal static class MnemonicHelper
+{
+ public static string GenerateMnemonic()
+ {
+ return WalletKey.GenerateMnemonic();
+ }
+}
\ No newline at end of file
diff --git a/gui/Data/Model/DbPersonaModel.cs b/gui/Data/Model/DbPersonaModel.cs
new file mode 100644
index 0000000..1f74096
--- /dev/null
+++ b/gui/Data/Model/DbPersonaModel.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Text.Json;
+using Dimension.MaskWalletCore;
+using MongoDB.Bson;
+using Realms;
+
+namespace Dimension.MaskCore.Data.Model;
+
+internal class DbPersonaModel : RealmObject
+{
+ [PrimaryKey] public ObjectId Id { get; set; } = ObjectId.GenerateNewId();
+ [Required] public string Name { get; set; } = string.Empty;
+ [Required] public string Identifier { get; set; } = string.Empty;
+ [Required] public string Mnemonic { get; set; } = string.Empty;
+ [Required] public string Path { get; set; } = string.Empty;
+ public bool WithPassword { get; set; }
+ [Required] public string Password { get; set; } = string.Empty;
+ [Required] public string PrivateKey { get; set; } = string.Empty;
+ [Required] public string PublicKey { get; set; } = string.Empty;
+ public string? LocalKey { get; set; }
+ public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
+ public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
+
+ public static DbPersonaModel FromPersona(PersonaKey persona, string mnemonic, string path, string password,
+ bool withPassword, string name)
+ {
+ return new DbPersonaModel
+ {
+ Name = name,
+ Identifier = persona.Identifier,
+ Mnemonic = mnemonic,
+ Path = path,
+ WithPassword = withPassword,
+ Password = password,
+ PrivateKey = JsonSerializer.Serialize(persona.PrivateKey),
+ PublicKey = JsonSerializer.Serialize(persona.PrivateKey with { d = null }),
+ LocalKey = persona.LocalKey == null ? null : JsonSerializer.Serialize(persona.LocalKey)
+ };
+ }
+}
\ No newline at end of file
diff --git a/gui/Data/Model/DbWalletModel.cs b/gui/Data/Model/DbWalletModel.cs
new file mode 100644
index 0000000..caa8d25
--- /dev/null
+++ b/gui/Data/Model/DbWalletModel.cs
@@ -0,0 +1,25 @@
+using System;
+using Dimension.MaskCore.Model;
+using MongoDB.Bson;
+using Realms;
+
+namespace Dimension.MaskCore.Data.Model;
+
+internal class DbWalletModel : RealmObject
+{
+ [PrimaryKey] public ObjectId Id { get; set; } = ObjectId.GenerateNewId();
+ [Required] public string Name { get; set; } = string.Empty;
+ [Required] public string Address { get; set; } = string.Empty;
+ public string DerivationPath { get; set; } = string.Empty;
+ public byte[] Data { get; set; } = Array.Empty();
+ private string PlatformTypeRaw { get; set; } = string.Empty;
+
+ public PlatformType PlatformType
+ {
+ get => Enum.TryParse(PlatformTypeRaw, out PlatformType result) ? result : PlatformType.Ethereum;
+ set => PlatformTypeRaw = value.ToString();
+ }
+
+ public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
+ public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
+}
\ No newline at end of file
diff --git a/gui/Data/Repository/PersonaRepository.cs b/gui/Data/Repository/PersonaRepository.cs
new file mode 100644
index 0000000..38eaade
--- /dev/null
+++ b/gui/Data/Repository/PersonaRepository.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using Dimension.MaskCore.Common.Extension;
+using Dimension.MaskCore.Data.Model;
+using Dimension.MaskCore.UI.Model;
+using Dimension.MaskWalletCore;
+using Realms;
+
+namespace Dimension.MaskCore.Data.Repository;
+
+internal class PersonaRepository
+{
+ private const string Path = "m/44'/60'/0'/0/0";
+ private const string Password = "";
+
+ private static readonly Realm _realm = Ioc.Default.GetRequiredService();
+
+ public IObservable> Personas { get; } = _realm.All()
+ .AsObservable()
+ .Select(it => it.Select(UiPersonaModel.FromDb).ToImmutableList());
+
+ public async Task CreatePersona(string name, string mnemonic)
+ {
+ if (string.IsNullOrEmpty(mnemonic) || string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentNullException(nameof(mnemonic));
+ }
+
+ if (_realm.All().Any(it => it.Mnemonic == mnemonic))
+ {
+ throw new ArgumentException($"Persona with name {name} already exists");
+ }
+
+ var persona = await Task.Run(() => PersonaKey.Create(
+ mnemonic,
+ Password,
+ Path,
+ CurveType.Secp256k1,
+ new EncryptionOption(EncryptionOption.EncVersion.V38)
+ ));
+ _realm.Write(() => { _realm.Add(DbPersonaModel.FromPersona(persona, mnemonic, Path, Password, false, name)); });
+ }
+
+ public void UpdatePersonaName(string identifier, string name)
+ {
+ var dbPersona = _realm.All().FirstOrDefault(it => it.Identifier == identifier);
+ if (dbPersona == null)
+ {
+ return;
+ }
+
+ _realm.Write(() => { dbPersona.Name = name; });
+ }
+
+ public void DeletePersona(string identifier)
+ {
+ var item = _realm.All().FirstOrDefault(it => it.Identifier == identifier);
+ if (item == null)
+ {
+ return;
+ }
+
+ _realm.Write(() => _realm.Remove(item));
+ }
+}
\ No newline at end of file
diff --git a/gui/Data/Repository/WalletRepository.cs b/gui/Data/Repository/WalletRepository.cs
new file mode 100644
index 0000000..fc06b8c
--- /dev/null
+++ b/gui/Data/Repository/WalletRepository.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using Dimension.MaskCore.Common.Extension;
+using Dimension.MaskCore.Data.Model;
+using Dimension.MaskCore.Model;
+using Dimension.MaskCore.UI.Model;
+using Dimension.MaskWalletCore;
+using Realms;
+
+namespace Dimension.MaskCore.Data.Repository;
+
+internal class WalletRepository
+{
+ private const string Path = "m/44'/60'/0'/0/0";
+ private const string Password = "";
+
+ private static readonly Realm _realm = Ioc.Default.GetRequiredService();
+
+ public IObservable> Wallets { get; } = _realm.All()
+ .AsObservable()
+ .Select(list => list.Select(UiWalletModel.From).ToImmutableList());
+
+ public async Task CreateWallet(string name, string mnemonic, string password = Password)
+ {
+ var wallet = await Task.Run(() => WalletKey.FromMnemonic(mnemonic, password));
+ var account = await Task.Run(() => wallet.AddNewAccountAtPath(CoinType.Ethereum, Path, name, password));
+ if (_realm.All().Any(it => it.Address == account.Address))
+ {
+ return;
+ }
+
+ var item = new DbWalletModel
+ {
+ Name = name,
+ Address = account.Address,
+ DerivationPath = account.DerivationPath,
+ Data = wallet.Data,
+ PlatformType = PlatformType.Ethereum
+ };
+ _realm.Write(() => _realm.Add(item));
+ }
+
+ public void RenameWallet(string address, string name)
+ {
+ var item = _realm.All().FirstOrDefault(it => it.Address == address);
+ if (item == null)
+ {
+ return;
+ }
+
+ _realm.Write(() => item.Name = name);
+ }
+
+ public void DeleteWallet(string address)
+ {
+ var item = _realm.All().FirstOrDefault(it => it.Address == address);
+ if (item == null)
+ {
+ return;
+ }
+
+ _realm.Write(() => _realm.Remove(item));
+ }
+}
\ No newline at end of file
diff --git a/gui/FodyWeavers.xml b/gui/FodyWeavers.xml
new file mode 100644
index 0000000..ef11585
--- /dev/null
+++ b/gui/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/gui/Lifecycle/Controls/Dialog.cs b/gui/Lifecycle/Controls/Dialog.cs
new file mode 100644
index 0000000..bbd9eb3
--- /dev/null
+++ b/gui/Lifecycle/Controls/Dialog.cs
@@ -0,0 +1,25 @@
+using System;
+using Avalonia.LogicalTree;
+using Avalonia.Styling;
+using FluentAvalonia.UI.Controls;
+
+namespace Dimension.MaskCore.Lifecycle.Controls;
+
+public class Dialog : Dialog where T : ViewModel.ViewModel
+{
+ public T ViewModel => (DataContext as T)!;
+}
+
+public class Dialog : ContentDialog, IStyleable
+{
+ Type IStyleable.StyleKey => typeof(ContentDialog);
+
+ protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
+ {
+ base.OnDetachedFromLogicalTree(e);
+ if (DataContext is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/gui/Lifecycle/Controls/Page.cs b/gui/Lifecycle/Controls/Page.cs
new file mode 100644
index 0000000..3e6484a
--- /dev/null
+++ b/gui/Lifecycle/Controls/Page.cs
@@ -0,0 +1,115 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.LogicalTree;
+using Avalonia.VisualTree;
+using Dimension.MaskCore.Lifecycle.ViewModel;
+using FluentAvalonia.UI.Controls;
+using FluentAvalonia.UI.Media.Animation;
+using FluentAvalonia.UI.Navigation;
+
+namespace Dimension.MaskCore.Lifecycle.Controls;
+
+public class Page : Page
+ where TViewModel : ViewModel.ViewModel, new() where TParameter : class
+{
+ protected override void OnCreated(object parameter)
+ {
+ base.OnCreated(parameter);
+ if (parameter is TParameter r)
+ {
+ OnCreated(r);
+ }
+ }
+
+ protected virtual void OnCreated(TParameter parameter)
+ {
+ if (ViewModel is IParameterizedViewModel parameterizedViewModel)
+ {
+ parameterizedViewModel.Initialize(parameter);
+ }
+ }
+}
+
+public class Page : Page where T : ViewModel.ViewModel, new()
+{
+ public T ViewModel => (DataContext as T)!;
+}
+
+public class Page : UserControl
+{
+ protected Frame? Frame { get; private set; }
+
+ protected Window Window => this.FindAncestorOfType();
+
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+ Frame = this.FindAncestorOfType();
+ Frame.Navigated += OnNavigated;
+ }
+
+ private void OnNavigated(object sender, NavigationEventArgs e)
+ {
+ if (ReferenceEquals(e.Content, this))
+ {
+ switch (e.NavigationMode)
+ {
+ case NavigationMode.New:
+ OnCreated(e.Parameter);
+ break;
+ case NavigationMode.Back:
+ break;
+ case NavigationMode.Forward:
+ break;
+ case NavigationMode.Refresh:
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+ }
+
+ protected virtual void OnCreated(object parameter)
+ {
+ }
+
+ protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
+ {
+ base.OnDetachedFromLogicalTree(e);
+ if (DataContext is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+
+ if (Frame != null)
+ {
+ Frame.Navigated -= OnNavigated;
+ Frame = null;
+ }
+ }
+
+ protected void GoBack()
+ {
+ Frame?.GoBack(new SlideNavigationTransitionInfo
+ { Effect = SlideNavigationTransitionEffect.FromLeft });
+ }
+
+ protected void Navigate(object? parameter = null)
+ {
+ Frame?.Navigate(typeof(T), parameter, new SlideNavigationTransitionInfo());
+ }
+
+ protected void Navigate(Type page, object? parameter = null)
+ {
+ Frame?.NavigateToType(page, parameter, new FrameNavigationOptions
+ {
+ TransitionInfoOverride = new SlideNavigationTransitionInfo()
+ });
+ }
+
+ protected void ClearBackStack()
+ {
+ Frame?.BackStack?.Clear();
+ }
+}
\ No newline at end of file
diff --git a/gui/Lifecycle/ViewModel/ViewModel.cs b/gui/Lifecycle/ViewModel/ViewModel.cs
new file mode 100644
index 0000000..ffb3bcf
--- /dev/null
+++ b/gui/Lifecycle/ViewModel/ViewModel.cs
@@ -0,0 +1,30 @@
+using System;
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Dimension.MaskCore.Lifecycle.ViewModel;
+
+[ObservableObject]
+public abstract partial class ViewModel : IDisposable
+{
+ protected internal ViewModelScope Scope { get; } = new();
+
+ public void Dispose()
+ {
+ Scope.Dispose();
+ }
+}
+
+public interface IParameterizedViewModel
+{
+ void Initialize(T parameter);
+}
+
+public abstract class ParameterizedViewModel : ViewModel, IParameterizedViewModel
+{
+ public void Initialize(T parameter)
+ {
+ InitializeCore(parameter);
+ }
+
+ protected abstract void InitializeCore(T parameter);
+}
\ No newline at end of file
diff --git a/gui/Lifecycle/ViewModel/ViewModelScope.cs b/gui/Lifecycle/ViewModel/ViewModelScope.cs
new file mode 100644
index 0000000..701467e
--- /dev/null
+++ b/gui/Lifecycle/ViewModel/ViewModelScope.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+
+namespace Dimension.MaskCore.Lifecycle.ViewModel;
+
+public sealed class ViewModelScope : IDisposable
+{
+ private readonly List _disposables = new();
+
+ public void Dispose()
+ {
+ _disposables.ForEach(x => x.Dispose());
+ }
+
+ internal void Add(IDisposable disposable)
+ {
+ _disposables.Add(disposable);
+ }
+}
+
+public static class RxExtensions
+{
+ public static void SubscribeIn(this IObservable source, ViewModel viewModel, Action onNext)
+ {
+ viewModel.Scope.Add(source.Subscribe(onNext));
+ }
+}
\ No newline at end of file
diff --git a/gui/MaskCore.csproj b/gui/MaskCore.csproj
new file mode 100644
index 0000000..82b12fc
--- /dev/null
+++ b/gui/MaskCore.csproj
@@ -0,0 +1,49 @@
+
+
+ WinExe
+ net6.0
+ enable
+ Dimension.MaskCore
+ Assets/favicon.ico
+ 0.1.0
+ 0.1.0
+ com.dimension
+
+ copyused
+ true
+
+
+
+
+
+
+
+
+
+
+ 0.10.13
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gui/Model/PlatformType.cs b/gui/Model/PlatformType.cs
new file mode 100644
index 0000000..7010b93
--- /dev/null
+++ b/gui/Model/PlatformType.cs
@@ -0,0 +1,6 @@
+namespace Dimension.MaskCore.Model;
+
+internal enum PlatformType
+{
+ Ethereum
+}
\ No newline at end of file
diff --git a/gui/Program.cs b/gui/Program.cs
new file mode 100644
index 0000000..560a247
--- /dev/null
+++ b/gui/Program.cs
@@ -0,0 +1,27 @@
+using System;
+using Avalonia;
+using Avalonia.ReactiveUI;
+
+namespace Dimension.MaskCore;
+
+internal class Program
+{
+ [STAThread]
+ public static void Main(string[] args)
+ {
+ BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+ }
+
+ public static AppBuilder BuildAvaloniaApp()
+ {
+ return AppBuilder.Configure()
+ .UsePlatformDetect()
+ .LogToTrace()
+ .UseReactiveUI()
+ .With(new Win32PlatformOptions
+ {
+ UseWindowsUIComposition = true
+ });
+ }
+}
\ No newline at end of file
diff --git a/gui/UI/Controls/MnemonicValidateControl.axaml b/gui/UI/Controls/MnemonicValidateControl.axaml
new file mode 100644
index 0000000..4238ee3
--- /dev/null
+++ b/gui/UI/Controls/MnemonicValidateControl.axaml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gui/UI/Controls/MnemonicValidateControl.axaml.cs b/gui/UI/Controls/MnemonicValidateControl.axaml.cs
new file mode 100644
index 0000000..af6ff42
--- /dev/null
+++ b/gui/UI/Controls/MnemonicValidateControl.axaml.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Windows.Input;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Dimension.MaskCore.Common.Extension;
+using Dimension.MaskCore.UI.Model;
+using DynamicData;
+
+namespace Dimension.MaskCore.UI.Controls;
+
+internal partial class MnemonicValidateControl : UserControl
+{
+ public static readonly DirectProperty ConfirmCommandProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(ConfirmCommand), o => o.ConfirmCommand, (o, v) => o.ConfirmCommand = v);
+
+ public static readonly DirectProperty> MnemonicWordsProperty =
+ AvaloniaProperty.RegisterDirect>(
+ nameof(MnemonicWords), o => o.MnemonicWords, (o, v) => o.MnemonicWords = v);
+
+ public static readonly DirectProperty IsValidProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(IsValid),
+ o => o.IsValid);
+
+ private ICommand? _confirmCommand;
+ private bool _isValid;
+
+ private IReadOnlyCollection _mnemonicWords = new List();
+
+ public MnemonicValidateControl()
+ {
+ InitializeComponent();
+ }
+
+ public ICommand? ConfirmCommand
+ {
+ get => _confirmCommand;
+ set => SetAndRaise(ConfirmCommandProperty, ref _confirmCommand, value);
+ }
+
+ public IReadOnlyCollection MnemonicWords
+ {
+ get => _mnemonicWords;
+ set
+ {
+ SetAndRaise(MnemonicWordsProperty, ref _mnemonicWords, value);
+ SelectedMnemonic.Clear();
+ RandomMnemonic.Clear();
+ RandomMnemonic.AddRange(value
+ .Randomize()
+ .Select((word, index) => new MnemonicModel(word, index)));
+ }
+ }
+
+ public ObservableCollection SelectedMnemonic { get; set; } = new();
+ public ObservableCollection RandomMnemonic { get; set; } = new();
+
+ public bool IsValid
+ {
+ get => _isValid;
+ private set => SetAndRaise(IsValidProperty, ref _isValid, value);
+ }
+
+ private void CheckValid()
+ {
+ IsValid = SelectedMnemonic.Select(x => x.Word).SequenceEqual(MnemonicWords);
+ }
+
+ private void ClearClicked(object? sender, RoutedEventArgs e)
+ {
+ SelectedMnemonic.Clear();
+ RandomMnemonic.AddRange(MnemonicWords.Select((word, index) => new MnemonicModel(word, index)));
+ CheckValid();
+ }
+
+ private void AddWord(MnemonicModel item)
+ {
+ if (!RandomMnemonic.Contains(item))
+ {
+ return;
+ }
+
+ RandomMnemonic.Remove(item);
+ SelectedMnemonic.Add(item);
+ CheckValid();
+ }
+
+ private void RemoveWord(MnemonicModel item)
+ {
+ if (!SelectedMnemonic.Contains(item))
+ {
+ return;
+ }
+
+ SelectedMnemonic.Remove(item);
+ RandomMnemonic.Add(item);
+ CheckValid();
+ }
+
+ public event EventHandler? Confirm;
+
+ private void OnConfirmClicked(object? sender, RoutedEventArgs e)
+ {
+ Confirm?.Invoke(this, EventArgs.Empty);
+ }
+}
\ No newline at end of file
diff --git a/gui/UI/Model/UiPersonaModel.cs b/gui/UI/Model/UiPersonaModel.cs
new file mode 100644
index 0000000..ade66d7
--- /dev/null
+++ b/gui/UI/Model/UiPersonaModel.cs
@@ -0,0 +1,13 @@
+using Dimension.MaskCore.Data.Model;
+
+namespace Dimension.MaskCore.UI.Model;
+
+internal record UiPersonaModel(string Name, string Identifier)
+{
+ public static UiPersonaModel FromDb(DbPersonaModel model)
+ {
+ return new(model.Name, model.Identifier);
+ }
+}
+
+internal record MnemonicModel(string Word, int Index);
\ No newline at end of file
diff --git a/gui/UI/Model/UiWalletModel.cs b/gui/UI/Model/UiWalletModel.cs
new file mode 100644
index 0000000..d171cd9
--- /dev/null
+++ b/gui/UI/Model/UiWalletModel.cs
@@ -0,0 +1,11 @@
+using Dimension.MaskCore.Data.Model;
+
+namespace Dimension.MaskCore.UI.Model;
+
+internal record UiWalletModel(string Address, string Name)
+{
+ public static UiWalletModel From(DbWalletModel item)
+ {
+ return new UiWalletModel(item.Address, item.Name);
+ }
+}
\ No newline at end of file
diff --git a/gui/UI/Pages/Persona/CreatePersona/CreatePersonaPage.axaml b/gui/UI/Pages/Persona/CreatePersona/CreatePersonaPage.axaml
new file mode 100644
index 0000000..6096094
--- /dev/null
+++ b/gui/UI/Pages/Persona/CreatePersona/CreatePersonaPage.axaml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gui/UI/Pages/Persona/CreatePersona/CreatePersonaPage.axaml.cs b/gui/UI/Pages/Persona/CreatePersona/CreatePersonaPage.axaml.cs
new file mode 100644
index 0000000..00ee926
--- /dev/null
+++ b/gui/UI/Pages/Persona/CreatePersona/CreatePersonaPage.axaml.cs
@@ -0,0 +1,27 @@
+using Avalonia.Interactivity;
+using Dimension.MaskCore.Lifecycle.Controls;
+using Dimension.MaskCore.UI.Pages.Persona.MnemonicValidate;
+
+namespace Dimension.MaskCore.UI.Pages.Persona.CreatePersona;
+
+internal partial class CreatePersonaPage : Page
+{
+ public CreatePersonaPage()
+ {
+ InitializeComponent();
+ }
+
+ private void BackClicked(object? sender, RoutedEventArgs e)
+ {
+ GoBack();
+ }
+
+ private void NextClicked(object? sender, RoutedEventArgs e)
+ {
+ Navigate(new PersonaMnemonicValidateParameter(
+ string.Join(" ", ViewModel.Mnemonic),
+ ViewModel.Name
+ )
+ );
+ }
+}
\ No newline at end of file
diff --git a/gui/UI/Pages/Persona/CreatePersona/CreatePersonaViewModel.cs b/gui/UI/Pages/Persona/CreatePersona/CreatePersonaViewModel.cs
new file mode 100644
index 0000000..6e0d93e
--- /dev/null
+++ b/gui/UI/Pages/Persona/CreatePersona/CreatePersonaViewModel.cs
@@ -0,0 +1,12 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using Dimension.MaskCore.Common.Helpers;
+using Dimension.MaskCore.Lifecycle.ViewModel;
+
+namespace Dimension.MaskCore.UI.Pages.Persona.CreatePersona;
+
+internal partial class CreatePersonaViewModel : ViewModel
+{
+ [ObservableProperty] private string _name = string.Empty;
+
+ public string[] Mnemonic { get; } = MnemonicHelper.GenerateMnemonic().Split(' ');
+}
\ No newline at end of file
diff --git a/gui/UI/Pages/Persona/MnemonicValidate/PersonaMnemonicValidatePage.axaml b/gui/UI/Pages/Persona/MnemonicValidate/PersonaMnemonicValidatePage.axaml
new file mode 100644
index 0000000..7c12351
--- /dev/null
+++ b/gui/UI/Pages/Persona/MnemonicValidate/PersonaMnemonicValidatePage.axaml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gui/UI/Pages/Persona/MnemonicValidate/PersonaMnemonicValidatePage.axaml.cs b/gui/UI/Pages/Persona/MnemonicValidate/PersonaMnemonicValidatePage.axaml.cs
new file mode 100644
index 0000000..31f4873
--- /dev/null
+++ b/gui/UI/Pages/Persona/MnemonicValidate/PersonaMnemonicValidatePage.axaml.cs
@@ -0,0 +1,29 @@
+using Avalonia.Interactivity;
+using Dimension.MaskCore.Lifecycle.Controls;
+
+namespace Dimension.MaskCore.UI.Pages.Persona.MnemonicValidate;
+
+internal partial class
+ PersonaMnemonicValidatePage : Page
+{
+ public PersonaMnemonicValidatePage()
+ {
+ InitializeComponent();
+ ViewModel.OnComplete = OnComplete;
+ }
+
+ private async void OnComplete()
+ {
+ await new Dialog
+ {
+ Title = "Persona Created",
+ CloseButtonText = "OK"
+ }.ShowAsync();
+ Navigate();
+ }
+
+ private void BackClicked(object? sender, RoutedEventArgs e)
+ {
+ GoBack();
+ }
+}
\ No newline at end of file
diff --git a/gui/UI/Pages/Persona/MnemonicValidate/PersonaMnemonicValidateViewModel.cs b/gui/UI/Pages/Persona/MnemonicValidate/PersonaMnemonicValidateViewModel.cs
new file mode 100644
index 0000000..e5e648d
--- /dev/null
+++ b/gui/UI/Pages/Persona/MnemonicValidate/PersonaMnemonicValidateViewModel.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using CommunityToolkit.Mvvm.Input;
+using Dimension.MaskCore.Data.Repository;
+using Dimension.MaskCore.Lifecycle.ViewModel;
+
+namespace Dimension.MaskCore.UI.Pages.Persona.MnemonicValidate;
+
+internal partial class PersonaMnemonicValidateViewModel : ParameterizedViewModel
+{
+ private readonly PersonaRepository _personaRepository = Ioc.Default.GetRequiredService();
+ [ObservableProperty] private IReadOnlyCollection _words = new List();
+ public Action? OnComplete { get; set; }
+ public PersonaMnemonicValidateParameter? Parameter { get; private set; }
+
+ protected override void InitializeCore(PersonaMnemonicValidateParameter parameter)
+ {
+ Parameter = parameter;
+ Words = Parameter.WordList.ToImmutableList();
+ }
+
+ [ICommand]
+ private async Task Confirm()
+ {
+ if (Parameter != null)
+ {
+ await _personaRepository.CreatePersona(Parameter.Name, Parameter.Mnemonic);
+ OnComplete?.Invoke();
+ }
+ }
+}
+
+internal record PersonaMnemonicValidateParameter(string Mnemonic, string Name)
+{
+ public string[] WordList => Mnemonic.Split(' ');
+}
\ No newline at end of file
diff --git a/gui/UI/Pages/Persona/PersonaPage.axaml b/gui/UI/Pages/Persona/PersonaPage.axaml
new file mode 100644
index 0000000..8cdc8b1
--- /dev/null
+++ b/gui/UI/Pages/Persona/PersonaPage.axaml
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gui/UI/Pages/Persona/PersonaPage.axaml.cs b/gui/UI/Pages/Persona/PersonaPage.axaml.cs
new file mode 100644
index 0000000..fd632db
--- /dev/null
+++ b/gui/UI/Pages/Persona/PersonaPage.axaml.cs
@@ -0,0 +1,25 @@
+using Avalonia.Interactivity;
+using Dimension.MaskCore.Lifecycle.Controls;
+using Dimension.MaskCore.UI.Model;
+using Dimension.MaskCore.UI.Pages.Persona.CreatePersona;
+using Dimension.MaskCore.UI.Pages.Persona.RenamePersona;
+
+namespace Dimension.MaskCore.UI.Pages.Persona;
+
+internal partial class PersonaPage : Page
+{
+ public PersonaPage()
+ {
+ InitializeComponent();
+ }
+
+ private void New_OnClicked(object? sender, RoutedEventArgs e)
+ {
+ Navigate();
+ }
+
+ private void RenamePersona(UiPersonaModel item)
+ {
+ new RenamePersonaDialog(item).ShowAsync();
+ }
+}
\ No newline at end of file
diff --git a/gui/UI/Pages/Persona/PersonaViewModel.cs b/gui/UI/Pages/Persona/PersonaViewModel.cs
new file mode 100644
index 0000000..66ba459
--- /dev/null
+++ b/gui/UI/Pages/Persona/PersonaViewModel.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using CommunityToolkit.Mvvm.Input;
+using Dimension.MaskCore.Data.Repository;
+using Dimension.MaskCore.Lifecycle.ViewModel;
+using Dimension.MaskCore.UI.Model;
+
+namespace Dimension.MaskCore.UI.Pages.Persona;
+
+internal partial class PersonaViewModel : ViewModel
+{
+ private readonly PersonaRepository _personaRepository = Ioc.Default.GetRequiredService();
+ private readonly BehaviorSubject _searchTextSubject = new(string.Empty);
+ [ObservableProperty] private string _searchText = string.Empty;
+
+ public PersonaViewModel()
+ {
+ Personas = _personaRepository.Personas.CombineLatest(_searchTextSubject).Throttle(TimeSpan.FromSeconds(0.3))
+ .Select(
+ it =>
+ {
+ var (personas, searchText) = it;
+ if (string.IsNullOrWhiteSpace(searchText))
+ {
+ return personas;
+ }
+
+ return personas.Where(persona =>
+ persona.Name.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
+ persona.Identifier.Contains(searchText, StringComparison.OrdinalIgnoreCase)).ToImmutableList();
+ });
+ }
+
+ public IObservable> Personas { get; }
+
+ [ICommand]
+ private void DeletePersona(UiPersonaModel model)
+ {
+ _personaRepository.DeletePersona(model.Identifier);
+ }
+
+ partial void OnSearchTextChanged(string value)
+ {
+ _searchTextSubject.OnNext(value);
+ }
+}
\ No newline at end of file
diff --git a/gui/UI/Pages/Persona/RenamePersona/RenamePersonaDialog.axaml b/gui/UI/Pages/Persona/RenamePersona/RenamePersonaDialog.axaml
new file mode 100644
index 0000000..5857fca
--- /dev/null
+++ b/gui/UI/Pages/Persona/RenamePersona/RenamePersonaDialog.axaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gui/UI/Pages/Persona/RenamePersona/RenamePersonaDialog.axaml.cs b/gui/UI/Pages/Persona/RenamePersona/RenamePersonaDialog.axaml.cs
new file mode 100644
index 0000000..d7e7081
--- /dev/null
+++ b/gui/UI/Pages/Persona/RenamePersona/RenamePersonaDialog.axaml.cs
@@ -0,0 +1,18 @@
+using Dimension.MaskCore.Lifecycle.Controls;
+using Dimension.MaskCore.UI.Model;
+
+namespace Dimension.MaskCore.UI.Pages.Persona.RenamePersona;
+
+internal partial class RenamePersonaDialog : Dialog
+{
+ public RenamePersonaDialog()
+ {
+ InitializeComponent();
+ }
+
+ public RenamePersonaDialog(UiPersonaModel item)
+ {
+ InitializeComponent();
+ ViewModel.Initialize(item);
+ }
+}
\ No newline at end of file
diff --git a/gui/UI/Pages/Persona/RenamePersona/RenamePersonaViewModel.cs b/gui/UI/Pages/Persona/RenamePersona/RenamePersonaViewModel.cs
new file mode 100644
index 0000000..9c6ab0f
--- /dev/null
+++ b/gui/UI/Pages/Persona/RenamePersona/RenamePersonaViewModel.cs
@@ -0,0 +1,32 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using CommunityToolkit.Mvvm.Input;
+using Dimension.MaskCore.Data.Repository;
+using Dimension.MaskCore.Lifecycle.ViewModel;
+using Dimension.MaskCore.UI.Model;
+
+namespace Dimension.MaskCore.UI.Pages.Persona.RenamePersona;
+
+internal partial class RenamePersonaViewModel : ViewModel
+{
+ private readonly PersonaRepository _repository = Ioc.Default.GetRequiredService();
+ [ObservableProperty] private string _name = string.Empty;
+ private UiPersonaModel? _persona;
+
+ public void Initialize(UiPersonaModel item)
+ {
+ Name = item.Name;
+ _persona = item;
+ }
+
+ [ICommand]
+ private void Rename()
+ {
+ if (_persona == null)
+ {
+ return;
+ }
+
+ _repository.UpdatePersonaName(_persona.Identifier, Name);
+ }
+}
\ No newline at end of file
diff --git a/gui/UI/Pages/Settings/SettingsPage.axaml b/gui/UI/Pages/Settings/SettingsPage.axaml
new file mode 100644
index 0000000..74be337
--- /dev/null
+++ b/gui/UI/Pages/Settings/SettingsPage.axaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gui/UI/Pages/Settings/SettingsPage.axaml.cs b/gui/UI/Pages/Settings/SettingsPage.axaml.cs
new file mode 100644
index 0000000..ecd42b3
--- /dev/null
+++ b/gui/UI/Pages/Settings/SettingsPage.axaml.cs
@@ -0,0 +1,27 @@
+using System.Diagnostics;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Dimension.MaskCore.Lifecycle.Controls;
+
+namespace Dimension.MaskCore.UI.Pages.Settings;
+
+internal partial class SettingsPage : Page
+{
+ public SettingsPage()
+ {
+ InitializeComponent();
+ }
+
+ private void ContactButton_Clicked(object? sender, RoutedEventArgs e)
+ {
+ e.Handled = true;
+ if (sender is Button { Tag: string url } && !string.IsNullOrEmpty(url))
+ {
+ Process.Start(new ProcessStartInfo
+ {
+ FileName = url,
+ UseShellExecute = true
+ })?.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/gui/UI/Pages/Settings/SettingsViewModel.cs b/gui/UI/Pages/Settings/SettingsViewModel.cs
new file mode 100644
index 0000000..4ea834d
--- /dev/null
+++ b/gui/UI/Pages/Settings/SettingsViewModel.cs
@@ -0,0 +1,7 @@
+using Dimension.MaskCore.Lifecycle.ViewModel;
+
+namespace Dimension.MaskCore.UI.Pages.Settings;
+
+internal class SettingsViewModel : ViewModel
+{
+}
\ No newline at end of file
diff --git a/gui/UI/Pages/Wallet/CreateWallet/CreateWalletPage.axaml b/gui/UI/Pages/Wallet/CreateWallet/CreateWalletPage.axaml
new file mode 100644
index 0000000..5bd228c
--- /dev/null
+++ b/gui/UI/Pages/Wallet/CreateWallet/CreateWalletPage.axaml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gui/UI/Pages/Wallet/CreateWallet/CreateWalletPage.axaml.cs b/gui/UI/Pages/Wallet/CreateWallet/CreateWalletPage.axaml.cs
new file mode 100644
index 0000000..0c40b15
--- /dev/null
+++ b/gui/UI/Pages/Wallet/CreateWallet/CreateWalletPage.axaml.cs
@@ -0,0 +1,27 @@
+using Avalonia.Interactivity;
+using Dimension.MaskCore.Lifecycle.Controls;
+using Dimension.MaskCore.UI.Pages.Wallet.MnemonicValidate;
+
+namespace Dimension.MaskCore.UI.Pages.Wallet.CreateWallet;
+
+internal partial class CreateWalletPage : Page
+{
+ public CreateWalletPage()
+ {
+ InitializeComponent();
+ }
+
+ private void BackClicked(object? sender, RoutedEventArgs e)
+ {
+ GoBack();
+ }
+
+ private void NextClicked(object? sender, RoutedEventArgs e)
+ {
+ Navigate(new WalletMnemonicValidateParameter(
+ string.Join(" ", ViewModel.Mnemonic),
+ ViewModel.Name
+ )
+ );
+ }
+}
\ No newline at end of file
diff --git a/gui/UI/Pages/Wallet/CreateWallet/CreateWalletViewModel.cs b/gui/UI/Pages/Wallet/CreateWallet/CreateWalletViewModel.cs
new file mode 100644
index 0000000..de8eaaa
--- /dev/null
+++ b/gui/UI/Pages/Wallet/CreateWallet/CreateWalletViewModel.cs
@@ -0,0 +1,12 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using Dimension.MaskCore.Common.Helpers;
+using Dimension.MaskCore.Lifecycle.ViewModel;
+
+namespace Dimension.MaskCore.UI.Pages.Wallet.CreateWallet;
+
+internal partial class CreateWalletViewModel : ViewModel
+{
+ [ObservableProperty] private string _name = string.Empty;
+
+ public string[] Mnemonic { get; } = MnemonicHelper.GenerateMnemonic().Split(' ');
+}
\ No newline at end of file
diff --git a/gui/UI/Pages/Wallet/MnemonicValidate/WalletMnemonicValidatePage.axaml b/gui/UI/Pages/Wallet/MnemonicValidate/WalletMnemonicValidatePage.axaml
new file mode 100644
index 0000000..e1e84b5
--- /dev/null
+++ b/gui/UI/Pages/Wallet/MnemonicValidate/WalletMnemonicValidatePage.axaml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gui/UI/Pages/Wallet/MnemonicValidate/WalletMnemonicValidatePage.axaml.cs b/gui/UI/Pages/Wallet/MnemonicValidate/WalletMnemonicValidatePage.axaml.cs
new file mode 100644
index 0000000..d332f72
--- /dev/null
+++ b/gui/UI/Pages/Wallet/MnemonicValidate/WalletMnemonicValidatePage.axaml.cs
@@ -0,0 +1,29 @@
+using Avalonia.Interactivity;
+using Dimension.MaskCore.Lifecycle.Controls;
+
+namespace Dimension.MaskCore.UI.Pages.Wallet.MnemonicValidate;
+
+internal partial class
+ WalletMnemonicValidatePage : Page
+{
+ public WalletMnemonicValidatePage()
+ {
+ InitializeComponent();
+ ViewModel.OnComplete = OnComplete;
+ }
+
+ private async void OnComplete()
+ {
+ await new Dialog
+ {
+ Title = "Wallet Created",
+ CloseButtonText = "OK"
+ }.ShowAsync();
+ Navigate();
+ }
+
+ private void BackClicked(object? sender, RoutedEventArgs e)
+ {
+ GoBack();
+ }
+}
\ No newline at end of file
diff --git a/gui/UI/Pages/Wallet/MnemonicValidate/WalletMnemonicValidateViewModel.cs b/gui/UI/Pages/Wallet/MnemonicValidate/WalletMnemonicValidateViewModel.cs
new file mode 100644
index 0000000..4968204
--- /dev/null
+++ b/gui/UI/Pages/Wallet/MnemonicValidate/WalletMnemonicValidateViewModel.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using CommunityToolkit.Mvvm.Input;
+using Dimension.MaskCore.Data.Repository;
+using Dimension.MaskCore.Lifecycle.ViewModel;
+
+namespace Dimension.MaskCore.UI.Pages.Wallet.MnemonicValidate;
+
+internal partial class WalletMnemonicValidateViewModel : ParameterizedViewModel
+{
+ private readonly WalletRepository _repository = Ioc.Default.GetRequiredService();
+ [ObservableProperty] private IReadOnlyCollection _words = new List();
+ public Action? OnComplete { get; set; }
+ public WalletMnemonicValidateParameter? Parameter { get; private set; }
+
+ protected override void InitializeCore(WalletMnemonicValidateParameter parameter)
+ {
+ Parameter = parameter;
+ Words = Parameter.WordList.ToImmutableList();
+ }
+
+ [ICommand]
+ private async Task Confirm()
+ {
+ if (Parameter != null)
+ {
+ await _repository.CreateWallet(Parameter.Name, Parameter.Mnemonic);
+ OnComplete?.Invoke();
+ }
+ }
+}
+
+internal record WalletMnemonicValidateParameter(string Mnemonic, string Name)
+{
+ public string[] WordList => Mnemonic.Split(' ');
+}
\ No newline at end of file
diff --git a/gui/UI/Pages/Wallet/RenameWallet/RenameWalletDialog.axaml b/gui/UI/Pages/Wallet/RenameWallet/RenameWalletDialog.axaml
new file mode 100644
index 0000000..7e1be54
--- /dev/null
+++ b/gui/UI/Pages/Wallet/RenameWallet/RenameWalletDialog.axaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gui/UI/Pages/Wallet/RenameWallet/RenameWalletDialog.axaml.cs b/gui/UI/Pages/Wallet/RenameWallet/RenameWalletDialog.axaml.cs
new file mode 100644
index 0000000..7382913
--- /dev/null
+++ b/gui/UI/Pages/Wallet/RenameWallet/RenameWalletDialog.axaml.cs
@@ -0,0 +1,18 @@
+using Dimension.MaskCore.Lifecycle.Controls;
+using Dimension.MaskCore.UI.Model;
+
+namespace Dimension.MaskCore.UI.Pages.Wallet.RenameWallet;
+
+internal partial class RenameWalletDialog : Dialog
+{
+ public RenameWalletDialog()
+ {
+ InitializeComponent();
+ }
+
+ public RenameWalletDialog(UiWalletModel item)
+ {
+ InitializeComponent();
+ ViewModel.Initialize(item);
+ }
+}
\ No newline at end of file
diff --git a/gui/UI/Pages/Wallet/RenameWallet/RenameWalletViewModel.cs b/gui/UI/Pages/Wallet/RenameWallet/RenameWalletViewModel.cs
new file mode 100644
index 0000000..4ecebeb
--- /dev/null
+++ b/gui/UI/Pages/Wallet/RenameWallet/RenameWalletViewModel.cs
@@ -0,0 +1,32 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using CommunityToolkit.Mvvm.Input;
+using Dimension.MaskCore.Data.Repository;
+using Dimension.MaskCore.Lifecycle.ViewModel;
+using Dimension.MaskCore.UI.Model;
+
+namespace Dimension.MaskCore.UI.Pages.Wallet.RenameWallet;
+
+internal partial class RenameWalletViewModel : ViewModel
+{
+ private readonly WalletRepository _repository = Ioc.Default.GetRequiredService();
+ [ObservableProperty] private string _name = string.Empty;
+ private UiWalletModel? _wallet;
+
+ public void Initialize(UiWalletModel item)
+ {
+ Name = item.Name;
+ _wallet = item;
+ }
+
+ [ICommand]
+ private void Rename()
+ {
+ if (_wallet == null)
+ {
+ return;
+ }
+
+ _repository.RenameWallet(_wallet.Address, Name);
+ }
+}
\ No newline at end of file
diff --git a/gui/UI/Pages/Wallet/WalletPage.axaml b/gui/UI/Pages/Wallet/WalletPage.axaml
new file mode 100644
index 0000000..4f8c75b
--- /dev/null
+++ b/gui/UI/Pages/Wallet/WalletPage.axaml
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gui/UI/Pages/Wallet/WalletPage.axaml.cs b/gui/UI/Pages/Wallet/WalletPage.axaml.cs
new file mode 100644
index 0000000..2f88eaa
--- /dev/null
+++ b/gui/UI/Pages/Wallet/WalletPage.axaml.cs
@@ -0,0 +1,25 @@
+using Avalonia.Interactivity;
+using Dimension.MaskCore.Lifecycle.Controls;
+using Dimension.MaskCore.UI.Model;
+using Dimension.MaskCore.UI.Pages.Wallet.CreateWallet;
+using Dimension.MaskCore.UI.Pages.Wallet.RenameWallet;
+
+namespace Dimension.MaskCore.UI.Pages.Wallet;
+
+internal partial class WalletPage : Page
+{
+ public WalletPage()
+ {
+ InitializeComponent();
+ }
+
+ private void New_OnClicked(object? sender, RoutedEventArgs e)
+ {
+ Navigate();
+ }
+
+ private void RenameWallet(UiWalletModel item)
+ {
+ new RenameWalletDialog(item).ShowAsync();
+ }
+}
\ No newline at end of file
diff --git a/gui/UI/Pages/Wallet/WalletViewModel.cs b/gui/UI/Pages/Wallet/WalletViewModel.cs
new file mode 100644
index 0000000..65e9f64
--- /dev/null
+++ b/gui/UI/Pages/Wallet/WalletViewModel.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using CommunityToolkit.Mvvm.Input;
+using Dimension.MaskCore.Data.Repository;
+using Dimension.MaskCore.Lifecycle.ViewModel;
+using Dimension.MaskCore.UI.Model;
+using TextCopy;
+
+namespace Dimension.MaskCore.UI.Pages.Wallet;
+
+internal partial class WalletViewModel : ViewModel
+{
+ private readonly WalletRepository _repository = Ioc.Default.GetRequiredService();
+ private readonly BehaviorSubject _searchTextSubject = new(string.Empty);
+ [ObservableProperty] private string _searchText = string.Empty;
+
+ public WalletViewModel()
+ {
+ Wallets = _repository.Wallets.CombineLatest(_searchTextSubject).Throttle(TimeSpan.FromSeconds(0.3)).Select(
+ it =>
+ {
+ var (wallets, searchText) = it;
+ if (string.IsNullOrWhiteSpace(searchText))
+ {
+ return wallets;
+ }
+
+ return wallets.Where(wallet =>
+ wallet.Name.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
+ wallet.Address.Contains(searchText, StringComparison.OrdinalIgnoreCase)).ToImmutableList();
+ });
+ }
+
+ public IObservable> Wallets { get; }
+
+ [ICommand]
+ private void DeleteWallet(UiWalletModel model)
+ {
+ _repository.DeleteWallet(model.Address);
+ }
+
+ [ICommand]
+ private void CopyAddress(UiWalletModel model)
+ {
+ ClipboardService.SetText(model.Address);
+ }
+
+ partial void OnSearchTextChanged(string value)
+ {
+ _searchTextSubject.OnNext(value);
+ }
+}
\ No newline at end of file
diff --git a/gui/UI/Shell/RootShell.axaml b/gui/UI/Shell/RootShell.axaml
new file mode 100644
index 0000000..1ea0abb
--- /dev/null
+++ b/gui/UI/Shell/RootShell.axaml
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/gui/UI/Shell/RootShell.axaml.cs b/gui/UI/Shell/RootShell.axaml.cs
new file mode 100644
index 0000000..7be303b
--- /dev/null
+++ b/gui/UI/Shell/RootShell.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace Dimension.MaskCore.UI.Shell;
+
+internal partial class RootShell : UserControl
+{
+ public RootShell()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/gui/UI/Shell/RootWindow.axaml b/gui/UI/Shell/RootWindow.axaml
new file mode 100644
index 0000000..abd574d
--- /dev/null
+++ b/gui/UI/Shell/RootWindow.axaml
@@ -0,0 +1,14 @@
+
+
+
\ No newline at end of file
diff --git a/gui/UI/Shell/RootWindow.axaml.cs b/gui/UI/Shell/RootWindow.axaml.cs
new file mode 100644
index 0000000..c161f45
--- /dev/null
+++ b/gui/UI/Shell/RootWindow.axaml.cs
@@ -0,0 +1,15 @@
+using Avalonia;
+using FluentAvalonia.UI.Controls;
+
+namespace Dimension.MaskCore.UI.Shell;
+
+internal partial class RootWindow : CoreWindow
+{
+ public RootWindow()
+ {
+ InitializeComponent();
+#if DEBUG
+ this.AttachDevTools();
+#endif
+ }
+}
\ No newline at end of file