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} + + ) : ( + + } + > + 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} + + + + ); +} 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? + + + + + + 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 ( + + ); +} + +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} + /> + + + + + + { + 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)} + /> + ))} + + + + + + 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} + > + + + ))} + + + + + + + + + + ); +} 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. + + + + + + + + + + ); +} 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 ( + + ); +} 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 ? ( + + ) : ( + <> + + + + )} + + + + + + + + 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. + + + + + + + + + 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. + + + + + + + + + ); +} 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 ( + <> + + + + + + + + + ); +} 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 ( + + ); +}; + +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={ + + + + + + } + 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(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} + + + + + + + + )} + + ); +} 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]) => ( + + ))} + +
+ + + +
+ ); +} + +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 + + + <TextField + label="Send" + size="medium" + variant="outlined" + value={btcFieldValue} + onChange={onBtcAmountChange} + error={!!getBtcFieldError()} + helperText={getBtcFieldError()} + autoFocus + InputProps={{ + endAdornment: <InputAdornment position="end">BTC</InputAdornment>, + }} + /> + <Box className={classes.swapIconOuter}> + <ArrowDownwardIcon fontSize="small" /> + </Box> + <TextField + label="Receive" + variant="outlined" + size="medium" + value={xmrFieldValue.toFixed(6)} + InputProps={{ + endAdornment: <InputAdornment position="end">XMR</InputAdornment>, + }} + /> + <ProviderSelect /> + <Fab variant="extended" color="primary" onClick={handleGuideDialogOpen}> + <SwapHorizIcon className={classes.swapIcon} /> + Swap + </Fab> + <SwapDialog + open={showDialog || forceShowDialog} + onClose={() => setShowDialog(false)} + /> + </Box> + ); +} + +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 ? ( + <Alert severity="info"> + <Box className={classes.noProvidersAlertOuter}> + <Typography> + Currently, the public registry of providers seems to be unreachable. + Here's what you can do: + <ul> + <li> + Try discovering a provider by connecting to a rendezvous point + </li> + <li> + Try again later when the public registry may be reachable again + </li> + </ul> + </Typography> + <Box> + <ListSellersDialogOpenButton /> + </Box> + </Box> + </Alert> + ) : ( + <Alert severity="info"> + <Box className={classes.noProvidersAlertOuter}> + <Typography> + Currently, there are no providers (trading partners) available in the + official registry. Here's what you can do: + <ul> + <li> + Try discovering a provider by connecting to a rendezvous point + </li> + <li>Add a new provider to the public registry</li> + <li>Try again later when more providers may be available</li> + </ul> + </Typography> + <Box> + <ProviderSubmitDialogOpenButton /> + <ListSellersDialogOpenButton /> + </Box> + </Box> + </Alert> + ); + + return ( + <Box> + {alertBox} + <SwapDialog open={forceShowDialog} onClose={() => {}} /> + </Box> + ); +} + +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 + <Box className={classes.inner} component={Paper} elevation={15}> + <Title /> + <LinearProgress /> + </Box> + ); +} + +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 <ProviderLoadingSwapWidget />; + } + if (selectedProvider) { + return <HasProviderSwapWidget selectedProvider={selectedProvider} />; + } + return <HasNoProvidersSwapWidget />; +} 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 ( + <Box className={classes.outer}> + <Typography variant="h3">Wallet</Typography> + <Alert severity="info"> + You do not have to deposit money before starting a swap. Instead, you + will be greeted with a deposit address after you initiate one. + </Alert> + <Typography variant="subtitle1"> + 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. + </Typography> + <WithdrawWidget /> + </Box> + ); +} 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 ( + <IpcInvokeButton + loadIcon={<CircularProgress size={24} />} + size="small" + isIconButton + endIcon={<RefreshIcon />} + 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 ( + <> + <InfoBox + title={ + <Box className={classes.title}> + Wallet Balance + <WalletRefreshButton /> + </Box> + } + mainContent={ + <Typography variant="h5"> + <SatsAmount amount={walletBalance} /> + </Typography> + } + icon={<BitcoinIcon />} + additionalContent={ + <Button + variant="contained" + color="primary" + endIcon={<SendIcon />} + size="large" + onClick={onShowDialog} + disabled={ + walletBalance === null || checkingBalance || walletBalance <= 0 + } + > + Withdraw + </Button> + } + loading={false} + /> + <WithdrawDialog open={showDialog} onClose={() => 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 ( + <IconButton onClick={() => closeSnackbar(snackbarId)}> + <Close /> + </IconButton> + ); +} + +export default function GlobalSnackbarManager({ + children, +}: { + children: ReactNode; +}) { + return ( + <SnackbarProvider + action={(snackbarId) => <CloseSnackbarButton snackbarId={snackbarId} />} + Components={{ + success: StyledMaterialDesignContent, + error: StyledMaterialDesignContent, + default: StyledMaterialDesignContent, + info: StyledMaterialDesignContent, + warning: StyledMaterialDesignContent, + }} + > + {children} + </SnackbarProvider> + ); +} 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 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <link + rel="stylesheet" + href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" + /> + <style> + ::-webkit-scrollbar { + display: none; + } + *, *::after, *::before { + -webkit-user-select: none; + -webkit-user-drag: none; + -webkit-app-region: no-drag; + } + </style> + </head> + <body> + <div id="root"></div> + </body> +</html> 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( + <Provider store={store}> + <App /> + </Provider>, + 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<typeof store.getState>; 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<Alert[]>) { + slice.alerts = action.payload; + }, + removeAlert(slice, action: PayloadAction<number>) { + 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<ProviderStatus[]>, + ) { + 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<ExtendedProviderStatus[]>, + ) { + 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<number>) => { + state.btcPrice = action.payload; + }, + setXmrPrice: (state, action: PayloadAction<number>) => { + 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<number>) { + slice.state.balance = action.payload; + }, + rpcSetWithdrawTxId(slice, action: PayloadAction<string>) { + 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<GetSwapInfoResponse>) { + slice.state.swapInfos[action.payload.swapId] = action.payload; + }, + rpcSetEndpointBusy(slice, action: PayloadAction<string>) { + if (!slice.busyEndpoints.includes(action.payload)) { + slice.busyEndpoints.push(action.payload); + } + }, + rpcSetEndpointFree(slice, action: PayloadAction<string>) { + 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<string | null>) { + 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<string>) { + 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<AppDispatch>(); +export const useAppSelector: TypedUseSelectorHook<RootState> = 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<T> { + 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 @@ +/// <reference types="vite/client" /> + 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==