commit bc188870afe1abe98447be0699339e081b72b7a4
Author: binarybaron <86064887+binarybaron@users.noreply.github.com>
Date: Sat Jul 6 21:34:33 2024 +0200
Initial commit
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..dfe07704
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 00000000..24d7cc6d
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,3 @@
+{
+ "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
+}
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..102e3668
--- /dev/null
+++ b/README.md
@@ -0,0 +1,7 @@
+# Tauri + React + Typescript
+
+This template should help get you started developing with Tauri, React and Typescript in Vite.
+
+## Recommended IDE Setup
+
+- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
diff --git a/index.html b/index.html
new file mode 100644
index 00000000..70cdab17
--- /dev/null
+++ b/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ Tauri + React + Typescript
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..c483bc8e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "unstoppableswap-gui-rs",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "tauri": "tauri"
+ },
+ "dependencies": {
+ "@material-ui/core": "^4.12.4",
+ "@material-ui/icons": "^4.11.3",
+ "@material-ui/lab": "^4.0.0-alpha.61",
+ "@reduxjs/toolkit": "^2.2.6",
+ "@tauri-apps/api": ">=2.0.0-beta.0",
+ "@tauri-apps/plugin-shell": ">=2.0.0-beta.0",
+ "humanize-duration": "^3.32.1",
+ "lodash": "^4.17.21",
+ "multiaddr": "^10.0.1",
+ "notistack": "^3.0.1",
+ "pino": "^9.2.0",
+ "pino-pretty": "^11.2.1",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-qr-code": "^2.0.15",
+ "react-redux": "^9.1.2",
+ "react-router-dom": "^6.24.1",
+ "semver": "^7.6.2",
+ "virtua": "^0.33.2"
+ },
+ "devDependencies": {
+ "@tauri-apps/cli": ">=2.0.0-beta.0",
+ "@types/humanize-duration": "^3.27.4",
+ "@types/lodash": "^4.17.6",
+ "@types/node": "^20.14.10",
+ "@types/react": "^18.2.15",
+ "@types/react-dom": "^18.2.7",
+ "@types/semver": "^7.5.8",
+ "@vitejs/plugin-react": "^4.2.1",
+ "internal-ip": "^7.0.0",
+ "typescript": "^5.2.2",
+ "vite": "^5.3.1",
+ "vite-tsconfig-paths": "^4.3.2"
+ }
+}
diff --git a/public/tauri.svg b/public/tauri.svg
new file mode 100644
index 00000000..31b62c92
--- /dev/null
+++ b/public/tauri.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/public/vite.svg b/public/vite.svg
new file mode 100644
index 00000000..e7b8dfb1
--- /dev/null
+++ b/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore
new file mode 100644
index 00000000..b21bd681
--- /dev/null
+++ b/src-tauri/.gitignore
@@ -0,0 +1,7 @@
+# Generated by Cargo
+# will have compiled files and executables
+/target/
+
+# Generated by Tauri
+# will have schema files for capabilities auto-completion
+/gen/schemas
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
new file mode 100644
index 00000000..8bb2da79
--- /dev/null
+++ b/src-tauri/Cargo.lock
@@ -0,0 +1,4346 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
+
+[[package]]
+name = "atk"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4af014b17dd80e8af9fa689b2d4a211ddba6eb583c1622f35d0cb543f6b17e4"
+dependencies = [
+ "atk-sys",
+ "glib",
+ "libc",
+]
+
+[[package]]
+name = "atk-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
+
+[[package]]
+name = "backtrace"
+version = "0.3.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "block"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block2"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
+dependencies = [
+ "objc2",
+]
+
+[[package]]
+name = "brotli"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "2.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+
+[[package]]
+name = "bytemuck"
+version = "1.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cairo-rs"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
+dependencies = [
+ "bitflags 2.6.0",
+ "cairo-sys-rs",
+ "glib",
+ "libc",
+ "once_cell",
+ "thiserror",
+]
+
+[[package]]
+name = "cairo-sys-rs"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "camino"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cargo-platform"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cargo_metadata"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037"
+dependencies = [
+ "camino",
+ "cargo-platform",
+ "semver",
+ "serde",
+ "serde_json",
+ "thiserror",
+]
+
+[[package]]
+name = "cargo_toml"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a969e13a7589e9e3e4207e153bae624ade2b5622fb4684a4923b23ec3d57719"
+dependencies = [
+ "serde",
+ "toml 0.8.2",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74b6a57f98764a267ff415d50a25e6e166f3831a5071af4995296ea97d210490"
+
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
+name = "cfb"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
+dependencies = [
+ "byteorder",
+ "fnv",
+ "uuid",
+]
+
+[[package]]
+name = "cfg-expr"
+version = "0.15.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
+dependencies = [
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chrono"
+version = "0.4.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "num-traits",
+ "serde",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "cocoa"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c"
+dependencies = [
+ "bitflags 1.3.2",
+ "block",
+ "cocoa-foundation",
+ "core-foundation",
+ "core-graphics",
+ "foreign-types",
+ "libc",
+ "objc",
+]
+
+[[package]]
+name = "cocoa-foundation"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
+dependencies = [
+ "bitflags 1.3.2",
+ "block",
+ "core-foundation",
+ "core-graphics-types",
+ "libc",
+ "objc",
+]
+
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
+
+[[package]]
+name = "core-graphics"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "core-graphics-types",
+ "foreign-types",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "cssparser"
+version = "0.27.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a"
+dependencies = [
+ "cssparser-macros",
+ "dtoa-short",
+ "itoa 0.4.8",
+ "matches",
+ "phf 0.8.0",
+ "proc-macro2",
+ "quote",
+ "smallvec",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "cssparser-macros"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
+dependencies = [
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "ctor"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
+dependencies = [
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "darling"
+version = "0.20.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "deranged"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
+dependencies = [
+ "powerfmt",
+ "serde",
+]
+
+[[package]]
+name = "derive_more"
+version = "0.99.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dirs"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "dispatch"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
+
+[[package]]
+name = "dlopen2"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6"
+dependencies = [
+ "dlopen2_derive",
+ "libc",
+ "once_cell",
+ "winapi",
+]
+
+[[package]]
+name = "dlopen2_derive"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "dpi"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "dtoa"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653"
+
+[[package]]
+name = "dtoa-short"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
+dependencies = [
+ "dtoa",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
+
+[[package]]
+name = "embed-resource"
+version = "2.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6985554d0688b687c5cb73898a34fbe3ad6c24c58c238a4d91d5e840670ee9d"
+dependencies = [
+ "cc",
+ "memchr",
+ "rustc_version",
+ "toml 0.8.2",
+ "vswhom",
+ "winreg",
+]
+
+[[package]]
+name = "embed_plist"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "erased-serde"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d"
+dependencies = [
+ "serde",
+ "typeid",
+]
+
+[[package]]
+name = "fdeflate"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "field-offset"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
+dependencies = [
+ "memoffset",
+ "rustc_version",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+dependencies = [
+ "foreign-types-macros",
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-macros"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
+dependencies = [
+ "mac",
+ "new_debug_unreachable",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
+
+[[package]]
+name = "futures-task"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+
+[[package]]
+name = "futures-util"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "gdk"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5ba081bdef3b75ebcdbfc953699ed2d7417d6bd853347a42a37d76406a33646"
+dependencies = [
+ "cairo-rs",
+ "gdk-pixbuf",
+ "gdk-sys",
+ "gio",
+ "glib",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gdk-pixbuf"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec"
+dependencies = [
+ "gdk-pixbuf-sys",
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "gdk-pixbuf-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gdk-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkwayland-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a90fbf5c033c65d93792192a49a8efb5bb1e640c419682a58bb96f5ae77f3d4a"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkx11"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db2ea8a4909d530f79921290389cbd7c34cb9d623bfe970eaae65ca5f9cd9cce"
+dependencies = [
+ "gdk",
+ "gdkx11-sys",
+ "gio",
+ "glib",
+ "libc",
+ "x11",
+]
+
+[[package]]
+name = "gdkx11-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fee8f00f4ee46cad2939b8990f5c70c94ff882c3028f3cc5abf950fa4ab53043"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "libc",
+ "system-deps",
+ "x11",
+]
+
+[[package]]
+name = "generator"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
+dependencies = [
+ "cc",
+ "libc",
+ "log",
+ "rustversion",
+ "windows 0.48.0",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "gimli"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
+
+[[package]]
+name = "gio"
+version = "0.18.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "gio-sys",
+ "glib",
+ "libc",
+ "once_cell",
+ "pin-project-lite",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gio-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+ "winapi",
+]
+
+[[package]]
+name = "glib"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5"
+dependencies = [
+ "bitflags 2.6.0",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-task",
+ "futures-util",
+ "gio-sys",
+ "glib-macros",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "memchr",
+ "once_cell",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "glib-macros"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro-crate 2.0.2",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "glib-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
+dependencies = [
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+
+[[package]]
+name = "gobject-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93c4f5e0e20b60e10631a5f06da7fe3dda744b05ad0ea71fee2f47adf865890c"
+dependencies = [
+ "atk",
+ "cairo-rs",
+ "field-offset",
+ "futures-channel",
+ "gdk",
+ "gdk-pixbuf",
+ "gio",
+ "glib",
+ "gtk-sys",
+ "gtk3-macros",
+ "libc",
+ "pango",
+ "pkg-config",
+]
+
+[[package]]
+name = "gtk-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722"
+dependencies = [
+ "atk-sys",
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk3-macros"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e"
+dependencies = [
+ "proc-macro-crate 1.3.1",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "html5ever"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
+dependencies = [
+ "log",
+ "mac",
+ "markup5ever",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "http"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa 1.0.11",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
+
+[[package]]
+name = "hyper"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4fe55fb7a772d59a5ff1dfbff4fe0258d19b89fec4b233e75d35d5d2316badc"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa 1.0.11",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows-core 0.52.0",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "ico"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3804960be0bb5e4edb1e1ad67afd321a9ecfd875c3e65c099468fd2717d7cae"
+dependencies = [
+ "byteorder",
+ "png",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.14.5",
+ "serde",
+]
+
+[[package]]
+name = "infer"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb33622da908807a06f9513c19b3c1ad50fab3e4137d82a78107d502075aa199"
+dependencies = [
+ "cfb",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
+
+[[package]]
+name = "is-docker"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "is-wsl"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
+dependencies = [
+ "is-docker",
+ "once_cell",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+
+[[package]]
+name = "itoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+
+[[package]]
+name = "javascriptcore-rs"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc"
+dependencies = [
+ "bitflags 1.3.2",
+ "glib",
+ "javascriptcore-rs-sys",
+]
+
+[[package]]
+name = "javascriptcore-rs-sys"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
+[[package]]
+name = "js-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "json-patch"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec9ad60d674508f3ca8f380a928cfe7b096bc729c4e2dbfe3852bc45da3ab30b"
+dependencies = [
+ "serde",
+ "serde_json",
+ "thiserror",
+]
+
+[[package]]
+name = "keyboard-types"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a"
+dependencies = [
+ "bitflags 2.6.0",
+ "serde",
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "kuchikiki"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8"
+dependencies = [
+ "cssparser",
+ "html5ever",
+ "indexmap 1.9.3",
+ "matches",
+ "selectors",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libappindicator"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a"
+dependencies = [
+ "glib",
+ "gtk",
+ "gtk-sys",
+ "libappindicator-sys",
+ "log",
+]
+
+[[package]]
+name = "libappindicator-sys"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf"
+dependencies = [
+ "gtk-sys",
+ "libloading",
+ "once_cell",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.155"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
+
+[[package]]
+name = "libloading"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
+dependencies = [
+ "cfg-if",
+ "winapi",
+]
+
+[[package]]
+name = "libredox"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+dependencies = [
+ "bitflags 2.6.0",
+ "libc",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
+
+[[package]]
+name = "loom"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
+dependencies = [
+ "cfg-if",
+ "generator",
+ "scoped-tls",
+ "serde",
+ "serde_json",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "mac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
+
+[[package]]
+name = "malloc_buf"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "markup5ever"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
+dependencies = [
+ "log",
+ "phf 0.10.1",
+ "phf_codegen 0.10.0",
+ "string_cache",
+ "string_cache_codegen",
+ "tendril",
+]
+
+[[package]]
+name = "matchers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+dependencies = [
+ "regex-automata 0.1.10",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
+dependencies = [
+ "adler",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
+dependencies = [
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "muda"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b959f97c97044e4c96e32e1db292a7d594449546a3c6b77ae613dc3a5b5145"
+dependencies = [
+ "cocoa",
+ "crossbeam-channel",
+ "dpi",
+ "gtk",
+ "keyboard-types",
+ "objc",
+ "once_cell",
+ "png",
+ "serde",
+ "thiserror",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "ndk"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0"
+dependencies = [
+ "bitflags 1.3.2",
+ "jni-sys",
+ "ndk-sys",
+ "num_enum",
+ "raw-window-handle 0.5.2",
+ "thiserror",
+]
+
+[[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
+[[package]]
+name = "ndk-sys"
+version = "0.4.1+23.1.7779620"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3"
+dependencies = [
+ "jni-sys",
+]
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
+[[package]]
+name = "nodrop"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9"
+dependencies = [
+ "num_enum_derive",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799"
+dependencies = [
+ "proc-macro-crate 1.3.1",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "objc"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
+dependencies = [
+ "malloc_buf",
+ "objc_exception",
+]
+
+[[package]]
+name = "objc-sys"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
+
+[[package]]
+name = "objc2"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
+dependencies = [
+ "objc-sys",
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2-app-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
+dependencies = [
+ "bitflags 2.6.0",
+ "block2",
+ "libc",
+ "objc2",
+ "objc2-core-data",
+ "objc2-core-image",
+ "objc2-foundation",
+ "objc2-quartz-core",
+]
+
+[[package]]
+name = "objc2-core-data"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
+dependencies = [
+ "bitflags 2.6.0",
+ "block2",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-image"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
+dependencies = [
+ "block2",
+ "objc2",
+ "objc2-foundation",
+ "objc2-metal",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8"
+
+[[package]]
+name = "objc2-foundation"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
+dependencies = [
+ "bitflags 2.6.0",
+ "block2",
+ "dispatch",
+ "libc",
+ "objc2",
+]
+
+[[package]]
+name = "objc2-metal"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
+dependencies = [
+ "bitflags 2.6.0",
+ "block2",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
+dependencies = [
+ "bitflags 2.6.0",
+ "block2",
+ "objc2",
+ "objc2-foundation",
+ "objc2-metal",
+]
+
+[[package]]
+name = "objc_exception"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "objc_id"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
+dependencies = [
+ "objc",
+]
+
+[[package]]
+name = "object"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "open"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d2c909a3fce3bd80efef4cd1c6c056bd9376a8fe06fcfdbebaf32cb485a7e37"
+dependencies = [
+ "is-wsl",
+ "libc",
+ "pathdiff",
+]
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "os_pipe"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29d73ba8daf8fac13b0501d1abeddcfe21ba7401ada61a819144b6c2a4f32209"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
+[[package]]
+name = "pango"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4"
+dependencies = [
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+ "pango-sys",
+]
+
+[[package]]
+name = "pango-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "pathdiff"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "phf"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
+dependencies = [
+ "phf_macros 0.8.0",
+ "phf_shared 0.8.0",
+ "proc-macro-hack",
+]
+
+[[package]]
+name = "phf"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
+dependencies = [
+ "phf_shared 0.10.0",
+]
+
+[[package]]
+name = "phf"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
+dependencies = [
+ "phf_macros 0.11.2",
+ "phf_shared 0.11.2",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
+dependencies = [
+ "phf_generator 0.8.0",
+ "phf_shared 0.8.0",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
+dependencies = [
+ "phf_generator 0.10.0",
+ "phf_shared 0.10.0",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
+dependencies = [
+ "phf_shared 0.8.0",
+ "rand 0.7.3",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
+dependencies = [
+ "phf_shared 0.10.0",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
+dependencies = [
+ "phf_shared 0.11.2",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c"
+dependencies = [
+ "phf_generator 0.8.0",
+ "phf_shared 0.8.0",
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
+dependencies = [
+ "phf_generator 0.11.2",
+ "phf_shared 0.11.2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pin-project"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
+
+[[package]]
+name = "plist"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016"
+dependencies = [
+ "base64 0.22.1",
+ "indexmap 2.2.6",
+ "quick-xml",
+ "serde",
+ "time",
+]
+
+[[package]]
+name = "png"
+version = "0.17.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1"
+dependencies = [
+ "bitflags 1.3.2",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit 0.19.15",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
+dependencies = [
+ "toml_datetime",
+ "toml_edit 0.20.2",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.20+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.32.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc",
+ "rand_pcg",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.15",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "raw-window-handle"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9"
+
+[[package]]
+name = "raw-window-handle"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd"
+dependencies = [
+ "bitflags 2.6.0",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
+dependencies = [
+ "getrandom 0.2.15",
+ "libredox",
+ "thiserror",
+]
+
+[[package]]
+name = "regex"
+version = "1.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata 0.4.7",
+ "regex-syntax 0.8.4",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+dependencies = [
+ "regex-syntax 0.6.29",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax 0.8.4",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
+
+[[package]]
+name = "reqwest"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-util",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+ "winreg",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
+
+[[package]]
+name = "ryu"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schemars"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
+dependencies = [
+ "dyn-clone",
+ "indexmap 1.9.3",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+ "url",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "scoped-tls"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "selectors"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe"
+dependencies = [
+ "bitflags 1.3.2",
+ "cssparser",
+ "derive_more",
+ "fxhash",
+ "log",
+ "matches",
+ "phf 0.8.0",
+ "phf_codegen 0.8.0",
+ "precomputed-hash",
+ "servo_arc",
+ "smallvec",
+ "thin-slice",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.203"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-untagged"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2676ba99bd82f75cae5cbd2c8eda6fa0b8760f18978ea840e980dd5567b5c5b6"
+dependencies = [
+ "erased-serde",
+ "serde",
+ "typeid",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.203"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
+dependencies = [
+ "itoa 1.0.11",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_repr"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa 1.0.11",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_with"
+version = "3.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e73139bc5ec2d45e6c5fd85be5a46949c1c39a4c18e56915f5eb4c12f975e377"
+dependencies = [
+ "base64 0.22.1",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.2.6",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b80d3d6b56b64335c0180e5ffde23b3c5e08c14c585b51a15bd0e95393f46703"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "serialize-to-javascript"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb"
+dependencies = [
+ "serde",
+ "serde_json",
+ "serialize-to-javascript-impl",
+]
+
+[[package]]
+name = "serialize-to-javascript-impl"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "servo_arc"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432"
+dependencies = [
+ "nodrop",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shared_child"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+
+[[package]]
+name = "siphasher"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+
+[[package]]
+name = "socket2"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "softbuffer"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d623bff5d06f60d738990980d782c8c866997d9194cfe79ecad00aa2f76826dd"
+dependencies = [
+ "bytemuck",
+ "cfg_aliases",
+ "core-graphics",
+ "foreign-types",
+ "js-sys",
+ "log",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+ "objc2-quartz-core",
+ "raw-window-handle 0.6.2",
+ "redox_syscall",
+ "wasm-bindgen",
+ "web-sys",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "soup3"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f"
+dependencies = [
+ "futures-channel",
+ "gio",
+ "glib",
+ "libc",
+ "soup3-sys",
+]
+
+[[package]]
+name = "soup3-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "state"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8"
+dependencies = [
+ "loom",
+]
+
+[[package]]
+name = "string_cache"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b"
+dependencies = [
+ "new_debug_unreachable",
+ "once_cell",
+ "parking_lot",
+ "phf_shared 0.10.0",
+ "precomputed-hash",
+ "serde",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988"
+dependencies = [
+ "phf_generator 0.10.0",
+ "phf_shared 0.10.0",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "swift-rs"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bbdb58577b6301f8d17ae2561f32002a5bae056d444e0f69e611e504a276204"
+dependencies = [
+ "base64 0.21.7",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
+
+[[package]]
+name = "system-deps"
+version = "6.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
+dependencies = [
+ "cfg-expr",
+ "heck 0.5.0",
+ "pkg-config",
+ "toml 0.8.2",
+ "version-compare",
+]
+
+[[package]]
+name = "tao"
+version = "0.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea538df05fbc2dcbbd740ba0cfe8607688535f4798d213cbbfa13ce494f3451f"
+dependencies = [
+ "bitflags 2.6.0",
+ "cocoa",
+ "core-foundation",
+ "core-graphics",
+ "crossbeam-channel",
+ "dispatch",
+ "dlopen2",
+ "dpi",
+ "gdkwayland-sys",
+ "gdkx11-sys",
+ "gtk",
+ "instant",
+ "jni",
+ "lazy_static",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-context",
+ "ndk-sys",
+ "objc",
+ "once_cell",
+ "parking_lot",
+ "raw-window-handle 0.6.2",
+ "scopeguard",
+ "tao-macros",
+ "unicode-segmentation",
+ "url",
+ "windows 0.57.0",
+ "windows-core 0.57.0",
+ "windows-version",
+ "x11-dl",
+]
+
+[[package]]
+name = "tao-macros"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec114582505d158b669b136e6851f85840c109819d77c42bb7c0709f727d18c2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
+
+[[package]]
+name = "tauri"
+version = "2.0.0-beta.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68725c4f17f62f0fb1fa2eecaf391200bf00a9414c84f30783ddca10570690c3"
+dependencies = [
+ "anyhow",
+ "bytes",
+ "cocoa",
+ "dirs",
+ "dunce",
+ "embed_plist",
+ "futures-util",
+ "getrandom 0.2.15",
+ "glob",
+ "gtk",
+ "heck 0.5.0",
+ "http",
+ "jni",
+ "libc",
+ "log",
+ "mime",
+ "muda",
+ "objc",
+ "percent-encoding",
+ "raw-window-handle 0.6.2",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "serialize-to-javascript",
+ "state",
+ "swift-rs",
+ "tauri-build",
+ "tauri-macros",
+ "tauri-runtime",
+ "tauri-runtime-wry",
+ "tauri-utils",
+ "thiserror",
+ "tokio",
+ "tray-icon",
+ "url",
+ "urlpattern",
+ "webkit2gtk",
+ "webview2-com",
+ "window-vibrancy",
+ "windows 0.57.0",
+]
+
+[[package]]
+name = "tauri-build"
+version = "2.0.0-beta.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1822847744f663babbfc8b7532a104734e9cf99e3408bba7109018bf9177917"
+dependencies = [
+ "anyhow",
+ "cargo_toml",
+ "dirs",
+ "glob",
+ "heck 0.5.0",
+ "json-patch",
+ "schemars",
+ "semver",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "tauri-winres",
+ "toml 0.8.2",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-codegen"
+version = "2.0.0-beta.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e36fa3c2e3bd935827fef1eed459885414fb27c82f687d8b9a15112c8a5c8f0"
+dependencies = [
+ "base64 0.22.1",
+ "brotli",
+ "ico",
+ "json-patch",
+ "plist",
+ "png",
+ "proc-macro2",
+ "quote",
+ "semver",
+ "serde",
+ "serde_json",
+ "sha2",
+ "syn 2.0.68",
+ "tauri-utils",
+ "thiserror",
+ "time",
+ "url",
+ "uuid",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-macros"
+version = "2.0.0-beta.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aba4bed4648c3cb17d421af5783c7c29a033a94ab8597ef3791dadea69289d"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+ "tauri-codegen",
+ "tauri-utils",
+]
+
+[[package]]
+name = "tauri-plugin"
+version = "2.0.0-beta.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "431ac9636bf81e7a04042399918ffa6b9d2413926dabc9366a24f6b487f64653"
+dependencies = [
+ "anyhow",
+ "glob",
+ "plist",
+ "schemars",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "toml 0.8.2",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-plugin-shell"
+version = "2.0.0-beta.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85f0347c4d056cca543b5f2dd74c33b64182553e03d1dba2738fe2a95f0ec9ef"
+dependencies = [
+ "encoding_rs",
+ "log",
+ "open",
+ "os_pipe",
+ "regex",
+ "schemars",
+ "serde",
+ "serde_json",
+ "shared_child",
+ "tauri",
+ "tauri-plugin",
+ "thiserror",
+ "tokio",
+]
+
+[[package]]
+name = "tauri-runtime"
+version = "2.0.0-beta.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5fa872242a432195b814e87f91ce10f293ae5b01fbd1eb139455496260aa7c9"
+dependencies = [
+ "dpi",
+ "gtk",
+ "http",
+ "jni",
+ "raw-window-handle 0.6.2",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "thiserror",
+ "url",
+ "windows 0.57.0",
+]
+
+[[package]]
+name = "tauri-runtime-wry"
+version = "2.0.0-beta.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ad6d5ef3c05d1c4b6cf97b9eac1ca1ad8ff2a7057ad0a92b3e4c476f009341e"
+dependencies = [
+ "cocoa",
+ "gtk",
+ "http",
+ "jni",
+ "log",
+ "percent-encoding",
+ "raw-window-handle 0.6.2",
+ "softbuffer",
+ "tao",
+ "tauri-runtime",
+ "tauri-utils",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "windows 0.57.0",
+ "wry",
+]
+
+[[package]]
+name = "tauri-utils"
+version = "2.0.0-beta.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f81a672883c9a67eb24727c99cce583625c919a5fb696c661603b426c463c72"
+dependencies = [
+ "brotli",
+ "cargo_metadata",
+ "ctor",
+ "dunce",
+ "glob",
+ "html5ever",
+ "infer",
+ "json-patch",
+ "kuchikiki",
+ "log",
+ "memchr",
+ "phf 0.11.2",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "schemars",
+ "semver",
+ "serde",
+ "serde-untagged",
+ "serde_json",
+ "serde_with",
+ "swift-rs",
+ "thiserror",
+ "toml 0.8.2",
+ "url",
+ "urlpattern",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-winres"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5993dc129e544393574288923d1ec447c857f3f644187f4fbf7d9a875fbfc4fb"
+dependencies = [
+ "embed-resource",
+ "toml 0.7.8",
+]
+
+[[package]]
+name = "tendril"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
+dependencies = [
+ "futf",
+ "mac",
+ "utf-8",
+]
+
+[[package]]
+name = "thin-slice"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
+
+[[package]]
+name = "thiserror"
+version = "1.0.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "time"
+version = "0.3.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
+dependencies = [
+ "deranged",
+ "itoa 1.0.11",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
+
+[[package]]
+name = "time-macros"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6b6a2fb3a985e99cebfaefa9faa3024743da73304ca1c683a36429613d3d22"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "num_cpus",
+ "pin-project-lite",
+ "socket2",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit 0.19.15",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit 0.20.2",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.19.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
+dependencies = [
+ "indexmap 2.2.6",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
+dependencies = [
+ "indexmap 2.2.6",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project",
+ "pin-project-lite",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
+
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "tray-icon"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ad8319cca93189ea9ab1b290de0595960529750b6b8b501a399ed1ec3775d60"
+dependencies = [
+ "cocoa",
+ "core-graphics",
+ "crossbeam-channel",
+ "dirs",
+ "libappindicator",
+ "muda",
+ "objc",
+ "once_cell",
+ "png",
+ "serde",
+ "thiserror",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "typeid"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "059d83cc991e7a42fc37bd50941885db0888e34209f8cfd9aab07ddec03bc9cf"
+
+[[package]]
+name = "typenum"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+
+[[package]]
+name = "unic-char-property"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
+dependencies = [
+ "unic-char-range",
+]
+
+[[package]]
+name = "unic-char-range"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
+
+[[package]]
+name = "unic-common"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
+
+[[package]]
+name = "unic-ucd-ident"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987"
+dependencies = [
+ "unic-char-property",
+ "unic-char-range",
+ "unic-ucd-version",
+]
+
+[[package]]
+name = "unic-ucd-version"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
+dependencies = [
+ "unic-common",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
+
+[[package]]
+name = "unstoppableswap-gui-rs"
+version = "0.0.0"
+dependencies = [
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-build",
+ "tauri-plugin-shell",
+]
+
+[[package]]
+name = "url"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "urlpattern"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9bd5ff03aea02fa45b13a7980151fe45009af1980ba69f651ec367121a31609"
+dependencies = [
+ "derive_more",
+ "regex",
+ "serde",
+ "unic-ucd-ident",
+ "url",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "uuid"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439"
+dependencies = [
+ "getrandom 0.2.15",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
+
+[[package]]
+name = "version-compare"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "vswhom"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
+dependencies = [
+ "libc",
+ "vswhom-sys",
+]
+
+[[package]]
+name = "vswhom-sys"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
+
+[[package]]
+name = "wasm-streams"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webkit2gtk"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a"
+dependencies = [
+ "bitflags 1.3.2",
+ "cairo-rs",
+ "gdk",
+ "gdk-sys",
+ "gio",
+ "gio-sys",
+ "glib",
+ "glib-sys",
+ "gobject-sys",
+ "gtk",
+ "gtk-sys",
+ "javascriptcore-rs",
+ "libc",
+ "once_cell",
+ "soup3",
+ "webkit2gtk-sys",
+]
+
+[[package]]
+name = "webkit2gtk-sys"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c"
+dependencies = [
+ "bitflags 1.3.2",
+ "cairo-sys-rs",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "gtk-sys",
+ "javascriptcore-rs-sys",
+ "libc",
+ "pkg-config",
+ "soup3-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "webview2-com"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6516cfa64c6b3212686080eeec378e662c2af54bb2a5b2a22749673f5cb2226f"
+dependencies = [
+ "webview2-com-macros",
+ "webview2-com-sys",
+ "windows 0.57.0",
+ "windows-core 0.57.0",
+ "windows-implement",
+ "windows-interface",
+]
+
+[[package]]
+name = "webview2-com-macros"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac1345798ecd8122468840bcdf1b95e5dc6d2206c5e4b0eafa078d061f59c9bc"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "webview2-com-sys"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c76d5b77320ff155660be1df3e6588bc85c75f1a9feef938cc4dc4dd60d1d7cf"
+dependencies = [
+ "thiserror",
+ "windows 0.57.0",
+ "windows-core 0.57.0",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "window-vibrancy"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33082acd404763b315866e14a0d5193f3422c81086657583937a750cdd3ec340"
+dependencies = [
+ "cocoa",
+ "objc",
+ "raw-window-handle 0.6.2",
+ "windows-sys 0.52.0",
+ "windows-version",
+]
+
+[[package]]
+name = "windows"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
+dependencies = [
+ "windows-core 0.57.0",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-result",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.57.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-version"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6998aa457c9ba8ff2fb9f13e9d2a930dabcea28f1d0ab94d687d8b3654844515"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winnow"
+version = "0.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winreg"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "wry"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68b00c945786b02d7805d09a969fa36d0eee4e0bd4fb3ec2a79d2bf45a1b44cd"
+dependencies = [
+ "base64 0.22.1",
+ "block",
+ "cocoa",
+ "core-graphics",
+ "crossbeam-channel",
+ "dpi",
+ "dunce",
+ "gdkx11",
+ "gtk",
+ "html5ever",
+ "http",
+ "javascriptcore-rs",
+ "jni",
+ "kuchikiki",
+ "libc",
+ "ndk",
+ "ndk-context",
+ "ndk-sys",
+ "objc",
+ "objc_id",
+ "once_cell",
+ "percent-encoding",
+ "raw-window-handle 0.6.2",
+ "sha2",
+ "soup3",
+ "tao-macros",
+ "thiserror",
+ "webkit2gtk",
+ "webkit2gtk-sys",
+ "webview2-com",
+ "windows 0.57.0",
+ "windows-core 0.57.0",
+ "windows-version",
+ "x11-dl",
+]
+
+[[package]]
+name = "x11"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "x11-dl"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
+dependencies = [
+ "libc",
+ "once_cell",
+ "pkg-config",
+]
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
new file mode 100644
index 00000000..f8a12294
--- /dev/null
+++ b/src-tauri/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "unstoppableswap-gui-rs"
+version = "0.0.0"
+description = "A Tauri App"
+authors = ["you"]
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[lib]
+name = "unstoppableswap_gui_rs_lib"
+crate-type = ["lib", "cdylib", "staticlib"]
+
+[build-dependencies]
+tauri-build = { version = "2.0.0-beta", features = [] }
+
+[dependencies]
+tauri = { version = "2.0.0-beta", features = [] }
+tauri-plugin-shell = "2.0.0-beta"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+
diff --git a/src-tauri/build.rs b/src-tauri/build.rs
new file mode 100644
index 00000000..d860e1e6
--- /dev/null
+++ b/src-tauri/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+ tauri_build::build()
+}
diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json
new file mode 100644
index 00000000..e14ca1d8
--- /dev/null
+++ b/src-tauri/capabilities/default.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "../gen/schemas/desktop-schema.json",
+ "identifier": "default",
+ "description": "Capability for the main window",
+ "windows": ["main"],
+ "permissions": [
+ "path:default",
+ "event:default",
+ "window:default",
+ "app:default",
+ "image:default",
+ "resources:default",
+ "menu:default",
+ "tray:default",
+ "shell:allow-open"
+ ]
+}
diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png
new file mode 100644
index 00000000..6be5e50e
Binary files /dev/null and b/src-tauri/icons/128x128.png differ
diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png
new file mode 100644
index 00000000..e81becee
Binary files /dev/null and b/src-tauri/icons/128x128@2x.png differ
diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png
new file mode 100644
index 00000000..a437dd51
Binary files /dev/null and b/src-tauri/icons/32x32.png differ
diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png
new file mode 100644
index 00000000..0ca4f271
Binary files /dev/null and b/src-tauri/icons/Square107x107Logo.png differ
diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png
new file mode 100644
index 00000000..b81f8203
Binary files /dev/null and b/src-tauri/icons/Square142x142Logo.png differ
diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png
new file mode 100644
index 00000000..624c7bfb
Binary files /dev/null and b/src-tauri/icons/Square150x150Logo.png differ
diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png
new file mode 100644
index 00000000..c021d2ba
Binary files /dev/null and b/src-tauri/icons/Square284x284Logo.png differ
diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png
new file mode 100644
index 00000000..62197002
Binary files /dev/null and b/src-tauri/icons/Square30x30Logo.png differ
diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png
new file mode 100644
index 00000000..f9bc0483
Binary files /dev/null and b/src-tauri/icons/Square310x310Logo.png differ
diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png
new file mode 100644
index 00000000..d5fbfb2a
Binary files /dev/null and b/src-tauri/icons/Square44x44Logo.png differ
diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png
new file mode 100644
index 00000000..63440d79
Binary files /dev/null and b/src-tauri/icons/Square71x71Logo.png differ
diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png
new file mode 100644
index 00000000..f3f705af
Binary files /dev/null and b/src-tauri/icons/Square89x89Logo.png differ
diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png
new file mode 100644
index 00000000..45563882
Binary files /dev/null and b/src-tauri/icons/StoreLogo.png differ
diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns
new file mode 100644
index 00000000..12a5bcee
Binary files /dev/null and b/src-tauri/icons/icon.icns differ
diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico
new file mode 100644
index 00000000..b3636e4b
Binary files /dev/null and b/src-tauri/icons/icon.ico differ
diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png
new file mode 100644
index 00000000..e1cd2619
Binary files /dev/null and b/src-tauri/icons/icon.png differ
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
new file mode 100644
index 00000000..291bed73
--- /dev/null
+++ b/src-tauri/src/lib.rs
@@ -0,0 +1,14 @@
+// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
+#[tauri::command]
+fn greet(name: &str) -> String {
+ format!("Hello, {}! You've been greeted from Rust!", name)
+}
+
+#[cfg_attr(mobile, tauri::mobile_entry_point)]
+pub fn run() {
+ tauri::Builder::default()
+ .plugin(tauri_plugin_shell::init())
+ .invoke_handler(tauri::generate_handler![greet])
+ .run(tauri::generate_context!())
+ .expect("error while running tauri application");
+}
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
new file mode 100644
index 00000000..e8958d27
--- /dev/null
+++ b/src-tauri/src/main.rs
@@ -0,0 +1,6 @@
+// Prevents additional console window on Windows in release, DO NOT REMOVE!!
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+
+fn main() {
+ unstoppableswap_gui_rs_lib::run()
+}
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
new file mode 100644
index 00000000..2fb97fd8
--- /dev/null
+++ b/src-tauri/tauri.conf.json
@@ -0,0 +1,34 @@
+{
+ "productName": "unstoppableswap-gui-rs",
+ "version": "0.0.0",
+ "identifier": "com.tauri.dev",
+ "build": {
+ "beforeDevCommand": "yarn dev",
+ "devUrl": "http://localhost:1420",
+ "beforeBuildCommand": "yarn build",
+ "frontendDist": "../dist"
+ },
+ "app": {
+ "windows": [
+ {
+ "title": "unstoppableswap-gui-rs",
+ "width": 800,
+ "height": 600
+ }
+ ],
+ "security": {
+ "csp": null
+ }
+ },
+ "bundle": {
+ "active": true,
+ "targets": "all",
+ "icon": [
+ "icons/32x32.png",
+ "icons/128x128.png",
+ "icons/128x128@2x.png",
+ "icons/icon.icns",
+ "icons/icon.ico"
+ ]
+ }
+}
diff --git a/src/models/apiModel.ts b/src/models/apiModel.ts
new file mode 100644
index 00000000..bc89c619
--- /dev/null
+++ b/src/models/apiModel.ts
@@ -0,0 +1,28 @@
+export interface ExtendedProviderStatus extends ProviderStatus {
+ uptime?: number;
+ age?: number;
+ relevancy?: number;
+ version?: string;
+ recommended?: boolean;
+}
+
+export interface ProviderStatus extends ProviderQuote, Provider {}
+
+export interface ProviderQuote {
+ price: number;
+ minSwapAmount: number;
+ maxSwapAmount: number;
+}
+
+export interface Provider {
+ multiAddr: string;
+ testnet: boolean;
+ peerId: string;
+}
+
+export interface Alert {
+ id: number;
+ title: string;
+ body: string;
+ severity: 'info' | 'warning' | 'error';
+}
diff --git a/src/models/cliModel.ts b/src/models/cliModel.ts
new file mode 100644
index 00000000..7c713fce
--- /dev/null
+++ b/src/models/cliModel.ts
@@ -0,0 +1,406 @@
+export enum SwapSpawnType {
+ INIT = 'init',
+ RESUME = 'resume',
+ CANCEL_REFUND = 'cancel-refund',
+}
+
+export type CliLogSpanType = string | 'BitcoinWalletSubscription';
+
+export interface CliLog {
+ timestamp: string;
+ level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'TRACE';
+ fields: {
+ message: string;
+ [index: string]: unknown;
+ };
+ spans?: {
+ name: CliLogSpanType;
+ [index: string]: unknown;
+ }[];
+}
+
+export function isCliLog(log: unknown): log is CliLog {
+ if (log && typeof log === 'object') {
+ return (
+ 'timestamp' in (log as CliLog) &&
+ 'level' in (log as CliLog) &&
+ 'fields' in (log as CliLog) &&
+ typeof (log as CliLog).fields?.message === 'string'
+ );
+ }
+ return false;
+}
+
+export interface CliLogStartedRpcServer extends CliLog {
+ fields: {
+ message: 'Started RPC server';
+ addr: string;
+ };
+}
+
+export function isCliLogStartedRpcServer(
+ log: CliLog,
+): log is CliLogStartedRpcServer {
+ return log.fields.message === 'Started RPC server';
+}
+
+export interface CliLogReleasingSwapLockLog extends CliLog {
+ fields: {
+ message: 'Releasing swap lock';
+ swap_id: string;
+ };
+}
+
+export function isCliLogReleasingSwapLockLog(
+ log: CliLog,
+): log is CliLogReleasingSwapLockLog {
+ return log.fields.message === 'Releasing swap lock';
+}
+
+export interface CliLogApiCallError extends CliLog {
+ fields: {
+ message: 'API call resulted in an error';
+ err: string;
+ };
+}
+
+export function isCliLogApiCallError(log: CliLog): log is CliLogApiCallError {
+ return log.fields.message === 'API call resulted in an error';
+}
+
+export interface CliLogAcquiringSwapLockLog extends CliLog {
+ fields: {
+ message: 'Acquiring swap lock';
+ swap_id: string;
+ };
+}
+
+export function isCliLogAcquiringSwapLockLog(
+ log: CliLog,
+): log is CliLogAcquiringSwapLockLog {
+ return log.fields.message === 'Acquiring swap lock';
+}
+
+export interface CliLogReceivedQuote extends CliLog {
+ fields: {
+ message: 'Received quote';
+ price: string;
+ minimum_amount: string;
+ maximum_amount: string;
+ };
+}
+
+export function isCliLogReceivedQuote(log: CliLog): log is CliLogReceivedQuote {
+ return log.fields.message === 'Received quote';
+}
+
+export interface CliLogWaitingForBtcDeposit extends CliLog {
+ fields: {
+ message: 'Waiting for Bitcoin deposit';
+ deposit_address: string;
+ min_deposit_until_swap_will_start: string;
+ max_deposit_until_maximum_amount_is_reached: string;
+ max_giveable: string;
+ minimum_amount: string;
+ maximum_amount: string;
+ min_bitcoin_lock_tx_fee: string;
+ price: string;
+ };
+}
+
+export function isCliLogWaitingForBtcDeposit(
+ log: CliLog,
+): log is CliLogWaitingForBtcDeposit {
+ return log.fields.message === 'Waiting for Bitcoin deposit';
+}
+
+export interface CliLogReceivedBtc extends CliLog {
+ fields: {
+ message: 'Received Bitcoin';
+ max_giveable: string;
+ new_balance: string;
+ };
+}
+
+export function isCliLogReceivedBtc(log: CliLog): log is CliLogReceivedBtc {
+ return log.fields.message === 'Received Bitcoin';
+}
+
+export interface CliLogDeterminedSwapAmount extends CliLog {
+ fields: {
+ message: 'Determined swap amount';
+ amount: string;
+ fees: string;
+ };
+}
+
+export function isCliLogDeterminedSwapAmount(
+ log: CliLog,
+): log is CliLogDeterminedSwapAmount {
+ return log.fields.message === 'Determined swap amount';
+}
+
+export interface CliLogStartedSwap extends CliLog {
+ fields: {
+ message: 'Starting new swap';
+ swap_id: string;
+ };
+}
+
+export function isCliLogStartedSwap(log: CliLog): log is CliLogStartedSwap {
+ return log.fields.message === 'Starting new swap';
+}
+
+export interface CliLogPublishedBtcTx extends CliLog {
+ fields: {
+ message: 'Published Bitcoin transaction';
+ txid: string;
+ kind: 'lock' | 'cancel' | 'withdraw' | 'refund';
+ };
+}
+
+export function isCliLogPublishedBtcTx(
+ log: CliLog,
+): log is CliLogPublishedBtcTx {
+ return log.fields.message === 'Published Bitcoin transaction';
+}
+
+export interface CliLogBtcTxFound extends CliLog {
+ fields: {
+ message: 'Found relevant Bitcoin transaction';
+ txid: string;
+ status: string;
+ };
+}
+
+export function isCliLogBtcTxFound(log: CliLog): log is CliLogBtcTxFound {
+ return log.fields.message === 'Found relevant Bitcoin transaction';
+}
+
+export interface CliLogBtcTxStatusChanged extends CliLog {
+ fields: {
+ message: 'Bitcoin transaction status changed';
+ txid: string;
+ new_status: string;
+ };
+}
+
+export function isCliLogBtcTxStatusChanged(
+ log: CliLog,
+): log is CliLogBtcTxStatusChanged {
+ return log.fields.message === 'Bitcoin transaction status changed';
+}
+
+export interface CliLogAliceLockedXmr extends CliLog {
+ fields: {
+ message: 'Alice locked Monero';
+ txid: string;
+ };
+}
+
+export function isCliLogAliceLockedXmr(
+ log: CliLog,
+): log is CliLogAliceLockedXmr {
+ return log.fields.message === 'Alice locked Monero';
+}
+
+export interface CliLogReceivedXmrLockTxConfirmation extends CliLog {
+ fields: {
+ message: 'Received new confirmation for Monero lock tx';
+ txid: string;
+ seen_confirmations: string;
+ needed_confirmations: string;
+ };
+}
+
+export function isCliLogReceivedXmrLockTxConfirmation(
+ log: CliLog,
+): log is CliLogReceivedXmrLockTxConfirmation {
+ return log.fields.message === 'Received new confirmation for Monero lock tx';
+}
+
+export interface CliLogAdvancingState extends CliLog {
+ fields: {
+ message: 'Advancing state';
+ state:
+ | 'quote has been requested'
+ | 'execution setup done'
+ | 'btc is locked'
+ | 'XMR lock transaction transfer proof received'
+ | 'xmr is locked'
+ | 'encrypted signature is sent'
+ | 'btc is redeemed'
+ | 'cancel timelock is expired'
+ | 'btc is cancelled'
+ | 'btc is refunded'
+ | 'xmr is redeemed'
+ | 'btc is punished'
+ | 'safely aborted';
+ };
+}
+
+export function isCliLogAdvancingState(
+ log: CliLog,
+): log is CliLogAdvancingState {
+ return log.fields.message === 'Advancing state';
+}
+
+export interface CliLogRedeemedXmr extends CliLog {
+ fields: {
+ message: 'Successfully transferred XMR to wallet';
+ monero_receive_address: string;
+ txid: string;
+ };
+}
+
+export function isCliLogRedeemedXmr(log: CliLog): log is CliLogRedeemedXmr {
+ return log.fields.message === 'Successfully transferred XMR to wallet';
+}
+
+export interface YouHaveBeenPunishedCliLog extends CliLog {
+ fields: {
+ message: 'You have been punished for not refunding in time';
+ };
+}
+
+export function isYouHaveBeenPunishedCliLog(
+ log: CliLog,
+): log is YouHaveBeenPunishedCliLog {
+ return (
+ log.fields.message === 'You have been punished for not refunding in time'
+ );
+}
+
+function getCliLogSpanAttribute(log: CliLog, key: string): T | null {
+ const span = log.spans?.find((s) => s[key]);
+ if (!span) {
+ return null;
+ }
+ return span[key] as T;
+}
+
+export function getCliLogSpanSwapId(log: CliLog): string | null {
+ return getCliLogSpanAttribute(log, 'swap_id');
+}
+
+export function getCliLogSpanLogReferenceId(log: CliLog): string | null {
+ return (
+ getCliLogSpanAttribute(log, 'log_reference_id')?.replace(
+ /"/g,
+ '',
+ ) || null
+ );
+}
+
+export function hasCliLogOneOfMultipleSpans(
+ log: CliLog,
+ spanNames: string[],
+): boolean {
+ return log.spans?.some((s) => spanNames.includes(s.name)) ?? false;
+}
+
+export interface CliLogStartedSyncingMoneroWallet extends CliLog {
+ fields: {
+ message: 'Syncing Monero wallet';
+ current_sync_height?: boolean;
+ };
+}
+
+export function isCliLogStartedSyncingMoneroWallet(
+ log: CliLog,
+): log is CliLogStartedSyncingMoneroWallet {
+ return log.fields.message === 'Syncing Monero wallet';
+}
+
+export interface CliLogFinishedSyncingMoneroWallet extends CliLog {
+ fields: {
+ message: 'Synced Monero wallet';
+ };
+}
+
+export interface CliLogFailedToSyncMoneroWallet extends CliLog {
+ fields: {
+ message: 'Failed to sync Monero wallet';
+ error: string;
+ };
+}
+
+export function isCliLogFailedToSyncMoneroWallet(
+ log: CliLog,
+): log is CliLogFailedToSyncMoneroWallet {
+ return log.fields.message === 'Failed to sync Monero wallet';
+}
+
+export function isCliLogFinishedSyncingMoneroWallet(
+ log: CliLog,
+): log is CliLogFinishedSyncingMoneroWallet {
+ return log.fields.message === 'Monero wallet synced';
+}
+
+export interface CliLogDownloadingMoneroWalletRpc extends CliLog {
+ fields: {
+ message: 'Downloading monero-wallet-rpc';
+ progress: string;
+ size: string;
+ download_url: string;
+ };
+}
+
+export function isCliLogDownloadingMoneroWalletRpc(
+ log: CliLog,
+): log is CliLogDownloadingMoneroWalletRpc {
+ return log.fields.message === 'Downloading monero-wallet-rpc';
+}
+
+export interface CliLogStartedSyncingMoneroWallet extends CliLog {
+ fields: {
+ message: 'Syncing Monero wallet';
+ current_sync_height?: boolean;
+ };
+}
+
+export interface CliLogDownloadingMoneroWalletRpc extends CliLog {
+ fields: {
+ message: 'Downloading monero-wallet-rpc';
+ progress: string;
+ size: string;
+ download_url: string;
+ };
+}
+
+export interface CliLogGotNotificationForNewBlock extends CliLog {
+ fields: {
+ message: 'Got notification for new block';
+ block_height: string;
+ };
+}
+
+export function isCliLogGotNotificationForNewBlock(
+ log: CliLog,
+): log is CliLogGotNotificationForNewBlock {
+ return log.fields.message === 'Got notification for new block';
+}
+
+export interface CliLogAttemptingToCooperativelyRedeemXmr extends CliLog {
+ fields: {
+ message: 'Attempting to cooperatively redeem XMR after being punished';
+ };
+}
+
+export function isCliLogAttemptingToCooperativelyRedeemXmr(
+ log: CliLog,
+): log is CliLogAttemptingToCooperativelyRedeemXmr {
+ return log.fields.message === 'Attempting to cooperatively redeem XMR after being punished';
+}
+
+export interface CliLogAliceHasAcceptedOurRequestToCooperativelyRedeemTheXmr extends CliLog {
+ fields: {
+ message: 'Alice has accepted our request to cooperatively redeem the XMR';
+ };
+}
+
+export function isCliLogAliceHasAcceptedOurRequestToCooperativelyRedeemTheXmr(
+ log: CliLog,
+): log is CliLogAliceHasAcceptedOurRequestToCooperativelyRedeemTheXmr {
+ return log.fields.message === 'Alice has accepted our request to cooperatively redeem the XMR';
+}
\ No newline at end of file
diff --git a/src/models/downloaderModel.ts b/src/models/downloaderModel.ts
new file mode 100644
index 00000000..779d4378
--- /dev/null
+++ b/src/models/downloaderModel.ts
@@ -0,0 +1,4 @@
+export interface Binary {
+ dirPath: string; // Path without filename appended
+ fileName: string;
+}
diff --git a/src/models/rpcModel.ts b/src/models/rpcModel.ts
new file mode 100644
index 00000000..506fb399
--- /dev/null
+++ b/src/models/rpcModel.ts
@@ -0,0 +1,336 @@
+import { piconerosToXmr, satsToBtc } from 'utils/conversionUtils';
+import { exhaustiveGuard } from 'utils/typescriptUtils';
+
+export enum RpcMethod {
+ GET_BTC_BALANCE = 'get_bitcoin_balance',
+ WITHDRAW_BTC = 'withdraw_btc',
+ BUY_XMR = 'buy_xmr',
+ RESUME_SWAP = 'resume_swap',
+ LIST_SELLERS = 'list_sellers',
+ CANCEL_REFUND_SWAP = 'cancel_refund_swap',
+ GET_SWAP_INFO = 'get_swap_info',
+ SUSPEND_CURRENT_SWAP = 'suspend_current_swap',
+ GET_HISTORY = 'get_history',
+ GET_MONERO_RECOVERY_KEYS = 'get_monero_recovery_info',
+}
+
+export enum RpcProcessStateType {
+ STARTED = 'starting...',
+ LISTENING_FOR_CONNECTIONS = 'running',
+ EXITED = 'exited',
+ NOT_STARTED = 'not started',
+}
+
+export type RawRpcResponseSuccess = {
+ jsonrpc: string;
+ id: string;
+ result: T;
+};
+
+export type RawRpcResponseError = {
+ jsonrpc: string;
+ id: string;
+ error: { code: number; message: string };
+};
+
+export type RawRpcResponse = RawRpcResponseSuccess | RawRpcResponseError;
+
+export function isSuccessResponse(
+ response: RawRpcResponse,
+): response is RawRpcResponseSuccess {
+ return 'result' in response;
+}
+
+export function isErrorResponse(
+ response: RawRpcResponse,
+): response is RawRpcResponseError {
+ return 'error' in response;
+}
+
+export interface RpcSellerStatus {
+ status:
+ | {
+ Online: {
+ price: number;
+ min_quantity: number;
+ max_quantity: number;
+ };
+ }
+ | 'Unreachable';
+ multiaddr: string;
+}
+
+export interface WithdrawBitcoinResponse {
+ txid: string;
+}
+
+export interface BuyXmrResponse {
+ swapId: string;
+}
+
+export type SwapTimelockInfoNone = {
+ None: {
+ blocks_left: number;
+ };
+};
+
+export type SwapTimelockInfoCancelled = {
+ Cancel: {
+ blocks_left: number;
+ };
+};
+
+export type SwapTimelockInfoPunished = 'Punish';
+
+export type SwapTimelockInfo =
+ | SwapTimelockInfoNone
+ | SwapTimelockInfoCancelled
+ | SwapTimelockInfoPunished;
+
+export function isSwapTimelockInfoNone(
+ info: SwapTimelockInfo,
+): info is SwapTimelockInfoNone {
+ return typeof info === 'object' && 'None' in info;
+}
+
+export function isSwapTimelockInfoCancelled(
+ info: SwapTimelockInfo,
+): info is SwapTimelockInfoCancelled {
+ return typeof info === 'object' && 'Cancel' in info;
+}
+
+export function isSwapTimelockInfoPunished(
+ info: SwapTimelockInfo,
+): info is SwapTimelockInfoPunished {
+ return info === 'Punish';
+}
+
+export type SwapSellerInfo = {
+ peerId: string;
+ addresses: string[];
+};
+
+export interface GetSwapInfoResponse {
+ swapId: string;
+ completed: boolean;
+ seller: SwapSellerInfo;
+ startDate: string;
+ stateName: SwapStateName;
+ timelock: null | SwapTimelockInfo;
+ txLockId: string;
+ txCancelFee: number;
+ txRefundFee: number;
+ txLockFee: number;
+ btcAmount: number;
+ xmrAmount: number;
+ btcRefundAddress: string;
+ cancelTimelock: number;
+ punishTimelock: number;
+}
+
+export type MoneroRecoveryResponse = {
+ address: string;
+ spend_key: string;
+ view_key: string;
+ restore_height: number;
+};
+
+export interface BalanceBitcoinResponse {
+ balance: number;
+}
+
+export interface GetHistoryResponse {
+ swaps: [swapId: string, stateName: SwapStateName][];
+}
+
+export enum SwapStateName {
+ Started = 'quote has been requested',
+ SwapSetupCompleted = 'execution setup done',
+ BtcLocked = 'btc is locked',
+ XmrLockProofReceived = 'XMR lock transaction transfer proof received',
+ XmrLocked = 'xmr is locked',
+ EncSigSent = 'encrypted signature is sent',
+ BtcRedeemed = 'btc is redeemed',
+ CancelTimelockExpired = 'cancel timelock is expired',
+ BtcCancelled = 'btc is cancelled',
+ BtcRefunded = 'btc is refunded',
+ XmrRedeemed = 'xmr is redeemed',
+ BtcPunished = 'btc is punished',
+ SafelyAborted = 'safely aborted',
+}
+
+export type SwapStateNameRunningSwap = Exclude<
+ SwapStateName,
+ | SwapStateName.Started
+ | SwapStateName.SwapSetupCompleted
+ | SwapStateName.BtcRefunded
+ | SwapStateName.BtcPunished
+ | SwapStateName.SafelyAborted
+ | SwapStateName.XmrRedeemed
+>;
+
+export type GetSwapInfoResponseRunningSwap = GetSwapInfoResponse & {
+ stateName: SwapStateNameRunningSwap;
+};
+
+export function isSwapStateNameRunningSwap(
+ state: SwapStateName,
+): state is SwapStateNameRunningSwap {
+ return ![
+ SwapStateName.Started,
+ SwapStateName.SwapSetupCompleted,
+ SwapStateName.BtcRefunded,
+ SwapStateName.BtcPunished,
+ SwapStateName.SafelyAborted,
+ SwapStateName.XmrRedeemed,
+ ].includes(state);
+}
+
+export type SwapStateNameCompletedSwap =
+ | SwapStateName.XmrRedeemed
+ | SwapStateName.BtcRefunded
+ | SwapStateName.BtcPunished
+ | SwapStateName.SafelyAborted;
+
+export function isSwapStateNameCompletedSwap(
+ state: SwapStateName,
+): state is SwapStateNameCompletedSwap {
+ return [
+ SwapStateName.XmrRedeemed,
+ SwapStateName.BtcRefunded,
+ SwapStateName.BtcPunished,
+ SwapStateName.SafelyAborted,
+ ].includes(state);
+}
+
+export type SwapStateNamePossiblyCancellableSwap =
+ | SwapStateName.BtcLocked
+ | SwapStateName.XmrLockProofReceived
+ | SwapStateName.XmrLocked
+ | SwapStateName.EncSigSent
+ | SwapStateName.CancelTimelockExpired;
+
+/**
+Checks if a swap is in a state where it can possibly be cancelled
+
+The following conditions must be met:
+ - The bitcoin must be locked
+ - The bitcoin must not be redeemed
+ - The bitcoin must not be cancelled
+ - The bitcoin must not be refunded
+ - The bitcoin must not be punished
+
+See: https://github.com/comit-network/xmr-btc-swap/blob/7023e75bb51ab26dff4c8fcccdc855d781ca4b15/swap/src/cli/cancel.rs#L16-L35
+ */
+export function isSwapStateNamePossiblyCancellableSwap(
+ state: SwapStateName,
+): state is SwapStateNamePossiblyCancellableSwap {
+ return [
+ SwapStateName.BtcLocked,
+ SwapStateName.XmrLockProofReceived,
+ SwapStateName.XmrLocked,
+ SwapStateName.EncSigSent,
+ SwapStateName.CancelTimelockExpired,
+ ].includes(state);
+}
+
+export type SwapStateNamePossiblyRefundableSwap =
+ | SwapStateName.BtcLocked
+ | SwapStateName.XmrLockProofReceived
+ | SwapStateName.XmrLocked
+ | SwapStateName.EncSigSent
+ | SwapStateName.CancelTimelockExpired
+ | SwapStateName.BtcCancelled;
+
+/**
+Checks if a swap is in a state where it can possibly be refunded (meaning it's not impossible)
+
+The following conditions must be met:
+ - The bitcoin must be locked
+ - The bitcoin must not be redeemed
+ - The bitcoin must not be refunded
+ - The bitcoin must not be punished
+
+See: https://github.com/comit-network/xmr-btc-swap/blob/7023e75bb51ab26dff4c8fcccdc855d781ca4b15/swap/src/cli/refund.rs#L16-L34
+ */
+export function isSwapStateNamePossiblyRefundableSwap(
+ state: SwapStateName,
+): state is SwapStateNamePossiblyRefundableSwap {
+ return [
+ SwapStateName.BtcLocked,
+ SwapStateName.XmrLockProofReceived,
+ SwapStateName.XmrLocked,
+ SwapStateName.EncSigSent,
+ SwapStateName.CancelTimelockExpired,
+ SwapStateName.BtcCancelled,
+ ].includes(state);
+}
+
+/**
+ * Type guard for GetSwapInfoResponseRunningSwap
+ * "running" means the swap is in progress and not yet completed
+ * If a swap is not "running" it means it is either completed or no Bitcoin have been locked yet
+ * @param response
+ */
+export function isGetSwapInfoResponseRunningSwap(
+ response: GetSwapInfoResponse,
+): response is GetSwapInfoResponseRunningSwap {
+ return isSwapStateNameRunningSwap(response.stateName);
+}
+
+export function isSwapMoneroRecoverable(swapStateName: SwapStateName): boolean {
+ return [SwapStateName.BtcRedeemed].includes(swapStateName);
+}
+
+// See https://github.com/comit-network/xmr-btc-swap/blob/50ae54141255e03dba3d2b09036b1caa4a63e5a3/swap/src/protocol/bob/state.rs#L55
+export function getHumanReadableDbStateType(type: SwapStateName): string {
+ switch (type) {
+ case SwapStateName.Started:
+ return 'Quote has been requested';
+ case SwapStateName.SwapSetupCompleted:
+ return 'Swap has been initiated';
+ case SwapStateName.BtcLocked:
+ return 'Bitcoin has been locked';
+ case SwapStateName.XmrLockProofReceived:
+ return 'Monero lock transaction transfer proof has been received';
+ case SwapStateName.XmrLocked:
+ return 'Monero has been locked';
+ case SwapStateName.EncSigSent:
+ return 'Encrypted signature has been sent';
+ case SwapStateName.BtcRedeemed:
+ return 'Bitcoin has been redeemed';
+ case SwapStateName.CancelTimelockExpired:
+ return 'Cancel timelock has expired';
+ case SwapStateName.BtcCancelled:
+ return 'Swap has been cancelled';
+ case SwapStateName.BtcRefunded:
+ return 'Bitcoin has been refunded';
+ case SwapStateName.XmrRedeemed:
+ return 'Monero has been redeemed';
+ case SwapStateName.BtcPunished:
+ return 'Bitcoin has been punished';
+ case SwapStateName.SafelyAborted:
+ return 'Swap has been safely aborted';
+ default:
+ return exhaustiveGuard(type);
+ }
+}
+
+export function getSwapTxFees(swap: GetSwapInfoResponse): number {
+ return satsToBtc(swap.txLockFee);
+}
+
+export function getSwapBtcAmount(swap: GetSwapInfoResponse): number {
+ return satsToBtc(swap.btcAmount);
+}
+
+export function getSwapXmrAmount(swap: GetSwapInfoResponse): number {
+ return piconerosToXmr(swap.xmrAmount);
+}
+
+export function getSwapExchangeRate(swap: GetSwapInfoResponse): number {
+ const btcAmount = getSwapBtcAmount(swap);
+ const xmrAmount = getSwapXmrAmount(swap);
+
+ return btcAmount / xmrAmount;
+}
diff --git a/src/models/storeModel.ts b/src/models/storeModel.ts
new file mode 100644
index 00000000..de6b8754
--- /dev/null
+++ b/src/models/storeModel.ts
@@ -0,0 +1,218 @@
+import { CliLog, SwapSpawnType } from './cliModel';
+import { Provider } from './apiModel';
+
+export interface SwapSlice {
+ state: SwapState | null;
+ logs: CliLog[];
+ processRunning: boolean;
+ provider: Provider | null;
+ spawnType: SwapSpawnType | null;
+ swapId: string | null;
+}
+
+export type MoneroWalletRpcUpdateState = {
+ progress: string;
+ downloadUrl: string;
+};
+
+export interface SwapState {
+ type: SwapStateType;
+}
+
+export enum SwapStateType {
+ INITIATED = 'initiated',
+ RECEIVED_QUOTE = 'received quote',
+ WAITING_FOR_BTC_DEPOSIT = 'waiting for btc deposit',
+ STARTED = 'started',
+ BTC_LOCK_TX_IN_MEMPOOL = 'btc lock tx is in mempool',
+ XMR_LOCK_TX_IN_MEMPOOL = 'xmr lock tx is in mempool',
+ XMR_LOCKED = 'xmr is locked',
+ BTC_REDEEMED = 'btc redeemed',
+ XMR_REDEEM_IN_MEMPOOL = 'xmr redeem tx is in mempool',
+ PROCESS_EXITED = 'process exited',
+ BTC_CANCELLED = 'btc cancelled',
+ BTC_REFUNDED = 'btc refunded',
+ BTC_PUNISHED = 'btc punished',
+ ATTEMPTING_COOPERATIVE_REDEEM = 'attempting cooperative redeem',
+ COOPERATIVE_REDEEM_REJECTED = 'cooperative redeem rejected',
+}
+
+export function isSwapState(state?: SwapState | null): state is SwapState {
+ return state?.type != null;
+}
+
+export interface SwapStateInitiated extends SwapState {
+ type: SwapStateType.INITIATED;
+}
+
+export function isSwapStateInitiated(
+ state?: SwapState | null,
+): state is SwapStateInitiated {
+ return state?.type === SwapStateType.INITIATED;
+}
+
+export interface SwapStateReceivedQuote extends SwapState {
+ type: SwapStateType.RECEIVED_QUOTE;
+ price: number;
+ minimumSwapAmount: number;
+ maximumSwapAmount: number;
+}
+
+export function isSwapStateReceivedQuote(
+ state?: SwapState | null,
+): state is SwapStateReceivedQuote {
+ return state?.type === SwapStateType.RECEIVED_QUOTE;
+}
+
+export interface SwapStateWaitingForBtcDeposit extends SwapState {
+ type: SwapStateType.WAITING_FOR_BTC_DEPOSIT;
+ depositAddress: string;
+ maxGiveable: number;
+ minimumAmount: number;
+ maximumAmount: number;
+ minDeposit: number;
+ maxDeposit: number;
+ minBitcoinLockTxFee: number;
+ price: number | null;
+}
+
+export function isSwapStateWaitingForBtcDeposit(
+ state?: SwapState | null,
+): state is SwapStateWaitingForBtcDeposit {
+ return state?.type === SwapStateType.WAITING_FOR_BTC_DEPOSIT;
+}
+
+export interface SwapStateStarted extends SwapState {
+ type: SwapStateType.STARTED;
+ txLockDetails: {
+ amount: number;
+ fees: number;
+ } | null;
+}
+
+export function isSwapStateStarted(
+ state?: SwapState | null,
+): state is SwapStateStarted {
+ return state?.type === SwapStateType.STARTED;
+}
+
+export interface SwapStateBtcLockInMempool extends SwapState {
+ type: SwapStateType.BTC_LOCK_TX_IN_MEMPOOL;
+ bobBtcLockTxId: string;
+ bobBtcLockTxConfirmations: number;
+}
+
+export function isSwapStateBtcLockInMempool(
+ state?: SwapState | null,
+): state is SwapStateBtcLockInMempool {
+ return state?.type === SwapStateType.BTC_LOCK_TX_IN_MEMPOOL;
+}
+
+export interface SwapStateXmrLockInMempool extends SwapState {
+ type: SwapStateType.XMR_LOCK_TX_IN_MEMPOOL;
+ aliceXmrLockTxId: string;
+ aliceXmrLockTxConfirmations: number;
+}
+
+export function isSwapStateXmrLockInMempool(
+ state?: SwapState | null,
+): state is SwapStateXmrLockInMempool {
+ return state?.type === SwapStateType.XMR_LOCK_TX_IN_MEMPOOL;
+}
+
+export interface SwapStateXmrLocked extends SwapState {
+ type: SwapStateType.XMR_LOCKED;
+}
+
+export function isSwapStateXmrLocked(
+ state?: SwapState | null,
+): state is SwapStateXmrLocked {
+ return state?.type === SwapStateType.XMR_LOCKED;
+}
+
+export interface SwapStateBtcRedemeed extends SwapState {
+ type: SwapStateType.BTC_REDEEMED;
+}
+
+export function isSwapStateBtcRedemeed(
+ state?: SwapState | null,
+): state is SwapStateBtcRedemeed {
+ return state?.type === SwapStateType.BTC_REDEEMED;
+}
+
+export interface SwapStateAttemptingCooperativeRedeeem extends SwapState {
+ type: SwapStateType.ATTEMPTING_COOPERATIVE_REDEEM;
+}
+
+export function isSwapStateAttemptingCooperativeRedeeem(
+ state?: SwapState | null,
+): state is SwapStateAttemptingCooperativeRedeeem {
+ return state?.type === SwapStateType.ATTEMPTING_COOPERATIVE_REDEEM;
+}
+
+export interface SwapStateCooperativeRedeemRejected extends SwapState {
+ type: SwapStateType.COOPERATIVE_REDEEM_REJECTED;
+ reason: string;
+}
+
+export function isSwapStateCooperativeRedeemRejected(
+ state?: SwapState | null,
+): state is SwapStateCooperativeRedeemRejected {
+ return state?.type === SwapStateType.COOPERATIVE_REDEEM_REJECTED;
+}
+
+export interface SwapStateXmrRedeemInMempool extends SwapState {
+ type: SwapStateType.XMR_REDEEM_IN_MEMPOOL;
+ bobXmrRedeemTxId: string;
+ bobXmrRedeemAddress: string;
+}
+
+export function isSwapStateXmrRedeemInMempool(
+ state?: SwapState | null,
+): state is SwapStateXmrRedeemInMempool {
+ return state?.type === SwapStateType.XMR_REDEEM_IN_MEMPOOL;
+}
+
+export interface SwapStateBtcCancelled extends SwapState {
+ type: SwapStateType.BTC_CANCELLED;
+ btcCancelTxId: string;
+}
+
+export function isSwapStateBtcCancelled(
+ state?: SwapState | null,
+): state is SwapStateBtcCancelled {
+ return state?.type === SwapStateType.BTC_CANCELLED;
+}
+
+export interface SwapStateBtcRefunded extends SwapState {
+ type: SwapStateType.BTC_REFUNDED;
+ bobBtcRefundTxId: string;
+}
+
+export function isSwapStateBtcRefunded(
+ state?: SwapState | null,
+): state is SwapStateBtcRefunded {
+ return state?.type === SwapStateType.BTC_REFUNDED;
+}
+
+export interface SwapStateBtcPunished extends SwapState {
+ type: SwapStateType.BTC_PUNISHED;
+}
+
+export function isSwapStateBtcPunished(
+ state?: SwapState | null,
+): state is SwapStateBtcPunished {
+ return state?.type === SwapStateType.BTC_PUNISHED;
+}
+
+export interface SwapStateProcessExited extends SwapState {
+ type: SwapStateType.PROCESS_EXITED;
+ prevState: SwapState | null;
+ rpcError: string | null;
+}
+
+export function isSwapStateProcessExited(
+ state?: SwapState | null,
+): state is SwapStateProcessExited {
+ return state?.type === SwapStateType.PROCESS_EXITED;
+}
diff --git a/src/renderer/api.ts b/src/renderer/api.ts
new file mode 100644
index 00000000..222d0307
--- /dev/null
+++ b/src/renderer/api.ts
@@ -0,0 +1,61 @@
+import { Alert, ExtendedProviderStatus } from 'models/apiModel';
+
+const API_BASE_URL = 'https://api.unstoppableswap.net';
+
+export async function fetchProvidersViaHttp(): Promise<
+ ExtendedProviderStatus[]
+> {
+ const response = await fetch(`${API_BASE_URL}/api/list`);
+ return (await response.json()) as ExtendedProviderStatus[];
+}
+
+export async function fetchAlertsViaHttp(): Promise {
+ const response = await fetch(`${API_BASE_URL}/api/alerts`);
+ return (await response.json()) as Alert[];
+}
+
+export async function submitFeedbackViaHttp(
+ body: string,
+ attachedData: string,
+): Promise {
+ type Response = {
+ feedbackId: string;
+ };
+
+ const response = await fetch(`${API_BASE_URL}/api/submit-feedback`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ body, attachedData }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Status: ${response.status}`);
+ }
+
+ const responseBody = (await response.json()) as Response;
+
+ return responseBody.feedbackId;
+}
+
+async function fetchCurrencyUsdPrice(currency: string): Promise {
+ try {
+ const response = await fetch(
+ `https://api.coingecko.com/api/v3/simple/price?ids=${currency}&vs_currencies=usd`,
+ );
+ const data = await response.json();
+ return data[currency].usd;
+ } catch (error) {
+ console.error(`Error fetching ${currency} price:`, error);
+ throw error;
+ }
+}
+
+export async function fetchBtcPrice(): Promise {
+ return fetchCurrencyUsdPrice('bitcoin');
+}
+
+export async function fetchXmrPrice(): Promise {
+ return fetchCurrencyUsdPrice('monero');
+}
diff --git a/src/renderer/components/App.tsx b/src/renderer/components/App.tsx
new file mode 100644
index 00000000..1063537e
--- /dev/null
+++ b/src/renderer/components/App.tsx
@@ -0,0 +1,67 @@
+import { Box, makeStyles, CssBaseline } from '@material-ui/core';
+import { createTheme, ThemeProvider } from '@material-ui/core/styles';
+import { indigo } from '@material-ui/core/colors';
+import { MemoryRouter as Router, Routes, Route } from 'react-router-dom';
+import Navigation, { drawerWidth } from './navigation/Navigation';
+import HistoryPage from './pages/history/HistoryPage';
+import SwapPage from './pages/swap/SwapPage';
+import WalletPage from './pages/wallet/WalletPage';
+import HelpPage from './pages/help/HelpPage';
+import GlobalSnackbarProvider from './snackbar/GlobalSnackbarProvider';
+
+const useStyles = makeStyles((theme) => ({
+ innerContent: {
+ padding: theme.spacing(4),
+ marginLeft: drawerWidth,
+ maxHeight: `100vh`,
+ flex: 1,
+ },
+}));
+
+const theme = createTheme({
+ palette: {
+ type: 'dark',
+ primary: {
+ main: '#f4511e',
+ },
+ secondary: indigo,
+ },
+ transitions: {
+ create: () => 'none',
+ },
+ props: {
+ MuiButtonBase: {
+ disableRipple: true,
+ },
+ },
+});
+
+function InnerContent() {
+ const classes = useStyles();
+
+ return (
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ );
+}
+
+export default function App() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/renderer/components/IpcInvokeButton.tsx b/src/renderer/components/IpcInvokeButton.tsx
new file mode 100644
index 00000000..c2a39266
--- /dev/null
+++ b/src/renderer/components/IpcInvokeButton.tsx
@@ -0,0 +1,166 @@
+import {
+ Button,
+ ButtonProps,
+ CircularProgress,
+ IconButton,
+ Tooltip,
+} from '@material-ui/core';
+import { ReactElement, ReactNode, useEffect, useState } from 'react';
+import { useSnackbar } from 'notistack';
+import { useAppSelector } from 'store/hooks';
+import { RpcProcessStateType } from 'models/rpcModel';
+import { isExternalRpc } from 'store/config';
+
+function IpcButtonTooltip({
+ requiresRpcAndNotReady,
+ children,
+ processType,
+ tooltipTitle,
+}: {
+ requiresRpcAndNotReady: boolean;
+ children: ReactElement;
+ processType: RpcProcessStateType;
+ tooltipTitle?: string;
+}) {
+ if (tooltipTitle) {
+ return {children} ;
+ }
+
+ const getMessage = () => {
+ if (!requiresRpcAndNotReady) return '';
+
+ switch (processType) {
+ case RpcProcessStateType.LISTENING_FOR_CONNECTIONS:
+ return '';
+ case RpcProcessStateType.STARTED:
+ return 'Cannot execute this action because the Swap Daemon is still starting and not yet ready to accept connections. Please wait a moment and try again';
+ case RpcProcessStateType.EXITED:
+ return 'Cannot execute this action because the Swap Daemon has been stopped. Please start the Swap Daemon again to continue';
+ case RpcProcessStateType.NOT_STARTED:
+ return 'Cannot execute this action because the Swap Daemon has not been started yet. Please start the Swap Daemon first';
+ default:
+ return '';
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+interface IpcInvokeButtonProps {
+ ipcArgs: unknown[];
+ ipcChannel: string;
+ onSuccess?: (data: T) => void;
+ isLoadingOverride?: boolean;
+ isIconButton?: boolean;
+ loadIcon?: ReactNode;
+ requiresRpc?: boolean;
+ disabled?: boolean;
+ displayErrorSnackbar?: boolean;
+ tooltipTitle?: string;
+}
+
+const DELAY_BEFORE_SHOWING_LOADING_MS = 0;
+
+export default function IpcInvokeButton({
+ disabled,
+ ipcChannel,
+ ipcArgs,
+ onSuccess,
+ onClick,
+ endIcon,
+ loadIcon,
+ isLoadingOverride,
+ isIconButton,
+ requiresRpc,
+ displayErrorSnackbar,
+ tooltipTitle,
+ ...rest
+}: IpcInvokeButtonProps & ButtonProps) {
+ const { enqueueSnackbar } = useSnackbar();
+
+ const rpcProcessType = useAppSelector((state) => state.rpc.process.type);
+ const isRpcReady =
+ rpcProcessType === RpcProcessStateType.LISTENING_FOR_CONNECTIONS;
+ const [isPending, setIsPending] = useState(false);
+ const [hasMinLoadingTimePassed, setHasMinLoadingTimePassed] = useState(false);
+
+ const isLoading = (isPending && hasMinLoadingTimePassed) || isLoadingOverride;
+ const actualEndIcon = isLoading
+ ? loadIcon ||
+ : endIcon;
+
+ useEffect(() => {
+ setHasMinLoadingTimePassed(false);
+ setTimeout(
+ () => setHasMinLoadingTimePassed(true),
+ DELAY_BEFORE_SHOWING_LOADING_MS,
+ );
+ }, [isPending]);
+
+ async function handleClick(event: React.MouseEvent) {
+ onClick?.(event);
+
+ if (!isPending) {
+ setIsPending(true);
+ try {
+ // const result = await ipcRenderer.invoke(ipcChannel, ...ipcArgs);
+ throw new Error('Not implemented');
+ // onSuccess?.(result);
+ } catch (e: unknown) {
+ if (displayErrorSnackbar) {
+ enqueueSnackbar((e as Error).message, {
+ autoHideDuration: 60 * 1000,
+ variant: 'error',
+ });
+ }
+ } finally {
+ setIsPending(false);
+ }
+ }
+ }
+
+ const requiresRpcAndNotReady =
+ !!requiresRpc && !isRpcReady && !isExternalRpc();
+ const isDisabled = disabled || requiresRpcAndNotReady || isLoading;
+
+ return (
+
+
+ {isIconButton ? (
+
+ {actualEndIcon}
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+IpcInvokeButton.defaultProps = {
+ requiresRpc: true,
+ disabled: false,
+ onSuccess: undefined,
+ isLoadingOverride: false,
+ isIconButton: false,
+ loadIcon: undefined,
+ displayErrorSnackbar: true,
+};
diff --git a/src/renderer/components/alert/FundsLeftInWalletAlert.tsx b/src/renderer/components/alert/FundsLeftInWalletAlert.tsx
new file mode 100644
index 00000000..269b8973
--- /dev/null
+++ b/src/renderer/components/alert/FundsLeftInWalletAlert.tsx
@@ -0,0 +1,30 @@
+import { Button } from '@material-ui/core';
+import Alert from '@material-ui/lab/Alert';
+import { useNavigate } from 'react-router-dom';
+import { useAppSelector } from 'store/hooks';
+
+export default function FundsLeftInWalletAlert() {
+ const fundsLeft = useAppSelector((state) => state.rpc.state.balance);
+ const navigate = useNavigate();
+
+ if (fundsLeft != null && fundsLeft > 0) {
+ return (
+ navigate('/wallet')}
+ >
+ View
+
+ }
+ >
+ There are some Bitcoin left in your wallet
+
+ );
+ }
+ return null;
+}
diff --git a/src/renderer/components/alert/MoneroWalletRpcUpdatingAlert.tsx b/src/renderer/components/alert/MoneroWalletRpcUpdatingAlert.tsx
new file mode 100644
index 00000000..d58a2dd1
--- /dev/null
+++ b/src/renderer/components/alert/MoneroWalletRpcUpdatingAlert.tsx
@@ -0,0 +1,30 @@
+import { Alert } from '@material-ui/lab';
+import { Box, LinearProgress } from '@material-ui/core';
+import { useAppSelector } from 'store/hooks';
+
+export default function MoneroWalletRpcUpdatingAlert() {
+ const updateState = useAppSelector(
+ (s) => s.rpc.state.moneroWalletRpc.updateState,
+ );
+
+ if (updateState === false) {
+ return null;
+ }
+
+ const progress = Number.parseFloat(
+ updateState.progress.substring(0, updateState.progress.length - 1),
+ );
+
+ return (
+
+
+ The Monero wallet is updating. This may take a few moments
+
+
+
+ );
+}
diff --git a/src/renderer/components/alert/RemainingFundsWillBeUsedAlert.tsx b/src/renderer/components/alert/RemainingFundsWillBeUsedAlert.tsx
new file mode 100644
index 00000000..e297d063
--- /dev/null
+++ b/src/renderer/components/alert/RemainingFundsWillBeUsedAlert.tsx
@@ -0,0 +1,35 @@
+import { Alert } from '@material-ui/lab';
+import { Box, makeStyles } from '@material-ui/core';
+import { useAppSelector } from 'store/hooks';
+import WalletRefreshButton from '../pages/wallet/WalletRefreshButton';
+import { SatsAmount } from '../other/Units';
+
+const useStyles = makeStyles((theme) => ({
+ outer: {
+ paddingBottom: theme.spacing(1),
+ },
+}));
+
+export default function RemainingFundsWillBeUsedAlert() {
+ const classes = useStyles();
+ const balance = useAppSelector((s) => s.rpc.state.balance);
+
+ if (balance == null || balance <= 0) {
+ return <>>;
+ }
+
+ return (
+
+ }
+ variant="filled"
+ >
+ The remaining funds of in the wallet
+ will be used for the next swap. If the remaining funds exceed the
+ minimum swap amount of the provider, a swap will be initiated
+ instantaneously.
+
+
+ );
+}
diff --git a/src/renderer/components/alert/RpcStatusAlert.tsx b/src/renderer/components/alert/RpcStatusAlert.tsx
new file mode 100644
index 00000000..a03aff54
--- /dev/null
+++ b/src/renderer/components/alert/RpcStatusAlert.tsx
@@ -0,0 +1,27 @@
+import { Alert } from '@material-ui/lab';
+import { CircularProgress } from '@material-ui/core';
+import { useAppSelector } from 'store/hooks';
+import { RpcProcessStateType } from 'models/rpcModel';
+
+export default function RpcStatusAlert() {
+ const rpcProcess = useAppSelector((s) => s.rpc.process);
+ if (rpcProcess.type === RpcProcessStateType.STARTED) {
+ return (
+ }>
+ The swap daemon is starting
+
+ );
+ }
+ if (rpcProcess.type === RpcProcessStateType.LISTENING_FOR_CONNECTIONS) {
+ return The swap daemon is running ;
+ }
+ if (rpcProcess.type === RpcProcessStateType.NOT_STARTED) {
+ return The swap daemon is being started ;
+ }
+ if (rpcProcess.type === RpcProcessStateType.EXITED) {
+ return (
+ The swap daemon has stopped unexpectedly
+ );
+ }
+ return <>>;
+}
diff --git a/src/renderer/components/alert/SwapMightBeCancelledAlert.tsx b/src/renderer/components/alert/SwapMightBeCancelledAlert.tsx
new file mode 100644
index 00000000..4bd2cd62
--- /dev/null
+++ b/src/renderer/components/alert/SwapMightBeCancelledAlert.tsx
@@ -0,0 +1,97 @@
+import { makeStyles } from '@material-ui/core';
+import { Alert, AlertTitle } from '@material-ui/lab';
+import { useActiveSwapInfo } from 'store/hooks';
+import {
+ isSwapTimelockInfoCancelled,
+ isSwapTimelockInfoNone,
+} from 'models/rpcModel';
+import HumanizedBitcoinBlockDuration from '../other/HumanizedBitcoinBlockDuration';
+
+const useStyles = makeStyles((theme) => ({
+ outer: {
+ marginBottom: theme.spacing(1),
+ },
+ list: {
+ margin: theme.spacing(0.25),
+ },
+}));
+
+export default function SwapMightBeCancelledAlert({
+ bobBtcLockTxConfirmations,
+}: {
+ bobBtcLockTxConfirmations: number;
+}) {
+ const classes = useStyles();
+ const swap = useActiveSwapInfo();
+
+ if (
+ bobBtcLockTxConfirmations < 5 ||
+ swap === null ||
+ swap.timelock === null
+ ) {
+ return <>>;
+ }
+
+ const { timelock } = swap;
+ const punishTimelockOffset = swap.punishTimelock;
+
+ return (
+
+ Be careful!
+ The swap provider has taken a long time to lock their Monero. This might
+ mean that:
+
+
+ There is a technical issue that prevents them from locking their funds
+
+ They are a malicious actor (unlikely)
+
+
+ There is still hope for the swap to be successful but you have to be extra
+ careful. Regardless of why it has taken them so long, it is important that
+ you refund the swap within the required time period if the swap is not
+ completed. If you fail to to do so, you will be punished and lose your
+ money.
+
+ {isSwapTimelockInfoNone(timelock) && (
+ <>
+
+
+ You will be able to refund in about{' '}
+
+
+
+
+
+
+ If you have not refunded or completed the swap in about{' '}
+
+ , you will lose your funds.
+
+
+ >
+ )}
+ {isSwapTimelockInfoCancelled(timelock) && (
+
+
+ If you have not refunded or completed the swap in about{' '}
+
+ , you will lose your funds.
+
+
+ )}
+
+ As long as you see this screen, the swap will be refunded
+ automatically when the time comes. If this fails, you have to manually
+ refund by navigating to the History page.
+
+
+
+ );
+}
diff --git a/src/renderer/components/alert/SwapStatusAlert.tsx b/src/renderer/components/alert/SwapStatusAlert.tsx
new file mode 100644
index 00000000..91fcb06b
--- /dev/null
+++ b/src/renderer/components/alert/SwapStatusAlert.tsx
@@ -0,0 +1,233 @@
+import { Alert, AlertTitle } from '@material-ui/lab/';
+import { Box, makeStyles } from '@material-ui/core';
+import { ReactNode } from 'react';
+import { exhaustiveGuard } from 'utils/typescriptUtils';
+import {
+ SwapCancelRefundButton,
+ SwapResumeButton,
+} from '../pages/history/table/HistoryRowActions';
+import HumanizedBitcoinBlockDuration from '../other/HumanizedBitcoinBlockDuration';
+import {
+ GetSwapInfoResponse,
+ GetSwapInfoResponseRunningSwap,
+ isGetSwapInfoResponseRunningSwap,
+ isSwapTimelockInfoCancelled,
+ isSwapTimelockInfoNone,
+ isSwapTimelockInfoPunished,
+ SwapStateName,
+ SwapTimelockInfoCancelled,
+ SwapTimelockInfoNone,
+} from '../../../models/rpcModel';
+import { SwapMoneroRecoveryButton } from '../pages/history/table/SwapMoneroRecoveryButton';
+
+const useStyles = makeStyles({
+ box: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '0.5rem',
+ },
+ list: {
+ padding: '0px',
+ margin: '0px',
+ },
+});
+
+/**
+ * Component for displaying a list of messages.
+ * @param messages - Array of messages to display.
+ * @returns JSX.Element
+ */
+const MessageList = ({ messages }: { messages: ReactNode[] }) => {
+ const classes = useStyles();
+ return (
+
+ {messages.map((msg, i) => (
+ // eslint-disable-next-line react/no-array-index-key
+ {msg}
+ ))}
+
+ );
+};
+
+/**
+ * Sub-component for displaying alerts when the swap is in a safe state.
+ * @param swap - The swap information.
+ * @returns JSX.Element
+ */
+const BitcoinRedeemedStateAlert = ({ swap }: { swap: GetSwapInfoResponse }) => {
+ const classes = useStyles();
+ return (
+
+
+
+
+ );
+};
+
+/**
+ * Sub-component for displaying alerts when the swap is in a state with no timelock info.
+ * @param swap - The swap information.
+ * @param punishTimelockOffset - The punish timelock offset.
+ * @returns JSX.Element
+ */
+const BitcoinLockedNoTimelockExpiredStateAlert = ({
+ timelock,
+ punishTimelockOffset,
+}: {
+ timelock: SwapTimelockInfoNone;
+ punishTimelockOffset: number;
+}) => (
+
+ Your Bitcoin is locked. If the swap is not completed in approximately{' '}
+ ,
+ you need to refund
+ >,
+ <>
+ You will lose your funds if you do not refund or complete the swap
+ within{' '}
+
+ >,
+ ]}
+ />
+);
+
+/**
+ * Sub-component for displaying alerts when the swap timelock is expired
+ * The swap could be cancelled but not necessarily (the transaction might not have been published yet)
+ * But it doesn't matter because the swap cannot be completed anymore
+ * @param swap - The swap information.
+ * @returns JSX.Element
+ */
+const BitcoinPossiblyCancelledAlert = ({
+ swap,
+ timelock,
+}: {
+ swap: GetSwapInfoResponse;
+ timelock: SwapTimelockInfoCancelled;
+}) => {
+ const classes = useStyles();
+ return (
+
+
+ You will lose your funds if you do not refund within{' '}
+
+ >,
+ ]}
+ />
+
+
+ );
+};
+
+/**
+ * Sub-component for displaying alerts requiring immediate action.
+ * @returns JSX.Element
+ */
+const ImmediateActionAlert = () => (
+ <>Resume the swap immediately to avoid losing your funds>
+);
+
+/**
+ * Main component for displaying the appropriate swap alert status text.
+ * @param swap - The swap information.
+ * @returns JSX.Element | null
+ */
+function SwapAlertStatusText({
+ swap,
+}: {
+ swap: GetSwapInfoResponseRunningSwap;
+}) {
+ switch (swap.stateName) {
+ // This is the state where the swap is safe because the other party has redeemed the Bitcoin
+ // It cannot be punished anymore
+ case SwapStateName.BtcRedeemed:
+ return ;
+
+ // These are states that are at risk of punishment because the Bitcoin have been locked
+ // but has not been redeemed yet by the other party
+ case SwapStateName.BtcLocked:
+ case SwapStateName.XmrLockProofReceived:
+ case SwapStateName.XmrLocked:
+ case SwapStateName.EncSigSent:
+ case SwapStateName.CancelTimelockExpired:
+ case SwapStateName.BtcCancelled:
+ if (swap.timelock !== null) {
+ if (isSwapTimelockInfoNone(swap.timelock)) {
+ return (
+
+ );
+ }
+
+ if (isSwapTimelockInfoCancelled(swap.timelock)) {
+ return (
+
+ );
+ }
+
+ if (isSwapTimelockInfoPunished(swap.timelock)) {
+ return ;
+ }
+
+ // We have covered all possible timelock states above
+ // If we reach this point, it means we have missed a case
+ return exhaustiveGuard(swap.timelock);
+ }
+ return ;
+ default:
+ return exhaustiveGuard(swap.stateName);
+ }
+}
+
+/**
+ * Main component for displaying the swap status alert.
+ * @param swap - The swap information.
+ * @returns JSX.Element | null
+ */
+export default function SwapStatusAlert({
+ swap,
+}: {
+ swap: GetSwapInfoResponse;
+}): JSX.Element | null {
+ // If the swap is not running, there is no need to display the alert
+ // This is either because the swap is finished or has not started yet (e.g. in the setup phase, no Bitcoin locked)
+ if (!isGetSwapInfoResponseRunningSwap(swap)) {
+ return null;
+ }
+
+ return (
+ }
+ variant="filled"
+ >
+
+ Swap {swap.swapId.substring(0, 5)}... is unfinished
+
+
+
+ );
+}
diff --git a/src/renderer/components/alert/SwapTxLockAlertsBox.tsx b/src/renderer/components/alert/SwapTxLockAlertsBox.tsx
new file mode 100644
index 00000000..055573e7
--- /dev/null
+++ b/src/renderer/components/alert/SwapTxLockAlertsBox.tsx
@@ -0,0 +1,28 @@
+import { Box, makeStyles } from '@material-ui/core';
+import { useSwapInfosSortedByDate } from 'store/hooks';
+import SwapStatusAlert from './SwapStatusAlert';
+
+const useStyles = makeStyles((theme) => ({
+ outer: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: theme.spacing(1),
+ },
+}));
+
+export default function SwapTxLockAlertsBox() {
+ const classes = useStyles();
+
+ // We specifically choose ALL swaps here
+ // If a swap is in a state where an Alert is not needed (becaue no Bitcoin have been locked or because the swap has been completed)
+ // the SwapStatusAlert component will not render an Alert
+ const swaps = useSwapInfosSortedByDate();
+
+ return (
+
+ {swaps.map((swap) => (
+
+ ))}
+
+ );
+}
diff --git a/src/renderer/components/alert/UnfinishedSwapsAlert.tsx b/src/renderer/components/alert/UnfinishedSwapsAlert.tsx
new file mode 100644
index 00000000..bdeda601
--- /dev/null
+++ b/src/renderer/components/alert/UnfinishedSwapsAlert.tsx
@@ -0,0 +1,33 @@
+import { Button } from '@material-ui/core';
+import Alert from '@material-ui/lab/Alert';
+import { useNavigate } from 'react-router-dom';
+import { useResumeableSwapsCount } from 'store/hooks';
+
+export default function UnfinishedSwapsAlert() {
+ const resumableSwapsCount = useResumeableSwapsCount();
+ const navigate = useNavigate();
+
+ if (resumableSwapsCount > 0) {
+ return (
+ navigate('/history')}
+ >
+ VIEW
+
+ }
+ >
+ You have{' '}
+ {resumableSwapsCount > 1
+ ? `${resumableSwapsCount} unfinished swaps`
+ : 'one unfinished swap'}
+
+ );
+ }
+ return null;
+}
diff --git a/src/renderer/components/icons/BitcoinIcon.tsx b/src/renderer/components/icons/BitcoinIcon.tsx
new file mode 100644
index 00000000..b42f17f9
--- /dev/null
+++ b/src/renderer/components/icons/BitcoinIcon.tsx
@@ -0,0 +1,24 @@
+import { SvgIcon } from '@material-ui/core';
+import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon';
+
+export default function BitcoinIcon(props: SvgIconProps) {
+ return (
+ // eslint-disable-next-line react/jsx-props-no-spreading
+
+
+
+
+
+ );
+}
diff --git a/src/renderer/components/icons/DiscordIcon.tsx b/src/renderer/components/icons/DiscordIcon.tsx
new file mode 100644
index 00000000..d913b884
--- /dev/null
+++ b/src/renderer/components/icons/DiscordIcon.tsx
@@ -0,0 +1,24 @@
+import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon';
+import { SvgIcon } from '@material-ui/core';
+
+export default function DiscordIcon(props: SvgIconProps) {
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/renderer/components/icons/LinkIconButton.tsx b/src/renderer/components/icons/LinkIconButton.tsx
new file mode 100644
index 00000000..ad6c6ef4
--- /dev/null
+++ b/src/renderer/components/icons/LinkIconButton.tsx
@@ -0,0 +1,16 @@
+import { ReactNode } from 'react';
+import { IconButton } from '@material-ui/core';
+
+export default function LinkIconButton({
+ url,
+ children,
+}: {
+ url: string;
+ children: ReactNode;
+}) {
+ return (
+ window.open(url, '_blank')}>
+ {children}
+
+ );
+}
diff --git a/src/renderer/components/icons/MoneroIcon.tsx b/src/renderer/components/icons/MoneroIcon.tsx
new file mode 100644
index 00000000..b5433385
--- /dev/null
+++ b/src/renderer/components/icons/MoneroIcon.tsx
@@ -0,0 +1,28 @@
+import { SvgIcon } from '@material-ui/core';
+import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon';
+
+export default function MoneroIcon(props: SvgIconProps) {
+ return (
+ // eslint-disable-next-line react/jsx-props-no-spreading
+
+
+
+
+
+
+ );
+}
diff --git a/src/renderer/components/icons/TorIcon.tsx b/src/renderer/components/icons/TorIcon.tsx
new file mode 100644
index 00000000..4d77e587
--- /dev/null
+++ b/src/renderer/components/icons/TorIcon.tsx
@@ -0,0 +1,24 @@
+import { SvgIcon } from '@material-ui/core';
+import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon';
+
+export default function TorIcon(props: SvgIconProps) {
+ return (
+ // eslint-disable-next-line react/jsx-props-no-spreading
+
+
+
+
+
+ );
+}
diff --git a/src/renderer/components/inputs/BitcoinAddressTextField.tsx b/src/renderer/components/inputs/BitcoinAddressTextField.tsx
new file mode 100644
index 00000000..a5ec21db
--- /dev/null
+++ b/src/renderer/components/inputs/BitcoinAddressTextField.tsx
@@ -0,0 +1,41 @@
+import { useEffect } from 'react';
+import { TextField } from '@material-ui/core';
+import { TextFieldProps } from '@material-ui/core/TextField/TextField';
+import { isBtcAddressValid } from 'utils/conversionUtils';
+import { isTestnet } from 'store/config';
+
+export default function BitcoinAddressTextField({
+ address,
+ onAddressChange,
+ onAddressValidityChange,
+ helperText,
+ ...props
+}: {
+ address: string;
+ onAddressChange: (address: string) => void;
+ onAddressValidityChange: (valid: boolean) => void;
+ helperText: string;
+} & TextFieldProps) {
+ const placeholder = isTestnet() ? 'tb1q4aelwalu...' : 'bc18ociqZ9mZ...';
+ const errorText = isBtcAddressValid(address, isTestnet())
+ ? null
+ : `Only bech32 addresses are supported. They begin with "${
+ isTestnet() ? 'tb1' : 'bc1'
+ }"`;
+
+ useEffect(() => {
+ onAddressValidityChange(!errorText);
+ }, [address, errorText, onAddressValidityChange]);
+
+ return (
+ onAddressChange(e.target.value)}
+ error={!!errorText && address.length > 0}
+ helperText={address.length > 0 ? errorText || helperText : helperText}
+ placeholder={placeholder}
+ variant="outlined"
+ {...props}
+ />
+ );
+}
diff --git a/src/renderer/components/inputs/MoneroAddressTextField.tsx b/src/renderer/components/inputs/MoneroAddressTextField.tsx
new file mode 100644
index 00000000..4418c588
--- /dev/null
+++ b/src/renderer/components/inputs/MoneroAddressTextField.tsx
@@ -0,0 +1,39 @@
+import { useEffect } from 'react';
+import { TextField } from '@material-ui/core';
+import { TextFieldProps } from '@material-ui/core/TextField/TextField';
+import { isXmrAddressValid } from 'utils/conversionUtils';
+import { isTestnet } from 'store/config';
+
+export default function MoneroAddressTextField({
+ address,
+ onAddressChange,
+ onAddressValidityChange,
+ helperText,
+ ...props
+}: {
+ address: string;
+ onAddressChange: (address: string) => void;
+ onAddressValidityChange: (valid: boolean) => void;
+ helperText: string;
+} & TextFieldProps) {
+ const placeholder = isTestnet() ? '59McWTPGc745...' : '888tNkZrPN6J...';
+ const errorText = isXmrAddressValid(address, isTestnet())
+ ? null
+ : 'Not a valid Monero address';
+
+ useEffect(() => {
+ onAddressValidityChange(!errorText);
+ }, [address, onAddressValidityChange, errorText]);
+
+ return (
+ onAddressChange(e.target.value)}
+ error={!!errorText && address.length > 0}
+ helperText={address.length > 0 ? errorText || helperText : helperText}
+ placeholder={placeholder}
+ variant="outlined"
+ {...props}
+ />
+ );
+}
diff --git a/src/renderer/components/modal/DialogHeader.tsx b/src/renderer/components/modal/DialogHeader.tsx
new file mode 100644
index 00000000..c550b65f
--- /dev/null
+++ b/src/renderer/components/modal/DialogHeader.tsx
@@ -0,0 +1,22 @@
+import { DialogTitle, makeStyles, Typography } from '@material-ui/core';
+
+const useStyles = makeStyles({
+ root: {
+ display: 'flex',
+ justifyContent: 'space-between',
+ },
+});
+
+type DialogTitleProps = {
+ title: string;
+};
+
+export default function DialogHeader({ title }: DialogTitleProps) {
+ const classes = useStyles();
+
+ return (
+
+ {title}
+
+ );
+}
diff --git a/src/renderer/components/modal/PaperTextBox.tsx b/src/renderer/components/modal/PaperTextBox.tsx
new file mode 100644
index 00000000..490f94aa
--- /dev/null
+++ b/src/renderer/components/modal/PaperTextBox.tsx
@@ -0,0 +1,33 @@
+import { Button, makeStyles, Paper, Typography } from '@material-ui/core';
+
+const useStyles = makeStyles((theme) => ({
+ logsOuter: {
+ overflow: 'auto',
+ padding: theme.spacing(1),
+ marginTop: theme.spacing(1),
+ marginBottom: theme.spacing(1),
+ maxHeight: '10rem',
+ },
+ copyButton: {
+ marginTop: theme.spacing(1),
+ },
+}));
+
+export default function PaperTextBox({ stdOut }: { stdOut: string }) {
+ const classes = useStyles();
+
+ function handleCopyLogs() {
+ throw new Error('Not implemented');
+ }
+
+ return (
+
+
+ {stdOut}
+
+
+ Copy
+
+
+ );
+}
diff --git a/src/renderer/components/modal/SwapSuspendAlert.tsx b/src/renderer/components/modal/SwapSuspendAlert.tsx
new file mode 100644
index 00000000..fa594cfe
--- /dev/null
+++ b/src/renderer/components/modal/SwapSuspendAlert.tsx
@@ -0,0 +1,44 @@
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogContentText,
+ DialogTitle,
+} from '@material-ui/core';
+import IpcInvokeButton from '../IpcInvokeButton';
+
+type SwapCancelAlertProps = {
+ open: boolean;
+ onClose: () => void;
+};
+
+export default function SwapSuspendAlert({
+ open,
+ onClose,
+}: SwapCancelAlertProps) {
+ return (
+
+ Force stop running operation?
+
+
+ Are you sure you want to force stop the running swap?
+
+
+
+
+ No
+
+
+ Force stop
+
+
+
+ );
+}
diff --git a/src/renderer/components/modal/feedback/FeedbackDialog.tsx b/src/renderer/components/modal/feedback/FeedbackDialog.tsx
new file mode 100644
index 00000000..b8ed07d5
--- /dev/null
+++ b/src/renderer/components/modal/feedback/FeedbackDialog.tsx
@@ -0,0 +1,170 @@
+import {
+ Box,
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogContentText,
+ DialogTitle,
+ MenuItem,
+ Select,
+ TextField,
+} from '@material-ui/core';
+import { useState } from 'react';
+import { useSnackbar } from 'notistack';
+import {
+ useActiveSwapInfo,
+ useAppSelector,
+} from 'store/hooks';
+import { parseDateString } from 'utils/parseUtils';
+import { store } from 'renderer/store/storeRenderer';
+import { CliLog } from 'models/cliModel';
+import { submitFeedbackViaHttp } from '../../../api';
+import { PiconeroAmount } from '../../other/Units';
+import LoadingButton from '../../other/LoadingButton';
+
+async function submitFeedback(body: string, swapId: string | number) {
+ let attachedBody = '';
+
+ if (swapId !== 0 && typeof swapId === 'string') {
+ const swapInfo = store.getState().rpc.state.swapInfos[swapId];
+ const logs = [] as CliLog[];
+
+ throw new Error('Not implemented');
+
+ if (swapInfo === undefined) {
+ throw new Error(`Swap with id ${swapId} not found`);
+ }
+
+ attachedBody = `${JSON.stringify(swapInfo, null, 4)} \n\nLogs: ${logs
+ .map((l) => JSON.stringify(l))
+ .join('\n====\n')}`;
+ }
+
+ await submitFeedbackViaHttp(body, attachedBody);
+}
+
+/*
+ * This component is a dialog that allows the user to submit feedback to the
+ * developers. The user can enter a message and optionally attach logs from a
+ * specific swap.
+ * selectedSwap = 0 means no swap is attached
+ */
+function SwapSelectDropDown({
+ selectedSwap,
+ setSelectedSwap,
+}: {
+ selectedSwap: string | number;
+ setSelectedSwap: (swapId: string | number) => void;
+}) {
+ const swaps = useAppSelector((state) =>
+ Object.values(state.rpc.state.swapInfos),
+ );
+
+ return (
+ setSelectedSwap(e.target.value as string)}
+ >
+ Do not attach logs
+ {swaps.map((swap) => (
+
+ Swap {swap.swapId.substring(0, 5)}... from{' '}
+ {new Date(parseDateString(swap.startDate)).toDateString()} (
+ )
+
+ ))}
+
+ );
+}
+
+const MAX_FEEDBACK_LENGTH = 4000;
+
+export default function FeedbackDialog({
+ open,
+ onClose,
+}: {
+ open: boolean;
+ onClose: () => void;
+}) {
+ const [pending, setPending] = useState(false);
+ const [bodyText, setBodyText] = useState('');
+ const currentSwapId = useActiveSwapInfo();
+
+ const { enqueueSnackbar } = useSnackbar();
+
+ const [selectedAttachedSwap, setSelectedAttachedSwap] = useState<
+ string | number
+ >(currentSwapId?.swapId || 0);
+
+ const bodyTooLong = bodyText.length > MAX_FEEDBACK_LENGTH;
+
+ return (
+
+ Submit Feedback
+
+
+ Got something to say? Drop us a message below. If you had an issue
+ with a specific swap, select it from the dropdown to attach the logs.
+ It will help us figure out what went wrong. Hit that submit button
+ when you are ready. We appreciate you taking the time to share your
+ thoughts!
+
+
+ setBodyText(e.target.value)}
+ label={
+ bodyTooLong
+ ? `Text is too long (${bodyText.length}/${MAX_FEEDBACK_LENGTH})`
+ : 'Feedback'
+ }
+ multiline
+ minRows={4}
+ maxRows={4}
+ fullWidth
+ error={bodyTooLong}
+ />
+
+
+
+
+ Cancel
+ {
+ if (pending) {
+ return;
+ }
+
+ try {
+ setPending(true);
+ await submitFeedback(bodyText, selectedAttachedSwap);
+ enqueueSnackbar('Feedback submitted successfully!', {
+ variant: 'success',
+ });
+ } catch (e) {
+ console.error(`Failed to submit feedback: ${e}`);
+ enqueueSnackbar(`Failed to submit feedback (${e})`, {
+ variant: 'error',
+ });
+ } finally {
+ setPending(false);
+ }
+ onClose();
+ }}
+ loading={pending}
+ >
+ Submit
+
+
+
+ );
+}
diff --git a/src/renderer/components/modal/listSellers/ListSellersDialog.tsx b/src/renderer/components/modal/listSellers/ListSellersDialog.tsx
new file mode 100644
index 00000000..72c2dded
--- /dev/null
+++ b/src/renderer/components/modal/listSellers/ListSellersDialog.tsx
@@ -0,0 +1,136 @@
+import { ChangeEvent, useState } from 'react';
+import {
+ DialogTitle,
+ Dialog,
+ DialogContent,
+ DialogContentText,
+ TextField,
+ DialogActions,
+ Button,
+ Box,
+ Chip,
+ makeStyles,
+ Theme,
+} from '@material-ui/core';
+import { Multiaddr } from 'multiaddr';
+import { useSnackbar } from 'notistack';
+import IpcInvokeButton from '../../IpcInvokeButton';
+
+const PRESET_RENDEZVOUS_POINTS = [
+ '/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE',
+ '/dns4/eratosthen.es/tcp/7798/p2p/12D3KooWAh7EXXa2ZyegzLGdjvj1W4G3EXrTGrf6trraoT1MEobs',
+];
+
+const useStyles = makeStyles((theme: Theme) => ({
+ chipOuter: {
+ display: 'flex',
+ flexWrap: 'wrap',
+ gap: theme.spacing(1),
+ },
+}));
+
+type ListSellersDialogProps = {
+ open: boolean;
+ onClose: () => void;
+};
+
+export default function ListSellersDialog({
+ open,
+ onClose,
+}: ListSellersDialogProps) {
+ const classes = useStyles();
+ const [rendezvousAddress, setRendezvousAddress] = useState('');
+ const { enqueueSnackbar } = useSnackbar();
+
+ function handleMultiAddrChange(event: ChangeEvent) {
+ setRendezvousAddress(event.target.value);
+ }
+
+ function getMultiAddressError(): string | null {
+ try {
+ const multiAddress = new Multiaddr(rendezvousAddress);
+ if (!multiAddress.protoNames().includes('p2p')) {
+ return 'The multi address must contain the peer id (/p2p/)';
+ }
+ return null;
+ } catch (e) {
+ return 'Not a valid multi address';
+ }
+ }
+
+ function handleSuccess(amountOfSellers: number) {
+ let message: string;
+
+ switch (amountOfSellers) {
+ case 0:
+ message = `No providers were discovered at the rendezvous point`;
+ break;
+ case 1:
+ message = `Discovered one provider at the rendezvous point`;
+ break;
+ default:
+ message = `Discovered ${amountOfSellers} providers at the rendezvous point`;
+ }
+
+ enqueueSnackbar(message, {
+ variant: 'success',
+ autoHideDuration: 5000,
+ });
+
+ onClose();
+ }
+
+ return (
+
+ Discover swap providers
+
+
+ The rendezvous protocol provides a way to discover providers (trading
+ partners) without relying on one singular centralized institution. By
+ manually connecting to a rendezvous point run by a volunteer, you can
+ discover providers and then connect and swap with them.
+
+
+
+ {PRESET_RENDEZVOUS_POINTS.map((rAddress) => (
+ setRendezvousAddress(rAddress)}
+ />
+ ))}
+
+
+
+ Cancel
+
+ Connect
+
+
+
+ );
+}
diff --git a/src/renderer/components/modal/provider/ProviderInfo.tsx b/src/renderer/components/modal/provider/ProviderInfo.tsx
new file mode 100644
index 00000000..592bf1de
--- /dev/null
+++ b/src/renderer/components/modal/provider/ProviderInfo.tsx
@@ -0,0 +1,75 @@
+import { makeStyles, Box, Typography, Chip, Tooltip } from '@material-ui/core';
+import { VerifiedUser } from '@material-ui/icons';
+import { satsToBtc, secondsToDays } from 'utils/conversionUtils';
+import { ExtendedProviderStatus } from 'models/apiModel';
+import {
+ MoneroBitcoinExchangeRate,
+ SatsAmount,
+} from 'renderer/components/other/Units';
+
+const useStyles = makeStyles((theme) => ({
+ content: {
+ flex: 1,
+ '& *': {
+ lineBreak: 'anywhere',
+ },
+ },
+ chipsOuter: {
+ display: 'flex',
+ marginTop: theme.spacing(1),
+ gap: theme.spacing(0.5),
+ flexWrap: 'wrap',
+ },
+}));
+
+export default function ProviderInfo({
+ provider,
+}: {
+ provider: ExtendedProviderStatus;
+}) {
+ const classes = useStyles();
+
+ return (
+
+
+ Swap Provider
+
+
+ {provider.multiAddr}
+
+
+ {provider.peerId.substring(0, 8)}...{provider.peerId.slice(-8)}
+
+
+ Exchange rate:{' '}
+
+
+ Minimum swap amount:
+
+ Maximum swap amount:
+
+
+
+ {provider.uptime && (
+
+
+
+ )}
+ {provider.age ? (
+
+ ) : (
+
+ )}
+ {provider.recommended === true && (
+
+ } color="primary" />
+
+ )}
+
+
+ );
+}
diff --git a/src/renderer/components/modal/provider/ProviderListDialog.tsx b/src/renderer/components/modal/provider/ProviderListDialog.tsx
new file mode 100644
index 00000000..7309ae51
--- /dev/null
+++ b/src/renderer/components/modal/provider/ProviderListDialog.tsx
@@ -0,0 +1,129 @@
+import {
+ Avatar,
+ List,
+ ListItem,
+ ListItemAvatar,
+ ListItemText,
+ DialogTitle,
+ Dialog,
+ DialogActions,
+ Button,
+ DialogContent,
+ makeStyles,
+ CircularProgress,
+} from '@material-ui/core';
+import AddIcon from '@material-ui/icons/Add';
+import { useState } from 'react';
+import SearchIcon from '@material-ui/icons/Search';
+import { ExtendedProviderStatus } from 'models/apiModel';
+import {
+ useAllProviders,
+ useAppDispatch,
+ useIsRpcEndpointBusy,
+} from 'store/hooks';
+import { setSelectedProvider } from 'store/features/providersSlice';
+import { RpcMethod } from 'models/rpcModel';
+import ProviderSubmitDialog from './ProviderSubmitDialog';
+import ListSellersDialog from '../listSellers/ListSellersDialog';
+import ProviderInfo from './ProviderInfo';
+
+const useStyles = makeStyles({
+ dialogContent: {
+ padding: 0,
+ },
+});
+
+type ProviderSelectDialogProps = {
+ open: boolean;
+ onClose: () => void;
+};
+
+export function ProviderSubmitDialogOpenButton() {
+ const [open, setOpen] = useState(false);
+
+ return (
+ {
+ // Prevents background from being clicked and reopening dialog
+ if (!open) {
+ setOpen(true);
+ }
+ }}
+ >
+ setOpen(false)} />
+
+
+
+
+
+
+
+ );
+}
+
+export function ListSellersDialogOpenButton() {
+ const [open, setOpen] = useState(false);
+ const running = useIsRpcEndpointBusy(RpcMethod.LIST_SELLERS);
+
+ return (
+ {
+ // Prevents background from being clicked and reopening dialog
+ if (!open) {
+ setOpen(true);
+ }
+ }}
+ >
+ setOpen(false)} />
+
+ {running ? : }
+
+
+
+ );
+}
+
+export default function ProviderListDialog({
+ open,
+ onClose,
+}: ProviderSelectDialogProps) {
+ const classes = useStyles();
+ const providers = useAllProviders();
+ const dispatch = useAppDispatch();
+
+ function handleProviderChange(provider: ExtendedProviderStatus) {
+ dispatch(setSelectedProvider(provider));
+ onClose();
+ }
+
+ return (
+
+ Select a swap provider
+
+
+
+ {providers.map((provider) => (
+ handleProviderChange(provider)}
+ key={provider.peerId}
+ >
+
+
+ ))}
+
+
+
+
+
+
+ Cancel
+
+
+ );
+}
diff --git a/src/renderer/components/modal/provider/ProviderSelect.tsx b/src/renderer/components/modal/provider/ProviderSelect.tsx
new file mode 100644
index 00000000..1aeea214
--- /dev/null
+++ b/src/renderer/components/modal/provider/ProviderSelect.tsx
@@ -0,0 +1,62 @@
+import {
+ makeStyles,
+ Card,
+ CardContent,
+ Box,
+ IconButton,
+} from '@material-ui/core';
+import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos';
+import { useState } from 'react';
+import { useAppSelector } from 'store/hooks';
+import ProviderInfo from './ProviderInfo';
+import ProviderListDialog from './ProviderListDialog';
+
+const useStyles = makeStyles({
+ inner: {
+ textAlign: 'left',
+ width: '100%',
+ height: '100%',
+ },
+ providerCard: {
+ width: '100%',
+ },
+ providerCardContent: {
+ display: 'flex',
+ alignItems: 'center',
+ },
+});
+
+export default function ProviderSelect() {
+ const classes = useStyles();
+ const [selectDialogOpen, setSelectDialogOpen] = useState(false);
+ const selectedProvider = useAppSelector(
+ (state) => state.providers.selectedProvider,
+ );
+
+ if (!selectedProvider) return <>No provider selected>;
+
+ function handleSelectDialogClose() {
+ setSelectDialogOpen(false);
+ }
+
+ function handleSelectDialogOpen() {
+ setSelectDialogOpen(true);
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/renderer/components/modal/provider/ProviderSubmitDialog.tsx b/src/renderer/components/modal/provider/ProviderSubmitDialog.tsx
new file mode 100644
index 00000000..0452db5e
--- /dev/null
+++ b/src/renderer/components/modal/provider/ProviderSubmitDialog.tsx
@@ -0,0 +1,111 @@
+import { ChangeEvent, useState } from 'react';
+import {
+ DialogTitle,
+ Dialog,
+ DialogContent,
+ DialogContentText,
+ TextField,
+ DialogActions,
+ Button,
+} from '@material-ui/core';
+import { Multiaddr } from 'multiaddr';
+
+type ProviderSubmitDialogProps = {
+ open: boolean;
+ onClose: () => void;
+};
+
+export default function ProviderSubmitDialog({
+ open,
+ onClose,
+}: ProviderSubmitDialogProps) {
+ const [multiAddr, setMultiAddr] = useState('');
+ const [peerId, setPeerId] = useState('');
+
+ async function handleProviderSubmit() {
+ if (multiAddr && peerId) {
+ await fetch('https://api.unstoppableswap.net/api/submit-provider', {
+ method: 'post',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ multiAddr,
+ peerId,
+ }),
+ });
+ setMultiAddr('');
+ setPeerId('');
+ onClose();
+ }
+ }
+
+ function handleMultiAddrChange(event: ChangeEvent) {
+ setMultiAddr(event.target.value);
+ }
+
+ function handlePeerIdChange(event: ChangeEvent) {
+ setPeerId(event.target.value);
+ }
+
+ function getMultiAddressError(): string | null {
+ try {
+ const multiAddress = new Multiaddr(multiAddr);
+ if (multiAddress.protoNames().includes('p2p')) {
+ return 'The multi address should not contain the peer id (/p2p/)';
+ }
+ if (multiAddress.protoNames().find((name) => name.includes('onion'))) {
+ return 'It is currently not possible to add a provider that is only reachable via Tor';
+ }
+ return null;
+ } catch (e) {
+ return 'Not a valid multi address';
+ }
+ }
+
+ return (
+
+ Submit a provider to the public registry
+
+
+ If the provider is valid and reachable, it will be displayed to all
+ other users to trade with.
+
+
+
+
+
+ Cancel
+
+ Submit
+
+
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/BitcoinQrCode.tsx b/src/renderer/components/modal/swap/BitcoinQrCode.tsx
new file mode 100644
index 00000000..83ef6545
--- /dev/null
+++ b/src/renderer/components/modal/swap/BitcoinQrCode.tsx
@@ -0,0 +1,22 @@
+import QRCode from 'react-qr-code';
+import { Box } from '@material-ui/core';
+
+export default function BitcoinQrCode({ address }: { address: string }) {
+ return (
+
+
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/BitcoinTransactionInfoBox.tsx b/src/renderer/components/modal/swap/BitcoinTransactionInfoBox.tsx
new file mode 100644
index 00000000..252833d4
--- /dev/null
+++ b/src/renderer/components/modal/swap/BitcoinTransactionInfoBox.tsx
@@ -0,0 +1,25 @@
+import { isTestnet } from 'store/config';
+import { getBitcoinTxExplorerUrl } from 'utils/conversionUtils';
+import BitcoinIcon from 'renderer/components/icons/BitcoinIcon';
+import { ReactNode } from 'react';
+import TransactionInfoBox from './TransactionInfoBox';
+
+type Props = {
+ title: string;
+ txId: string;
+ additionalContent: ReactNode;
+ loading: boolean;
+};
+
+export default function BitcoinTransactionInfoBox({ txId, ...props }: Props) {
+ const explorerUrl = getBitcoinTxExplorerUrl(txId, isTestnet());
+
+ return (
+ }
+ {...props}
+ />
+ );
+}
diff --git a/src/renderer/components/modal/swap/CircularProgressWithSubtitle.tsx b/src/renderer/components/modal/swap/CircularProgressWithSubtitle.tsx
new file mode 100644
index 00000000..f82eab86
--- /dev/null
+++ b/src/renderer/components/modal/swap/CircularProgressWithSubtitle.tsx
@@ -0,0 +1,35 @@
+import {
+ Box,
+ CircularProgress,
+ makeStyles,
+ Typography,
+} from '@material-ui/core';
+import { ReactNode } from 'react';
+
+const useStyles = makeStyles((theme) => ({
+ subtitle: {
+ paddingTop: theme.spacing(1),
+ },
+}));
+
+export default function CircularProgressWithSubtitle({
+ description,
+}: {
+ description: string | ReactNode;
+}) {
+ const classes = useStyles();
+
+ return (
+
+
+
+ {description}
+
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/ClipbiardIconButton.tsx b/src/renderer/components/modal/swap/ClipbiardIconButton.tsx
new file mode 100644
index 00000000..8ad35b68
--- /dev/null
+++ b/src/renderer/components/modal/swap/ClipbiardIconButton.tsx
@@ -0,0 +1,17 @@
+import { Button } from '@material-ui/core';
+import { ButtonProps } from '@material-ui/core/Button/Button';
+
+export default function ClipboardIconButton({
+ text,
+ ...props
+}: { text: string } & ButtonProps) {
+ function writeToClipboard() {
+ throw new Error('Not implemented');
+ }
+
+ return (
+
+ Copy
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/DepositAddressInfoBox.tsx b/src/renderer/components/modal/swap/DepositAddressInfoBox.tsx
new file mode 100644
index 00000000..9de60c26
--- /dev/null
+++ b/src/renderer/components/modal/swap/DepositAddressInfoBox.tsx
@@ -0,0 +1,53 @@
+import { ReactNode } from 'react';
+import { Box, Typography } from '@material-ui/core';
+import FileCopyOutlinedIcon from '@material-ui/icons/FileCopyOutlined';
+import InfoBox from './InfoBox';
+import ClipboardIconButton from './ClipbiardIconButton';
+import BitcoinQrCode from './BitcoinQrCode';
+
+type Props = {
+ title: string;
+ address: string;
+ additionalContent: ReactNode;
+ icon: ReactNode;
+};
+
+export default function DepositAddressInfoBox({
+ title,
+ address,
+ additionalContent,
+ icon,
+}: Props) {
+ return (
+ {address}}
+ additionalContent={
+
+
+ }
+ color="primary"
+ variant="contained"
+ size="medium"
+ />
+
+ {additionalContent}
+
+
+
+
+ }
+ icon={icon}
+ loading={false}
+ />
+ );
+}
diff --git a/src/renderer/components/modal/swap/InfoBox.tsx b/src/renderer/components/modal/swap/InfoBox.tsx
new file mode 100644
index 00000000..e58a45bf
--- /dev/null
+++ b/src/renderer/components/modal/swap/InfoBox.tsx
@@ -0,0 +1,53 @@
+import {
+ Box,
+ LinearProgress,
+ makeStyles,
+ Paper,
+ Typography,
+} from '@material-ui/core';
+import { ReactNode } from 'react';
+
+type Props = {
+ title: ReactNode;
+ mainContent: ReactNode;
+ additionalContent: ReactNode;
+ loading: boolean;
+ icon: ReactNode;
+};
+
+const useStyles = makeStyles((theme) => ({
+ outer: {
+ padding: theme.spacing(1.5),
+ overflow: 'hidden',
+ display: 'flex',
+ flexDirection: 'column',
+ gap: theme.spacing(1),
+ },
+ upperContent: {
+ display: 'flex',
+ alignItems: 'center',
+ gap: theme.spacing(0.5),
+ },
+}));
+
+export default function InfoBox({
+ title,
+ mainContent,
+ additionalContent,
+ icon,
+ loading,
+}: Props) {
+ const classes = useStyles();
+
+ return (
+
+ {title}
+
+ {icon}
+ {mainContent}
+
+ {loading ? : null}
+ {additionalContent}
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/MoneroTransactionInfoBox.tsx b/src/renderer/components/modal/swap/MoneroTransactionInfoBox.tsx
new file mode 100644
index 00000000..894f23d6
--- /dev/null
+++ b/src/renderer/components/modal/swap/MoneroTransactionInfoBox.tsx
@@ -0,0 +1,25 @@
+import { isTestnet } from 'store/config';
+import { getMoneroTxExplorerUrl } from 'utils/conversionUtils';
+import MoneroIcon from 'renderer/components/icons/MoneroIcon';
+import { ReactNode } from 'react';
+import TransactionInfoBox from './TransactionInfoBox';
+
+type Props = {
+ title: string;
+ txId: string;
+ additionalContent: ReactNode;
+ loading: boolean;
+};
+
+export default function MoneroTransactionInfoBox({ txId, ...props }: Props) {
+ const explorerUrl = getMoneroTxExplorerUrl(txId, isTestnet());
+
+ return (
+ }
+ {...props}
+ />
+ );
+}
diff --git a/src/renderer/components/modal/swap/SwapDialog.tsx b/src/renderer/components/modal/swap/SwapDialog.tsx
new file mode 100644
index 00000000..b7adced4
--- /dev/null
+++ b/src/renderer/components/modal/swap/SwapDialog.tsx
@@ -0,0 +1,90 @@
+import { useState } from 'react';
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ makeStyles,
+} from '@material-ui/core';
+import { useAppDispatch, useAppSelector } from 'store/hooks';
+import { swapReset } from 'store/features/swapSlice';
+import SwapStatePage from './pages/SwapStatePage';
+import SwapStateStepper from './SwapStateStepper';
+import SwapSuspendAlert from '../SwapSuspendAlert';
+import SwapDialogTitle from './SwapDialogTitle';
+import DebugPage from './pages/DebugPage';
+
+const useStyles = makeStyles({
+ content: {
+ minHeight: '25rem',
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'space-between',
+ },
+});
+
+export default function SwapDialog({
+ open,
+ onClose,
+}: {
+ open: boolean;
+ onClose: () => void;
+}) {
+ const classes = useStyles();
+ const swap = useAppSelector((state) => state.swap);
+ const [debug, setDebug] = useState(false);
+ const [openSuspendAlert, setOpenSuspendAlert] = useState(false);
+ const dispatch = useAppDispatch();
+
+ function onCancel() {
+ if (swap.processRunning) {
+ setOpenSuspendAlert(true);
+ } else {
+ onClose();
+ setTimeout(() => dispatch(swapReset()), 0);
+ }
+ }
+
+ // This prevents an issue where the Dialog is shown for a split second without a present swap state
+ if (!open) return null;
+
+ return (
+
+
+
+
+ {debug ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+
+
+ Cancel
+
+
+ Done
+
+
+
+ setOpenSuspendAlert(false)}
+ />
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/SwapDialogTitle.tsx b/src/renderer/components/modal/swap/SwapDialogTitle.tsx
new file mode 100644
index 00000000..7592fc3d
--- /dev/null
+++ b/src/renderer/components/modal/swap/SwapDialogTitle.tsx
@@ -0,0 +1,45 @@
+import {
+ Box,
+ DialogTitle,
+ makeStyles,
+ Typography,
+} from '@material-ui/core';
+import TorStatusBadge from './pages/TorStatusBadge';
+import FeedbackSubmitBadge from './pages/FeedbackSubmitBadge';
+import DebugPageSwitchBadge from './pages/DebugPageSwitchBadge';
+
+const useStyles = makeStyles((theme) => ({
+ root: {
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ rightSide: {
+ display: 'flex',
+ alignItems: 'center',
+ gridGap: theme.spacing(1),
+ },
+}));
+
+export default function SwapDialogTitle({
+ title,
+ debug,
+ setDebug,
+}: {
+ title: string;
+ debug: boolean;
+ setDebug: (d: boolean) => void;
+}) {
+ const classes = useStyles();
+
+ return (
+
+ {title}
+
+
+
+
+
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/SwapStateStepper.tsx b/src/renderer/components/modal/swap/SwapStateStepper.tsx
new file mode 100644
index 00000000..10d0491b
--- /dev/null
+++ b/src/renderer/components/modal/swap/SwapStateStepper.tsx
@@ -0,0 +1,166 @@
+import { Step, StepLabel, Stepper, Typography } from '@material-ui/core';
+import { SwapSpawnType } from 'models/cliModel';
+import { SwapStateName } from 'models/rpcModel';
+import { useActiveSwapInfo, useAppSelector } from 'store/hooks';
+import { exhaustiveGuard } from 'utils/typescriptUtils';
+
+export enum PathType {
+ HAPPY_PATH = 'happy path',
+ UNHAPPY_PATH = 'unhappy path',
+}
+
+function getActiveStep(
+ stateName: SwapStateName | null,
+ processExited: boolean,
+): [PathType, number, boolean] {
+ switch (stateName) {
+ /// // Happy Path
+ // Step: 0 (Waiting for Bitcoin lock tx to be published)
+ case null:
+ return [PathType.HAPPY_PATH, 0, false];
+ case SwapStateName.Started:
+ case SwapStateName.SwapSetupCompleted:
+ return [PathType.HAPPY_PATH, 0, processExited];
+
+ // Step: 1 (Waiting for Bitcoin Lock confirmation and XMR Lock Publication)
+ // We have locked the Bitcoin and are waiting for the other party to lock their XMR
+ case SwapStateName.BtcLocked:
+ return [PathType.HAPPY_PATH, 1, processExited];
+
+ // Step: 2 (Waiting for XMR Lock confirmation)
+ // We have locked the Bitcoin and the other party has locked their XMR
+ case SwapStateName.XmrLockProofReceived:
+ return [PathType.HAPPY_PATH, 1, processExited];
+
+ // Step: 3 (Sending Encrypted Signature and waiting for Bitcoin Redemption)
+ // The XMR lock transaction has been confirmed
+ // We now need to send the encrypted signature to the other party and wait for them to redeem the Bitcoin
+ case SwapStateName.XmrLocked:
+ case SwapStateName.EncSigSent:
+ return [PathType.HAPPY_PATH, 2, processExited];
+
+ // Step: 4 (Waiting for XMR Redemption)
+ case SwapStateName.BtcRedeemed:
+ return [PathType.HAPPY_PATH, 3, processExited];
+
+ // Step: 4 (Completed) (Swap completed, XMR redeemed)
+ case SwapStateName.XmrRedeemed:
+ return [PathType.HAPPY_PATH, 4, false];
+
+ // Edge Case of Happy Path where the swap is safely aborted. We "fail" at the first step.
+ case SwapStateName.SafelyAborted:
+ return [PathType.HAPPY_PATH, 0, true];
+
+ // // Unhappy Path
+ // Step: 1 (Cancelling swap, checking if cancel transaction has been published already by the other party)
+ case SwapStateName.CancelTimelockExpired:
+ return [PathType.UNHAPPY_PATH, 0, processExited];
+
+ // Step: 2 (Attempt to publish the Bitcoin refund transaction)
+ case SwapStateName.BtcCancelled:
+ return [PathType.UNHAPPY_PATH, 1, processExited];
+
+ // Step: 2 (Completed) (Bitcoin refunded)
+ case SwapStateName.BtcRefunded:
+ return [PathType.UNHAPPY_PATH, 2, false];
+
+ // Step: 2 (We failed to publish the Bitcoin refund transaction)
+ // We failed to publish the Bitcoin refund transaction because the timelock has expired.
+ // We will be punished. Nothing we can do about it now.
+ case SwapStateName.BtcPunished:
+ return [PathType.UNHAPPY_PATH, 1, true];
+ default:
+ return exhaustiveGuard(stateName);
+ }
+}
+
+function HappyPathStepper({
+ activeStep,
+ error,
+}: {
+ activeStep: number;
+ error: boolean;
+}) {
+ return (
+
+
+ ~12min}
+ error={error && activeStep === 0}
+ >
+ Locking your BTC
+
+
+
+ ~18min}
+ error={error && activeStep === 1}
+ >
+ They lock their XMR
+
+
+
+ ~2min}
+ error={error && activeStep === 2}
+ >
+ They redeem the BTC
+
+
+
+ ~2min}
+ error={error && activeStep === 3}
+ >
+ Redeeming your XMR
+
+
+
+ );
+}
+
+function UnhappyPathStepper({
+ activeStep,
+ error,
+}: {
+ activeStep: number;
+ error: boolean;
+}) {
+ return (
+
+
+ ~20min}
+ error={error && activeStep === 0}
+ >
+ Cancelling swap
+
+
+
+ ~20min}
+ error={error && activeStep === 1}
+ >
+ Refunding your BTC
+
+
+
+ );
+}
+
+export default function SwapStateStepper() {
+ const currentSwapSpawnType = useAppSelector((s) => s.swap.spawnType);
+ const stateName = useActiveSwapInfo()?.stateName ?? null;
+ const processExited = useAppSelector((s) => !s.swap.processRunning);
+ const [pathType, activeStep, error] = getActiveStep(stateName, processExited);
+
+ // If the current swap is being manually cancelled and refund, we want to show the unhappy path even though the current state is not a "unhappy" state
+ if (currentSwapSpawnType === SwapSpawnType.CANCEL_REFUND) {
+ return ;
+ }
+
+ if (pathType === PathType.HAPPY_PATH) {
+ return ;
+ }
+ return ;
+}
diff --git a/src/renderer/components/modal/swap/TransactionInfoBox.tsx b/src/renderer/components/modal/swap/TransactionInfoBox.tsx
new file mode 100644
index 00000000..8f486e40
--- /dev/null
+++ b/src/renderer/components/modal/swap/TransactionInfoBox.tsx
@@ -0,0 +1,40 @@
+import { Link, Typography } from '@material-ui/core';
+import { ReactNode } from 'react';
+import InfoBox from './InfoBox';
+
+type TransactionInfoBoxProps = {
+ title: string;
+ txId: string;
+ explorerUrl: string;
+ additionalContent: ReactNode;
+ loading: boolean;
+ icon: JSX.Element;
+};
+
+export default function TransactionInfoBox({
+ title,
+ txId,
+ explorerUrl,
+ additionalContent,
+ icon,
+ loading,
+}: TransactionInfoBoxProps) {
+ return (
+ {txId}}
+ loading={loading}
+ additionalContent={
+ <>
+ {additionalContent}
+
+
+ View on explorer
+
+
+ >
+ }
+ icon={icon}
+ />
+ );
+}
diff --git a/src/renderer/components/modal/swap/pages/DebugPage.tsx b/src/renderer/components/modal/swap/pages/DebugPage.tsx
new file mode 100644
index 00000000..a3aee47c
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/DebugPage.tsx
@@ -0,0 +1,36 @@
+import { Box, DialogContentText } from '@material-ui/core';
+import { useActiveSwapInfo, useAppSelector } from 'store/hooks';
+import CliLogsBox from '../../../other/RenderedCliLog';
+import JsonTreeView from '../../../other/JSONViewTree';
+
+export default function DebugPage() {
+ const torStdOut = useAppSelector((s) => s.tor.stdOut);
+ const logs = useAppSelector((s) => s.swap.logs);
+ const guiState = useAppSelector((s) => s.swap);
+ const cliState = useActiveSwapInfo();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/pages/DebugPageSwitchBadge.tsx b/src/renderer/components/modal/swap/pages/DebugPageSwitchBadge.tsx
new file mode 100644
index 00000000..49a2ee43
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/DebugPageSwitchBadge.tsx
@@ -0,0 +1,26 @@
+import { Tooltip } from '@material-ui/core';
+import IconButton from '@material-ui/core/IconButton';
+import DeveloperBoardIcon from '@material-ui/icons/DeveloperBoard';
+
+export default function DebugPageSwitchBadge({
+ enabled,
+ setEnabled,
+}: {
+ enabled: boolean;
+ setEnabled: (enabled: boolean) => void;
+}) {
+ const handleToggle = () => {
+ setEnabled(!enabled);
+ };
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/pages/FeedbackSubmitBadge.tsx b/src/renderer/components/modal/swap/pages/FeedbackSubmitBadge.tsx
new file mode 100644
index 00000000..08ddccd7
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/FeedbackSubmitBadge.tsx
@@ -0,0 +1,22 @@
+import { IconButton } from '@material-ui/core';
+import FeedbackIcon from '@material-ui/icons/Feedback';
+import FeedbackDialog from '../../feedback/FeedbackDialog';
+import { useState } from 'react';
+
+export default function FeedbackSubmitBadge() {
+ const [showFeedbackDialog, setShowFeedbackDialog] = useState(false);
+
+ return (
+ <>
+ {showFeedbackDialog && (
+ setShowFeedbackDialog(false)}
+ />
+ )}
+ setShowFeedbackDialog(true)}>
+
+
+ >
+ );
+}
diff --git a/src/renderer/components/modal/swap/pages/SwapStatePage.tsx b/src/renderer/components/modal/swap/pages/SwapStatePage.tsx
new file mode 100644
index 00000000..de0429a2
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/SwapStatePage.tsx
@@ -0,0 +1,106 @@
+import { Box } from '@material-ui/core';
+import { useAppSelector } from 'store/hooks';
+import {
+ isSwapStateBtcCancelled,
+ isSwapStateBtcLockInMempool,
+ isSwapStateBtcPunished,
+ isSwapStateBtcRedemeed,
+ isSwapStateBtcRefunded,
+ isSwapStateInitiated,
+ isSwapStateProcessExited,
+ isSwapStateReceivedQuote,
+ isSwapStateStarted,
+ isSwapStateWaitingForBtcDeposit,
+ isSwapStateXmrLocked,
+ isSwapStateXmrLockInMempool,
+ isSwapStateXmrRedeemInMempool,
+ SwapState,
+} from '../../../../../models/storeModel';
+import InitiatedPage from './init/InitiatedPage';
+import WaitingForBitcoinDepositPage from './init/WaitingForBitcoinDepositPage';
+import StartedPage from './in_progress/StartedPage';
+import BitcoinLockTxInMempoolPage from './in_progress/BitcoinLockTxInMempoolPage';
+import XmrLockTxInMempoolPage from './in_progress/XmrLockInMempoolPage';
+// eslint-disable-next-line import/no-cycle
+import ProcessExitedPage from './exited/ProcessExitedPage';
+import XmrRedeemInMempoolPage from './done/XmrRedeemInMempoolPage';
+import ReceivedQuotePage from './in_progress/ReceivedQuotePage';
+import BitcoinRedeemedPage from './in_progress/BitcoinRedeemedPage';
+import InitPage from './init/InitPage';
+import XmrLockedPage from './in_progress/XmrLockedPage';
+import BitcoinCancelledPage from './in_progress/BitcoinCancelledPage';
+import BitcoinRefundedPage from './done/BitcoinRefundedPage';
+import BitcoinPunishedPage from './done/BitcoinPunishedPage';
+import { SyncingMoneroWalletPage } from './in_progress/SyncingMoneroWalletPage';
+
+export default function SwapStatePage({
+ swapState,
+}: {
+ swapState: SwapState | null;
+}) {
+ const isSyncingMoneroWallet = useAppSelector(
+ (state) => state.rpc.state.moneroWallet.isSyncing,
+ );
+
+ if (isSyncingMoneroWallet) {
+ return ;
+ }
+
+ if (swapState === null) {
+ return ;
+ }
+ if (isSwapStateInitiated(swapState)) {
+ return ;
+ }
+ if (isSwapStateReceivedQuote(swapState)) {
+ return ;
+ }
+ if (isSwapStateWaitingForBtcDeposit(swapState)) {
+ return ;
+ }
+ if (isSwapStateStarted(swapState)) {
+ return ;
+ }
+ if (isSwapStateBtcLockInMempool(swapState)) {
+ return ;
+ }
+ if (isSwapStateXmrLockInMempool(swapState)) {
+ return ;
+ }
+ if (isSwapStateXmrLocked(swapState)) {
+ return ;
+ }
+ if (isSwapStateBtcRedemeed(swapState)) {
+ return ;
+ }
+ if (isSwapStateXmrRedeemInMempool(swapState)) {
+ return ;
+ }
+ if (isSwapStateBtcCancelled(swapState)) {
+ return ;
+ }
+ if (isSwapStateBtcRefunded(swapState)) {
+ return ;
+ }
+ if (isSwapStateBtcPunished(swapState)) {
+ return ;
+ }
+ if (isSwapStateProcessExited(swapState)) {
+ return ;
+ }
+
+ console.error(
+ `No swap state page found for swap state State: ${JSON.stringify(
+ swapState,
+ null,
+ 4,
+ )}`,
+ );
+ return (
+
+ No information to display
+
+ State: ${JSON.stringify(swapState, null, 4)}
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/pages/TorStatusBadge.tsx b/src/renderer/components/modal/swap/pages/TorStatusBadge.tsx
new file mode 100644
index 00000000..6cf6d18e
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/TorStatusBadge.tsx
@@ -0,0 +1,19 @@
+import { IconButton, Tooltip } from '@material-ui/core';
+import { useAppSelector } from 'store/hooks';
+import TorIcon from '../../../icons/TorIcon';
+
+export default function TorStatusBadge() {
+ const tor = useAppSelector((s) => s.tor);
+
+ if (tor.processRunning) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return <>>;
+}
diff --git a/src/renderer/components/modal/swap/pages/done/BitcoinPunishedPage.tsx b/src/renderer/components/modal/swap/pages/done/BitcoinPunishedPage.tsx
new file mode 100644
index 00000000..e98f9fc1
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/done/BitcoinPunishedPage.tsx
@@ -0,0 +1,15 @@
+import { Box, DialogContentText } from '@material-ui/core';
+import FeedbackInfoBox from '../../../../pages/help/FeedbackInfoBox';
+
+export default function BitcoinPunishedPage() {
+ return (
+
+
+ Unfortunately, the swap was not successful, and you've incurred a
+ penalty because the swap was not refunded in time. Both the Bitcoin and
+ Monero are irretrievable.
+
+
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/pages/done/BitcoinRefundedPage.tsx b/src/renderer/components/modal/swap/pages/done/BitcoinRefundedPage.tsx
new file mode 100644
index 00000000..91f3a4cd
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/done/BitcoinRefundedPage.tsx
@@ -0,0 +1,43 @@
+import { Box, DialogContentText } from '@material-ui/core';
+import { SwapStateBtcRefunded } from 'models/storeModel';
+import { useActiveSwapInfo } from 'store/hooks';
+import BitcoinTransactionInfoBox from '../../BitcoinTransactionInfoBox';
+import FeedbackInfoBox from '../../../../pages/help/FeedbackInfoBox';
+
+export default function BitcoinRefundedPage({
+ state,
+}: {
+ state: SwapStateBtcRefunded | null;
+}) {
+ const swap = useActiveSwapInfo();
+ const additionalContent = swap
+ ? `Refund address: ${swap.btcRefundAddress}`
+ : null;
+
+ return (
+
+
+ Unfortunately, the swap was not successful. However, rest assured that
+ all your Bitcoin has been refunded to the specified address. The swap
+ process is now complete, and you are free to exit the application.
+
+
+ {state && (
+
+ )}
+
+
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/pages/done/XmrRedeemInMempoolPage.tsx b/src/renderer/components/modal/swap/pages/done/XmrRedeemInMempoolPage.tsx
new file mode 100644
index 00000000..18f49597
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/done/XmrRedeemInMempoolPage.tsx
@@ -0,0 +1,49 @@
+import { Box, DialogContentText } from '@material-ui/core';
+import { SwapStateXmrRedeemInMempool } from 'models/storeModel';
+import { useActiveSwapInfo } from 'store/hooks';
+import { getSwapXmrAmount } from 'models/rpcModel';
+import MoneroTransactionInfoBox from '../../MoneroTransactionInfoBox';
+import FeedbackInfoBox from '../../../../pages/help/FeedbackInfoBox';
+
+type XmrRedeemInMempoolPageProps = {
+ state: SwapStateXmrRedeemInMempool | null;
+};
+
+export default function XmrRedeemInMempoolPage({
+ state,
+}: XmrRedeemInMempoolPageProps) {
+ const swap = useActiveSwapInfo();
+ const additionalContent = swap
+ ? `This transaction transfers ${getSwapXmrAmount(swap).toFixed(6)} XMR to ${
+ state?.bobXmrRedeemAddress
+ }`
+ : null;
+
+ return (
+
+
+ The swap was successful and the Monero has been sent to the address you
+ specified. The swap is completed and you may exit the application now.
+
+
+ {state && (
+ <>
+
+ >
+ )}
+
+
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/pages/exited/ProcessExitedAndNotDonePage.tsx b/src/renderer/components/modal/swap/pages/exited/ProcessExitedAndNotDonePage.tsx
new file mode 100644
index 00000000..beea3154
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/exited/ProcessExitedAndNotDonePage.tsx
@@ -0,0 +1,71 @@
+import { Box, DialogContentText } from '@material-ui/core';
+import { useActiveSwapInfo, useAppSelector } from 'store/hooks';
+import { SwapStateProcessExited } from 'models/storeModel';
+import CliLogsBox from '../../../../other/RenderedCliLog';
+import { SwapSpawnType } from 'models/cliModel';
+
+export default function ProcessExitedAndNotDonePage({
+ state,
+}: {
+ state: SwapStateProcessExited;
+}) {
+ const swap = useActiveSwapInfo();
+ const logs = useAppSelector((s) => s.swap.logs);
+ const spawnType = useAppSelector((s) => s.swap.spawnType);
+
+ function getText() {
+ const isCancelRefund = spawnType === SwapSpawnType.CANCEL_REFUND;
+ const hasRpcError = state.rpcError != null;
+ const hasSwap = swap != null;
+
+ let messages = [];
+
+ messages.push(
+ isCancelRefund
+ ? 'The manual cancel and refund was unsuccessful.'
+ : 'The swap exited unexpectedly without completing.',
+ );
+
+ if (!hasSwap && !isCancelRefund) {
+ messages.push('No funds were locked.');
+ }
+
+ messages.push(
+ hasRpcError
+ ? 'Check the error and the logs below for more information.'
+ : 'Check the logs below for more information.',
+ );
+
+ if (hasSwap) {
+ messages.push(`The swap is in the "${swap.stateName}" state.`);
+ if (!isCancelRefund) {
+ messages.push(
+ 'Try resuming the swap or attempt to initiate a manual cancel and refund.',
+ );
+ }
+ }
+
+ return messages.join(' ');
+ }
+
+ return (
+
+ {getText()}
+
+ {state.rpcError && (
+
+ )}
+
+
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/pages/exited/ProcessExitedPage.tsx b/src/renderer/components/modal/swap/pages/exited/ProcessExitedPage.tsx
new file mode 100644
index 00000000..2e78ff86
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/exited/ProcessExitedPage.tsx
@@ -0,0 +1,47 @@
+import { useActiveSwapInfo } from 'store/hooks';
+import { SwapStateName } from 'models/rpcModel';
+import {
+ isSwapStateBtcPunished,
+ isSwapStateBtcRefunded,
+ isSwapStateXmrRedeemInMempool,
+ SwapStateProcessExited,
+} from '../../../../../../models/storeModel';
+import XmrRedeemInMempoolPage from '../done/XmrRedeemInMempoolPage';
+import BitcoinPunishedPage from '../done/BitcoinPunishedPage';
+// eslint-disable-next-line import/no-cycle
+import SwapStatePage from '../SwapStatePage';
+import BitcoinRefundedPage from '../done/BitcoinRefundedPage';
+import ProcessExitedAndNotDonePage from './ProcessExitedAndNotDonePage';
+
+type ProcessExitedPageProps = {
+ state: SwapStateProcessExited;
+};
+
+export default function ProcessExitedPage({ state }: ProcessExitedPageProps) {
+ const swap = useActiveSwapInfo();
+
+ // If we have a swap state, for a "done" state we should use it to display additional information that can't be extracted from the database
+ if (
+ isSwapStateXmrRedeemInMempool(state.prevState) ||
+ isSwapStateBtcRefunded(state.prevState) ||
+ isSwapStateBtcPunished(state.prevState)
+ ) {
+ return ;
+ }
+
+ // If we don't have a swap state for a "done" state, we should fall back to using the database to display as much information as we can
+ if (swap) {
+ if (swap.stateName === SwapStateName.XmrRedeemed) {
+ return ;
+ }
+ if (swap.stateName === SwapStateName.BtcRefunded) {
+ return ;
+ }
+ if (swap.stateName === SwapStateName.BtcPunished) {
+ return ;
+ }
+ }
+
+ // If the swap is not a "done" state (or we don't have a db state because the swap did complete the SwapSetup yet) we should tell the user and show logs
+ return ;
+}
diff --git a/src/renderer/components/modal/swap/pages/in_progress/BitcoinCancelledPage.tsx b/src/renderer/components/modal/swap/pages/in_progress/BitcoinCancelledPage.tsx
new file mode 100644
index 00000000..60f06c5d
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/in_progress/BitcoinCancelledPage.tsx
@@ -0,0 +1,5 @@
+import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle';
+
+export default function BitcoinCancelledPage() {
+ return ;
+}
diff --git a/src/renderer/components/modal/swap/pages/in_progress/BitcoinLockTxInMempoolPage.tsx b/src/renderer/components/modal/swap/pages/in_progress/BitcoinLockTxInMempoolPage.tsx
new file mode 100644
index 00000000..9c55d0e7
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/in_progress/BitcoinLockTxInMempoolPage.tsx
@@ -0,0 +1,38 @@
+import { Box, DialogContentText } from '@material-ui/core';
+import { SwapStateBtcLockInMempool } from 'models/storeModel';
+import BitcoinTransactionInfoBox from '../../BitcoinTransactionInfoBox';
+import SwapMightBeCancelledAlert from '../../../../alert/SwapMightBeCancelledAlert';
+
+type BitcoinLockTxInMempoolPageProps = {
+ state: SwapStateBtcLockInMempool;
+};
+
+export default function BitcoinLockTxInMempoolPage({
+ state,
+}: BitcoinLockTxInMempoolPageProps) {
+ return (
+
+
+
+ The Bitcoin lock transaction has been published. The swap will proceed
+ once the transaction is confirmed and the swap provider locks their
+ Monero.
+
+
+ Most swap providers require one confirmation before locking their
+ Monero
+
+ Confirmations: {state.bobBtcLockTxConfirmations}
+ >
+ }
+ />
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/pages/in_progress/BitcoinRedeemedPage.tsx b/src/renderer/components/modal/swap/pages/in_progress/BitcoinRedeemedPage.tsx
new file mode 100644
index 00000000..c484ce97
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/in_progress/BitcoinRedeemedPage.tsx
@@ -0,0 +1,5 @@
+import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle';
+
+export default function BitcoinRedeemedPage() {
+ return ;
+}
diff --git a/src/renderer/components/modal/swap/pages/in_progress/ReceivedQuotePage.tsx b/src/renderer/components/modal/swap/pages/in_progress/ReceivedQuotePage.tsx
new file mode 100644
index 00000000..b7d70261
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/in_progress/ReceivedQuotePage.tsx
@@ -0,0 +1,7 @@
+import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle';
+
+export default function ReceivedQuotePage() {
+ return (
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/pages/in_progress/StartedPage.tsx b/src/renderer/components/modal/swap/pages/in_progress/StartedPage.tsx
new file mode 100644
index 00000000..8dd260c0
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/in_progress/StartedPage.tsx
@@ -0,0 +1,16 @@
+import { SwapStateStarted } from 'models/storeModel';
+import { BitcoinAmount } from 'renderer/components/other/Units';
+import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle';
+
+export default function StartedPage({ state }: { state: SwapStateStarted }) {
+ const description = state.txLockDetails ? (
+ <>
+ Locking with a
+ network fee of
+ >
+ ) : (
+ 'Locking Bitcoin'
+ );
+
+ return ;
+}
diff --git a/src/renderer/components/modal/swap/pages/in_progress/SyncingMoneroWalletPage.tsx b/src/renderer/components/modal/swap/pages/in_progress/SyncingMoneroWalletPage.tsx
new file mode 100644
index 00000000..17e37f13
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/in_progress/SyncingMoneroWalletPage.tsx
@@ -0,0 +1,7 @@
+import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle';
+
+export function SyncingMoneroWalletPage() {
+ return (
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/pages/in_progress/XmrLockInMempoolPage.tsx b/src/renderer/components/modal/swap/pages/in_progress/XmrLockInMempoolPage.tsx
new file mode 100644
index 00000000..59a24ad8
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/in_progress/XmrLockInMempoolPage.tsx
@@ -0,0 +1,29 @@
+import { Box, DialogContentText } from '@material-ui/core';
+import { SwapStateXmrLockInMempool } from 'models/storeModel';
+import MoneroTransactionInfoBox from '../../MoneroTransactionInfoBox';
+
+type XmrLockTxInMempoolPageProps = {
+ state: SwapStateXmrLockInMempool;
+};
+
+export default function XmrLockTxInMempoolPage({
+ state,
+}: XmrLockTxInMempoolPageProps) {
+ const additionalContent = `Confirmations: ${state.aliceXmrLockTxConfirmations}/10`;
+
+ return (
+
+
+ They have published their Monero lock transaction. The swap will proceed
+ once the transaction has been confirmed.
+
+
+
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/pages/in_progress/XmrLockedPage.tsx b/src/renderer/components/modal/swap/pages/in_progress/XmrLockedPage.tsx
new file mode 100644
index 00000000..8fe447d3
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/in_progress/XmrLockedPage.tsx
@@ -0,0 +1,7 @@
+import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle';
+
+export default function XmrLockedPage() {
+ return (
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/pages/init/DepositAmountHelper.tsx b/src/renderer/components/modal/swap/pages/init/DepositAmountHelper.tsx
new file mode 100644
index 00000000..87f3748a
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/init/DepositAmountHelper.tsx
@@ -0,0 +1,91 @@
+import { useState } from 'react';
+import { Box, makeStyles, TextField, Typography } from '@material-ui/core';
+import { SwapStateWaitingForBtcDeposit } from 'models/storeModel';
+import { useAppSelector } from 'store/hooks';
+import { satsToBtc } from 'utils/conversionUtils';
+import { MoneroAmount } from '../../../../other/Units';
+
+const MONERO_FEE = 0.000016;
+
+const useStyles = makeStyles((theme) => ({
+ outer: {
+ display: 'flex',
+ alignItems: 'center',
+ gap: theme.spacing(1),
+ },
+ textField: {
+ '& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button': {
+ display: 'none',
+ },
+ '& input[type=number]': {
+ MozAppearance: 'textfield',
+ },
+ maxWidth: theme.spacing(16),
+ },
+}));
+
+function calcBtcAmountWithoutFees(amount: number, fees: number) {
+ return amount - fees;
+}
+
+export default function DepositAmountHelper({
+ state,
+}: {
+ state: SwapStateWaitingForBtcDeposit;
+}) {
+ const classes = useStyles();
+ const [amount, setAmount] = useState(state.minDeposit);
+ const bitcoinBalance = useAppSelector((s) => s.rpc.state.balance) || 0;
+
+ function getTotalAmountAfterDeposit() {
+ return amount + satsToBtc(bitcoinBalance);
+ }
+
+ function hasError() {
+ return (
+ amount < state.minDeposit ||
+ getTotalAmountAfterDeposit() > state.maximumAmount
+ );
+ }
+
+ function calcXMRAmount(): number | null {
+ if (Number.isNaN(amount)) return null;
+ if (hasError()) return null;
+ if (state.price == null) return null;
+
+ console.log(
+ `Calculating calcBtcAmountWithoutFees(${getTotalAmountAfterDeposit()}, ${
+ state.minBitcoinLockTxFee
+ }) / ${state.price} - ${MONERO_FEE}`,
+ );
+
+ return (
+ calcBtcAmountWithoutFees(
+ getTotalAmountAfterDeposit(),
+ state.minBitcoinLockTxFee,
+ ) /
+ state.price -
+ MONERO_FEE
+ );
+ }
+
+ return (
+
+
+ Depositing {bitcoinBalance > 0 && <>another>}
+
+ setAmount(parseFloat(e.target.value))}
+ size="small"
+ type="number"
+ className={classes.textField}
+ />
+
+ BTC will give you approximately{' '}
+ .
+
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/pages/init/DownloadingMoneroWalletRpcPage.tsx b/src/renderer/components/modal/swap/pages/init/DownloadingMoneroWalletRpcPage.tsx
new file mode 100644
index 00000000..7cc43bd6
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/init/DownloadingMoneroWalletRpcPage.tsx
@@ -0,0 +1,14 @@
+import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle';
+import { MoneroWalletRpcUpdateState } from '../../../../../../models/storeModel';
+
+export default function DownloadingMoneroWalletRpcPage({
+ updateState,
+}: {
+ updateState: MoneroWalletRpcUpdateState;
+}) {
+ return (
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/pages/init/InitPage.tsx b/src/renderer/components/modal/swap/pages/init/InitPage.tsx
new file mode 100644
index 00000000..90f3277f
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/init/InitPage.tsx
@@ -0,0 +1,82 @@
+import { Box, DialogContentText, makeStyles } from '@material-ui/core';
+import { useState } from 'react';
+import BitcoinAddressTextField from 'renderer/components/inputs/BitcoinAddressTextField';
+import MoneroAddressTextField from 'renderer/components/inputs/MoneroAddressTextField';
+import { useAppSelector } from 'store/hooks';
+import PlayArrowIcon from '@material-ui/icons/PlayArrow';
+import { isTestnet } from 'store/config';
+import RemainingFundsWillBeUsedAlert from '../../../../alert/RemainingFundsWillBeUsedAlert';
+import IpcInvokeButton from '../../../../IpcInvokeButton';
+
+const useStyles = makeStyles((theme) => ({
+ initButton: {
+ marginTop: theme.spacing(1),
+ },
+ fieldsOuter: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: theme.spacing(2),
+ },
+}));
+
+export default function InitPage() {
+ const classes = useStyles();
+ const [redeemAddress, setRedeemAddress] = useState(
+ ''
+ );
+ const [refundAddress, setRefundAddress] = useState(
+ ''
+ );
+ const [redeemAddressValid, setRedeemAddressValid] = useState(false);
+ const [refundAddressValid, setRefundAddressValid] = useState(false);
+ const selectedProvider = useAppSelector(
+ (state) => state.providers.selectedProvider,
+ );
+
+ return (
+
+
+
+ Please specify the address to which the Monero should be sent upon
+ completion of the swap and the address for receiving a Bitcoin refund if
+ the swap fails.
+
+
+
+
+
+
+
+
+ }
+ ipcChannel="spawn-buy-xmr"
+ ipcArgs={[selectedProvider, redeemAddress, refundAddress]}
+ displayErrorSnackbar={false}
+ >
+ Start swap
+
+
+ );
+}
diff --git a/src/renderer/components/modal/swap/pages/init/InitiatedPage.tsx b/src/renderer/components/modal/swap/pages/init/InitiatedPage.tsx
new file mode 100644
index 00000000..b4890f95
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/init/InitiatedPage.tsx
@@ -0,0 +1,21 @@
+import { useAppSelector } from 'store/hooks';
+import { SwapSpawnType } from 'models/cliModel';
+import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle';
+
+export default function InitiatedPage() {
+ const description = useAppSelector((s) => {
+ switch (s.swap.spawnType) {
+ case SwapSpawnType.INIT:
+ return 'Requesting quote from provider...';
+ case SwapSpawnType.RESUME:
+ return 'Resuming swap...';
+ case SwapSpawnType.CANCEL_REFUND:
+ return 'Attempting to cancel & refund swap...';
+ default:
+ // Should never be hit
+ return 'Initiating swap...';
+ }
+ });
+
+ return ;
+}
diff --git a/src/renderer/components/modal/swap/pages/init/WaitingForBitcoinDepositPage.tsx b/src/renderer/components/modal/swap/pages/init/WaitingForBitcoinDepositPage.tsx
new file mode 100644
index 00000000..fc45eab0
--- /dev/null
+++ b/src/renderer/components/modal/swap/pages/init/WaitingForBitcoinDepositPage.tsx
@@ -0,0 +1,86 @@
+import { Box, makeStyles, Typography } from '@material-ui/core';
+import { SwapStateWaitingForBtcDeposit } from 'models/storeModel';
+import { useAppSelector } from 'store/hooks';
+import DepositAddressInfoBox from '../../DepositAddressInfoBox';
+import BitcoinIcon from '../../../../icons/BitcoinIcon';
+import DepositAmountHelper from './DepositAmountHelper';
+import {
+ BitcoinAmount,
+ MoneroBitcoinExchangeRate,
+ SatsAmount,
+} from '../../../../other/Units';
+
+const useStyles = makeStyles((theme) => ({
+ amountHelper: {
+ display: 'flex',
+ alignItems: 'center',
+ },
+ additionalContent: {
+ paddingTop: theme.spacing(1),
+ gap: theme.spacing(0.5),
+ display: 'flex',
+ flexDirection: 'column',
+ },
+}));
+
+type WaitingForBtcDepositPageProps = {
+ state: SwapStateWaitingForBtcDeposit;
+};
+
+export default function WaitingForBtcDepositPage({
+ state,
+}: WaitingForBtcDepositPageProps) {
+ const classes = useStyles();
+ const bitcoinBalance = useAppSelector((s) => s.rpc.state.balance) || 0;
+
+ // TODO: Account for BTC lock tx fees
+ return (
+
+
+
+
+ {bitcoinBalance > 0 ? (
+
+ You have already deposited{' '}
+
+
+ ) : null}
+
+ Send any amount between{' '}
+ and{' '}
+ to the address
+ above
+ {bitcoinBalance > 0 && (
+ <> (on top of the already deposited funds)>
+ )}
+
+
+ All Bitcoin sent to this this address will converted into
+ Monero at an exchance rate of{' '}
+
+
+
+ The network fee of{' '}
+ will
+ automatically be deducted from the deposited coins
+
+
+ The swap will start automatically as soon as the minimum
+ amount is deposited
+
+
+
+
+
+ }
+ icon={ }
+ />
+
+ );
+}
diff --git a/src/renderer/components/modal/wallet/WithdrawDialog.tsx b/src/renderer/components/modal/wallet/WithdrawDialog.tsx
new file mode 100644
index 00000000..23cd6c32
--- /dev/null
+++ b/src/renderer/components/modal/wallet/WithdrawDialog.tsx
@@ -0,0 +1,34 @@
+import { Dialog } from '@material-ui/core';
+import { useAppDispatch, useIsRpcEndpointBusy } from 'store/hooks';
+import { RpcMethod } from 'models/rpcModel';
+import { rpcResetWithdrawTxId } from 'store/features/rpcSlice';
+import WithdrawStatePage from './WithdrawStatePage';
+import DialogHeader from '../DialogHeader';
+
+export default function WithdrawDialog({
+ open,
+ onClose,
+}: {
+ open: boolean;
+ onClose: () => void;
+}) {
+ const isRpcEndpointBusy = useIsRpcEndpointBusy(RpcMethod.WITHDRAW_BTC);
+ const dispatch = useAppDispatch();
+
+ function onCancel() {
+ if (!isRpcEndpointBusy) {
+ onClose();
+ dispatch(rpcResetWithdrawTxId());
+ }
+ }
+
+ // This prevents an issue where the Dialog is shown for a split second without a present withdraw state
+ if (!open && !isRpcEndpointBusy) return null;
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/renderer/components/modal/wallet/WithdrawDialogContent.tsx b/src/renderer/components/modal/wallet/WithdrawDialogContent.tsx
new file mode 100644
index 00000000..87083079
--- /dev/null
+++ b/src/renderer/components/modal/wallet/WithdrawDialogContent.tsx
@@ -0,0 +1,27 @@
+import { ReactNode } from 'react';
+import { Box, DialogContent, makeStyles } from '@material-ui/core';
+import WithdrawStepper from './WithdrawStepper';
+
+const useStyles = makeStyles({
+ outer: {
+ minHeight: '15rem',
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'space-between',
+ },
+});
+
+export default function WithdrawDialogContent({
+ children,
+}: {
+ children: ReactNode;
+}) {
+ const classes = useStyles();
+
+ return (
+
+ {children}
+
+
+ );
+}
diff --git a/src/renderer/components/modal/wallet/WithdrawStatePage.tsx b/src/renderer/components/modal/wallet/WithdrawStatePage.tsx
new file mode 100644
index 00000000..0e2a7771
--- /dev/null
+++ b/src/renderer/components/modal/wallet/WithdrawStatePage.tsx
@@ -0,0 +1,27 @@
+import { useAppSelector, useIsRpcEndpointBusy } from 'store/hooks';
+import { RpcMethod } from 'models/rpcModel';
+import AddressInputPage from './pages/AddressInputPage';
+import InitiatedPage from './pages/InitiatedPage';
+import BtcTxInMempoolPageContent from './pages/BitcoinWithdrawTxInMempoolPage';
+
+export default function WithdrawStatePage({
+ onCancel,
+}: {
+ onCancel: () => void;
+}) {
+ const isRpcEndpointBusy = useIsRpcEndpointBusy(RpcMethod.WITHDRAW_BTC);
+ const withdrawTxId = useAppSelector((state) => state.rpc.state.withdrawTxId);
+
+ if (withdrawTxId !== null) {
+ return (
+
+ );
+ }
+ if (isRpcEndpointBusy) {
+ return ;
+ }
+ return ;
+}
diff --git a/src/renderer/components/modal/wallet/WithdrawStepper.tsx b/src/renderer/components/modal/wallet/WithdrawStepper.tsx
new file mode 100644
index 00000000..cbc8bef2
--- /dev/null
+++ b/src/renderer/components/modal/wallet/WithdrawStepper.tsx
@@ -0,0 +1,32 @@
+import { Step, StepLabel, Stepper } from '@material-ui/core';
+import { useAppSelector, useIsRpcEndpointBusy } from 'store/hooks';
+import { RpcMethod } from 'models/rpcModel';
+
+function getActiveStep(
+ isWithdrawInProgress: boolean,
+ withdrawTxId: string | null,
+) {
+ if (isWithdrawInProgress) {
+ return 1;
+ }
+ if (withdrawTxId !== null) {
+ return 2;
+ }
+ return 0;
+}
+
+export default function WithdrawStepper() {
+ const isWithdrawInProgress = useIsRpcEndpointBusy(RpcMethod.WITHDRAW_BTC);
+ const withdrawTxId = useAppSelector((s) => s.rpc.state.withdrawTxId);
+
+ return (
+
+
+ Enter withdraw address
+
+
+ Transfer funds to wallet
+
+
+ );
+}
diff --git a/src/renderer/components/modal/wallet/pages/AddressInputPage.tsx b/src/renderer/components/modal/wallet/pages/AddressInputPage.tsx
new file mode 100644
index 00000000..37d4126a
--- /dev/null
+++ b/src/renderer/components/modal/wallet/pages/AddressInputPage.tsx
@@ -0,0 +1,49 @@
+import { useState } from 'react';
+import { Button, DialogActions, DialogContentText } from '@material-ui/core';
+import BitcoinAddressTextField from '../../../inputs/BitcoinAddressTextField';
+import WithdrawDialogContent from '../WithdrawDialogContent';
+import IpcInvokeButton from '../../../IpcInvokeButton';
+
+export default function AddressInputPage({
+ onCancel,
+}: {
+ onCancel: () => void;
+}) {
+ const [withdrawAddressValid, setWithdrawAddressValid] = useState(false);
+ const [withdrawAddress, setWithdrawAddress] = useState('');
+
+ return (
+ <>
+
+
+ To withdraw the BTC of the internal wallet, please enter an address.
+ All funds will be sent to that address.
+
+
+
+
+
+
+
+ Cancel
+
+
+ Withdraw
+
+
+ >
+ );
+}
diff --git a/src/renderer/components/modal/wallet/pages/BitcoinWithdrawTxInMempoolPage.tsx b/src/renderer/components/modal/wallet/pages/BitcoinWithdrawTxInMempoolPage.tsx
new file mode 100644
index 00000000..153e9941
--- /dev/null
+++ b/src/renderer/components/modal/wallet/pages/BitcoinWithdrawTxInMempoolPage.tsx
@@ -0,0 +1,36 @@
+import { Button, DialogActions, DialogContentText } from '@material-ui/core';
+import BitcoinTransactionInfoBox from '../../swap/BitcoinTransactionInfoBox';
+import WithdrawDialogContent from '../WithdrawDialogContent';
+
+export default function BtcTxInMempoolPageContent({
+ withdrawTxId,
+ onCancel,
+}: {
+ withdrawTxId: string;
+ onCancel: () => void;
+}) {
+ return (
+ <>
+
+
+ All funds of the internal Bitcoin wallet have been transferred to your
+ withdraw address.
+
+
+
+
+
+ Cancel
+
+
+ Done
+
+
+ >
+ );
+}
diff --git a/src/renderer/components/modal/wallet/pages/InitiatedPage.tsx b/src/renderer/components/modal/wallet/pages/InitiatedPage.tsx
new file mode 100644
index 00000000..875737a3
--- /dev/null
+++ b/src/renderer/components/modal/wallet/pages/InitiatedPage.tsx
@@ -0,0 +1,21 @@
+import { Button, DialogActions } from '@material-ui/core';
+import CircularProgressWithSubtitle from '../../swap/CircularProgressWithSubtitle';
+import WithdrawDialogContent from '../WithdrawDialogContent';
+
+export default function InitiatedPage({ onCancel }: { onCancel: () => void }) {
+ return (
+ <>
+
+
+
+
+
+ Cancel
+
+
+ Done
+
+
+ >
+ );
+}
diff --git a/src/renderer/components/navigation/Navigation.tsx b/src/renderer/components/navigation/Navigation.tsx
new file mode 100644
index 00000000..5228a7b4
--- /dev/null
+++ b/src/renderer/components/navigation/Navigation.tsx
@@ -0,0 +1,41 @@
+import { Drawer, makeStyles, Box } from '@material-ui/core';
+import NavigationHeader from './NavigationHeader';
+import NavigationFooter from './NavigationFooter';
+
+export const drawerWidth = 240;
+
+const useStyles = makeStyles({
+ drawer: {
+ width: drawerWidth,
+ flexShrink: 0,
+ },
+ drawerPaper: {
+ width: drawerWidth,
+ },
+ drawerContainer: {
+ overflow: 'auto',
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'space-between',
+ height: '100%',
+ },
+});
+
+export default function Navigation() {
+ const classes = useStyles();
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/renderer/components/navigation/NavigationFooter.tsx b/src/renderer/components/navigation/NavigationFooter.tsx
new file mode 100644
index 00000000..01df1bb3
--- /dev/null
+++ b/src/renderer/components/navigation/NavigationFooter.tsx
@@ -0,0 +1,47 @@
+import RedditIcon from '@material-ui/icons/Reddit';
+import GitHubIcon from '@material-ui/icons/GitHub';
+import { Box, makeStyles } from '@material-ui/core';
+import LinkIconButton from '../icons/LinkIconButton';
+import UnfinishedSwapsAlert from '../alert/UnfinishedSwapsAlert';
+import FundsLeftInWalletAlert from '../alert/FundsLeftInWalletAlert';
+import RpcStatusAlert from '../alert/RpcStatusAlert';
+import DiscordIcon from '../icons/DiscordIcon';
+import { DISCORD_URL } from '../pages/help/ContactInfoBox';
+import MoneroWalletRpcUpdatingAlert from '../alert/MoneroWalletRpcUpdatingAlert';
+
+const useStyles = makeStyles((theme) => ({
+ outer: {
+ display: 'flex',
+ flexDirection: 'column',
+ padding: theme.spacing(1),
+ gap: theme.spacing(1),
+ },
+ linksOuter: {
+ display: 'flex',
+ justifyContent: 'space-evenly',
+ },
+}));
+
+export default function NavigationFooter() {
+ const classes = useStyles();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/renderer/components/navigation/NavigationHeader.tsx b/src/renderer/components/navigation/NavigationHeader.tsx
new file mode 100644
index 00000000..0d208e20
--- /dev/null
+++ b/src/renderer/components/navigation/NavigationHeader.tsx
@@ -0,0 +1,30 @@
+import { Box, List } from '@material-ui/core';
+import SwapHorizOutlinedIcon from '@material-ui/icons/SwapHorizOutlined';
+import HistoryOutlinedIcon from '@material-ui/icons/HistoryOutlined';
+import AccountBalanceWalletIcon from '@material-ui/icons/AccountBalanceWallet';
+import HelpOutlineIcon from '@material-ui/icons/HelpOutline';
+import RouteListItemIconButton from './RouteListItemIconButton';
+import UnfinishedSwapsBadge from './UnfinishedSwapsCountBadge';
+
+export default function NavigationHeader() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/renderer/components/navigation/RouteListItemIconButton.tsx b/src/renderer/components/navigation/RouteListItemIconButton.tsx
new file mode 100644
index 00000000..4e29f8b0
--- /dev/null
+++ b/src/renderer/components/navigation/RouteListItemIconButton.tsx
@@ -0,0 +1,22 @@
+import { ReactNode } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { ListItem, ListItemIcon, ListItemText } from '@material-ui/core';
+
+export default function RouteListItemIconButton({
+ name,
+ route,
+ children,
+}: {
+ name: string;
+ route: string;
+ children: ReactNode;
+}) {
+ const navigate = useNavigate();
+
+ return (
+ navigate(route)} key={name}>
+ {children}
+
+
+ );
+}
diff --git a/src/renderer/components/navigation/UnfinishedSwapsCountBadge.tsx b/src/renderer/components/navigation/UnfinishedSwapsCountBadge.tsx
new file mode 100644
index 00000000..1304b775
--- /dev/null
+++ b/src/renderer/components/navigation/UnfinishedSwapsCountBadge.tsx
@@ -0,0 +1,19 @@
+import { Badge } from '@material-ui/core';
+import { useResumeableSwapsCount } from 'store/hooks';
+
+export default function UnfinishedSwapsBadge({
+ children,
+}: {
+ children: JSX.Element;
+}) {
+ const resumableSwapsCount = useResumeableSwapsCount();
+
+ if (resumableSwapsCount > 0) {
+ return (
+
+ {children}
+
+ );
+ }
+ return children;
+}
diff --git a/src/renderer/components/other/ExpandableSearchBox.tsx b/src/renderer/components/other/ExpandableSearchBox.tsx
new file mode 100644
index 00000000..3cbee92c
--- /dev/null
+++ b/src/renderer/components/other/ExpandableSearchBox.tsx
@@ -0,0 +1,44 @@
+import { useState } from 'react';
+import { Box, IconButton, TextField } from '@material-ui/core';
+import SearchIcon from '@material-ui/icons/Search';
+import CloseIcon from '@material-ui/icons/Close';
+
+export function ExpandableSearchBox({
+ query,
+ setQuery,
+}: {
+ query: string;
+ setQuery: (query: string) => void;
+}) {
+ const [expanded, setExpanded] = useState(false);
+
+ return (
+
+
+ {expanded ? (
+ <>
+ setQuery(e.target.value)}
+ autoFocus
+ size="small"
+ />
+ {
+ setExpanded(false);
+ setQuery('');
+ }}
+ size="small"
+ >
+
+
+ >
+ ) : (
+ setExpanded(true)} size="small">
+
+
+ )}
+
+
+ );
+}
diff --git a/src/renderer/components/other/HumanizedBitcoinBlockDuration.tsx b/src/renderer/components/other/HumanizedBitcoinBlockDuration.tsx
new file mode 100644
index 00000000..00ed29cc
--- /dev/null
+++ b/src/renderer/components/other/HumanizedBitcoinBlockDuration.tsx
@@ -0,0 +1,17 @@
+import humanizeDuration from 'humanize-duration';
+
+const AVG_BLOCK_TIME_MS = 10 * 60 * 1000;
+
+export default function HumanizedBitcoinBlockDuration({
+ blocks,
+}: {
+ blocks: number;
+}) {
+ return (
+ <>
+ {`${humanizeDuration(blocks * AVG_BLOCK_TIME_MS, {
+ conjunction: ' and ',
+ })} (${blocks} blocks)`}
+ >
+ );
+}
diff --git a/src/renderer/components/other/JSONViewTree.tsx b/src/renderer/components/other/JSONViewTree.tsx
new file mode 100644
index 00000000..ecdd2979
--- /dev/null
+++ b/src/renderer/components/other/JSONViewTree.tsx
@@ -0,0 +1,50 @@
+import TreeView from '@material-ui/lab/TreeView';
+import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
+import ChevronRightIcon from '@material-ui/icons/ChevronRight';
+import TreeItem from '@material-ui/lab/TreeItem';
+import ScrollablePaperTextBox from './ScrollablePaperTextBox';
+
+interface JsonTreeViewProps {
+ data: any;
+ label: string;
+}
+
+export default function JsonTreeView({ data, label }: JsonTreeViewProps) {
+ const renderTree = (nodes: any, parentId: string) => {
+ return Object.keys(nodes).map((key, _) => {
+ const nodeId = `${parentId}.${key}`;
+ if (typeof nodes[key] === 'object' && nodes[key] !== null) {
+ return (
+
+ {renderTree(nodes[key], nodeId)}
+
+ );
+ }
+ return (
+
+ );
+ });
+ };
+
+ return (
+ }
+ defaultExpandIcon={ }
+ defaultExpanded={['root']}
+ >
+
+ {renderTree(data ?? {}, 'root')}
+
+ ,
+ ]}
+ />
+ );
+}
diff --git a/src/renderer/components/other/LoadingButton.tsx b/src/renderer/components/other/LoadingButton.tsx
new file mode 100644
index 00000000..dafe51a9
--- /dev/null
+++ b/src/renderer/components/other/LoadingButton.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import Button, { ButtonProps } from '@material-ui/core/Button';
+import CircularProgress from '@material-ui/core/CircularProgress';
+
+interface LoadingButtonProps extends ButtonProps {
+ loading: boolean;
+}
+
+const LoadingButton: React.FC = ({
+ loading,
+ disabled,
+ children,
+ ...props
+}) => {
+ return (
+ }
+ >
+ {children}
+
+ );
+};
+
+export default LoadingButton;
diff --git a/src/renderer/components/other/RenderedCliLog.tsx b/src/renderer/components/other/RenderedCliLog.tsx
new file mode 100644
index 00000000..53ad0264
--- /dev/null
+++ b/src/renderer/components/other/RenderedCliLog.tsx
@@ -0,0 +1,91 @@
+import { Box, Chip, Typography } from '@material-ui/core';
+import { useMemo, useState } from 'react';
+import { CliLog } from 'models/cliModel';
+import { logsToRawString } from 'utils/parseUtils';
+import ScrollablePaperTextBox from './ScrollablePaperTextBox';
+
+function RenderedCliLog({ log }: { log: CliLog }) {
+ const { timestamp, level, fields } = log;
+
+ const levelColorMap = {
+ DEBUG: '#1976d2', // Blue
+ INFO: '#388e3c', // Green
+ WARN: '#fbc02d', // Yellow
+ ERROR: '#d32f2f', // Red
+ TRACE: '#8e24aa', // Purple
+ };
+
+ return (
+
+
+
+
+ {fields.message}
+
+
+ {Object.entries(fields).map(([key, value]) => {
+ if (key !== 'message') {
+ return (
+
+ {key}: {JSON.stringify(value)}
+
+ );
+ }
+ return null;
+ })}
+
+
+ );
+}
+
+export default function CliLogsBox({
+ label,
+ logs,
+}: {
+ label: string;
+ logs: (CliLog | string)[];
+}) {
+ const [searchQuery, setSearchQuery] = useState('');
+
+ const memoizedLogs = useMemo(() => {
+ if (searchQuery.length === 0) {
+ return logs;
+ }
+ return logs.filter((log) =>
+ JSON.stringify(log).toLowerCase().includes(searchQuery.toLowerCase()),
+ );
+ }, [logs, searchQuery]);
+
+ return (
+
+ typeof log === 'string' ? (
+ {log}
+ ) : (
+
+ ),
+ )}
+ />
+ );
+}
diff --git a/src/renderer/components/other/ScrollablePaperTextBox.tsx b/src/renderer/components/other/ScrollablePaperTextBox.tsx
new file mode 100644
index 00000000..59350c26
--- /dev/null
+++ b/src/renderer/components/other/ScrollablePaperTextBox.tsx
@@ -0,0 +1,90 @@
+import { Box, Divider, IconButton, Paper, Typography } from '@material-ui/core';
+import { ReactNode, useRef } from 'react';
+import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
+import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
+import { VList, VListHandle } from 'virtua';
+import FileCopyOutlinedIcon from '@material-ui/icons/FileCopyOutlined';
+import { ExpandableSearchBox } from './ExpandableSearchBox';
+
+const MIN_HEIGHT = '10rem';
+
+export default function ScrollablePaperTextBox({
+ rows,
+ title,
+ copyValue,
+ searchQuery,
+ setSearchQuery,
+ minHeight,
+}: {
+ rows: ReactNode[];
+ title: string;
+ copyValue: string;
+ searchQuery?: string;
+ setSearchQuery?: (query: string) => void;
+ minHeight?: string;
+}) {
+ const virtuaEl = useRef(null);
+
+ function onCopy() {
+ navigator.clipboard.writeText(copyValue);
+ }
+
+ function scrollToBottom() {
+ virtuaEl.current?.scrollToIndex(rows.length - 1);
+ }
+
+ function scrollToTop() {
+ virtuaEl.current?.scrollToIndex(0);
+ }
+
+ return (
+
+ {title}
+
+
+
+ {rows}
+
+
+
+
+
+
+
+
+
+
+
+
+ {searchQuery !== undefined && setSearchQuery !== undefined && (
+
+ )}
+
+
+ );
+}
+
+ScrollablePaperTextBox.defaultProps = {
+ searchQuery: undefined,
+ setSearchQuery: undefined,
+ minHeight: MIN_HEIGHT,
+};
diff --git a/src/renderer/components/other/Units.tsx b/src/renderer/components/other/Units.tsx
new file mode 100644
index 00000000..445f2dcf
--- /dev/null
+++ b/src/renderer/components/other/Units.tsx
@@ -0,0 +1,80 @@
+import { piconerosToXmr, satsToBtc } from 'utils/conversionUtils';
+import { Tooltip } from '@material-ui/core';
+import { useAppSelector } from 'store/hooks';
+
+type Amount = number | null | undefined;
+
+export function AmountWithUnit({
+ amount,
+ unit,
+ fixedPrecision,
+ dollarRate,
+}: {
+ amount: Amount;
+ unit: string;
+ fixedPrecision: number;
+ dollarRate?: Amount;
+}) {
+ return (
+
+
+ {amount != null
+ ? Number.parseFloat(amount.toFixed(fixedPrecision))
+ : '?'}{' '}
+ {unit}
+
+
+ );
+}
+
+AmountWithUnit.defaultProps = {
+ dollarRate: null,
+};
+
+export function BitcoinAmount({ amount }: { amount: Amount }) {
+ const btcUsdRate = useAppSelector((state) => state.rates.btcPrice);
+
+ return (
+
+ );
+}
+
+export function MoneroAmount({ amount }: { amount: Amount }) {
+ const xmrUsdRate = useAppSelector((state) => state.rates.xmrPrice);
+
+ return (
+
+ );
+}
+
+export function MoneroBitcoinExchangeRate({ rate }: { rate: Amount }) {
+ return ;
+}
+
+export function SatsAmount({ amount }: { amount: Amount }) {
+ const btcAmount = amount == null ? null : satsToBtc(amount);
+ return ;
+}
+
+export function PiconeroAmount({ amount }: { amount: Amount }) {
+ return (
+
+ );
+}
diff --git a/src/renderer/components/pages/help/ContactInfoBox.tsx b/src/renderer/components/pages/help/ContactInfoBox.tsx
new file mode 100644
index 00000000..2352276b
--- /dev/null
+++ b/src/renderer/components/pages/help/ContactInfoBox.tsx
@@ -0,0 +1,51 @@
+import { Box, Button, makeStyles, Typography } from '@material-ui/core';
+import InfoBox from '../../modal/swap/InfoBox';
+
+const useStyles = makeStyles((theme) => ({
+ spacedBox: {
+ display: 'flex',
+ gap: theme.spacing(1),
+ },
+}));
+
+const GITHUB_ISSUE_URL =
+ 'https://github.com/UnstoppableSwap/unstoppableswap-gui/issues/new/choose';
+const MATRIX_ROOM_URL = 'https://matrix.to/#/#unstoppableswap:matrix.org';
+export const DISCORD_URL = 'https://discord.gg/APJ6rJmq';
+
+export default function ContactInfoBox() {
+ const classes = useStyles();
+
+ return (
+
+ If you need help or just want to reach out to the contributors of this
+ project you can open a GitHub issue, join our Matrix room or Discord
+
+ }
+ additionalContent={
+
+ window.open(GITHUB_ISSUE_URL)}
+ >
+ Open GitHub issue
+
+ window.open(MATRIX_ROOM_URL)}
+ >
+ Join Matrix room
+
+ window.open(DISCORD_URL)}>
+ Join Discord
+
+
+ }
+ icon={null}
+ loading={false}
+ />
+ );
+}
diff --git a/src/renderer/components/pages/help/DonateInfoBox.tsx b/src/renderer/components/pages/help/DonateInfoBox.tsx
new file mode 100644
index 00000000..a3dd510a
--- /dev/null
+++ b/src/renderer/components/pages/help/DonateInfoBox.tsx
@@ -0,0 +1,25 @@
+import { Typography } from '@material-ui/core';
+import DepositAddressInfoBox from '../../modal/swap/DepositAddressInfoBox';
+import MoneroIcon from '../../icons/MoneroIcon';
+
+const XMR_DONATE_ADDRESS =
+ '87jS4C7ngk9EHdqFFuxGFgg8AyH63dRUoULshWDybFJaP75UA89qsutG5B1L1QTc4w228nsqsv8EjhL7bz8fB3611Mh98mg';
+
+export default function DonateInfoBox() {
+ return (
+ }
+ additionalContent={
+
+ We rely on generous donors like you to keep development moving
+ forward. To bring Atomic Swaps to life, we need resources. If you have
+ the possibility, please consider making a donation to the project. All
+ funds will be used to support contributors and critical
+ infrastructure.
+
+ }
+ />
+ );
+}
diff --git a/src/renderer/components/pages/help/FeedbackInfoBox.tsx b/src/renderer/components/pages/help/FeedbackInfoBox.tsx
new file mode 100644
index 00000000..7c89d2fd
--- /dev/null
+++ b/src/renderer/components/pages/help/FeedbackInfoBox.tsx
@@ -0,0 +1,35 @@
+import { Button, Typography } from '@material-ui/core';
+import { useState } from 'react';
+import InfoBox from '../../modal/swap/InfoBox';
+import FeedbackDialog from '../../modal/feedback/FeedbackDialog';
+
+export default function FeedbackInfoBox() {
+ const [showDialog, setShowDialog] = useState(false);
+
+ return (
+
+ The main goal of this project is to make Atomic Swaps easier to use,
+ and for that we need genuine users' input. Please leave some
+ feedback, it takes just two minutes. I'll read each and every
+ survey response and take your feedback into consideration.
+
+ }
+ additionalContent={
+ <>
+ setShowDialog(true)}>
+ Give feedback
+
+ setShowDialog(false)}
+ />
+ >
+ }
+ icon={null}
+ loading={false}
+ />
+ );
+}
diff --git a/src/renderer/components/pages/help/HelpPage.tsx b/src/renderer/components/pages/help/HelpPage.tsx
new file mode 100644
index 00000000..7a6a2d1f
--- /dev/null
+++ b/src/renderer/components/pages/help/HelpPage.tsx
@@ -0,0 +1,28 @@
+import { Box, makeStyles } from '@material-ui/core';
+import ContactInfoBox from './ContactInfoBox';
+import FeedbackInfoBox from './FeedbackInfoBox';
+import DonateInfoBox from './DonateInfoBox';
+import TorInfoBox from './TorInfoBox';
+import RpcControlBox from './RpcControlBox';
+
+const useStyles = makeStyles((theme) => ({
+ outer: {
+ display: 'flex',
+ gap: theme.spacing(2),
+ flexDirection: 'column',
+ },
+}));
+
+export default function HelpPage() {
+ const classes = useStyles();
+
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/renderer/components/pages/help/RpcControlBox.tsx b/src/renderer/components/pages/help/RpcControlBox.tsx
new file mode 100644
index 00000000..78dc1395
--- /dev/null
+++ b/src/renderer/components/pages/help/RpcControlBox.tsx
@@ -0,0 +1,74 @@
+import { Box, makeStyles } from '@material-ui/core';
+import IpcInvokeButton from 'renderer/components/IpcInvokeButton';
+import { useAppSelector } from 'store/hooks';
+import StopIcon from '@material-ui/icons/Stop';
+import PlayArrowIcon from '@material-ui/icons/PlayArrow';
+import { RpcProcessStateType } from 'models/rpcModel';
+import InfoBox from '../../modal/swap/InfoBox';
+import CliLogsBox from '../../other/RenderedCliLog';
+import FolderOpenIcon from '@material-ui/icons/FolderOpen';
+
+const useStyles = makeStyles((theme) => ({
+ actionsOuter: {
+ display: 'flex',
+ gap: theme.spacing(1),
+ alignItems: 'center',
+ },
+}));
+
+export default function RpcControlBox() {
+ const rpcProcess = useAppSelector((state) => state.rpc.process);
+ const isRunning =
+ rpcProcess.type === RpcProcessStateType.STARTED ||
+ rpcProcess.type === RpcProcessStateType.LISTENING_FOR_CONNECTIONS;
+ const classes = useStyles();
+
+ return (
+
+ ) : null
+ }
+ additionalContent={
+
+ }
+ disabled={isRunning}
+ requiresRpc={false}
+ >
+ Start Daemon
+
+ }
+ disabled={!isRunning}
+ requiresRpc={false}
+ >
+ Stop Daemon
+
+ }
+ requiresRpc={false}
+ isIconButton
+ size="small"
+ tooltipTitle="Open the data directory of the Swap Daemon in your file explorer"
+ />
+
+ }
+ icon={null}
+ loading={false}
+ />
+ );
+}
diff --git a/src/renderer/components/pages/help/TorInfoBox.tsx b/src/renderer/components/pages/help/TorInfoBox.tsx
new file mode 100644
index 00000000..d787fa8e
--- /dev/null
+++ b/src/renderer/components/pages/help/TorInfoBox.tsx
@@ -0,0 +1,71 @@
+import { Box, makeStyles, Typography } from '@material-ui/core';
+import IpcInvokeButton from 'renderer/components/IpcInvokeButton';
+import { useAppSelector } from 'store/hooks';
+import StopIcon from '@material-ui/icons/Stop';
+import PlayArrowIcon from '@material-ui/icons/PlayArrow';
+import InfoBox from '../../modal/swap/InfoBox';
+import CliLogsBox from '../../other/RenderedCliLog';
+
+const useStyles = makeStyles((theme) => ({
+ actionsOuter: {
+ display: 'flex',
+ gap: theme.spacing(1),
+ },
+}));
+
+export default function TorInfoBox() {
+ const isTorRunning = useAppSelector((state) => state.tor.processRunning);
+ const torStdOut = useAppSelector((s) => s.tor.stdOut);
+ const classes = useStyles();
+
+ return (
+
+
+ Tor is a network that allows you to anonymously connect to the
+ internet. It is a free and open network that is operated by
+ volunteers. You can start and stop Tor by clicking the buttons
+ below. If Tor is running, all traffic will be routed through it and
+ the swap provider will not be able to see your IP address.
+
+
+
+ }
+ additionalContent={
+
+ }
+ requiresRpc={false}
+ >
+ Start Tor
+
+ }
+ requiresRpc={false}
+ >
+ Stop Tor
+
+
+ }
+ icon={null}
+ loading={false}
+ />
+ );
+}
diff --git a/src/renderer/components/pages/history/HistoryPage.tsx b/src/renderer/components/pages/history/HistoryPage.tsx
new file mode 100644
index 00000000..7ea72a48
--- /dev/null
+++ b/src/renderer/components/pages/history/HistoryPage.tsx
@@ -0,0 +1,18 @@
+import { Typography } from '@material-ui/core';
+import { useIsSwapRunning } from 'store/hooks';
+import HistoryTable from './table/HistoryTable';
+import SwapDialog from '../../modal/swap/SwapDialog';
+import SwapTxLockAlertsBox from '../../alert/SwapTxLockAlertsBox';
+
+export default function HistoryPage() {
+ const showDialog = useIsSwapRunning();
+
+ return (
+ <>
+ History
+
+
+ {}} />
+ >
+ );
+}
diff --git a/src/renderer/components/pages/history/table/HistoryRow.tsx b/src/renderer/components/pages/history/table/HistoryRow.tsx
new file mode 100644
index 00000000..bbd0bf3c
--- /dev/null
+++ b/src/renderer/components/pages/history/table/HistoryRow.tsx
@@ -0,0 +1,86 @@
+import {
+ Box,
+ Collapse,
+ IconButton,
+ makeStyles,
+ TableCell,
+ TableRow,
+} from '@material-ui/core';
+import { useState } from 'react';
+import ArrowForwardIcon from '@material-ui/icons/ArrowForward';
+import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
+import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
+import {
+ getHumanReadableDbStateType,
+ getSwapBtcAmount,
+ getSwapXmrAmount,
+ GetSwapInfoResponse,
+} from '../../../../../models/rpcModel';
+import HistoryRowActions from './HistoryRowActions';
+import HistoryRowExpanded from './HistoryRowExpanded';
+import { BitcoinAmount, MoneroAmount } from '../../../other/Units';
+
+type HistoryRowProps = {
+ swap: GetSwapInfoResponse;
+};
+
+const useStyles = makeStyles((theme) => ({
+ amountTransferContainer: {
+ display: 'flex',
+ alignItems: 'center',
+ gap: theme.spacing(1),
+ },
+}));
+
+function AmountTransfer({
+ btcAmount,
+ xmrAmount,
+}: {
+ xmrAmount: number;
+ btcAmount: number;
+}) {
+ const classes = useStyles();
+
+ return (
+
+
+
+
+
+ );
+}
+
+export default function HistoryRow({ swap }: HistoryRowProps) {
+ const btcAmount = getSwapBtcAmount(swap);
+ const xmrAmount = getSwapXmrAmount(swap);
+
+ const [expanded, setExpanded] = useState(false);
+
+ return (
+ <>
+
+
+ setExpanded(!expanded)}>
+ {expanded ? : }
+
+
+ {swap.swapId.substring(0, 5)}...
+
+
+
+ {getHumanReadableDbStateType(swap.stateName)}
+
+
+
+
+
+
+
+
+ {expanded && }
+
+
+
+ >
+ );
+}
diff --git a/src/renderer/components/pages/history/table/HistoryRowActions.tsx b/src/renderer/components/pages/history/table/HistoryRowActions.tsx
new file mode 100644
index 00000000..0caf8543
--- /dev/null
+++ b/src/renderer/components/pages/history/table/HistoryRowActions.tsx
@@ -0,0 +1,90 @@
+import { Tooltip } from '@material-ui/core';
+import Button, { ButtonProps } from '@material-ui/core/Button/Button';
+import DoneIcon from '@material-ui/icons/Done';
+import ErrorIcon from '@material-ui/icons/Error';
+import { green, red } from '@material-ui/core/colors';
+import PlayArrowIcon from '@material-ui/icons/PlayArrow';
+import IpcInvokeButton from '../../../IpcInvokeButton';
+import {
+ GetSwapInfoResponse,
+ SwapStateName,
+ isSwapStateNamePossiblyCancellableSwap,
+ isSwapStateNamePossiblyRefundableSwap,
+} from '../../../../../models/rpcModel';
+
+export function SwapResumeButton({
+ swap,
+ ...props
+}: { swap: GetSwapInfoResponse } & ButtonProps) {
+ return (
+ }
+ requiresRpc
+ {...props}
+ >
+ Resume
+
+ );
+}
+
+export function SwapCancelRefundButton({
+ swap,
+ ...props
+}: { swap: GetSwapInfoResponse } & ButtonProps) {
+ const cancelOrRefundable =
+ isSwapStateNamePossiblyCancellableSwap(swap.stateName) ||
+ isSwapStateNamePossiblyRefundableSwap(swap.stateName);
+
+ if (!cancelOrRefundable) {
+ return <>>;
+ }
+
+ return (
+
+ Attempt manual Cancel & Refund
+
+ );
+}
+
+export default function HistoryRowActions({
+ swap,
+}: {
+ swap: GetSwapInfoResponse;
+}) {
+ if (swap.stateName === SwapStateName.XmrRedeemed) {
+ return (
+
+
+
+ );
+ }
+
+ if (swap.stateName === SwapStateName.BtcRefunded) {
+ return (
+
+
+
+ );
+ }
+
+ if (swap.stateName === SwapStateName.BtcPunished) {
+ return (
+
+
+
+ );
+ }
+
+ return ;
+}
diff --git a/src/renderer/components/pages/history/table/HistoryRowExpanded.tsx b/src/renderer/components/pages/history/table/HistoryRowExpanded.tsx
new file mode 100644
index 00000000..cc0deb98
--- /dev/null
+++ b/src/renderer/components/pages/history/table/HistoryRowExpanded.tsx
@@ -0,0 +1,134 @@
+import {
+ Box,
+ Link,
+ makeStyles,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableRow,
+} from '@material-ui/core';
+import { getBitcoinTxExplorerUrl } from 'utils/conversionUtils';
+import { isTestnet } from 'store/config';
+import {
+ getHumanReadableDbStateType,
+ getSwapBtcAmount,
+ getSwapExchangeRate,
+ getSwapTxFees,
+ getSwapXmrAmount,
+ GetSwapInfoResponse,
+} from '../../../../../models/rpcModel';
+import SwapLogFileOpenButton from './SwapLogFileOpenButton';
+import { SwapCancelRefundButton } from './HistoryRowActions';
+import { SwapMoneroRecoveryButton } from './SwapMoneroRecoveryButton';
+import {
+ BitcoinAmount,
+ MoneroAmount,
+ MoneroBitcoinExchangeRate,
+} from 'renderer/components/other/Units';
+
+const useStyles = makeStyles((theme) => ({
+ outer: {
+ display: 'grid',
+ padding: theme.spacing(1),
+ gap: theme.spacing(1),
+ },
+ actionsOuter: {
+ display: 'flex',
+ flexDirection: 'row',
+ gap: theme.spacing(1),
+ },
+}));
+
+export default function HistoryRowExpanded({
+ swap,
+}: {
+ swap: GetSwapInfoResponse;
+}) {
+ const classes = useStyles();
+
+ const { seller, startDate } = swap;
+ const btcAmount = getSwapBtcAmount(swap);
+ const xmrAmount = getSwapXmrAmount(swap);
+ const txFees = getSwapTxFees(swap);
+ const exchangeRate = getSwapExchangeRate(swap);
+
+ return (
+
+
+
+
+
+ Started on
+ {startDate}
+
+
+ Swap ID
+ {swap.swapId}
+
+
+ State Name
+
+ {getHumanReadableDbStateType(swap.stateName)}
+
+
+
+ Monero Amount
+
+
+
+
+
+ Bitcoin Amount
+
+
+
+
+
+ Exchange Rate
+
+
+
+
+
+ Bitcoin Network Fees
+
+
+
+
+
+ Provider Address
+
+ {seller.addresses.join(', ')}
+
+
+
+ Bitcoin lock transaction
+
+
+ {swap.txLockId}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/renderer/components/pages/history/table/HistoryTable.tsx b/src/renderer/components/pages/history/table/HistoryTable.tsx
new file mode 100644
index 00000000..163456cf
--- /dev/null
+++ b/src/renderer/components/pages/history/table/HistoryTable.tsx
@@ -0,0 +1,53 @@
+import {
+ Box,
+ makeStyles,
+ Paper,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+} from '@material-ui/core';
+import { sortBy } from 'lodash';
+import { parseDateString } from 'utils/parseUtils';
+import {
+ useAppSelector,
+ useSwapInfosSortedByDate,
+} from '../../../../../store/hooks';
+import HistoryRow from './HistoryRow';
+
+const useStyles = makeStyles((theme) => ({
+ outer: {
+ paddingTop: theme.spacing(1),
+ paddingBottom: theme.spacing(1),
+ },
+}));
+
+export default function HistoryTable() {
+ const classes = useStyles();
+ const swapSortedByDate = useSwapInfosSortedByDate();
+
+ return (
+
+
+
+
+
+
+ ID
+ Amount
+ State
+
+
+
+
+ {swapSortedByDate.map((swap) => (
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/src/renderer/components/pages/history/table/SwapLogFileOpenButton.tsx b/src/renderer/components/pages/history/table/SwapLogFileOpenButton.tsx
new file mode 100644
index 00000000..681f0b6c
--- /dev/null
+++ b/src/renderer/components/pages/history/table/SwapLogFileOpenButton.tsx
@@ -0,0 +1,45 @@
+import { ButtonProps } from '@material-ui/core/Button/Button';
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+} from '@material-ui/core';
+import { useState } from 'react';
+import { CliLog } from 'models/cliModel';
+import IpcInvokeButton from '../../../IpcInvokeButton';
+import CliLogsBox from '../../../other/RenderedCliLog';
+
+export default function SwapLogFileOpenButton({
+ swapId,
+ ...props
+}: { swapId: string } & ButtonProps) {
+ const [logs, setLogs] = useState(null);
+
+ return (
+ <>
+ {
+ setLogs(data as CliLog[]);
+ }}
+ {...props}
+ >
+ view log
+
+ {logs && (
+ setLogs(null)} fullWidth maxWidth="lg">
+ Logs of swap {swapId}
+
+
+
+
+ setLogs(null)}>Close
+
+
+ )}
+ >
+ );
+}
diff --git a/src/renderer/components/pages/history/table/SwapMoneroRecoveryButton.tsx b/src/renderer/components/pages/history/table/SwapMoneroRecoveryButton.tsx
new file mode 100644
index 00000000..e20e056e
--- /dev/null
+++ b/src/renderer/components/pages/history/table/SwapMoneroRecoveryButton.tsx
@@ -0,0 +1,119 @@
+import { ButtonProps } from '@material-ui/core/Button/Button';
+import {
+ Box,
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogContentText,
+ Link,
+} from '@material-ui/core';
+import { useAppDispatch, useAppSelector } from 'store/hooks';
+import { rpcResetMoneroRecoveryKeys } from 'store/features/rpcSlice';
+import {
+ GetSwapInfoResponse,
+ isSwapMoneroRecoverable,
+} from '../../../../../models/rpcModel';
+import IpcInvokeButton from '../../../IpcInvokeButton';
+import DialogHeader from '../../../modal/DialogHeader';
+import ScrollablePaperTextBox from '../../../other/ScrollablePaperTextBox';
+
+function MoneroRecoveryKeysDialog({ swap }: { swap: GetSwapInfoResponse }) {
+ const dispatch = useAppDispatch();
+ const keys = useAppSelector((s) => s.rpc.state.moneroRecovery);
+
+ function onClose() {
+ dispatch(rpcResetMoneroRecoveryKeys());
+ }
+
+ if (keys === null || keys.swapId !== swap.swapId) {
+ return <>>;
+ }
+
+ return (
+
+
+
+
+ You can use the keys below to manually redeem the Monero funds from
+ the multi-signature wallet.
+
+
+ This is useful if the swap daemon fails to redeem the funds itself
+
+
+ If you have come this far, there is no risk of losing funds. You
+ are the only one with access to these keys and can use them to
+ access your funds
+
+
+ View{' '}
+
+ this guide
+ {' '}
+ for a detailed description on how to import the keys and spend the
+ funds.
+
+
+
+
+ {[
+ ['Primary Address', keys.keys.address],
+ ['View Key', keys.keys.view_key],
+ ['Spend Key', keys.keys.spend_key],
+ ['Restore Height', keys.keys.restore_height.toString()],
+ ].map(([title, value]) => (
+
+ ))}
+
+
+
+
+ Done
+
+
+
+ );
+}
+
+export function SwapMoneroRecoveryButton({
+ swap,
+ ...props
+}: { swap: GetSwapInfoResponse } & ButtonProps) {
+ const isRecoverable = isSwapMoneroRecoverable(swap.stateName);
+
+ if (!isRecoverable) {
+ return <>>;
+ }
+
+ return (
+ <>
+
+ Display Monero Recovery Keys
+
+
+ >
+ );
+}
diff --git a/src/renderer/components/pages/swap/ApiAlertsBox.tsx b/src/renderer/components/pages/swap/ApiAlertsBox.tsx
new file mode 100644
index 00000000..ad2bb153
--- /dev/null
+++ b/src/renderer/components/pages/swap/ApiAlertsBox.tsx
@@ -0,0 +1,31 @@
+import { Box } from '@material-ui/core';
+import { Alert, AlertTitle } from '@material-ui/lab';
+import { removeAlert } from 'store/features/alertsSlice';
+import { useAppDispatch, useAppSelector } from 'store/hooks';
+
+export default function ApiAlertsBox() {
+ const alerts = useAppSelector((state) => state.alerts.alerts);
+ const dispatch = useAppDispatch();
+
+ function onRemoveAlert(id: number) {
+ dispatch(removeAlert(id));
+ }
+
+ if (alerts.length === 0) return null;
+
+ return (
+
+ {alerts.map((alert) => (
+ onRemoveAlert(alert.id)}
+ >
+ {alert.title}
+ {alert.body}
+
+ ))}
+
+ );
+}
diff --git a/src/renderer/components/pages/swap/SwapPage.tsx b/src/renderer/components/pages/swap/SwapPage.tsx
new file mode 100644
index 00000000..f13171c6
--- /dev/null
+++ b/src/renderer/components/pages/swap/SwapPage.tsx
@@ -0,0 +1,25 @@
+import { Box, makeStyles } from '@material-ui/core';
+import SwapWidget from './SwapWidget';
+import ApiAlertsBox from './ApiAlertsBox';
+
+const useStyles = makeStyles((theme) => ({
+ outer: {
+ display: 'flex',
+ width: '100%',
+ flexDirection: 'column',
+ alignItems: 'center',
+ paddingBottom: theme.spacing(1),
+ gap: theme.spacing(1),
+ },
+}));
+
+export default function SwapPage() {
+ const classes = useStyles();
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/renderer/components/pages/swap/SwapWidget.tsx b/src/renderer/components/pages/swap/SwapWidget.tsx
new file mode 100644
index 00000000..75840215
--- /dev/null
+++ b/src/renderer/components/pages/swap/SwapWidget.tsx
@@ -0,0 +1,274 @@
+import { ChangeEvent, useEffect, useState } from 'react';
+import {
+ makeStyles,
+ Box,
+ Paper,
+ Typography,
+ TextField,
+ LinearProgress,
+ Fab,
+} from '@material-ui/core';
+import InputAdornment from '@material-ui/core/InputAdornment';
+import ArrowDownwardIcon from '@material-ui/icons/ArrowDownward';
+import SwapHorizIcon from '@material-ui/icons/SwapHoriz';
+import { Alert } from '@material-ui/lab';
+import { satsToBtc } from 'utils/conversionUtils';
+import { useAppSelector } from 'store/hooks';
+import { ExtendedProviderStatus } from 'models/apiModel';
+import { isSwapState } from 'models/storeModel';
+import SwapDialog from '../../modal/swap/SwapDialog';
+import ProviderSelect from '../../modal/provider/ProviderSelect';
+import {
+ ListSellersDialogOpenButton,
+ ProviderSubmitDialogOpenButton,
+} from '../../modal/provider/ProviderListDialog';
+
+// After RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN failed reconnection attempts we can assume the public registry is down
+const RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN = 1;
+
+function isRegistryDown(reconnectionAttempts: number): boolean {
+ return reconnectionAttempts > RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN;
+}
+
+const useStyles = makeStyles((theme) => ({
+ inner: {
+ width: 'min(480px, 100%)',
+ minHeight: '150px',
+ display: 'grid',
+ padding: theme.spacing(1),
+ gridGap: theme.spacing(1),
+ },
+ header: {
+ padding: 0,
+ },
+ headerText: {
+ padding: theme.spacing(1),
+ },
+ providerInfo: {
+ padding: theme.spacing(1),
+ },
+ swapIconOuter: {
+ display: 'flex',
+ justifyContent: 'center',
+ },
+ swapIcon: {
+ marginRight: theme.spacing(1),
+ },
+ noProvidersAlertOuter: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: theme.spacing(1),
+ },
+ noProvidersAlertButtonsOuter: {
+ display: 'flex',
+ gap: theme.spacing(1),
+ },
+}));
+
+function Title() {
+ const classes = useStyles();
+
+ return (
+
+
+ Swap
+
+
+ );
+}
+
+function HasProviderSwapWidget({
+ selectedProvider,
+}: {
+ selectedProvider: ExtendedProviderStatus;
+}) {
+ const classes = useStyles();
+
+ const forceShowDialog = useAppSelector((state) =>
+ isSwapState(state.swap.state),
+ );
+ const [showDialog, setShowDialog] = useState(false);
+ const [btcFieldValue, setBtcFieldValue] = useState(
+ satsToBtc(selectedProvider.minSwapAmount),
+ );
+ const [xmrFieldValue, setXmrFieldValue] = useState(1);
+
+ function onBtcAmountChange(event: ChangeEvent) {
+ setBtcFieldValue(event.target.value);
+ }
+
+ function updateXmrValue() {
+ const parsedBtcAmount = Number(btcFieldValue);
+ if (Number.isNaN(parsedBtcAmount)) {
+ setXmrFieldValue(0);
+ } else {
+ const convertedXmrAmount =
+ parsedBtcAmount / satsToBtc(selectedProvider.price);
+ setXmrFieldValue(convertedXmrAmount);
+ }
+ }
+
+ function getBtcFieldError(): string | null {
+ const parsedBtcAmount = Number(btcFieldValue);
+ if (Number.isNaN(parsedBtcAmount)) {
+ return 'This is not a valid number';
+ }
+ if (parsedBtcAmount < satsToBtc(selectedProvider.minSwapAmount)) {
+ return `The minimum swap amount is ${satsToBtc(
+ selectedProvider.minSwapAmount,
+ )} BTC. Switch to a different provider if you want to swap less.`;
+ }
+ if (parsedBtcAmount > satsToBtc(selectedProvider.maxSwapAmount)) {
+ return `The maximum swap amount is ${satsToBtc(
+ selectedProvider.maxSwapAmount,
+ )} BTC. Switch to a different provider if you want to swap more.`;
+ }
+ return null;
+ }
+
+ function handleGuideDialogOpen() {
+ setShowDialog(true);
+ }
+
+ useEffect(updateXmrValue, [btcFieldValue, selectedProvider]);
+
+ return (
+ // 'elevation' prop can't be passed down (type def issue)
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+
+
+ BTC,
+ }}
+ />
+
+
+
+ XMR,
+ }}
+ />
+
+
+
+ Swap
+
+ setShowDialog(false)}
+ />
+
+ );
+}
+
+function HasNoProvidersSwapWidget() {
+ const forceShowDialog = useAppSelector((state) =>
+ isSwapState(state.swap.state),
+ );
+ const isPublicRegistryDown = useAppSelector((state) =>
+ isRegistryDown(
+ state.providers.registry.failedReconnectAttemptsSinceLastSuccess,
+ ),
+ );
+ const classes = useStyles();
+
+ const alertBox = isPublicRegistryDown ? (
+
+
+
+ Currently, the public registry of providers seems to be unreachable.
+ Here's what you can do:
+
+
+ Try discovering a provider by connecting to a rendezvous point
+
+
+ Try again later when the public registry may be reachable again
+
+
+
+
+
+
+
+
+ ) : (
+
+
+
+ Currently, there are no providers (trading partners) available in the
+ official registry. Here's what you can do:
+
+
+ Try discovering a provider by connecting to a rendezvous point
+
+ Add a new provider to the public registry
+ Try again later when more providers may be available
+
+
+
+
+
+
+
+
+ );
+
+ return (
+
+ {alertBox}
+ {}} />
+
+ );
+}
+
+function ProviderLoadingSwapWidget() {
+ const classes = useStyles();
+
+ return (
+ // 'elevation' prop can't be passed down (type def issue)
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+
+
+
+
+ );
+}
+
+export default function SwapWidget() {
+ const selectedProvider = useAppSelector(
+ (state) => state.providers.selectedProvider,
+ );
+ // If we fail more than RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN reconnect attempts, we'll show the "no providers" widget. We can assume the public registry is down.
+ const providerLoading = useAppSelector(
+ (state) =>
+ state.providers.registry.providers === null &&
+ !isRegistryDown(
+ state.providers.registry.failedReconnectAttemptsSinceLastSuccess,
+ ),
+ );
+
+ if (providerLoading) {
+ return ;
+ }
+ if (selectedProvider) {
+ return ;
+ }
+ return ;
+}
diff --git a/src/renderer/components/pages/wallet/WalletPage.tsx b/src/renderer/components/pages/wallet/WalletPage.tsx
new file mode 100644
index 00000000..aab2291e
--- /dev/null
+++ b/src/renderer/components/pages/wallet/WalletPage.tsx
@@ -0,0 +1,31 @@
+import { Box, makeStyles, Typography } from '@material-ui/core';
+import { Alert } from '@material-ui/lab';
+import WithdrawWidget from './WithdrawWidget';
+
+const useStyles = makeStyles((theme) => ({
+ outer: {
+ display: 'flex',
+ flexDirection: 'column',
+ gridGap: theme.spacing(0.5),
+ },
+}));
+
+export default function WalletPage() {
+ const classes = useStyles();
+
+ return (
+
+ Wallet
+
+ You do not have to deposit money before starting a swap. Instead, you
+ will be greeted with a deposit address after you initiate one.
+
+
+ If funds are left in your wallet after a swap, you can withdraw them to
+ your wallet. If you decide to leave them inside the internal wallet, the
+ funds will automatically be used when starting a new swap.
+
+
+
+ );
+}
diff --git a/src/renderer/components/pages/wallet/WalletRefreshButton.tsx b/src/renderer/components/pages/wallet/WalletRefreshButton.tsx
new file mode 100644
index 00000000..8bc57772
--- /dev/null
+++ b/src/renderer/components/pages/wallet/WalletRefreshButton.tsx
@@ -0,0 +1,16 @@
+import { CircularProgress } from '@material-ui/core';
+import RefreshIcon from '@material-ui/icons/Refresh';
+import IpcInvokeButton from '../../IpcInvokeButton';
+
+export default function WalletRefreshButton() {
+ return (
+ }
+ size="small"
+ isIconButton
+ endIcon={ }
+ ipcArgs={[]}
+ ipcChannel="spawn-balance-check"
+ />
+ );
+}
diff --git a/src/renderer/components/pages/wallet/WithdrawWidget.tsx b/src/renderer/components/pages/wallet/WithdrawWidget.tsx
new file mode 100644
index 00000000..45b85b0a
--- /dev/null
+++ b/src/renderer/components/pages/wallet/WithdrawWidget.tsx
@@ -0,0 +1,64 @@
+import { Box, Button, makeStyles, Typography } from '@material-ui/core';
+import { useState } from 'react';
+import SendIcon from '@material-ui/icons/Send';
+import { useAppSelector, useIsRpcEndpointBusy } from 'store/hooks';
+import { RpcMethod } from 'models/rpcModel';
+import BitcoinIcon from '../../icons/BitcoinIcon';
+import WithdrawDialog from '../../modal/wallet/WithdrawDialog';
+import WalletRefreshButton from './WalletRefreshButton';
+import InfoBox from '../../modal/swap/InfoBox';
+import { SatsAmount } from 'renderer/components/other/Units';
+
+const useStyles = makeStyles((theme) => ({
+ title: {
+ alignItems: 'center',
+ display: 'flex',
+ gap: theme.spacing(0.5),
+ },
+}));
+
+export default function WithdrawWidget() {
+ const classes = useStyles();
+ const walletBalance = useAppSelector((state) => state.rpc.state.balance);
+ const checkingBalance = useIsRpcEndpointBusy(RpcMethod.GET_BTC_BALANCE);
+ const [showDialog, setShowDialog] = useState(false);
+
+ function onShowDialog() {
+ setShowDialog(true);
+ }
+
+ return (
+ <>
+
+ Wallet Balance
+
+
+ }
+ mainContent={
+
+
+
+ }
+ icon={ }
+ additionalContent={
+ }
+ size="large"
+ onClick={onShowDialog}
+ disabled={
+ walletBalance === null || checkingBalance || walletBalance <= 0
+ }
+ >
+ Withdraw
+
+ }
+ loading={false}
+ />
+ setShowDialog(false)} />
+ >
+ );
+}
diff --git a/src/renderer/components/snackbar/GlobalSnackbarProvider.tsx b/src/renderer/components/snackbar/GlobalSnackbarProvider.tsx
new file mode 100644
index 00000000..3a09bd63
--- /dev/null
+++ b/src/renderer/components/snackbar/GlobalSnackbarProvider.tsx
@@ -0,0 +1,46 @@
+import {
+ MaterialDesignContent,
+ SnackbarKey,
+ SnackbarProvider,
+ useSnackbar,
+} from 'notistack';
+import { IconButton, styled } from '@material-ui/core';
+import { Close } from '@material-ui/icons';
+import { ReactNode } from 'react';
+
+const StyledMaterialDesignContent = styled(MaterialDesignContent)(() => ({
+ '&.notistack-MuiContent': {
+ maxWidth: '50vw',
+ },
+}));
+
+function CloseSnackbarButton({ snackbarId }: { snackbarId: SnackbarKey }) {
+ const { closeSnackbar } = useSnackbar();
+
+ return (
+ closeSnackbar(snackbarId)}>
+
+
+ );
+}
+
+export default function GlobalSnackbarManager({
+ children,
+}: {
+ children: ReactNode;
+}) {
+ return (
+ }
+ Components={{
+ success: StyledMaterialDesignContent,
+ error: StyledMaterialDesignContent,
+ default: StyledMaterialDesignContent,
+ info: StyledMaterialDesignContent,
+ warning: StyledMaterialDesignContent,
+ }}
+ >
+ {children}
+
+ );
+}
diff --git a/src/renderer/index.ejs b/src/renderer/index.ejs
new file mode 100644
index 00000000..d19d4b7f
--- /dev/null
+++ b/src/renderer/index.ejs
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/renderer/index.tsx b/src/renderer/index.tsx
new file mode 100644
index 00000000..61846800
--- /dev/null
+++ b/src/renderer/index.tsx
@@ -0,0 +1,57 @@
+import { render } from 'react-dom';
+import { Provider } from 'react-redux';
+import { store } from './store/storeRenderer';
+import { setRegistryProviders } from 'store/features/providersSlice';
+import { setAlerts } from 'store/features/alertsSlice';
+import { setXmrPrice, setBtcPrice } from 'store/features/ratesSlice';
+import {
+ fetchAlertsViaHttp,
+ fetchBtcPrice,
+ fetchProvidersViaHttp,
+ fetchXmrPrice,
+} from './api';
+import logger from '../utils/logger';
+import App from './components/App';
+
+render(
+
+
+ ,
+ document.getElementById('root'),
+);
+
+async function fetchInitialData() {
+ try {
+ const providerList = await fetchProvidersViaHttp();
+ store.dispatch(setRegistryProviders(providerList));
+
+ logger.info(
+ { providerList },
+ 'Fetched providers via UnstoppableSwap HTTP API',
+ );
+ } catch (e) {
+ logger.error(e, 'Failed to fetch providers via UnstoppableSwap HTTP API');
+ }
+
+ try {
+ const alerts = await fetchAlertsViaHttp();
+ store.dispatch(setAlerts(alerts));
+ logger.info({ alerts }, 'Fetched alerts via UnstoppableSwap HTTP API');
+ } catch (e) {
+ logger.error(e, 'Failed to fetch alerts via UnstoppableSwap HTTP API');
+ }
+
+ try {
+ const xmrPrice = await fetchXmrPrice();
+ store.dispatch(setXmrPrice(xmrPrice));
+ logger.info({ xmrPrice }, 'Fetched XMR price');
+
+ const btcPrice = await fetchBtcPrice();
+ store.dispatch(setBtcPrice(btcPrice));
+ logger.info({ btcPrice }, 'Fetched BTC price');
+ } catch (e) {
+ logger.error(e, 'Error retrieving fiat prices');
+ }
+}
+
+fetchInitialData();
\ No newline at end of file
diff --git a/src/renderer/store/storeRenderer.ts b/src/renderer/store/storeRenderer.ts
new file mode 100644
index 00000000..4656033a
--- /dev/null
+++ b/src/renderer/store/storeRenderer.ts
@@ -0,0 +1,9 @@
+import { configureStore } from '@reduxjs/toolkit';
+import { reducers } from 'store/combinedReducer';
+
+export const store = configureStore({
+ reducer: reducers,
+});
+
+export type AppDispatch = typeof store.dispatch;
+export type RootState = ReturnType;
diff --git a/src/store/combinedReducer.ts b/src/store/combinedReducer.ts
new file mode 100644
index 00000000..c2c18b82
--- /dev/null
+++ b/src/store/combinedReducer.ts
@@ -0,0 +1,15 @@
+import swapReducer from './features/swapSlice';
+import providersSlice from './features/providersSlice';
+import torSlice from './features/torSlice';
+import rpcSlice from './features/rpcSlice';
+import alertsSlice from './features/alertsSlice';
+import ratesSlice from './features/ratesSlice';
+
+export const reducers = {
+ swap: swapReducer,
+ providers: providersSlice,
+ tor: torSlice,
+ rpc: rpcSlice,
+ alerts: alertsSlice,
+ rates: ratesSlice,
+};
diff --git a/src/store/config.ts b/src/store/config.ts
new file mode 100644
index 00000000..2c475a5e
--- /dev/null
+++ b/src/store/config.ts
@@ -0,0 +1,18 @@
+import { ExtendedProviderStatus } from 'models/apiModel';
+
+export const isTestnet = () =>
+ false
+
+export const isExternalRpc = () =>
+ true
+
+export const isDevelopment =
+ true
+
+export function getStubTestnetProvider(): ExtendedProviderStatus | null {
+ return null;
+}
+
+export const getPlatform = () => {
+ return 'mac';
+};
diff --git a/src/store/features/alertsSlice.ts b/src/store/features/alertsSlice.ts
new file mode 100644
index 00000000..4a1ce04a
--- /dev/null
+++ b/src/store/features/alertsSlice.ts
@@ -0,0 +1,28 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { Alert } from 'models/apiModel';
+
+export interface AlertsSlice {
+ alerts: Alert[];
+}
+
+const initialState: AlertsSlice = {
+ alerts: [],
+};
+
+const alertsSlice = createSlice({
+ name: 'alerts',
+ initialState,
+ reducers: {
+ setAlerts(slice, action: PayloadAction) {
+ slice.alerts = action.payload;
+ },
+ removeAlert(slice, action: PayloadAction) {
+ slice.alerts = slice.alerts.filter(
+ (alert) => alert.id !== action.payload,
+ );
+ },
+ },
+});
+
+export const { setAlerts, removeAlert } = alertsSlice.actions;
+export default alertsSlice.reducer;
diff --git a/src/store/features/providersSlice.ts b/src/store/features/providersSlice.ts
new file mode 100644
index 00000000..d4af559d
--- /dev/null
+++ b/src/store/features/providersSlice.ts
@@ -0,0 +1,117 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { ExtendedProviderStatus, ProviderStatus } from 'models/apiModel';
+import { sortProviderList } from 'utils/sortUtils';
+import { isProviderCompatible } from 'utils/multiAddrUtils';
+import { getStubTestnetProvider } from 'store/config';
+
+const stubTestnetProvider = getStubTestnetProvider();
+
+export interface ProvidersSlice {
+ rendezvous: {
+ providers: (ExtendedProviderStatus | ProviderStatus)[];
+ };
+ registry: {
+ providers: ExtendedProviderStatus[] | null;
+ failedReconnectAttemptsSinceLastSuccess: number;
+ };
+ selectedProvider: ExtendedProviderStatus | null;
+}
+
+const initialState: ProvidersSlice = {
+ rendezvous: {
+ providers: [],
+ },
+ registry: {
+ providers: stubTestnetProvider ? [stubTestnetProvider] : null,
+ failedReconnectAttemptsSinceLastSuccess: 0,
+ },
+ selectedProvider: null,
+};
+
+function selectNewSelectedProvider(
+ slice: ProvidersSlice,
+ peerId?: string,
+): ProviderStatus {
+ const selectedPeerId = peerId || slice.selectedProvider?.peerId;
+
+ return (
+ slice.registry.providers?.find((prov) => prov.peerId === selectedPeerId) ||
+ slice.rendezvous.providers.find((prov) => prov.peerId === selectedPeerId) ||
+ slice.registry.providers?.at(0) ||
+ slice.rendezvous.providers[0] ||
+ null
+ );
+}
+
+export const providersSlice = createSlice({
+ name: 'providers',
+ initialState,
+ reducers: {
+ discoveredProvidersByRendezvous(
+ slice,
+ action: PayloadAction,
+ ) {
+ action.payload.forEach((discoveredProvider) => {
+ if (
+ !slice.registry.providers?.some(
+ (prov) =>
+ prov.peerId === discoveredProvider.peerId &&
+ prov.multiAddr === discoveredProvider.multiAddr,
+ )
+ ) {
+ const indexOfExistingProvider = slice.rendezvous.providers.findIndex(
+ (prov) =>
+ prov.peerId === discoveredProvider.peerId &&
+ prov.multiAddr === discoveredProvider.multiAddr,
+ );
+
+ // Avoid duplicates, replace instead
+ if (indexOfExistingProvider !== -1) {
+ slice.rendezvous.providers[indexOfExistingProvider] =
+ discoveredProvider;
+ } else {
+ slice.rendezvous.providers.push(discoveredProvider);
+ }
+ }
+ });
+
+ slice.rendezvous.providers = sortProviderList(slice.rendezvous.providers);
+ },
+ setRegistryProviders(
+ slice,
+ action: PayloadAction,
+ ) {
+ if (stubTestnetProvider) {
+ action.payload.push(stubTestnetProvider);
+ }
+
+ slice.registry.providers = sortProviderList(action.payload).filter(
+ isProviderCompatible,
+ );
+ slice.selectedProvider = selectNewSelectedProvider(slice);
+ },
+ increaseFailedRegistryReconnectAttemptsSinceLastSuccess(slice) {
+ slice.registry.failedReconnectAttemptsSinceLastSuccess += 1;
+ },
+ setSelectedProvider(
+ slice,
+ action: PayloadAction<{
+ peerId: string;
+ }>,
+ ) {
+ slice.selectedProvider = selectNewSelectedProvider(
+ slice,
+ action.payload.peerId,
+ );
+ },
+ },
+});
+
+export const {
+ discoveredProvidersByRendezvous,
+ setRegistryProviders,
+ increaseFailedRegistryReconnectAttemptsSinceLastSuccess,
+ setSelectedProvider,
+} = providersSlice.actions;
+
+export default providersSlice.reducer;
diff --git a/src/store/features/ratesSlice.ts b/src/store/features/ratesSlice.ts
new file mode 100644
index 00000000..67c8d11e
--- /dev/null
+++ b/src/store/features/ratesSlice.ts
@@ -0,0 +1,28 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+
+export interface RatesState {
+ btcPrice: number | null;
+ xmrPrice: number | null;
+}
+
+const initialState: RatesState = {
+ btcPrice: null,
+ xmrPrice: null,
+};
+
+const ratesSlice = createSlice({
+ name: 'rates',
+ initialState,
+ reducers: {
+ setBtcPrice: (state, action: PayloadAction) => {
+ state.btcPrice = action.payload;
+ },
+ setXmrPrice: (state, action: PayloadAction) => {
+ state.xmrPrice = action.payload;
+ },
+ },
+});
+
+export const { setBtcPrice, setXmrPrice } = ratesSlice.actions;
+
+export default ratesSlice.reducer;
diff --git a/src/store/features/rpcSlice.ts b/src/store/features/rpcSlice.ts
new file mode 100644
index 00000000..19f5865d
--- /dev/null
+++ b/src/store/features/rpcSlice.ts
@@ -0,0 +1,218 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { ExtendedProviderStatus, ProviderStatus } from 'models/apiModel';
+import { MoneroWalletRpcUpdateState } from 'models/storeModel';
+import {
+ GetSwapInfoResponse,
+ MoneroRecoveryResponse,
+ RpcProcessStateType,
+} from '../../models/rpcModel';
+import {
+ CliLog,
+ isCliLog,
+ isCliLogDownloadingMoneroWalletRpc,
+ isCliLogFailedToSyncMoneroWallet,
+ isCliLogFinishedSyncingMoneroWallet,
+ isCliLogStartedRpcServer,
+ isCliLogStartedSyncingMoneroWallet,
+} from '../../models/cliModel';
+import { getLogsAndStringsFromRawFileString } from 'utils/parseUtils';
+
+type Process =
+ | {
+ type: RpcProcessStateType.STARTED;
+ logs: (CliLog | string)[];
+ }
+ | {
+ type: RpcProcessStateType.LISTENING_FOR_CONNECTIONS;
+ logs: (CliLog | string)[];
+ address: string;
+ }
+ | {
+ type: RpcProcessStateType.EXITED;
+ logs: (CliLog | string)[];
+ exitCode: number | null;
+ }
+ | {
+ type: RpcProcessStateType.NOT_STARTED;
+ };
+
+interface State {
+ balance: number | null;
+ withdrawTxId: string | null;
+ rendezvous_discovered_sellers: (ExtendedProviderStatus | ProviderStatus)[];
+ swapInfos: {
+ [swapId: string]: GetSwapInfoResponse;
+ };
+ moneroRecovery: {
+ swapId: string;
+ keys: MoneroRecoveryResponse;
+ } | null;
+ moneroWallet: {
+ isSyncing: boolean;
+ };
+ moneroWalletRpc: {
+ updateState: false | MoneroWalletRpcUpdateState;
+ };
+}
+
+export interface RPCSlice {
+ process: Process;
+ state: State;
+ busyEndpoints: string[];
+}
+
+const initialState: RPCSlice = {
+ process: {
+ type: RpcProcessStateType.NOT_STARTED,
+ },
+ state: {
+ balance: null,
+ withdrawTxId: null,
+ rendezvous_discovered_sellers: [],
+ swapInfos: {},
+ moneroRecovery: null,
+ moneroWallet: {
+ isSyncing: false,
+ },
+ moneroWalletRpc: {
+ updateState: false,
+ },
+ },
+ busyEndpoints: [],
+};
+
+export const rpcSlice = createSlice({
+ name: 'rpc',
+ initialState,
+ reducers: {
+ rpcAddLogs(slice, action: PayloadAction<(CliLog | string)[]>) {
+ if (
+ slice.process.type === RpcProcessStateType.STARTED ||
+ slice.process.type === RpcProcessStateType.LISTENING_FOR_CONNECTIONS ||
+ slice.process.type === RpcProcessStateType.EXITED
+ ) {
+ const logs = action.payload;
+ slice.process.logs.push(...logs);
+
+ logs.filter(isCliLog).forEach((log) => {
+ if (
+ isCliLogStartedRpcServer(log) &&
+ slice.process.type === RpcProcessStateType.STARTED
+ ) {
+ slice.process = {
+ type: RpcProcessStateType.LISTENING_FOR_CONNECTIONS,
+ logs: slice.process.logs,
+ address: log.fields.addr,
+ };
+ } else if (isCliLogDownloadingMoneroWalletRpc(log)) {
+ slice.state.moneroWalletRpc.updateState = {
+ progress: log.fields.progress,
+ downloadUrl: log.fields.download_url,
+ };
+
+ if (log.fields.progress === '100%') {
+ slice.state.moneroWalletRpc.updateState = false;
+ }
+ } else if (isCliLogStartedSyncingMoneroWallet(log)) {
+ slice.state.moneroWallet.isSyncing = true;
+ } else if (isCliLogFinishedSyncingMoneroWallet(log)) {
+ slice.state.moneroWallet.isSyncing = false;
+ } else if (isCliLogFailedToSyncMoneroWallet(log)) {
+ slice.state.moneroWallet.isSyncing = false;
+ }
+ });
+ }
+ },
+ rpcInitiate(slice) {
+ slice.process = {
+ type: RpcProcessStateType.STARTED,
+ logs: [],
+ };
+ },
+ rpcProcessExited(
+ slice,
+ action: PayloadAction<{
+ exitCode: number | null;
+ exitSignal: NodeJS.Signals | null;
+ }>,
+ ) {
+ if (
+ slice.process.type === RpcProcessStateType.STARTED ||
+ slice.process.type === RpcProcessStateType.LISTENING_FOR_CONNECTIONS
+ ) {
+ slice.process = {
+ type: RpcProcessStateType.EXITED,
+ logs: slice.process.logs,
+ exitCode: action.payload.exitCode,
+ };
+ slice.state.moneroWalletRpc = {
+ updateState: false,
+ };
+ slice.state.moneroWallet = {
+ isSyncing: false,
+ };
+ }
+ },
+ rpcSetBalance(slice, action: PayloadAction) {
+ slice.state.balance = action.payload;
+ },
+ rpcSetWithdrawTxId(slice, action: PayloadAction) {
+ slice.state.withdrawTxId = action.payload;
+ },
+ rpcSetRendezvousDiscoveredProviders(
+ slice,
+ action: PayloadAction<(ExtendedProviderStatus | ProviderStatus)[]>,
+ ) {
+ slice.state.rendezvous_discovered_sellers = action.payload;
+ },
+ rpcResetWithdrawTxId(slice) {
+ slice.state.withdrawTxId = null;
+ },
+ rpcSetSwapInfo(slice, action: PayloadAction) {
+ slice.state.swapInfos[action.payload.swapId] = action.payload;
+ },
+ rpcSetEndpointBusy(slice, action: PayloadAction) {
+ if (!slice.busyEndpoints.includes(action.payload)) {
+ slice.busyEndpoints.push(action.payload);
+ }
+ },
+ rpcSetEndpointFree(slice, action: PayloadAction) {
+ const index = slice.busyEndpoints.indexOf(action.payload);
+ if (index >= 0) {
+ slice.busyEndpoints.splice(index);
+ }
+ },
+ rpcSetMoneroRecoveryKeys(
+ slice,
+ action: PayloadAction<[string, MoneroRecoveryResponse]>,
+ ) {
+ const swapId = action.payload[0];
+ const keys = action.payload[1];
+
+ slice.state.moneroRecovery = {
+ swapId,
+ keys,
+ };
+ },
+ rpcResetMoneroRecoveryKeys(slice) {
+ slice.state.moneroRecovery = null;
+ },
+ },
+});
+
+export const {
+ rpcProcessExited,
+ rpcAddLogs,
+ rpcInitiate,
+ rpcSetBalance,
+ rpcSetWithdrawTxId,
+ rpcResetWithdrawTxId,
+ rpcSetEndpointBusy,
+ rpcSetEndpointFree,
+ rpcSetRendezvousDiscoveredProviders,
+ rpcSetSwapInfo,
+ rpcSetMoneroRecoveryKeys,
+ rpcResetMoneroRecoveryKeys,
+} = rpcSlice.actions;
+
+export default rpcSlice.reducer;
diff --git a/src/store/features/swapSlice.ts b/src/store/features/swapSlice.ts
new file mode 100644
index 00000000..c1cd8eab
--- /dev/null
+++ b/src/store/features/swapSlice.ts
@@ -0,0 +1,323 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { extractAmountFromUnitString } from 'utils/parseUtils';
+import { Provider } from 'models/apiModel';
+import {
+ isSwapStateBtcLockInMempool,
+ isSwapStateProcessExited,
+ isSwapStateXmrLockInMempool,
+ SwapSlice,
+ SwapStateAttemptingCooperativeRedeeem,
+ SwapStateBtcCancelled,
+ SwapStateBtcLockInMempool,
+ SwapStateBtcPunished,
+ SwapStateBtcRedemeed,
+ SwapStateBtcRefunded,
+ SwapStateInitiated,
+ SwapStateProcessExited,
+ SwapStateReceivedQuote,
+ SwapStateStarted,
+ SwapStateType,
+ SwapStateWaitingForBtcDeposit,
+ SwapStateXmrLocked,
+ SwapStateXmrLockInMempool,
+ SwapStateXmrRedeemInMempool,
+} from '../../models/storeModel';
+import {
+ isCliLogAliceLockedXmr,
+ isCliLogBtcTxStatusChanged,
+ isCliLogPublishedBtcTx,
+ isCliLogReceivedQuote,
+ isCliLogReceivedXmrLockTxConfirmation,
+ isCliLogRedeemedXmr,
+ isCliLogStartedSwap,
+ isCliLogWaitingForBtcDeposit,
+ CliLog,
+ isCliLogAdvancingState,
+ SwapSpawnType,
+ isCliLogBtcTxFound,
+ isCliLogReleasingSwapLockLog,
+ isYouHaveBeenPunishedCliLog,
+ isCliLogAcquiringSwapLockLog,
+ isCliLogApiCallError,
+ isCliLogDeterminedSwapAmount,
+ isCliLogAttemptingToCooperativelyRedeemXmr,
+} from '../../models/cliModel';
+import logger from '../../utils/logger';
+
+const initialState: SwapSlice = {
+ state: null,
+ processRunning: false,
+ swapId: null,
+ logs: [],
+ provider: null,
+ spawnType: null,
+};
+
+export const swapSlice = createSlice({
+ name: 'swap',
+ initialState,
+ reducers: {
+ swapAddLog(
+ slice,
+ action: PayloadAction<{ logs: CliLog[]; isFromRestore: boolean }>,
+ ) {
+ const { logs } = action.payload;
+ slice.logs.push(...logs);
+
+ logs.forEach((log) => {
+ if (
+ isCliLogAcquiringSwapLockLog(log) &&
+ !action.payload.isFromRestore
+ ) {
+ slice.processRunning = true;
+ slice.swapId = log.fields.swap_id;
+ // TODO: Maybe we can infer more info here (state) from the log
+ } else if (isCliLogReceivedQuote(log)) {
+ const price = extractAmountFromUnitString(log.fields.price);
+ const minimumSwapAmount = extractAmountFromUnitString(
+ log.fields.minimum_amount,
+ );
+ const maximumSwapAmount = extractAmountFromUnitString(
+ log.fields.maximum_amount,
+ );
+
+ if (
+ price != null &&
+ minimumSwapAmount != null &&
+ maximumSwapAmount != null
+ ) {
+ const nextState: SwapStateReceivedQuote = {
+ type: SwapStateType.RECEIVED_QUOTE,
+ price,
+ minimumSwapAmount,
+ maximumSwapAmount,
+ };
+
+ slice.state = nextState;
+ }
+ } else if (isCliLogWaitingForBtcDeposit(log)) {
+ const maxGiveable = extractAmountFromUnitString(
+ log.fields.max_giveable,
+ );
+ const minDeposit = extractAmountFromUnitString(
+ log.fields.min_deposit_until_swap_will_start,
+ );
+ const maxDeposit = extractAmountFromUnitString(
+ log.fields.max_deposit_until_maximum_amount_is_reached,
+ );
+ const minimumAmount = extractAmountFromUnitString(
+ log.fields.minimum_amount,
+ );
+ const maximumAmount = extractAmountFromUnitString(
+ log.fields.maximum_amount,
+ );
+ const minBitcoinLockTxFee = extractAmountFromUnitString(
+ log.fields.min_bitcoin_lock_tx_fee,
+ );
+ const price = extractAmountFromUnitString(log.fields.price);
+
+ const depositAddress = log.fields.deposit_address;
+
+ if (
+ maxGiveable != null &&
+ minimumAmount != null &&
+ maximumAmount != null &&
+ minDeposit != null &&
+ maxDeposit != null &&
+ minBitcoinLockTxFee != null &&
+ price != null
+ ) {
+ const nextState: SwapStateWaitingForBtcDeposit = {
+ type: SwapStateType.WAITING_FOR_BTC_DEPOSIT,
+ depositAddress,
+ maxGiveable,
+ minimumAmount,
+ maximumAmount,
+ minDeposit,
+ maxDeposit,
+ price,
+ minBitcoinLockTxFee,
+ };
+
+ slice.state = nextState;
+ }
+ } else if (isCliLogDeterminedSwapAmount(log)) {
+ const amount = extractAmountFromUnitString(log.fields.amount);
+ const fees = extractAmountFromUnitString(log.fields.fees);
+
+ const nextState: SwapStateStarted = {
+ type: SwapStateType.STARTED,
+ txLockDetails:
+ amount != null && fees != null ? { amount, fees } : null,
+ };
+
+ slice.state = nextState;
+ } else if (isCliLogStartedSwap(log)) {
+ if (slice.state?.type !== SwapStateType.STARTED) {
+ const nextState: SwapStateStarted = {
+ type: SwapStateType.STARTED,
+ txLockDetails: null,
+ };
+
+ slice.state = nextState;
+ }
+
+ slice.swapId = log.fields.swap_id;
+ } else if (isCliLogPublishedBtcTx(log)) {
+ if (log.fields.kind === 'lock') {
+ const nextState: SwapStateBtcLockInMempool = {
+ type: SwapStateType.BTC_LOCK_TX_IN_MEMPOOL,
+ bobBtcLockTxId: log.fields.txid,
+ bobBtcLockTxConfirmations: 0,
+ };
+
+ slice.state = nextState;
+ } else if (log.fields.kind === 'cancel') {
+ const nextState: SwapStateBtcCancelled = {
+ type: SwapStateType.BTC_CANCELLED,
+ btcCancelTxId: log.fields.txid,
+ };
+
+ slice.state = nextState;
+ } else if (log.fields.kind === 'refund') {
+ const nextState: SwapStateBtcRefunded = {
+ type: SwapStateType.BTC_REFUNDED,
+ bobBtcRefundTxId: log.fields.txid,
+ };
+
+ slice.state = nextState;
+ }
+ } else if (isCliLogBtcTxStatusChanged(log) || isCliLogBtcTxFound(log)) {
+ if (isSwapStateBtcLockInMempool(slice.state)) {
+ if (slice.state.bobBtcLockTxId === log.fields.txid) {
+ const newStatusText = isCliLogBtcTxStatusChanged(log)
+ ? log.fields.new_status
+ : log.fields.status;
+
+ if (newStatusText.startsWith('confirmed with')) {
+ const confirmations = Number.parseInt(
+ newStatusText.split(' ')[2],
+ 10,
+ );
+
+ slice.state.bobBtcLockTxConfirmations = confirmations;
+ }
+ }
+ }
+ } else if (isCliLogAliceLockedXmr(log)) {
+ const nextState: SwapStateXmrLockInMempool = {
+ type: SwapStateType.XMR_LOCK_TX_IN_MEMPOOL,
+ aliceXmrLockTxId: log.fields.txid,
+ aliceXmrLockTxConfirmations: 0,
+ };
+
+ slice.state = nextState;
+ } else if (isCliLogReceivedXmrLockTxConfirmation(log)) {
+ if (isSwapStateXmrLockInMempool(slice.state)) {
+ if (slice.state.aliceXmrLockTxId === log.fields.txid) {
+ slice.state.aliceXmrLockTxConfirmations = Number.parseInt(
+ log.fields.seen_confirmations,
+ 10,
+ );
+ }
+ }
+ } else if (isCliLogAdvancingState(log)) {
+ if (log.fields.state === 'xmr is locked') {
+ const nextState: SwapStateXmrLocked = {
+ type: SwapStateType.XMR_LOCKED,
+ };
+
+ slice.state = nextState;
+ } else if (log.fields.state === 'btc is redeemed') {
+ const nextState: SwapStateBtcRedemeed = {
+ type: SwapStateType.BTC_REDEEMED,
+ };
+
+ slice.state = nextState;
+ }
+ } else if (isCliLogRedeemedXmr(log)) {
+ const nextState: SwapStateXmrRedeemInMempool = {
+ type: SwapStateType.XMR_REDEEM_IN_MEMPOOL,
+ bobXmrRedeemTxId: log.fields.txid,
+ bobXmrRedeemAddress: log.fields.monero_receive_address,
+ };
+
+ slice.state = nextState;
+ } else if (isYouHaveBeenPunishedCliLog(log)) {
+ const nextState: SwapStateBtcPunished = {
+ type: SwapStateType.BTC_PUNISHED,
+ };
+
+ slice.state = nextState;
+ } else if (isCliLogAttemptingToCooperativelyRedeemXmr(log)) {
+ const nextState: SwapStateAttemptingCooperativeRedeeem = {
+ type: SwapStateType.ATTEMPTING_COOPERATIVE_REDEEM,
+ };
+
+ slice.state = nextState;
+ }
+ else if (
+ isCliLogReleasingSwapLockLog(log) &&
+ !action.payload.isFromRestore
+ ) {
+ const nextState: SwapStateProcessExited = {
+ type: SwapStateType.PROCESS_EXITED,
+ prevState: slice.state,
+ rpcError: null,
+ };
+
+ slice.state = nextState;
+ slice.processRunning = false;
+ } else if (isCliLogApiCallError(log) && !action.payload.isFromRestore) {
+ if (isSwapStateProcessExited(slice.state)) {
+ slice.state.rpcError = log.fields.err;
+ }
+ } else {
+ logger.debug({ log }, `Swap log was not reduced`);
+ }
+ });
+ },
+ swapReset() {
+ return initialState;
+ },
+ swapInitiate(
+ swap,
+ action: PayloadAction<{
+ provider: Provider | null;
+ spawnType: SwapSpawnType;
+ swapId: string | null;
+ }>,
+ ) {
+ const nextState: SwapStateInitiated = {
+ type: SwapStateType.INITIATED,
+ };
+
+ swap.processRunning = true;
+ swap.state = nextState;
+ swap.logs = [];
+ swap.provider = action.payload.provider;
+ swap.spawnType = action.payload.spawnType;
+ swap.swapId = action.payload.swapId;
+ },
+ swapProcessExited(swap, action: PayloadAction) {
+ if (!swap.processRunning) {
+ logger.warn(`swapProcessExited called on a swap that is not running`);
+ return;
+ }
+
+ const nextState: SwapStateProcessExited = {
+ type: SwapStateType.PROCESS_EXITED,
+ prevState: swap.state,
+ rpcError: action.payload,
+ };
+
+ swap.state = nextState;
+ swap.processRunning = false;
+ },
+ },
+});
+
+export const { swapInitiate, swapProcessExited, swapReset, swapAddLog } =
+ swapSlice.actions;
+
+export default swapSlice.reducer;
diff --git a/src/store/features/torSlice.ts b/src/store/features/torSlice.ts
new file mode 100644
index 00000000..74b535b3
--- /dev/null
+++ b/src/store/features/torSlice.ts
@@ -0,0 +1,74 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+
+export interface TorSlice {
+ exitCode: number | null;
+ processRunning: boolean;
+ stdOut: string;
+ proxyStatus:
+ | false
+ | {
+ proxyHostname: string;
+ proxyPort: number;
+ bootstrapped: boolean;
+ };
+}
+
+const initialState: TorSlice = {
+ processRunning: false,
+ exitCode: null,
+ stdOut: '',
+ proxyStatus: false,
+};
+
+const socksListenerRegex =
+ /Opened Socks listener connection.*on (\d+\.\d+\.\d+\.\d+):(\d+)/;
+const bootstrapDoneRegex = /Bootstrapped 100% \(done\)/;
+
+export const torSlice = createSlice({
+ name: 'tor',
+ initialState,
+ reducers: {
+ torAppendStdOut(slice, action: PayloadAction) {
+ slice.stdOut += action.payload;
+
+ const logs = slice.stdOut.split('\n');
+ logs.forEach((log) => {
+ if (socksListenerRegex.test(log)) {
+ const match = socksListenerRegex.exec(log);
+ if (match) {
+ slice.proxyStatus = {
+ proxyHostname: match[1],
+ proxyPort: Number.parseInt(match[2], 10),
+ bootstrapped: slice.proxyStatus
+ ? slice.proxyStatus.bootstrapped
+ : false,
+ };
+ }
+ } else if (bootstrapDoneRegex.test(log)) {
+ if (slice.proxyStatus) {
+ slice.proxyStatus.bootstrapped = true;
+ }
+ }
+ });
+ },
+ torInitiate(slice) {
+ slice.processRunning = true;
+ },
+ torProcessExited(
+ slice,
+ action: PayloadAction<{
+ exitCode: number | null;
+ exitSignal: NodeJS.Signals | null;
+ }>,
+ ) {
+ slice.processRunning = false;
+ slice.exitCode = action.payload.exitCode;
+ slice.proxyStatus = false;
+ },
+ },
+});
+
+export const { torAppendStdOut, torInitiate, torProcessExited } =
+ torSlice.actions;
+
+export default torSlice.reducer;
diff --git a/src/store/hooks.ts b/src/store/hooks.ts
new file mode 100644
index 00000000..df19f6e5
--- /dev/null
+++ b/src/store/hooks.ts
@@ -0,0 +1,56 @@
+import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
+import type { AppDispatch, RootState } from 'renderer/store/storeRenderer';
+import { sortBy } from 'lodash';
+import { parseDateString } from 'utils/parseUtils';
+
+// Use throughout your app instead of plain `useDispatch` and `useSelector`
+export const useAppDispatch = () => useDispatch();
+export const useAppSelector: TypedUseSelectorHook = useSelector;
+
+export function useResumeableSwapsCount() {
+ return useAppSelector(
+ (state) =>
+ Object.values(state.rpc.state.swapInfos).filter(
+ (swapInfo) => !swapInfo.completed,
+ ).length,
+ );
+}
+
+export function useIsSwapRunning() {
+ return useAppSelector((state) => state.swap.state !== null);
+}
+
+export function useSwapInfo(swapId: string | null) {
+ return useAppSelector((state) =>
+ swapId ? state.rpc.state.swapInfos[swapId] ?? null : null,
+ );
+}
+
+export function useActiveSwapId() {
+ return useAppSelector((s) => s.swap.swapId);
+}
+
+export function useActiveSwapInfo() {
+ const swapId = useActiveSwapId();
+ return useSwapInfo(swapId);
+}
+
+export function useIsRpcEndpointBusy(method: string) {
+ return useAppSelector((state) => state.rpc.busyEndpoints.includes(method));
+}
+
+export function useAllProviders() {
+ return useAppSelector((state) => {
+ const registryProviders = state.providers.registry.providers || [];
+ const listSellersProviders = state.providers.rendezvous.providers || [];
+ return [...registryProviders, ...listSellersProviders];
+ });
+}
+
+export function useSwapInfosSortedByDate() {
+ const swapInfos = useAppSelector((state) => state.rpc.state.swapInfos);
+ return sortBy(
+ Object.values(swapInfos),
+ (swap) => -parseDateString(swap.startDate),
+ );
+}
diff --git a/src/utils/conversionUtils.ts b/src/utils/conversionUtils.ts
new file mode 100644
index 00000000..f43dcc6c
--- /dev/null
+++ b/src/utils/conversionUtils.ts
@@ -0,0 +1,42 @@
+export function satsToBtc(sats: number): number {
+ return sats / 100000000;
+}
+
+export function btcToSats(btc: number): number {
+ return btc * 100000000;
+}
+
+export function piconerosToXmr(piconeros: number): number {
+ return piconeros / 1000000000000;
+}
+
+export function isXmrAddressValid(address: string, stagenet: boolean) {
+ const re = stagenet
+ ? '[57][0-9AB][1-9A-HJ-NP-Za-km-z]{93}'
+ : '[48][0-9AB][1-9A-HJ-NP-Za-km-z]{93}';
+ return new RegExp(`(?:^${re}$)`).test(address);
+}
+
+export function isBtcAddressValid(address: string, testnet: boolean) {
+ const re = testnet
+ ? '(tb1)[a-zA-HJ-NP-Z0-9]{25,49}'
+ : '(bc1)[a-zA-HJ-NP-Z0-9]{25,49}';
+ return new RegExp(`(?:^${re}$)`).test(address);
+}
+
+export function getBitcoinTxExplorerUrl(txid: string, testnet: boolean) {
+ return `https://blockchair.com/bitcoin${
+ testnet ? '/testnet' : ''
+ }/transaction/${txid}`;
+}
+
+export function getMoneroTxExplorerUrl(txid: string, stagenet: boolean) {
+ if (stagenet) {
+ return `https://stagenet.xmrchain.net/tx/${txid}`;
+ }
+ return `https://xmrchain.net/tx/${txid}`;
+}
+
+export function secondsToDays(seconds: number): number {
+ return seconds / 86400;
+}
diff --git a/src/utils/cryptoUtils.ts b/src/utils/cryptoUtils.ts
new file mode 100644
index 00000000..4cef19f5
--- /dev/null
+++ b/src/utils/cryptoUtils.ts
@@ -0,0 +1,5 @@
+import { createHash } from 'crypto';
+
+export function sha256(data: string): string {
+ return createHash('md5').update(data).digest('hex');
+}
diff --git a/src/utils/event.ts b/src/utils/event.ts
new file mode 100644
index 00000000..7aace594
--- /dev/null
+++ b/src/utils/event.ts
@@ -0,0 +1,21 @@
+export class SingleTypeEventEmitter {
+ private listeners: Array<(data: T) => void> = [];
+
+ // Method to add a listener for the event
+ on(listener: (data: T) => void) {
+ this.listeners.push(listener);
+ }
+
+ // Method to remove a listener
+ off(listener: (data: T) => void) {
+ const index = this.listeners.indexOf(listener);
+ if (index > -1) {
+ this.listeners.splice(index, 1);
+ }
+ }
+
+ // Method to emit the event
+ emit(data: T) {
+ this.listeners.forEach((listener) => listener(data));
+ }
+}
diff --git a/src/utils/logger.ts b/src/utils/logger.ts
new file mode 100644
index 00000000..36d35c49
--- /dev/null
+++ b/src/utils/logger.ts
@@ -0,0 +1,7 @@
+import pino from 'pino';
+
+export default pino(
+ {
+ level: 'trace',
+ }
+);
diff --git a/src/utils/multiAddrUtils.ts b/src/utils/multiAddrUtils.ts
new file mode 100644
index 00000000..71a36696
--- /dev/null
+++ b/src/utils/multiAddrUtils.ts
@@ -0,0 +1,24 @@
+import { Multiaddr } from 'multiaddr';
+import semver from 'semver';
+import { ExtendedProviderStatus, Provider } from 'models/apiModel';
+import { isTestnet } from 'store/config';
+
+const MIN_ASB_VERSION = '0.12.0';
+
+export function providerToConcatenatedMultiAddr(provider: Provider) {
+ return new Multiaddr(provider.multiAddr)
+ .encapsulate(`/p2p/${provider.peerId}`)
+ .toString();
+}
+
+export function isProviderCompatible(
+ provider: ExtendedProviderStatus,
+): boolean {
+ if (provider.version) {
+ if (!semver.satisfies(provider.version, `>=${MIN_ASB_VERSION}`))
+ return false;
+ }
+ if (provider.testnet !== isTestnet()) return false;
+
+ return true;
+}
diff --git a/src/utils/parseUtils.ts b/src/utils/parseUtils.ts
new file mode 100644
index 00000000..642ddf08
--- /dev/null
+++ b/src/utils/parseUtils.ts
@@ -0,0 +1,65 @@
+import { CliLog, isCliLog } from 'models/cliModel';
+
+/*
+Extract btc amount from string
+
+E.g: "0.00100000 BTC"
+Output: 0.001
+ */
+export function extractAmountFromUnitString(text: string): number | null {
+ if (text != null) {
+ const parts = text.split(' ');
+ if (parts.length === 2) {
+ const amount = Number.parseFloat(parts[0]);
+ return amount;
+ }
+ }
+ return null;
+}
+
+// E.g 2021-12-29 14:25:59.64082 +00:00:00
+export function parseDateString(str: string): number {
+ const parts = str.split(' ').slice(0, -1);
+ if (parts.length !== 2) {
+ throw new Error(
+ `Date string does not consist solely of date and time Str: ${str} Parts: ${parts}`,
+ );
+ }
+ const wholeString = parts.join(' ');
+ const date = Date.parse(wholeString);
+ if (Number.isNaN(date)) {
+ throw new Error(
+ `Date string could not be parsed Str: ${str} Parts: ${parts}`,
+ );
+ }
+ return date;
+}
+
+export function getLinesOfString(data: string): string[] {
+ return data
+ .toString()
+ .replace('\r\n', '\n')
+ .replace('\r', '\n')
+ .split('\n')
+ .filter((l) => l.length > 0);
+}
+
+export function getLogsAndStringsFromRawFileString(
+ rawFileData: string,
+): (CliLog | string)[] {
+ return getLinesOfString(rawFileData).map((line) => {
+ try {
+ return JSON.parse(line);
+ } catch (e) {
+ return line;
+ }
+ });
+}
+
+export function getLogsFromRawFileString(rawFileData: string): CliLog[] {
+ return getLogsAndStringsFromRawFileString(rawFileData).filter(isCliLog);
+}
+
+export function logsToRawString(logs: (CliLog | string)[]): string {
+ return logs.map((l) => JSON.stringify(l)).join('\n');
+}
diff --git a/src/utils/sortUtils.ts b/src/utils/sortUtils.ts
new file mode 100644
index 00000000..0b6185c0
--- /dev/null
+++ b/src/utils/sortUtils.ts
@@ -0,0 +1,19 @@
+import { ExtendedProviderStatus } from 'models/apiModel';
+
+export function sortProviderList(list: ExtendedProviderStatus[]) {
+ return list.concat().sort((firstEl, secondEl) => {
+ // If neither of them have a relevancy score, sort by max swap amount
+ if (firstEl.relevancy === undefined && secondEl.relevancy === undefined) {
+ if (firstEl.maxSwapAmount > secondEl.maxSwapAmount) {
+ return -1;
+ }
+ }
+ // If only on of the two don't have a relevancy score, prioritize the one that does
+ if (firstEl.relevancy === undefined) return 1;
+ if (secondEl.relevancy === undefined) return -1;
+ if (firstEl.relevancy > secondEl.relevancy) {
+ return -1;
+ }
+ return 1;
+ });
+}
diff --git a/src/utils/typescriptUtils.tsx b/src/utils/typescriptUtils.tsx
new file mode 100644
index 00000000..19c75b2d
--- /dev/null
+++ b/src/utils/typescriptUtils.tsx
@@ -0,0 +1,7 @@
+export function exhaustiveGuard(_value: never): never {
+ throw new Error(
+ `ERROR! Reached forbidden guard function with unexpected value: ${JSON.stringify(
+ _value,
+ )}`,
+ );
+}
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 00000000..ed772106
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1,2 @@
+///
+
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..13e18bc2
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": false,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noFallthroughCasesInSwitch": false,
+
+ /* Path Resolving */
+ "baseUrl": "./src",
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 00000000..42872c59
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 00000000..bd8f1c61
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,33 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import { internalIpV4 } from "internal-ip";
+import tsconfigPaths from 'vite-tsconfig-paths';
+
+// @ts-expect-error process is a nodejs global
+const mobile = !!/android|ios/.exec(process.env.TAURI_ENV_PLATFORM);
+
+// https://vitejs.dev/config/
+export default defineConfig(async () => ({
+ plugins: [react(), tsconfigPaths()],
+ // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
+ //
+ // 1. prevent vite from obscuring rust errors
+ clearScreen: false,
+ // 2. tauri expects a fixed port, fail if that port is not available
+ server: {
+ port: 1420,
+ strictPort: true,
+ host: mobile ? "0.0.0.0" : false,
+ hmr: mobile
+ ? {
+ protocol: "ws",
+ host: await internalIpV4(),
+ port: 1421,
+ }
+ : undefined,
+ watch: {
+ // 3. tell vite to ignore watching `src-tauri`
+ ignored: ["**/src-tauri/**"],
+ },
+ },
+}));
diff --git a/yarn.lock b/yarn.lock
new file mode 100644
index 00000000..2e39084e
--- /dev/null
+++ b/yarn.lock
@@ -0,0 +1,1749 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@ampproject/remapping@^2.2.0":
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4"
+ integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==
+ dependencies:
+ "@jridgewell/gen-mapping" "^0.3.5"
+ "@jridgewell/trace-mapping" "^0.3.24"
+
+"@babel/code-frame@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465"
+ integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==
+ dependencies:
+ "@babel/highlight" "^7.24.7"
+ picocolors "^1.0.0"
+
+"@babel/compat-data@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.7.tgz#d23bbea508c3883ba8251fb4164982c36ea577ed"
+ integrity sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==
+
+"@babel/core@^7.24.5":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.7.tgz#b676450141e0b52a3d43bc91da86aa608f950ac4"
+ integrity sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==
+ dependencies:
+ "@ampproject/remapping" "^2.2.0"
+ "@babel/code-frame" "^7.24.7"
+ "@babel/generator" "^7.24.7"
+ "@babel/helper-compilation-targets" "^7.24.7"
+ "@babel/helper-module-transforms" "^7.24.7"
+ "@babel/helpers" "^7.24.7"
+ "@babel/parser" "^7.24.7"
+ "@babel/template" "^7.24.7"
+ "@babel/traverse" "^7.24.7"
+ "@babel/types" "^7.24.7"
+ convert-source-map "^2.0.0"
+ debug "^4.1.0"
+ gensync "^1.0.0-beta.2"
+ json5 "^2.2.3"
+ semver "^6.3.1"
+
+"@babel/generator@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.7.tgz#1654d01de20ad66b4b4d99c135471bc654c55e6d"
+ integrity sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==
+ dependencies:
+ "@babel/types" "^7.24.7"
+ "@jridgewell/gen-mapping" "^0.3.5"
+ "@jridgewell/trace-mapping" "^0.3.25"
+ jsesc "^2.5.1"
+
+"@babel/helper-compilation-targets@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz#4eb6c4a80d6ffeac25ab8cd9a21b5dfa48d503a9"
+ integrity sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==
+ dependencies:
+ "@babel/compat-data" "^7.24.7"
+ "@babel/helper-validator-option" "^7.24.7"
+ browserslist "^4.22.2"
+ lru-cache "^5.1.1"
+ semver "^6.3.1"
+
+"@babel/helper-environment-visitor@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz#4b31ba9551d1f90781ba83491dd59cf9b269f7d9"
+ integrity sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==
+ dependencies:
+ "@babel/types" "^7.24.7"
+
+"@babel/helper-function-name@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz#75f1e1725742f39ac6584ee0b16d94513da38dd2"
+ integrity sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==
+ dependencies:
+ "@babel/template" "^7.24.7"
+ "@babel/types" "^7.24.7"
+
+"@babel/helper-hoist-variables@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz#b4ede1cde2fd89436397f30dc9376ee06b0f25ee"
+ integrity sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==
+ dependencies:
+ "@babel/types" "^7.24.7"
+
+"@babel/helper-module-imports@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b"
+ integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==
+ dependencies:
+ "@babel/traverse" "^7.24.7"
+ "@babel/types" "^7.24.7"
+
+"@babel/helper-module-transforms@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz#31b6c9a2930679498db65b685b1698bfd6c7daf8"
+ integrity sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==
+ dependencies:
+ "@babel/helper-environment-visitor" "^7.24.7"
+ "@babel/helper-module-imports" "^7.24.7"
+ "@babel/helper-simple-access" "^7.24.7"
+ "@babel/helper-split-export-declaration" "^7.24.7"
+ "@babel/helper-validator-identifier" "^7.24.7"
+
+"@babel/helper-plugin-utils@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz#98c84fe6fe3d0d3ae7bfc3a5e166a46844feb2a0"
+ integrity sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==
+
+"@babel/helper-simple-access@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3"
+ integrity sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==
+ dependencies:
+ "@babel/traverse" "^7.24.7"
+ "@babel/types" "^7.24.7"
+
+"@babel/helper-split-export-declaration@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz#83949436890e07fa3d6873c61a96e3bbf692d856"
+ integrity sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==
+ dependencies:
+ "@babel/types" "^7.24.7"
+
+"@babel/helper-string-parser@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz#4d2d0f14820ede3b9807ea5fc36dfc8cd7da07f2"
+ integrity sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==
+
+"@babel/helper-validator-identifier@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db"
+ integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==
+
+"@babel/helper-validator-option@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz#24c3bb77c7a425d1742eec8fb433b5a1b38e62f6"
+ integrity sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==
+
+"@babel/helpers@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.7.tgz#aa2ccda29f62185acb5d42fb4a3a1b1082107416"
+ integrity sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==
+ dependencies:
+ "@babel/template" "^7.24.7"
+ "@babel/types" "^7.24.7"
+
+"@babel/highlight@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d"
+ integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.24.7"
+ chalk "^2.4.2"
+ js-tokens "^4.0.0"
+ picocolors "^1.0.0"
+
+"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.7.tgz#9a5226f92f0c5c8ead550b750f5608e766c8ce85"
+ integrity sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==
+
+"@babel/plugin-transform-react-jsx-self@^7.24.5":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz#66bff0248ea0b549972e733516ffad577477bdab"
+ integrity sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.24.7"
+
+"@babel/plugin-transform-react-jsx-source@^7.24.1":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz#1198aab2548ad19582013815c938d3ebd8291ee3"
+ integrity sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.24.7"
+
+"@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12"
+ integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==
+ dependencies:
+ regenerator-runtime "^0.14.0"
+
+"@babel/template@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.7.tgz#02efcee317d0609d2c07117cb70ef8fb17ab7315"
+ integrity sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==
+ dependencies:
+ "@babel/code-frame" "^7.24.7"
+ "@babel/parser" "^7.24.7"
+ "@babel/types" "^7.24.7"
+
+"@babel/traverse@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.7.tgz#de2b900163fa741721ba382163fe46a936c40cf5"
+ integrity sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==
+ dependencies:
+ "@babel/code-frame" "^7.24.7"
+ "@babel/generator" "^7.24.7"
+ "@babel/helper-environment-visitor" "^7.24.7"
+ "@babel/helper-function-name" "^7.24.7"
+ "@babel/helper-hoist-variables" "^7.24.7"
+ "@babel/helper-split-export-declaration" "^7.24.7"
+ "@babel/parser" "^7.24.7"
+ "@babel/types" "^7.24.7"
+ debug "^4.3.1"
+ globals "^11.1.0"
+
+"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.7.tgz#6027fe12bc1aa724cd32ab113fb7f1988f1f66f2"
+ integrity sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==
+ dependencies:
+ "@babel/helper-string-parser" "^7.24.7"
+ "@babel/helper-validator-identifier" "^7.24.7"
+ to-fast-properties "^2.0.0"
+
+"@emotion/hash@^0.8.0":
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
+ integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
+
+"@esbuild/aix-ppc64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f"
+ integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==
+
+"@esbuild/android-arm64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052"
+ integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==
+
+"@esbuild/android-arm@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28"
+ integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==
+
+"@esbuild/android-x64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e"
+ integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==
+
+"@esbuild/darwin-arm64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a"
+ integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==
+
+"@esbuild/darwin-x64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22"
+ integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==
+
+"@esbuild/freebsd-arm64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e"
+ integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==
+
+"@esbuild/freebsd-x64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261"
+ integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==
+
+"@esbuild/linux-arm64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b"
+ integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==
+
+"@esbuild/linux-arm@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9"
+ integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==
+
+"@esbuild/linux-ia32@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2"
+ integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==
+
+"@esbuild/linux-loong64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df"
+ integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==
+
+"@esbuild/linux-mips64el@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe"
+ integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==
+
+"@esbuild/linux-ppc64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4"
+ integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==
+
+"@esbuild/linux-riscv64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc"
+ integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==
+
+"@esbuild/linux-s390x@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de"
+ integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==
+
+"@esbuild/linux-x64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0"
+ integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==
+
+"@esbuild/netbsd-x64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047"
+ integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==
+
+"@esbuild/openbsd-x64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70"
+ integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==
+
+"@esbuild/sunos-x64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b"
+ integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==
+
+"@esbuild/win32-arm64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d"
+ integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==
+
+"@esbuild/win32-ia32@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b"
+ integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==
+
+"@esbuild/win32-x64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c"
+ integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==
+
+"@jridgewell/gen-mapping@^0.3.5":
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36"
+ integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==
+ dependencies:
+ "@jridgewell/set-array" "^1.2.1"
+ "@jridgewell/sourcemap-codec" "^1.4.10"
+ "@jridgewell/trace-mapping" "^0.3.24"
+
+"@jridgewell/resolve-uri@^3.1.0":
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
+ integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
+
+"@jridgewell/set-array@^1.2.1":
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280"
+ integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==
+
+"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
+ version "1.4.15"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
+ integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
+
+"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
+ version "0.3.25"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0"
+ integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==
+ dependencies:
+ "@jridgewell/resolve-uri" "^3.1.0"
+ "@jridgewell/sourcemap-codec" "^1.4.14"
+
+"@material-ui/core@^4.12.4":
+ version "4.12.4"
+ resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.12.4.tgz#4ac17488e8fcaf55eb6a7f5efb2a131e10138a73"
+ integrity sha512-tr7xekNlM9LjA6pagJmL8QCgZXaubWUwkJnoYcMKd4gw/t4XiyvnTkjdGrUVicyB2BsdaAv1tvow45bPM4sSwQ==
+ dependencies:
+ "@babel/runtime" "^7.4.4"
+ "@material-ui/styles" "^4.11.5"
+ "@material-ui/system" "^4.12.2"
+ "@material-ui/types" "5.1.0"
+ "@material-ui/utils" "^4.11.3"
+ "@types/react-transition-group" "^4.2.0"
+ clsx "^1.0.4"
+ hoist-non-react-statics "^3.3.2"
+ popper.js "1.16.1-lts"
+ prop-types "^15.7.2"
+ react-is "^16.8.0 || ^17.0.0"
+ react-transition-group "^4.4.0"
+
+"@material-ui/icons@^4.11.3":
+ version "4.11.3"
+ resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-4.11.3.tgz#b0693709f9b161ce9ccde276a770d968484ecff1"
+ integrity sha512-IKHlyx6LDh8n19vzwH5RtHIOHl9Tu90aAAxcbWME6kp4dmvODM3UvOHJeMIDzUbd4muuJKHmlNoBN+mDY4XkBA==
+ dependencies:
+ "@babel/runtime" "^7.4.4"
+
+"@material-ui/lab@^4.0.0-alpha.61":
+ version "4.0.0-alpha.61"
+ resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.61.tgz#9bf8eb389c0c26c15e40933cc114d4ad85e3d978"
+ integrity sha512-rSzm+XKiNUjKegj8bzt5+pygZeckNLOr+IjykH8sYdVk7dE9y2ZuUSofiMV2bJk3qU+JHwexmw+q0RyNZB9ugg==
+ dependencies:
+ "@babel/runtime" "^7.4.4"
+ "@material-ui/utils" "^4.11.3"
+ clsx "^1.0.4"
+ prop-types "^15.7.2"
+ react-is "^16.8.0 || ^17.0.0"
+
+"@material-ui/styles@^4.11.5":
+ version "4.11.5"
+ resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.5.tgz#19f84457df3aafd956ac863dbe156b1d88e2bbfb"
+ integrity sha512-o/41ot5JJiUsIETME9wVLAJrmIWL3j0R0Bj2kCOLbSfqEkKf0fmaPt+5vtblUh5eXr2S+J/8J3DaCb10+CzPGA==
+ dependencies:
+ "@babel/runtime" "^7.4.4"
+ "@emotion/hash" "^0.8.0"
+ "@material-ui/types" "5.1.0"
+ "@material-ui/utils" "^4.11.3"
+ clsx "^1.0.4"
+ csstype "^2.5.2"
+ hoist-non-react-statics "^3.3.2"
+ jss "^10.5.1"
+ jss-plugin-camel-case "^10.5.1"
+ jss-plugin-default-unit "^10.5.1"
+ jss-plugin-global "^10.5.1"
+ jss-plugin-nested "^10.5.1"
+ jss-plugin-props-sort "^10.5.1"
+ jss-plugin-rule-value-function "^10.5.1"
+ jss-plugin-vendor-prefixer "^10.5.1"
+ prop-types "^15.7.2"
+
+"@material-ui/system@^4.12.2":
+ version "4.12.2"
+ resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.12.2.tgz#f5c389adf3fce4146edd489bf4082d461d86aa8b"
+ integrity sha512-6CSKu2MtmiJgcCGf6nBQpM8fLkuB9F55EKfbdTC80NND5wpTmKzwdhLYLH3zL4cLlK0gVaaltW7/wMuyTnN0Lw==
+ dependencies:
+ "@babel/runtime" "^7.4.4"
+ "@material-ui/utils" "^4.11.3"
+ csstype "^2.5.2"
+ prop-types "^15.7.2"
+
+"@material-ui/types@5.1.0":
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/@material-ui/types/-/types-5.1.0.tgz#efa1c7a0b0eaa4c7c87ac0390445f0f88b0d88f2"
+ integrity sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==
+
+"@material-ui/utils@^4.11.3":
+ version "4.11.3"
+ resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-4.11.3.tgz#232bd86c4ea81dab714f21edad70b7fdf0253942"
+ integrity sha512-ZuQPV4rBK/V1j2dIkSSEcH5uT6AaHuKWFfotADHsC0wVL1NLd2WkFCm4ZZbX33iO4ydl6V0GPngKm8HZQ2oujg==
+ dependencies:
+ "@babel/runtime" "^7.4.4"
+ prop-types "^15.7.2"
+ react-is "^16.8.0 || ^17.0.0"
+
+"@reduxjs/toolkit@^2.2.6":
+ version "2.2.6"
+ resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.2.6.tgz#4a8356dad9d0c1ab255607a555d492168e0e3bc1"
+ integrity sha512-kH0r495c5z1t0g796eDQAkYbEQ3a1OLYN9o8jQQVZyKyw367pfRGS+qZLkHYvFHiUUdafpoSlQ2QYObIApjPWA==
+ dependencies:
+ immer "^10.0.3"
+ redux "^5.0.1"
+ redux-thunk "^3.1.0"
+ reselect "^5.1.0"
+
+"@remix-run/router@1.17.1":
+ version "1.17.1"
+ resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.17.1.tgz#bf93997beb81863fde042ebd05013a2618471362"
+ integrity sha512-mCOMec4BKd6BRGBZeSnGiIgwsbLGp3yhVqAD8H+PxiRNEHgDpZb8J1TnrSDlg97t0ySKMQJTHCWBCmBpSmkF6Q==
+
+"@rollup/rollup-android-arm-eabi@4.18.0":
+ version "4.18.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz#bbd0e616b2078cd2d68afc9824d1fadb2f2ffd27"
+ integrity sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==
+
+"@rollup/rollup-android-arm64@4.18.0":
+ version "4.18.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz#97255ef6384c5f73f4800c0de91f5f6518e21203"
+ integrity sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==
+
+"@rollup/rollup-darwin-arm64@4.18.0":
+ version "4.18.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz#b6dd74e117510dfe94541646067b0545b42ff096"
+ integrity sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==
+
+"@rollup/rollup-darwin-x64@4.18.0":
+ version "4.18.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz#e07d76de1cec987673e7f3d48ccb8e106d42c05c"
+ integrity sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==
+
+"@rollup/rollup-linux-arm-gnueabihf@4.18.0":
+ version "4.18.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz#9f1a6d218b560c9d75185af4b8bb42f9f24736b8"
+ integrity sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==
+
+"@rollup/rollup-linux-arm-musleabihf@4.18.0":
+ version "4.18.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz#53618b92e6ffb642c7b620e6e528446511330549"
+ integrity sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==
+
+"@rollup/rollup-linux-arm64-gnu@4.18.0":
+ version "4.18.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz#99a7ba5e719d4f053761a698f7b52291cefba577"
+ integrity sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==
+
+"@rollup/rollup-linux-arm64-musl@4.18.0":
+ version "4.18.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz#f53db99a45d9bc00ce94db8a35efa7c3c144a58c"
+ integrity sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==
+
+"@rollup/rollup-linux-powerpc64le-gnu@4.18.0":
+ version "4.18.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz#cbb0837408fe081ce3435cf3730e090febafc9bf"
+ integrity sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==
+
+"@rollup/rollup-linux-riscv64-gnu@4.18.0":
+ version "4.18.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz#8ed09c1d1262ada4c38d791a28ae0fea28b80cc9"
+ integrity sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==
+
+"@rollup/rollup-linux-s390x-gnu@4.18.0":
+ version "4.18.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz#938138d3c8e0c96f022252a28441dcfb17afd7ec"
+ integrity sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==
+
+"@rollup/rollup-linux-x64-gnu@4.18.0":
+ version "4.18.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz#1a7481137a54740bee1ded4ae5752450f155d942"
+ integrity sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==
+
+"@rollup/rollup-linux-x64-musl@4.18.0":
+ version "4.18.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz#f1186afc601ac4f4fc25fac4ca15ecbee3a1874d"
+ integrity sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==
+
+"@rollup/rollup-win32-arm64-msvc@4.18.0":
+ version "4.18.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz#ed6603e93636a96203c6915be4117245c1bd2daf"
+ integrity sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==
+
+"@rollup/rollup-win32-ia32-msvc@4.18.0":
+ version "4.18.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz#14e0b404b1c25ebe6157a15edb9c46959ba74c54"
+ integrity sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==
+
+"@rollup/rollup-win32-x64-msvc@4.18.0":
+ version "4.18.0"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz#5d694d345ce36b6ecf657349e03eb87297e68da4"
+ integrity sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==
+
+"@tauri-apps/api@2.0.0-beta.14", "@tauri-apps/api@>=2.0.0-beta.0":
+ version "2.0.0-beta.14"
+ resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.0.0-beta.14.tgz#8c1c65c07559cd29c5103a99e0abe5331cc2246f"
+ integrity sha512-YLYgHqdwWswr4Y70+hRzaLD6kLIUgHhE3shLXNquPiTaQ9+cX3Q2dB0AFfqsua6NXYFNe7LfkmMzaqEzqv3yQg==
+
+"@tauri-apps/cli-darwin-arm64@2.0.0-beta.21":
+ version "2.0.0-beta.21"
+ resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-beta.21.tgz#9dc6f306b14d58b0b4fbf218ffbb31831e28cf4d"
+ integrity sha512-okI7PRSC6RO4JfrOTqu4oWf0IfBPbkGHisyDOTay6K5uhz4zzry5fFJVa8S/DTrKtdjau4vcik/EDCxiGRun9Q==
+
+"@tauri-apps/cli-darwin-x64@2.0.0-beta.21":
+ version "2.0.0-beta.21"
+ resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-beta.21.tgz#77a0bdd820301f120acbb93c57b6c8acb9ae4f82"
+ integrity sha512-mXoJDXB6CBoqUnFb4TCsSVC6FJRZsN1DHRZAyn6iNLIhOrObcM4L2xz8rzt3WirANwJ/ayrNv95fEt8Fq1jmgA==
+
+"@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-beta.21":
+ version "2.0.0-beta.21"
+ resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-beta.21.tgz#bc9214feff536d917d55bddeadb724555f9ac698"
+ integrity sha512-LYPOx3LE2eZ0g8Zh/HYaNg6B1pZzH4BPMcma7wGZ0XPu+4fKLLGgav13xP2lknLnxiRP9jJCaTIBKXgcQEtLyg==
+
+"@tauri-apps/cli-linux-arm64-gnu@2.0.0-beta.21":
+ version "2.0.0-beta.21"
+ resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-beta.21.tgz#69167099a4756944eb5d3d15905cbf4d903307ad"
+ integrity sha512-VP2L729tgY889OZj5U436EntjwkI8MyVB+GrvBv8k2mj1nWB651KiVIpcUmsUgjXZ2r01bifN9J0l+3EFEXUAQ==
+
+"@tauri-apps/cli-linux-arm64-musl@2.0.0-beta.21":
+ version "2.0.0-beta.21"
+ resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-beta.21.tgz#d66796e672c2606d2e08a232def55919a5fa9542"
+ integrity sha512-s1rV01RIdowlPHfw7hTBnCEm2C3mZbynF+xpyRSv9vSczu4dpfwILMRwxB4nzMzdJ7RPHsf/R+5Ww86e8QM4Gw==
+
+"@tauri-apps/cli-linux-x64-gnu@2.0.0-beta.21":
+ version "2.0.0-beta.21"
+ resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-beta.21.tgz#ed02923c94b71f2377ef5c4cc72bf1de12487296"
+ integrity sha512-yGh7ktUycHT3mAnKxC7cx/vjcbjJzoxQCxnjWpmIayVwq+iXLD1mK7nRXRdJpL/rnBFTqqD29CKuypCEFiq3/A==
+
+"@tauri-apps/cli-linux-x64-musl@2.0.0-beta.21":
+ version "2.0.0-beta.21"
+ resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-beta.21.tgz#511293e6508a5d41e758d6f0bf98e834b22c63cb"
+ integrity sha512-+79b8O3tsjbGR47pJtcSKGmtqj4rsSxB5AfMb4UCkmoNkbaOzB0YS/ZieUGAb+SHXZ/MMs7mcl96N9SqYOL7hw==
+
+"@tauri-apps/cli-win32-arm64-msvc@2.0.0-beta.21":
+ version "2.0.0-beta.21"
+ resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.0-beta.21.tgz#736c5dba48385bfebf030f4ad641592f0db14258"
+ integrity sha512-rKlpcjx6t1ECZciMmHT5xkXKjC+O+TVxRKmA21tEq/Ezt7XdnufGko1hduwQmVJWkHxKg6ab7uf98ImMpDC5UA==
+
+"@tauri-apps/cli-win32-ia32-msvc@2.0.0-beta.21":
+ version "2.0.0-beta.21"
+ resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-beta.21.tgz#bf0a8dbfc1d5b724fd9f1ed2db14817821bd9b43"
+ integrity sha512-ExdhvRfgAoZi4/7re6OkmfqsHvTJQgWouTNphHWRilUEqBM7TEQV1UxYtwWfgyOKelyx4cxUYDFAJxootTb2Nw==
+
+"@tauri-apps/cli-win32-x64-msvc@2.0.0-beta.21":
+ version "2.0.0-beta.21"
+ resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-beta.21.tgz#56842ab8088a794276cbf74bf0edcda6e96ee8ee"
+ integrity sha512-JtNTwNXIOfE04Cs3ieTvkdcMyJM9Sujw5MM9zNmusJKE03s/OLqbNK/2ISlcb/puwYGGPhhyYtL5hCmYXIrHHQ==
+
+"@tauri-apps/cli@>=2.0.0-beta.0":
+ version "2.0.0-beta.21"
+ resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-2.0.0-beta.21.tgz#aef1b9f5d80da38265820ff3ab8558724e3309eb"
+ integrity sha512-lqV4pD0iTs8ASd19slH0eRoVAjbxtD0cCsZFVD7kG4sYkeZ0IkvtxbvnHAOUbALfvnHZr1dVXFDVxQUqJK2OXw==
+ optionalDependencies:
+ "@tauri-apps/cli-darwin-arm64" "2.0.0-beta.21"
+ "@tauri-apps/cli-darwin-x64" "2.0.0-beta.21"
+ "@tauri-apps/cli-linux-arm-gnueabihf" "2.0.0-beta.21"
+ "@tauri-apps/cli-linux-arm64-gnu" "2.0.0-beta.21"
+ "@tauri-apps/cli-linux-arm64-musl" "2.0.0-beta.21"
+ "@tauri-apps/cli-linux-x64-gnu" "2.0.0-beta.21"
+ "@tauri-apps/cli-linux-x64-musl" "2.0.0-beta.21"
+ "@tauri-apps/cli-win32-arm64-msvc" "2.0.0-beta.21"
+ "@tauri-apps/cli-win32-ia32-msvc" "2.0.0-beta.21"
+ "@tauri-apps/cli-win32-x64-msvc" "2.0.0-beta.21"
+
+"@tauri-apps/plugin-shell@>=2.0.0-beta.0":
+ version "2.0.0-beta.7"
+ resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-beta.7.tgz#43159959ff8ef83435df6d64be381606f6e02130"
+ integrity sha512-oJxWbEiNRcoMM0PrePjJnjPHEAN1sbYuWaQ1QMtLPdjHsl83RLk+RpFzkL5WvtGknfiKY7T2qEthOID4br+mvg==
+ dependencies:
+ "@tauri-apps/api" "2.0.0-beta.14"
+
+"@types/babel__core@^7.20.5":
+ version "7.20.5"
+ resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017"
+ integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==
+ dependencies:
+ "@babel/parser" "^7.20.7"
+ "@babel/types" "^7.20.7"
+ "@types/babel__generator" "*"
+ "@types/babel__template" "*"
+ "@types/babel__traverse" "*"
+
+"@types/babel__generator@*":
+ version "7.6.8"
+ resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.8.tgz#f836c61f48b1346e7d2b0d93c6dacc5b9535d3ab"
+ integrity sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@types/babel__template@*":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f"
+ integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==
+ dependencies:
+ "@babel/parser" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@types/babel__traverse@*":
+ version "7.20.6"
+ resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.6.tgz#8dc9f0ae0f202c08d8d4dab648912c8d6038e3f7"
+ integrity sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==
+ dependencies:
+ "@babel/types" "^7.20.7"
+
+"@types/estree@1.0.5":
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
+ integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
+
+"@types/humanize-duration@^3.27.4":
+ version "3.27.4"
+ resolved "https://registry.yarnpkg.com/@types/humanize-duration/-/humanize-duration-3.27.4.tgz#51d6d278213374735440bc3749de920935e9127e"
+ integrity sha512-yaf7kan2Sq0goxpbcwTQ+8E9RP6HutFBPv74T/IA/ojcHKhuKVlk2YFYyHhWZeLvZPzzLE3aatuQB4h0iqyyUA==
+
+"@types/lodash@^4.17.6":
+ version "4.17.6"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.6.tgz#193ced6a40c8006cfc1ca3f4553444fb38f0e543"
+ integrity sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==
+
+"@types/node@^20.14.10":
+ version "20.14.10"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.10.tgz#a1a218290f1b6428682e3af044785e5874db469a"
+ integrity sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==
+ dependencies:
+ undici-types "~5.26.4"
+
+"@types/prop-types@*":
+ version "15.7.12"
+ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6"
+ integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==
+
+"@types/react-dom@^18.2.7":
+ version "18.3.0"
+ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0"
+ integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==
+ dependencies:
+ "@types/react" "*"
+
+"@types/react-transition-group@^4.2.0":
+ version "4.4.10"
+ resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.10.tgz#6ee71127bdab1f18f11ad8fb3322c6da27c327ac"
+ integrity sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==
+ dependencies:
+ "@types/react" "*"
+
+"@types/react@*", "@types/react@^18.2.15":
+ version "18.3.3"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.3.tgz#9679020895318b0915d7a3ab004d92d33375c45f"
+ integrity sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==
+ dependencies:
+ "@types/prop-types" "*"
+ csstype "^3.0.2"
+
+"@types/semver@^7.5.8":
+ version "7.5.8"
+ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e"
+ integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==
+
+"@types/use-sync-external-store@^0.0.3":
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43"
+ integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==
+
+"@vitejs/plugin-react@^4.2.1":
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz#d0be6594051ded8957df555ff07a991fb618b48e"
+ integrity sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==
+ dependencies:
+ "@babel/core" "^7.24.5"
+ "@babel/plugin-transform-react-jsx-self" "^7.24.5"
+ "@babel/plugin-transform-react-jsx-source" "^7.24.1"
+ "@types/babel__core" "^7.20.5"
+ react-refresh "^0.14.2"
+
+abort-controller@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
+ integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
+ dependencies:
+ event-target-shim "^5.0.0"
+
+ansi-styles@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+ integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+ dependencies:
+ color-convert "^1.9.0"
+
+atomic-sleep@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
+ integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
+
+base64-js@^1.3.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+ integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
+browserslist@^4.22.2:
+ version "4.23.1"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.1.tgz#ce4af0534b3d37db5c1a4ca98b9080f985041e96"
+ integrity sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==
+ dependencies:
+ caniuse-lite "^1.0.30001629"
+ electron-to-chromium "^1.4.796"
+ node-releases "^2.0.14"
+ update-browserslist-db "^1.0.16"
+
+buffer@^6.0.3:
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
+ integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
+ dependencies:
+ base64-js "^1.3.1"
+ ieee754 "^1.2.1"
+
+caniuse-lite@^1.0.30001629:
+ version "1.0.30001640"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz#32c467d4bf1f1a0faa63fc793c2ba81169e7652f"
+ integrity sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==
+
+chalk@^2.4.2:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+ integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
+clsx@^1.0.4, clsx@^1.1.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
+ integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
+
+color-convert@^1.9.0:
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+ integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+ dependencies:
+ color-name "1.1.3"
+
+color-name@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+ integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
+
+colorette@^2.0.7:
+ version "2.0.20"
+ resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
+ integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
+
+convert-source-map@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
+ integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
+
+cross-spawn@^7.0.3:
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
+ integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+ dependencies:
+ path-key "^3.1.0"
+ shebang-command "^2.0.0"
+ which "^2.0.1"
+
+css-vendor@^2.0.8:
+ version "2.0.8"
+ resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d"
+ integrity sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==
+ dependencies:
+ "@babel/runtime" "^7.8.3"
+ is-in-browser "^1.0.2"
+
+csstype@^2.5.2:
+ version "2.6.21"
+ resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.21.tgz#2efb85b7cc55c80017c66a5ad7cbd931fda3a90e"
+ integrity sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==
+
+csstype@^3.0.2:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
+ integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
+
+dateformat@^4.6.3:
+ version "4.6.3"
+ resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5"
+ integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==
+
+debug@^4.1.0, debug@^4.1.1, debug@^4.3.1:
+ version "4.3.5"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e"
+ integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==
+ dependencies:
+ ms "2.1.2"
+
+default-gateway@^6.0.3:
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71"
+ integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==
+ dependencies:
+ execa "^5.0.0"
+
+dns-over-http-resolver@^1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/dns-over-http-resolver/-/dns-over-http-resolver-1.2.3.tgz#194d5e140a42153f55bb79ac5a64dd2768c36af9"
+ integrity sha512-miDiVSI6KSNbi4SVifzO/reD8rMnxgrlnkrlkugOLQpWQTe2qMdHsZp5DmfKjxNE+/T3VAAYLQUZMv9SMr6+AA==
+ dependencies:
+ debug "^4.3.1"
+ native-fetch "^3.0.0"
+ receptacle "^1.3.2"
+
+dom-helpers@^5.0.1:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
+ integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
+ dependencies:
+ "@babel/runtime" "^7.8.7"
+ csstype "^3.0.2"
+
+electron-to-chromium@^1.4.796:
+ version "1.4.818"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.818.tgz#7762c8bfd15a07c3833b7f5deed990e9e5a4c24f"
+ integrity sha512-eGvIk2V0dGImV9gWLq8fDfTTsCAeMDwZqEPMr+jMInxZdnp9Us8UpovYpRCf9NQ7VOFgrN2doNSgvISbsbNpxA==
+
+end-of-stream@^1.1.0:
+ version "1.4.4"
+ resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
+ integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
+ dependencies:
+ once "^1.4.0"
+
+err-code@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/err-code/-/err-code-3.0.1.tgz#a444c7b992705f2b120ee320b09972eef331c920"
+ integrity sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==
+
+esbuild@^0.21.3:
+ version "0.21.5"
+ resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
+ integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==
+ optionalDependencies:
+ "@esbuild/aix-ppc64" "0.21.5"
+ "@esbuild/android-arm" "0.21.5"
+ "@esbuild/android-arm64" "0.21.5"
+ "@esbuild/android-x64" "0.21.5"
+ "@esbuild/darwin-arm64" "0.21.5"
+ "@esbuild/darwin-x64" "0.21.5"
+ "@esbuild/freebsd-arm64" "0.21.5"
+ "@esbuild/freebsd-x64" "0.21.5"
+ "@esbuild/linux-arm" "0.21.5"
+ "@esbuild/linux-arm64" "0.21.5"
+ "@esbuild/linux-ia32" "0.21.5"
+ "@esbuild/linux-loong64" "0.21.5"
+ "@esbuild/linux-mips64el" "0.21.5"
+ "@esbuild/linux-ppc64" "0.21.5"
+ "@esbuild/linux-riscv64" "0.21.5"
+ "@esbuild/linux-s390x" "0.21.5"
+ "@esbuild/linux-x64" "0.21.5"
+ "@esbuild/netbsd-x64" "0.21.5"
+ "@esbuild/openbsd-x64" "0.21.5"
+ "@esbuild/sunos-x64" "0.21.5"
+ "@esbuild/win32-arm64" "0.21.5"
+ "@esbuild/win32-ia32" "0.21.5"
+ "@esbuild/win32-x64" "0.21.5"
+
+escalade@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27"
+ integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==
+
+escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+ integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
+
+event-target-shim@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
+ integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
+
+events@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
+ integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
+
+execa@^5.0.0:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
+ integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
+ dependencies:
+ cross-spawn "^7.0.3"
+ get-stream "^6.0.0"
+ human-signals "^2.1.0"
+ is-stream "^2.0.0"
+ merge-stream "^2.0.0"
+ npm-run-path "^4.0.1"
+ onetime "^5.1.2"
+ signal-exit "^3.0.3"
+ strip-final-newline "^2.0.0"
+
+fast-copy@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.2.tgz#59c68f59ccbcac82050ba992e0d5c389097c9d35"
+ integrity sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==
+
+fast-redact@^3.1.1:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4"
+ integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==
+
+fast-safe-stringify@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
+ integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
+
+fsevents@~2.3.2, fsevents@~2.3.3:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+ integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
+gensync@^1.0.0-beta.2:
+ version "1.0.0-beta.2"
+ resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
+ integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
+
+get-stream@^6.0.0:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
+ integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
+
+globals@^11.1.0:
+ version "11.12.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
+ integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
+
+globrex@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098"
+ integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==
+
+goober@^2.0.33:
+ version "2.1.14"
+ resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.14.tgz#4a5c94fc34dc086a8e6035360ae1800005135acd"
+ integrity sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==
+
+has-flag@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+ integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
+
+help-me@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/help-me/-/help-me-5.0.0.tgz#b1ebe63b967b74060027c2ac61f9be12d354a6f6"
+ integrity sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==
+
+hoist-non-react-statics@^3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
+ integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
+ dependencies:
+ react-is "^16.7.0"
+
+human-signals@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
+ integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
+
+humanize-duration@^3.32.1:
+ version "3.32.1"
+ resolved "https://registry.yarnpkg.com/humanize-duration/-/humanize-duration-3.32.1.tgz#922beff5da36fb1cee3de26ada24c592b0fe519b"
+ integrity sha512-inh5wue5XdfObhu/IGEMiA1nUXigSGcaKNemcbLRKa7jXYGDZXr3LoT9pTIzq2hPEbld7w/qv9h+ikWGz8fL1g==
+
+hyphenate-style-name@^1.0.3:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz#1797bf50369588b47b72ca6d5e65374607cf4436"
+ integrity sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==
+
+ieee754@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+ integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+
+immer@^10.0.3:
+ version "10.1.1"
+ resolved "https://registry.yarnpkg.com/immer/-/immer-10.1.1.tgz#206f344ea372d8ea176891545ee53ccc062db7bc"
+ integrity sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==
+
+internal-ip@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-7.0.0.tgz#5b1c6a9d7e188aa73a1b69717daf50c8d8ed774f"
+ integrity sha512-qE4TeD4brqC45Vq/+VASeMiS1KRyfBkR6HT2sh9pZVVCzSjPkaCEfKFU+dL0PRv7NHJtvoKN2r82G6wTfzorkw==
+ dependencies:
+ default-gateway "^6.0.3"
+ ipaddr.js "^2.0.1"
+ is-ip "^3.1.0"
+ p-event "^4.2.0"
+
+ip-regex@^4.0.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5"
+ integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==
+
+ipaddr.js@^2.0.1:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8"
+ integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==
+
+is-in-browser@^1.0.2, is-in-browser@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835"
+ integrity sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==
+
+is-ip@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/is-ip/-/is-ip-3.1.0.tgz#2ae5ddfafaf05cb8008a62093cf29734f657c5d8"
+ integrity sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==
+ dependencies:
+ ip-regex "^4.0.0"
+
+is-stream@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
+ integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+ integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
+
+joycon@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
+ integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==
+
+"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+ integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+jsesc@^2.5.1:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
+ integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
+
+json5@^2.2.3:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
+ integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
+
+jss-plugin-camel-case@^10.5.1:
+ version "10.10.0"
+ resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.10.0.tgz#27ea159bab67eb4837fa0260204eb7925d4daa1c"
+ integrity sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==
+ dependencies:
+ "@babel/runtime" "^7.3.1"
+ hyphenate-style-name "^1.0.3"
+ jss "10.10.0"
+
+jss-plugin-default-unit@^10.5.1:
+ version "10.10.0"
+ resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.10.0.tgz#db3925cf6a07f8e1dd459549d9c8aadff9804293"
+ integrity sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ==
+ dependencies:
+ "@babel/runtime" "^7.3.1"
+ jss "10.10.0"
+
+jss-plugin-global@^10.5.1:
+ version "10.10.0"
+ resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.10.0.tgz#1c55d3c35821fab67a538a38918292fc9c567efd"
+ integrity sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A==
+ dependencies:
+ "@babel/runtime" "^7.3.1"
+ jss "10.10.0"
+
+jss-plugin-nested@^10.5.1:
+ version "10.10.0"
+ resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.10.0.tgz#db872ed8925688806e77f1fc87f6e62264513219"
+ integrity sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA==
+ dependencies:
+ "@babel/runtime" "^7.3.1"
+ jss "10.10.0"
+ tiny-warning "^1.0.2"
+
+jss-plugin-props-sort@^10.5.1:
+ version "10.10.0"
+ resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.10.0.tgz#67f4dd4c70830c126f4ec49b4b37ccddb680a5d7"
+ integrity sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg==
+ dependencies:
+ "@babel/runtime" "^7.3.1"
+ jss "10.10.0"
+
+jss-plugin-rule-value-function@^10.5.1:
+ version "10.10.0"
+ resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.10.0.tgz#7d99e3229e78a3712f78ba50ab342e881d26a24b"
+ integrity sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g==
+ dependencies:
+ "@babel/runtime" "^7.3.1"
+ jss "10.10.0"
+ tiny-warning "^1.0.2"
+
+jss-plugin-vendor-prefixer@^10.5.1:
+ version "10.10.0"
+ resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.10.0.tgz#c01428ef5a89f2b128ec0af87a314d0c767931c7"
+ integrity sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg==
+ dependencies:
+ "@babel/runtime" "^7.3.1"
+ css-vendor "^2.0.8"
+ jss "10.10.0"
+
+jss@10.10.0, jss@^10.5.1:
+ version "10.10.0"
+ resolved "https://registry.yarnpkg.com/jss/-/jss-10.10.0.tgz#a75cc85b0108c7ac8c7b7d296c520a3e4fbc6ccc"
+ integrity sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw==
+ dependencies:
+ "@babel/runtime" "^7.3.1"
+ csstype "^3.0.2"
+ is-in-browser "^1.1.3"
+ tiny-warning "^1.0.2"
+
+lodash@^4.17.21:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+loose-envify@^1.1.0, loose-envify@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
+ integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
+ dependencies:
+ js-tokens "^3.0.0 || ^4.0.0"
+
+lru-cache@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
+ integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
+ dependencies:
+ yallist "^3.0.2"
+
+merge-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+ integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
+
+mimic-fn@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
+ integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+
+minimist@^1.2.6:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
+ integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+
+ms@2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+ integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+ms@^2.1.1:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+ integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+multiaddr@^10.0.1:
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/multiaddr/-/multiaddr-10.0.1.tgz#0d15848871370860a4d266bb44d93b3dac5d90ef"
+ integrity sha512-G5upNcGzEGuTHkzxezPrrD6CaIHR9uo+7MwqhNVcXTs33IInon4y7nMiGxl2CY5hG7chvYQUQhz5V52/Qe3cbg==
+ dependencies:
+ dns-over-http-resolver "^1.2.3"
+ err-code "^3.0.1"
+ is-ip "^3.1.0"
+ multiformats "^9.4.5"
+ uint8arrays "^3.0.0"
+ varint "^6.0.0"
+
+multiformats@^9.4.2, multiformats@^9.4.5:
+ version "9.9.0"
+ resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37"
+ integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==
+
+nanoid@^3.3.7:
+ version "3.3.7"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
+ integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
+
+native-fetch@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/native-fetch/-/native-fetch-3.0.0.tgz#06ccdd70e79e171c365c75117959cf4fe14a09bb"
+ integrity sha512-G3Z7vx0IFb/FQ4JxvtqGABsOTIqRWvgQz6e+erkB+JJD6LrszQtMozEHI4EkmgZQvnGHrpLVzUWk7t4sJCIkVw==
+
+node-releases@^2.0.14:
+ version "2.0.14"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b"
+ integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==
+
+notistack@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/notistack/-/notistack-3.0.1.tgz#daf59888ab7e2c30a1fa8f71f9cba2978773236e"
+ integrity sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA==
+ dependencies:
+ clsx "^1.1.0"
+ goober "^2.0.33"
+
+npm-run-path@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
+ integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
+ dependencies:
+ path-key "^3.0.0"
+
+object-assign@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+ integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
+
+on-exit-leak-free@^2.1.0:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8"
+ integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==
+
+once@^1.3.1, once@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
+ dependencies:
+ wrappy "1"
+
+onetime@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
+ integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
+ dependencies:
+ mimic-fn "^2.1.0"
+
+p-event@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.2.0.tgz#af4b049c8acd91ae81083ebd1e6f5cae2044c1b5"
+ integrity sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==
+ dependencies:
+ p-timeout "^3.1.0"
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+ integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==
+
+p-timeout@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe"
+ integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==
+ dependencies:
+ p-finally "^1.0.0"
+
+path-key@^3.0.0, path-key@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+ integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
+picocolors@^1.0.0, picocolors@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1"
+ integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==
+
+pino-abstract-transport@^1.0.0, pino-abstract-transport@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz#97f9f2631931e242da531b5c66d3079c12c9d1b5"
+ integrity sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==
+ dependencies:
+ readable-stream "^4.0.0"
+ split2 "^4.0.0"
+
+pino-pretty@^11.2.1:
+ version "11.2.1"
+ resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-11.2.1.tgz#de9a42ff8ea7b26da93506bb9e49d0b566c5ae96"
+ integrity sha512-O05NuD9tkRasFRWVaF/uHLOvoRDFD7tb5VMertr78rbsYFjYp48Vg3477EshVAF5eZaEw+OpDl/tu+B0R5o+7g==
+ dependencies:
+ colorette "^2.0.7"
+ dateformat "^4.6.3"
+ fast-copy "^3.0.2"
+ fast-safe-stringify "^2.1.1"
+ help-me "^5.0.0"
+ joycon "^3.1.1"
+ minimist "^1.2.6"
+ on-exit-leak-free "^2.1.0"
+ pino-abstract-transport "^1.0.0"
+ pump "^3.0.0"
+ readable-stream "^4.0.0"
+ secure-json-parse "^2.4.0"
+ sonic-boom "^4.0.1"
+ strip-json-comments "^3.1.1"
+
+pino-std-serializers@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz#7c625038b13718dbbd84ab446bd673dc52259e3b"
+ integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==
+
+pino@^9.2.0:
+ version "9.2.0"
+ resolved "https://registry.yarnpkg.com/pino/-/pino-9.2.0.tgz#e77a9516f3a3e5550d9b76d9f65ac6118ef02bdd"
+ integrity sha512-g3/hpwfujK5a4oVbaefoJxezLzsDgLcNJeITvC6yrfwYeT9la+edCK42j5QpEQSQCZgTKapXvnQIdgZwvRaZug==
+ dependencies:
+ atomic-sleep "^1.0.0"
+ fast-redact "^3.1.1"
+ on-exit-leak-free "^2.1.0"
+ pino-abstract-transport "^1.2.0"
+ pino-std-serializers "^7.0.0"
+ process-warning "^3.0.0"
+ quick-format-unescaped "^4.0.3"
+ real-require "^0.2.0"
+ safe-stable-stringify "^2.3.1"
+ sonic-boom "^4.0.1"
+ thread-stream "^3.0.0"
+
+popper.js@1.16.1-lts:
+ version "1.16.1-lts"
+ resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1-lts.tgz#cf6847b807da3799d80ee3d6d2f90df8a3f50b05"
+ integrity sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==
+
+postcss@^8.4.39:
+ version "8.4.39"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.39.tgz#aa3c94998b61d3a9c259efa51db4b392e1bde0e3"
+ integrity sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==
+ dependencies:
+ nanoid "^3.3.7"
+ picocolors "^1.0.1"
+ source-map-js "^1.2.0"
+
+process-warning@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-3.0.0.tgz#96e5b88884187a1dce6f5c3166d611132058710b"
+ integrity sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==
+
+process@^0.11.10:
+ version "0.11.10"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+ integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
+
+prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
+ version "15.8.1"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
+ integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
+ dependencies:
+ loose-envify "^1.4.0"
+ object-assign "^4.1.1"
+ react-is "^16.13.1"
+
+pump@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
+ integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
+qr.js@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f"
+ integrity sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==
+
+quick-format-unescaped@^4.0.3:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7"
+ integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==
+
+react-dom@^18.2.0:
+ version "18.3.1"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"
+ integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
+ dependencies:
+ loose-envify "^1.1.0"
+ scheduler "^0.23.2"
+
+react-is@^16.13.1, react-is@^16.7.0:
+ version "16.13.1"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
+ integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+
+"react-is@^16.8.0 || ^17.0.0":
+ version "17.0.2"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
+ integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
+
+react-qr-code@^2.0.15:
+ version "2.0.15"
+ resolved "https://registry.yarnpkg.com/react-qr-code/-/react-qr-code-2.0.15.tgz#fbfc12952c504bcd64275647e9d1ea63251742ce"
+ integrity sha512-MkZcjEXqVKqXEIMVE0mbcGgDpkfSdd8zhuzXEl9QzYeNcw8Hq2oVIzDLWuZN2PQBwM5PWjc2S31K8Q1UbcFMfw==
+ dependencies:
+ prop-types "^15.8.1"
+ qr.js "0.0.0"
+
+react-redux@^9.1.2:
+ version "9.1.2"
+ resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.1.2.tgz#deba38c64c3403e9abd0c3fbeab69ffd9d8a7e4b"
+ integrity sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==
+ dependencies:
+ "@types/use-sync-external-store" "^0.0.3"
+ use-sync-external-store "^1.0.0"
+
+react-refresh@^0.14.2:
+ version "0.14.2"
+ resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9"
+ integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==
+
+react-router-dom@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.24.1.tgz#b1a22f7d6c5a1bfce30732bd370713f991ab4de4"
+ integrity sha512-U19KtXqooqw967Vw0Qcn5cOvrX5Ejo9ORmOtJMzYWtCT4/WOfFLIZGGsVLxcd9UkBO0mSTZtXqhZBsWlHr7+Sg==
+ dependencies:
+ "@remix-run/router" "1.17.1"
+ react-router "6.24.1"
+
+react-router@6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.24.1.tgz#5a3bbba0000afba68d42915456ca4c806f37a7de"
+ integrity sha512-PTXFXGK2pyXpHzVo3rR9H7ip4lSPZZc0bHG5CARmj65fTT6qG7sTngmb6lcYu1gf3y/8KxORoy9yn59pGpCnpg==
+ dependencies:
+ "@remix-run/router" "1.17.1"
+
+react-transition-group@^4.4.0:
+ version "4.4.5"
+ resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"
+ integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==
+ dependencies:
+ "@babel/runtime" "^7.5.5"
+ dom-helpers "^5.0.1"
+ loose-envify "^1.4.0"
+ prop-types "^15.6.2"
+
+react@^18.2.0:
+ version "18.3.1"
+ resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
+ integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
+ dependencies:
+ loose-envify "^1.1.0"
+
+readable-stream@^4.0.0:
+ version "4.5.2"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09"
+ integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==
+ dependencies:
+ abort-controller "^3.0.0"
+ buffer "^6.0.3"
+ events "^3.3.0"
+ process "^0.11.10"
+ string_decoder "^1.3.0"
+
+real-require@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78"
+ integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==
+
+receptacle@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/receptacle/-/receptacle-1.3.2.tgz#a7994c7efafc7a01d0e2041839dab6c4951360d2"
+ integrity sha512-HrsFvqZZheusncQRiEE7GatOAETrARKV/lnfYicIm8lbvp/JQOdADOfhjBd2DajvoszEyxSM6RlAAIZgEoeu/A==
+ dependencies:
+ ms "^2.1.1"
+
+redux-thunk@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3"
+ integrity sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==
+
+redux@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b"
+ integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==
+
+regenerator-runtime@^0.14.0:
+ version "0.14.1"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
+ integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
+
+reselect@^5.1.0:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e"
+ integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==
+
+rollup@^4.13.0:
+ version "4.18.0"
+ resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.18.0.tgz#497f60f0c5308e4602cf41136339fbf87d5f5dda"
+ integrity sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==
+ dependencies:
+ "@types/estree" "1.0.5"
+ optionalDependencies:
+ "@rollup/rollup-android-arm-eabi" "4.18.0"
+ "@rollup/rollup-android-arm64" "4.18.0"
+ "@rollup/rollup-darwin-arm64" "4.18.0"
+ "@rollup/rollup-darwin-x64" "4.18.0"
+ "@rollup/rollup-linux-arm-gnueabihf" "4.18.0"
+ "@rollup/rollup-linux-arm-musleabihf" "4.18.0"
+ "@rollup/rollup-linux-arm64-gnu" "4.18.0"
+ "@rollup/rollup-linux-arm64-musl" "4.18.0"
+ "@rollup/rollup-linux-powerpc64le-gnu" "4.18.0"
+ "@rollup/rollup-linux-riscv64-gnu" "4.18.0"
+ "@rollup/rollup-linux-s390x-gnu" "4.18.0"
+ "@rollup/rollup-linux-x64-gnu" "4.18.0"
+ "@rollup/rollup-linux-x64-musl" "4.18.0"
+ "@rollup/rollup-win32-arm64-msvc" "4.18.0"
+ "@rollup/rollup-win32-ia32-msvc" "4.18.0"
+ "@rollup/rollup-win32-x64-msvc" "4.18.0"
+ fsevents "~2.3.2"
+
+safe-buffer@~5.2.0:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+ integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+safe-stable-stringify@^2.3.1:
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886"
+ integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==
+
+scheduler@^0.23.2:
+ version "0.23.2"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
+ integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==
+ dependencies:
+ loose-envify "^1.1.0"
+
+secure-json-parse@^2.4.0:
+ version "2.7.0"
+ resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862"
+ integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==
+
+semver@^6.3.1:
+ version "6.3.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
+ integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
+
+semver@^7.6.2:
+ version "7.6.2"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13"
+ integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==
+
+shebang-command@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+ integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+ dependencies:
+ shebang-regex "^3.0.0"
+
+shebang-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+ integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+signal-exit@^3.0.3:
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
+ integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
+
+sonic-boom@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.0.1.tgz#515b7cef2c9290cb362c4536388ddeece07aed30"
+ integrity sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ==
+ dependencies:
+ atomic-sleep "^1.0.0"
+
+source-map-js@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
+ integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
+
+split2@^4.0.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4"
+ integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==
+
+string_decoder@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+ integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+ dependencies:
+ safe-buffer "~5.2.0"
+
+strip-final-newline@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
+ integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
+
+strip-json-comments@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
+ integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
+
+supports-color@^5.3.0:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+ integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+ dependencies:
+ has-flag "^3.0.0"
+
+thread-stream@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-3.1.0.tgz#4b2ef252a7c215064507d4ef70c05a5e2d34c4f1"
+ integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==
+ dependencies:
+ real-require "^0.2.0"
+
+tiny-warning@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
+ integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
+
+to-fast-properties@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
+ integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==
+
+tsconfck@^3.0.3:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.1.tgz#c7284913262c293b43b905b8b034f524de4a3162"
+ integrity sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==
+
+typescript@^5.2.2:
+ version "5.5.3"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.3.tgz#e1b0a3c394190838a0b168e771b0ad56a0af0faa"
+ integrity sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==
+
+uint8arrays@^3.0.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.1.1.tgz#2d8762acce159ccd9936057572dade9459f65ae0"
+ integrity sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==
+ dependencies:
+ multiformats "^9.4.2"
+
+undici-types@~5.26.4:
+ version "5.26.5"
+ resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
+ integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
+
+update-browserslist-db@^1.0.16:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e"
+ integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==
+ dependencies:
+ escalade "^3.1.2"
+ picocolors "^1.0.1"
+
+use-sync-external-store@^1.0.0:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9"
+ integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==
+
+varint@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/varint/-/varint-6.0.0.tgz#9881eb0ce8feaea6512439d19ddf84bf551661d0"
+ integrity sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==
+
+virtua@^0.33.2:
+ version "0.33.2"
+ resolved "https://registry.yarnpkg.com/virtua/-/virtua-0.33.2.tgz#b9596387bc77664293359d438319e81180a0e051"
+ integrity sha512-4NgtryQH/idQ3oKkwM6DRCoCsn+IrjrStGcDOARPdlY7zIg0AtTcUq24nysM8YyHoS6KhqcVe8A3+lHJidNQWA==
+
+vite-tsconfig-paths@^4.3.2:
+ version "4.3.2"
+ resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz#321f02e4b736a90ff62f9086467faf4e2da857a9"
+ integrity sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==
+ dependencies:
+ debug "^4.1.1"
+ globrex "^0.1.2"
+ tsconfck "^3.0.3"
+
+vite@^5.3.1:
+ version "5.3.3"
+ resolved "https://registry.yarnpkg.com/vite/-/vite-5.3.3.tgz#5265b1f0a825b3b6564c2d07524777c83e3c04c2"
+ integrity sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==
+ dependencies:
+ esbuild "^0.21.3"
+ postcss "^8.4.39"
+ rollup "^4.13.0"
+ optionalDependencies:
+ fsevents "~2.3.3"
+
+which@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+ integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+ dependencies:
+ isexe "^2.0.0"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+ integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
+
+yallist@^3.0.2:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
+ integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==