From ae4ee902d59ce0035516c1e4cbfe91ec2dbf04a0 Mon Sep 17 00:00:00 2001 From: Ekaterina Broslavskaya Date: Wed, 29 Oct 2025 17:04:57 +0300 Subject: [PATCH] Update frontend and prepare architecture to multi-steward support (#46) * start to update ui * remove websocket ui * refactor code after ws removing * update frontend * Unable real consensus result * update proposal ui * add current pending proposal to ui section * Refactor UI and backend for ban request feature - Updated `build.rs` to ensure proper protobuf compilation. - Removed `tracing-subscriber` dependency from `Cargo.toml` and `Cargo.lock`. - Refactored `main.rs` in the desktop UI to improve state management and UI interactions for group chat. - Enhanced `Gateway` and `User` structs to support sending ban requests and processing related events. - Updated UI components to reflect changes in proposal handling and improve user experience during voting and group management. - Added tests for new functionality and improved error handling across modules. * Add mls_crypto integration and group member management features - Introduced `mls_crypto` as a dependency for wallet address normalization. - Enhanced the desktop UI to support group member management, including requesting user bans. - Implemented new commands and events in the `Gateway` and `User` structs for retrieving group members and processing ban requests. - Updated the `User` struct to include methods for fetching group members and validating wallet addresses. - Refactored various components to improve state management and UI interactions related to group chat and member actions. - Added error handling for invalid wallet addresses and improved overall user experience in group management features. * Replace Apache License with a new version and add MIT License; update README to reflect new licensing information and enhance user module documentation. Refactor user module to improve group management, consensus handling, and messaging features, including ban request processing and proposal management. * Update dependencies and refactor package names for consistency * update ci * update ci * - Added `mls_crypto` as a dependency for wallet address normalization. - Introduced a new CSS file for styling the desktop UI, improving overall aesthetics and user experience. - Enhanced the `User` and `Gateway` structs to support group member management, including ban requests and proposal handling. - Implemented new commands and events for retrieving group members and processing ban requests. - Refactored various components to improve state management and UI interactions related to group chat and member actions. - Updated the `Cargo.toml` and `Cargo.lock` files to reflect new dependencies and configurations. - Added new profiles for development builds targeting WebAssembly, server, and Android environments. --- .dockerignore | 4 - .github/workflows/ci.yml | 14 +- .gitignore | 1 + Cargo.lock | 5349 ++++- Cargo.toml | 49 +- Dockerfile | 21 - LICENSE | 201 - LICENSE-APACHE | 203 + LICENSE-MIT | 25 + README.md | 140 +- apps/de_mls_desktop_ui/Cargo.toml | 32 + apps/de_mls_desktop_ui/assets/main.css | 619 + apps/de_mls_desktop_ui/src/logging.rs | 63 + apps/de_mls_desktop_ui/src/main.rs | 971 + build.rs | 2 +- crates/de_mls_gateway/Cargo.toml | 27 + crates/de_mls_gateway/src/forwarder.rs | 151 + crates/de_mls_gateway/src/group.rs | 281 + crates/de_mls_gateway/src/lib.rs | 138 + crates/de_mls_ui_protocol/Cargo.toml | 15 + crates/de_mls_ui_protocol/src/lib.rs | 127 + crates/ui_bridge/Cargo.toml | 15 + crates/ui_bridge/src/lib.rs | 201 + docker-compose.yml | 24 - ds/Cargo.toml | 4 +- ds/src/lib.rs | 1 + ds/src/topic_filter.rs | 52 + ds/src/waku_actor.rs | 14 +- ds/tests/ds_waku_test.rs | 4 +- frontend/.gitignore | 10 - .../.netlify/functions-internal/render.json | 1 - .../.netlify/functions-internal/render.mjs | 37 - .../immutable/assets/Toaster.d4bfa763.css | 1 - .../immutable/assets/_layout.ba8665a3.css | 3085 --- .../Toaster.svelte_svelte_type_style_lang.js | 242 - frontend/.netlify/server/chunks/index.js | 78 - frontend/.netlify/server/chunks/index2.js | 92 - frontend/.netlify/server/chunks/index3.js | 250 - frontend/.netlify/server/chunks/internal.js | 179 - .../.netlify/server/chunks/shared-server.js | 11 - .../server/entries/fallbacks/error.svelte.js | 30 - .../server/entries/pages/_layout.svelte.js | 289 - .../server/entries/pages/_page.svelte.js | 39 - .../.netlify/server/entries/pages/_page.ts.js | 19 - .../server/entries/pages/chat/_page.svelte.js | 128 - frontend/.netlify/server/index.js | 2674 --- frontend/.netlify/server/internal.js | 10 - frontend/.netlify/server/manifest-full.js | 35 - frontend/.netlify/server/manifest.js | 35 - frontend/.netlify/server/nodes/0.js | 8 - frontend/.netlify/server/nodes/1.js | 8 - frontend/.netlify/server/nodes/2.js | 10 - frontend/.netlify/server/nodes/3.js | 8 - frontend/.netlify/serverless.js | 361 - frontend/.netlify/shims.js | 17792 ---------------- frontend/.npmrc | 1 - frontend/Dockerfile | 11 - frontend/package-lock.json | 2656 --- frontend/package.json | 33 - frontend/postcss.config.cjs | 6 - frontend/src/app.css | 3 - frontend/src/app.d.ts | 12 - frontend/src/app.html | 12 - frontend/src/lib/stores/user.ts | 7 - frontend/src/routes/+layout.svelte | 11 - frontend/src/routes/+page.svelte | 99 - frontend/src/routes/+page.ts | 18 - frontend/src/routes/chat/+page.svelte | 197 - frontend/static/favicon.png | Bin 1571 -> 0 bytes frontend/svelte.config.js | 16 - frontend/tailwind.config.cjs | 8 - frontend/tsconfig.json | 21 - frontend/vite.config.ts | 6 - mls_crypto/Cargo.toml | 2 +- mls_crypto/src/error.rs | 2 + mls_crypto/src/identity.rs | 106 +- mls_crypto/src/lib.rs | 2 + src/action_handlers.rs | 157 - src/bootstrap.rs | 108 + src/consensus/mod.rs | 53 +- src/consensus/service.rs | 121 +- src/error.rs | 20 +- src/group.rs | 98 +- src/group_registry.rs | 31 + src/lib.rs | 56 +- src/main.rs | 267 - src/message.rs | 197 +- src/protos/messages/v1/application.proto | 17 +- src/protos/messages/v1/consensus.proto | 33 +- src/state_machine.rs | 72 +- src/steward.rs | 10 +- src/user.rs | 1666 -- src/user/README.md | 133 + src/user/consensus.rs | 355 + src/user/groups.rs | 184 + src/user/messaging.rs | 139 + src/user/mod.rs | 122 + src/user/proposals.rs | 146 + src/user/steward.rs | 279 + src/user/waku.rs | 304 + src/user_actor.rs | 62 +- src/user_app_instance.rs | 192 +- src/ws_actor.rs | 174 - tests/consensus_multi_group_test.rs | 14 +- tests/consensus_realtime_test.rs | 18 +- tests/state_machine_test.rs | 10 +- tests/user_test.rs | 26 +- 107 files changed, 9923 insertions(+), 32550 deletions(-) delete mode 100644 .dockerignore delete mode 100644 Dockerfile delete mode 100644 LICENSE create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 apps/de_mls_desktop_ui/Cargo.toml create mode 100644 apps/de_mls_desktop_ui/assets/main.css create mode 100644 apps/de_mls_desktop_ui/src/logging.rs create mode 100644 apps/de_mls_desktop_ui/src/main.rs create mode 100644 crates/de_mls_gateway/Cargo.toml create mode 100644 crates/de_mls_gateway/src/forwarder.rs create mode 100644 crates/de_mls_gateway/src/group.rs create mode 100644 crates/de_mls_gateway/src/lib.rs create mode 100644 crates/de_mls_ui_protocol/Cargo.toml create mode 100644 crates/de_mls_ui_protocol/src/lib.rs create mode 100644 crates/ui_bridge/Cargo.toml create mode 100644 crates/ui_bridge/src/lib.rs delete mode 100644 docker-compose.yml create mode 100644 ds/src/topic_filter.rs delete mode 100644 frontend/.gitignore delete mode 100644 frontend/.netlify/functions-internal/render.json delete mode 100644 frontend/.netlify/functions-internal/render.mjs delete mode 100644 frontend/.netlify/server/_app/immutable/assets/Toaster.d4bfa763.css delete mode 100644 frontend/.netlify/server/_app/immutable/assets/_layout.ba8665a3.css delete mode 100644 frontend/.netlify/server/chunks/Toaster.svelte_svelte_type_style_lang.js delete mode 100644 frontend/.netlify/server/chunks/index.js delete mode 100644 frontend/.netlify/server/chunks/index2.js delete mode 100644 frontend/.netlify/server/chunks/index3.js delete mode 100644 frontend/.netlify/server/chunks/internal.js delete mode 100644 frontend/.netlify/server/chunks/shared-server.js delete mode 100644 frontend/.netlify/server/entries/fallbacks/error.svelte.js delete mode 100644 frontend/.netlify/server/entries/pages/_layout.svelte.js delete mode 100644 frontend/.netlify/server/entries/pages/_page.svelte.js delete mode 100644 frontend/.netlify/server/entries/pages/_page.ts.js delete mode 100644 frontend/.netlify/server/entries/pages/chat/_page.svelte.js delete mode 100644 frontend/.netlify/server/index.js delete mode 100644 frontend/.netlify/server/internal.js delete mode 100644 frontend/.netlify/server/manifest-full.js delete mode 100644 frontend/.netlify/server/manifest.js delete mode 100644 frontend/.netlify/server/nodes/0.js delete mode 100644 frontend/.netlify/server/nodes/1.js delete mode 100644 frontend/.netlify/server/nodes/2.js delete mode 100644 frontend/.netlify/server/nodes/3.js delete mode 100644 frontend/.netlify/serverless.js delete mode 100644 frontend/.netlify/shims.js delete mode 100644 frontend/.npmrc delete mode 100644 frontend/Dockerfile delete mode 100644 frontend/package-lock.json delete mode 100644 frontend/package.json delete mode 100644 frontend/postcss.config.cjs delete mode 100644 frontend/src/app.css delete mode 100644 frontend/src/app.d.ts delete mode 100644 frontend/src/app.html delete mode 100644 frontend/src/lib/stores/user.ts delete mode 100644 frontend/src/routes/+layout.svelte delete mode 100644 frontend/src/routes/+page.svelte delete mode 100644 frontend/src/routes/+page.ts delete mode 100644 frontend/src/routes/chat/+page.svelte delete mode 100644 frontend/static/favicon.png delete mode 100644 frontend/svelte.config.js delete mode 100644 frontend/tailwind.config.cjs delete mode 100644 frontend/tsconfig.json delete mode 100644 frontend/vite.config.ts delete mode 100644 src/action_handlers.rs create mode 100644 src/bootstrap.rs create mode 100644 src/group_registry.rs delete mode 100644 src/main.rs delete mode 100644 src/user.rs create mode 100644 src/user/README.md create mode 100644 src/user/consensus.rs create mode 100644 src/user/groups.rs create mode 100644 src/user/messaging.rs create mode 100644 src/user/mod.rs create mode 100644 src/user/proposals.rs create mode 100644 src/user/steward.rs create mode 100644 src/user/waku.rs delete mode 100644 src/ws_actor.rs diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 6f811b8..0000000 --- a/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -.env/ -.idea/ -target/ -frontend/ \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a2cad66..d27bd2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,10 +27,22 @@ jobs: uses: taiki-e/install-action@v2 with: tool: protoc@${{ env.PROTOC_VERSION }} + - name: Install system dependencies + run: | + sudo apt update + sudo apt install libwebkit2gtk-4.1-dev \ + build-essential \ + curl \ + wget \ + file \ + libxdo-dev \ + libssl-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev - name: Check formatting run: cargo fmt --all --check - name: Run clippy - run: cargo clippy --all-features --tests -- -D warnings + run: cargo clippy -p de_mls_desktop_ui --all-features --tests -- -D warnings docs: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index a977741..84c4a19 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ target/ .DS_Store src/.DS_Store .idea +apps/de_mls_desktop_ui/logs # files *.env diff --git a/Cargo.lock b/Cargo.lock index 16af5c5..49d225a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -81,9 +72,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy" -version = "0.11.1" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d2cc5aeb8dfa1e451a49fac87bc4b86c5de40ebea153ed88e83eb92b8151e74" +checksum = "ae62e633fa48b4190af5e841eb05179841bb8b713945103291e2c0867037c0d1" dependencies = [ "alloy-consensus", "alloy-contract", @@ -100,13 +91,14 @@ dependencies = [ "alloy-signer-local", "alloy-transport", "alloy-transport-http", + "alloy-trie", ] [[package]] name = "alloy-chains" -version = "0.1.69" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28e2652684758b0d9b389d248b209ed9fd9989ef489a550265fe4bb8454fe7eb" +checksum = "0bbb778f50ecb0cebfb5c05580948501927508da7bd628833a8c4bd8545e23e2" dependencies = [ "alloy-primitives", "num_enum", @@ -115,27 +107,35 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "0.11.1" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e32ef5c74bbeb1733c37f4ac7f866f8c8af208b7b4265e21af609dcac5bd5e" +checksum = "b9b151e38e42f1586a01369ec52a6934702731d07e8509a7307331b09f6c46dc" dependencies = [ "alloy-eips", "alloy-primitives", "alloy-rlp", "alloy-serde", "alloy-trie", + "alloy-tx-macros", "auto_impl", "c-kzg", - "derive_more 1.0.0", + "derive_more 2.0.1", + "either", "k256 0.13.4", + "once_cell", + "rand 0.8.5", + "secp256k1 0.30.0", "serde", + "serde_json", + "serde_with", + "thiserror 2.0.17", ] [[package]] name = "alloy-consensus-any" -version = "0.11.1" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa13b7b1e1e3fedc42f0728103bfa3b4d566d3d42b606db449504d88dbdbdcf" +checksum = "6e2d5e8668ef6215efdb7dcca6f22277b4e483a5650e05f5de22b2350971f4b8" dependencies = [ "alloy-consensus", "alloy-eips", @@ -147,10 +147,11 @@ dependencies = [ [[package]] name = "alloy-contract" -version = "0.11.1" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6180fb232becdea70fad57c63b6967f01f74ab9595671b870f504116dd29de" +checksum = "630288cf4f3a34a8c6bc75c03dce1dbd47833138f65f37d53a1661eafc96b83f" dependencies = [ + "alloy-consensus", "alloy-dyn-abi", "alloy-json-abi", "alloy-network", @@ -162,14 +163,15 @@ dependencies = [ "alloy-transport", "futures", "futures-util", - "thiserror 2.0.16", + "serde_json", + "thiserror 2.0.17", ] [[package]] name = "alloy-core" -version = "0.8.25" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d8bcce99ad10fe02640cfaec1c6bc809b837c783c1d52906aa5af66e2a196f6" +checksum = "5ca96214615ec8cf3fa2a54b32f486eb49100ca7fe7eb0b8c1137cd316e7250a" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -180,38 +182,38 @@ dependencies = [ [[package]] name = "alloy-dyn-abi" -version = "0.8.25" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb8e762aefd39a397ff485bc86df673465c4ad3ec8819cc60833a8a3ba5cdc87" +checksum = "3fdff496dd4e98a81f4861e66f7eaf5f2488971848bb42d9c892f871730245c8" dependencies = [ "alloy-json-abi", "alloy-primitives", "alloy-sol-type-parser", "alloy-sol-types", - "const-hex", - "itoa", + "itoa 1.0.15", "serde", "serde_json", - "winnow", + "winnow 0.7.13", ] [[package]] name = "alloy-eip2124" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "675264c957689f0fd75f5993a73123c2cc3b5c235a38f5b9037fe6c826bfb2c0" +checksum = "741bdd7499908b3aa0b159bba11e71c8cddd009a2c2eb7a06e825f1ec87900a5" dependencies = [ "alloy-primitives", "alloy-rlp", "crc", - "thiserror 2.0.16", + "serde", + "thiserror 2.0.17", ] [[package]] name = "alloy-eip2930" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0069cf0642457f87a01a014f6dc29d5d893cd4fd8fddf0c3cdfad1bb3ebafc41" +checksum = "7b82752a889170df67bbb36d42ca63c531eb16274f0d7299ae2a680facba17bd" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -220,22 +222,22 @@ dependencies = [ [[package]] name = "alloy-eip7702" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b15b13d38b366d01e818fe8e710d4d702ef7499eacd44926a06171dd9585d0c" +checksum = "9d4769c6ffddca380b0070d71c8b7f30bed375543fe76bb2f74ec0acf4b7cd16" dependencies = [ "alloy-primitives", "alloy-rlp", "k256 0.13.4", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] name = "alloy-eips" -version = "0.11.1" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5591581ca2ab0b3e7226a4047f9a1bfcf431da1d0cce3752fda609fea3c27e37" +checksum = "e5434834adaf64fa20a6fb90877bc1d33214c41b055cc49f82189c98614368cc" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -245,30 +247,46 @@ dependencies = [ "alloy-serde", "auto_impl", "c-kzg", - "derive_more 1.0.0", - "once_cell", + "derive_more 2.0.1", + "either", "serde", + "serde_with", "sha2 0.10.9", + "thiserror 2.0.17", ] [[package]] name = "alloy-genesis" -version = "0.11.1" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cded3a2d4bd7173f696458c5d4c98c18a628dfcc9f194385e80a486e412e2e0" +checksum = "919a8471cfbed7bcd8cf1197a57dda583ce0e10c6385f6ff4e8b41304b223392" dependencies = [ "alloy-eips", "alloy-primitives", "alloy-serde", "alloy-trie", "serde", + "serde_with", +] + +[[package]] +name = "alloy-hardforks" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165210652f71dfc094b051602bafd691f506c54050a174b1cba18fb5ef706a3" +dependencies = [ + "alloy-chains", + "alloy-eip2124", + "alloy-primitives", + "auto_impl", + "dyn-clone", ] [[package]] name = "alloy-json-abi" -version = "0.8.25" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6beff64ad0aa6ad1019a3db26fef565aefeb011736150ab73ed3366c3cfd1b" +checksum = "5513d5e6bd1cba6bdcf5373470f559f320c05c8c59493b6e98912fbe6733943f" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -278,23 +296,24 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "0.11.1" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "762414662d793d7aaa36ee3af6928b6be23227df1681ce9c039f6f11daadef64" +checksum = "d7c69f6c9c68a1287c9d5ff903d0010726934de0dac10989be37b75a29190d55" dependencies = [ "alloy-primitives", "alloy-sol-types", + "http", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", ] [[package]] name = "alloy-network" -version = "0.11.1" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be03f2ebc00cf88bd06d3c6caf387dceaa9c7e6b268216779fa68a9bf8ab4e6" +checksum = "8eaf2ae05219e73e0979cb2cf55612aafbab191d130f203079805eaf881cca58" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -309,17 +328,18 @@ dependencies = [ "alloy-sol-types", "async-trait", "auto_impl", + "derive_more 2.0.1", "futures-utils-wasm", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] name = "alloy-network-primitives" -version = "0.11.1" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a00ce618ae2f78369918be0c20f620336381502c83b6ed62c2f7b2db27698b0" +checksum = "e58f4f345cef483eab7374f2b6056973c7419ffe8ad35e994b7a7f5d8e0c7ba4" dependencies = [ "alloy-consensus", "alloy-eips", @@ -330,11 +350,12 @@ dependencies = [ [[package]] name = "alloy-node-bindings" -version = "0.11.1" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81a3906afb50446392eb798dae4b918ba4ffcca47542efda7215776ddc8b5037" +checksum = "61321a0dbc084c2c9f2b07aa34f10db7ac80065c01721e567e5426d882c73de6" dependencies = [ "alloy-genesis", + "alloy-hardforks", "alloy-network", "alloy-primitives", "alloy-signer", @@ -343,31 +364,31 @@ dependencies = [ "rand 0.8.5", "serde_json", "tempfile", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "url", ] [[package]] name = "alloy-primitives" -version = "0.8.25" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c77490fe91a0ce933a1f219029521f20fc28c2c0ca95d53fa4da9c00b8d9d4e" +checksum = "355bf68a433e0fd7f7d33d5a9fc2583fde70bf5c530f63b80845f8da5505cf28" dependencies = [ "alloy-rlp", "bytes", "cfg-if", "const-hex", "derive_more 2.0.1", - "foldhash", - "hashbrown 0.15.5", - "indexmap", - "itoa", + "foldhash 0.2.0", + "hashbrown 0.16.0", + "indexmap 2.12.0", + "itoa 1.0.15", "k256 0.13.4", "keccak-asm", "paste", "proptest", - "rand 0.8.5", + "rand 0.9.2", "ruint", "rustc-hash 2.1.1", "serde", @@ -377,9 +398,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "0.11.1" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbe0a2acff0c4bd1669c71251ce10fc455cbffa1b4d0a817d5ea4ba7e5bb3db7" +checksum = "de2597751539b1cc8fe4204e5325f9a9ed83fcacfb212018dfcfa7877e76de21" dependencies = [ "alloy-chains", "alloy-consensus", @@ -392,22 +413,24 @@ dependencies = [ "alloy-rpc-client", "alloy-rpc-types-anvil", "alloy-rpc-types-eth", + "alloy-signer", "alloy-sol-types", "alloy-transport", "alloy-transport-http", "async-stream", "async-trait", "auto_impl", - "dashmap", + "dashmap 6.1.0", + "either", "futures", "futures-utils-wasm", "lru", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project", "reqwest", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "url", @@ -433,14 +456,14 @@ checksum = "64b728d511962dda67c1bc7ea7c03736ec275ed2cf4c35d9585298ac9ccf3b73" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] name = "alloy-rpc-client" -version = "0.11.1" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b37cc3c7883dc41be1b01460127ad7930466d0a4bb6ba15a02ee34d2745e2d7c" +checksum = "edf8eb8be597cfa8c312934d2566ec4516f066d69164f9212d7a148979fdcfd8" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -453,7 +476,7 @@ dependencies = [ "serde_json", "tokio", "tokio-stream", - "tower 0.5.2", + "tower", "tracing", "url", "wasmtimer", @@ -461,9 +484,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "0.11.1" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f18e68a3882f372e045ddc89eb455469347767d17878ca492cfbac81e71a111" +checksum = "339af7336571dd39ae3a15bde08ae6a647e62f75350bd415832640268af92c06" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -473,9 +496,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-anvil" -version = "0.11.1" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10d06300df4a87d960add35909240fc72da355dd2ac926fa6999f9efafbdc5a7" +checksum = "83d98fb386a462e143f5efa64350860af39950c49e7c0cbdba419c16793116ef" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -485,9 +508,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "0.11.1" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "318ae46dd12456df42527c3b94c1ae9001e1ceb707f7afe2c7807ac4e49ebad9" +checksum = "fbde0801a32d21c5f111f037bee7e22874836fba7add34ed4a6919932dd7cf23" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -496,9 +519,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "0.11.1" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b4dbee4d82f8a22dde18c28257bed759afeae7ba73da4a1479a039fd1445d04" +checksum = "361cd87ead4ba7659bda8127902eda92d17fa7ceb18aba1676f7be10f7222487" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -511,14 +534,15 @@ dependencies = [ "itertools 0.14.0", "serde", "serde_json", - "thiserror 2.0.16", + "serde_with", + "thiserror 2.0.17", ] [[package]] name = "alloy-serde" -version = "0.11.1" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8732058f5ca28c1d53d241e8504620b997ef670315d7c8afab856b3e3b80d945" +checksum = "64600fc6c312b7e0ba76f73a381059af044f4f21f43e07f51f1fa76c868fe302" dependencies = [ "alloy-primitives", "serde", @@ -527,9 +551,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "0.11.1" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f96b3526fdd779a4bd0f37319cfb4172db52a7ac24cdbb8804b72091c18e1701" +checksum = "5772858492b26f780468ae693405f895d6a27dea6e3eab2c36b6217de47c2647" dependencies = [ "alloy-primitives", "async-trait", @@ -537,14 +561,14 @@ dependencies = [ "either", "elliptic-curve 0.13.8", "k256 0.13.4", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] name = "alloy-signer-local" -version = "0.11.1" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe8f78cd6b7501c7e813a1eb4a087b72d23af51f5bb66d4e948dc840bdd207d8" +checksum = "f4195b803d0a992d8dbaab2ca1986fc86533d4bc80967c0cce7668b26ad99ef9" dependencies = [ "alloy-consensus", "alloy-network", @@ -553,97 +577,101 @@ dependencies = [ "async-trait", "k256 0.13.4", "rand 0.8.5", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] name = "alloy-sol-macro" -version = "0.8.25" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10ae8e9a91d328ae954c22542415303919aabe976fe7a92eb06db1b68fd59f2" +checksum = "f3ce480400051b5217f19d6e9a82d9010cdde20f1ae9c00d53591e4a1afbb312" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] name = "alloy-sol-macro-expander" -version = "0.8.25" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83ad5da86c127751bc607c174d6c9fe9b85ef0889a9ca0c641735d77d4f98f26" +checksum = "6d792e205ed3b72f795a8044c52877d2e6b6e9b1d13f431478121d8d4eaa9028" dependencies = [ "alloy-json-abi", "alloy-sol-macro-input", "const-hex", - "heck", - "indexmap", + "heck 0.5.0", + "indexmap 2.12.0", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", "syn-solidity", "tiny-keccak", ] [[package]] name = "alloy-sol-macro-input" -version = "0.8.25" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3d30f0d3f9ba3b7686f3ff1de9ee312647aac705604417a2f40c604f409a9e" +checksum = "0bd1247a8f90b465ef3f1207627547ec16940c35597875cdc09c49d58b19693c" dependencies = [ "alloy-json-abi", "const-hex", "dunce", - "heck", + "heck 0.5.0", "macro-string", "proc-macro2", "quote", "serde_json", - "syn 2.0.106", + "syn 2.0.108", "syn-solidity", ] [[package]] name = "alloy-sol-type-parser" -version = "0.8.25" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d162f8524adfdfb0e4bd0505c734c985f3e2474eb022af32eef0d52a4f3935c" +checksum = "954d1b2533b9b2c7959652df3076954ecb1122a28cc740aa84e7b0a49f6ac0a9" dependencies = [ "serde", - "winnow", + "winnow 0.7.13", ] [[package]] name = "alloy-sol-types" -version = "0.8.25" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d43d5e60466a440230c07761aa67671d4719d46f43be8ea6e7ed334d8db4a9ab" +checksum = "70319350969a3af119da6fb3e9bddb1bce66c9ea933600cb297c8b1850ad2a3c" dependencies = [ "alloy-json-abi", "alloy-primitives", "alloy-sol-macro", - "const-hex", "serde", ] [[package]] name = "alloy-transport" -version = "0.11.1" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a8d762eadce3e9b65eac09879430c6f4fce3736cac3cac123f9b1bf435ddd13" +checksum = "025a940182bddaeb594c26fe3728525ae262d0806fe6a4befdf5d7bc13d54bce" dependencies = [ "alloy-json-rpc", + "alloy-primitives", + "auto_impl", "base64 0.22.1", + "derive_more 2.0.1", + "futures", "futures-utils-wasm", + "parking_lot 0.12.5", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", - "tower 0.5.2", + "tower", "tracing", "url", "wasmtimer", @@ -651,29 +679,29 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "0.11.1" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20819c4cb978fb39ce6ac31991ba90f386d595f922f42ef888b4a18be190713e" +checksum = "e3b5064d1e1e1aabc918b5954e7fb8154c39e77ec6903a581b973198b26628fa" dependencies = [ "alloy-json-rpc", "alloy-transport", "reqwest", "serde_json", - "tower 0.5.2", + "tower", "tracing", "url", ] [[package]] name = "alloy-trie" -version = "0.7.9" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a94854e420f07e962f7807485856cde359ab99ab6413883e15235ad996e8b" +checksum = "e3412d52bb97c6c6cc27ccc28d4e6e8cf605469101193b50b0bd5813b1f990b5" dependencies = [ "alloy-primitives", "alloy-rlp", "arrayvec", - "derive_more 1.0.0", + "derive_more 2.0.1", "nybbles", "serde", "smallvec", @@ -681,10 +709,17 @@ dependencies = [ ] [[package]] -name = "android-tzdata" -version = "0.1.1" +name = "alloy-tx-macros" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +checksum = "f8e52276fdb553d3c11563afad2898f4085165e4093604afe3d78b69afbf408f" +dependencies = [ + "alloy-primitives", + "darling", + "proc-macro2", + "quote", + "syn 2.0.108", +] [[package]] name = "android_system_properties" @@ -695,61 +730,11 @@ dependencies = [ "libc", ] -[[package]] -name = "anstream" -version = "0.6.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" -dependencies = [ - "windows-sys 0.60.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.60.2", -] - [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "ark-bn254" @@ -794,7 +779,7 @@ checksum = "e7e89fe77d1f0f4fe5b96dfc940923d88d17b6a773808124f21e764dfb063c6a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -905,7 +890,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -943,7 +928,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -1004,7 +989,7 @@ dependencies = [ "ark-ff 0.5.0", "ark-std 0.5.0", "tracing", - "tracing-subscriber", + "tracing-subscriber 0.2.25", ] [[package]] @@ -1050,7 +1035,7 @@ checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -1111,6 +1096,123 @@ dependencies = [ "serde", ] +[[package]] +name = "ashpd" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.8.5", + "serde", + "serde_repr", + "tokio", + "url", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.2", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.2", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.2", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -1130,9 +1232,15 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -1141,7 +1249,30 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", ] [[package]] @@ -1158,7 +1289,7 @@ checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -1167,73 +1298,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "axum" -version = "0.6.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" -dependencies = [ - "async-trait", - "axum-core", - "base64 0.21.7", - "bitflags 1.3.2", - "bytes", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sha1", - "sync_wrapper 0.1.2", - "tokio", - "tokio-tungstenite", - "tower 0.4.13", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "mime", - "rustversion", - "tower-layer", - "tower-service", -] - -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - [[package]] name = "base-x" version = "0.2.11" @@ -1252,6 +1316,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base256emoji" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" +dependencies = [ + "const-str", + "match-lookup", +] + [[package]] name = "base64" version = "0.13.1" @@ -1337,9 +1411,12 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] [[package]] name = "bitvec" @@ -1362,6 +1439,12 @@ dependencies = [ "digest 0.10.7", ] +[[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.9.0" @@ -1381,10 +1464,41 @@ dependencies = [ ] [[package]] -name = "blst" -version = "0.3.15" +name = "block2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fd49896f12ac9b6dcd7a5998466b9b58263a695a3dd1ecc1aaca2e12a90b080" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.3", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "blst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" dependencies = [ "cc", "glob", @@ -1433,9 +1547,9 @@ dependencies = [ [[package]] name = "c-kzg" -version = "1.0.3" +version = "2.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0307f72feab3300336fb803a57134159f6e20139af1357f36c54cb90d8e8928" +checksum = "e00bf4b112b07b505472dbefd19e37e53307e2bfed5a79e0cc161d58ccd0e687" dependencies = [ "blst", "cc", @@ -1447,14 +1561,46 @@ dependencies = [ ] [[package]] -name = "cc" -version = "1.2.33" +name = "cairo-rs" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ + "bitflags 2.10.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[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 = "cc" +version = "1.2.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" +dependencies = [ + "find-msvc-tools", "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cexpr" version = "0.6.0" @@ -1465,10 +1611,37 @@ dependencies = [ ] [[package]] -name = "cfg-if" -version = "1.0.3" +name = "cfb" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" @@ -1496,18 +1669,45 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" version = "0.4.4" @@ -1527,26 +1727,107 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading", + "libloading 0.8.9", ] [[package]] -name = "colorchoice" -version = "1.0.4" +name = "cocoa" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation 0.1.2", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types 0.5.0", + "libc", + "objc", +] + +[[package]] +name = "cocoa" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c" +dependencies = [ + "bitflags 2.10.0", + "block", + "cocoa-foundation 0.2.1", + "core-foundation 0.10.1", + "core-graphics 0.24.0", + "foreign-types 0.5.0", + "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 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" +dependencies = [ + "bitflags 2.10.0", + "block", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "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 = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] [[package]] name = "const-hex" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dccd746bf9b1038c0507b7cec21eb2b11222db96a2902c96e8c185d6d20fb9c4" +checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" dependencies = [ "cfg-if", "cpufeatures", - "hex", "proptest", - "serde", + "serde_core", ] [[package]] @@ -1556,10 +1837,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] -name = "const_format" -version = "0.2.34" +name = "const-serialize" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +checksum = "08259976d62c715c4826cb4a3d64a3a9e5c5f68f964ff6087319857f569f93a6" +dependencies = [ + "const-serialize-macro", + "serde", +] + +[[package]] +name = "const-serialize-macro" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04382d0d9df7434af6b1b49ea1a026ef39df1b0738b1cc373368cf175354f6eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "const-str" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" + +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" dependencies = [ "const_format_proc_macros", ] @@ -1575,6 +1883,21 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1585,12 +1908,70 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[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 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "foreign-types 0.5.0", + "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 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", + "libc", +] + [[package]] name = "core2" version = "0.4.0" @@ -1639,6 +2020,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -1715,6 +2105,33 @@ dependencies = [ "subtle", ] +[[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.108", +] + [[package]] name = "ctr" version = "0.9.2" @@ -1748,7 +2165,56 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "serde", + "strsim", + "syn 2.0.108", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core 0.9.12", ] [[package]] @@ -1762,7 +2228,7 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core 0.9.11", + "parking_lot_core 0.9.12", ] [[package]] @@ -1788,25 +2254,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] -name = "de-mls" -version = "2.0.0" +name = "de_mls" +version = "2.1.0" dependencies = [ "alloy", "anyhow", - "axum", "bytes", "chrono", "ds", "ecies", - "env_logger", "futures", + "http", "kameo", "libsecp256k1", - "log", "mls_crypto", "openmls", "openmls_basic_credential", @@ -1822,12 +2286,71 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tokio-util", - "tower-http 0.4.4", + "tower-http", + "tower-layer", + "tracing", "uuid", "waku-bindings", "waku-sys", ] +[[package]] +name = "de_mls_desktop_ui" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "de_mls", + "de_mls_gateway", + "de_mls_ui_protocol", + "dioxus", + "dioxus-desktop", + "futures", + "hex", + "mls_crypto", + "once_cell", + "parking_lot 0.12.5", + "thiserror 2.0.17", + "tokio", + "tracing", + "tracing-appender", + "tracing-subscriber 0.3.20", + "ui_bridge", + "uuid", +] + +[[package]] +name = "de_mls_gateway" +version = "0.1.0" +dependencies = [ + "anyhow", + "de_mls", + "de_mls_ui_protocol", + "ds", + "futures", + "hex", + "kameo", + "mls_crypto", + "once_cell", + "parking_lot 0.12.5", + "tokio", + "tracing", + "tracing-subscriber 0.3.20", + "uuid", +] + +[[package]] +name = "de_mls_ui_protocol" +version = "0.1.0" +dependencies = [ + "de_mls", + "mls_crypto", + "serde", + "serde_json", + "thiserror 2.0.17", + "uuid", +] + [[package]] name = "der" version = "0.6.1" @@ -1849,6 +2372,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + [[package]] name = "derivative" version = "2.2.0" @@ -1862,11 +2395,15 @@ dependencies = [ [[package]] name = "derive_more" -version = "1.0.0" +version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ - "derive_more-impl 1.0.0", + "convert_case 0.4.0", + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "syn 2.0.108", ] [[package]] @@ -1875,18 +2412,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" dependencies = [ - "derive_more-impl 2.0.1", -] - -[[package]] -name = "derive_more-impl" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", + "derive_more-impl", ] [[package]] @@ -1897,7 +2423,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", "unicode-xid", ] @@ -1922,6 +2448,465 @@ dependencies = [ "subtle", ] +[[package]] +name = "dioxus" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a247114500f1a78e87022defa8173de847accfada8e8809dfae23a118a580c" +dependencies = [ + "dioxus-cli-config", + "dioxus-config-macro", + "dioxus-core", + "dioxus-core-macro", + "dioxus-desktop", + "dioxus-devtools", + "dioxus-document", + "dioxus-fullstack", + "dioxus-history", + "dioxus-hooks", + "dioxus-html", + "dioxus-logger", + "dioxus-router", + "dioxus-signals", + "dioxus-web", + "manganis", + "warnings", +] + +[[package]] +name = "dioxus-cli-config" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd16948f1ffdb068dd9a64812158073a4250e2af4e98ea31fdac0312e6bce86" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "dioxus-config-macro" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75cbf582fbb1c32d34a1042ea675469065574109c95154468710a4d73ee98b49" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "dioxus-core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c03f451a119e47433c16e2d8eb5b15bf7d6e6734eb1a4c47574e6711dadff8d" +dependencies = [ + "const_format", + "dioxus-core-types", + "futures-channel", + "futures-util", + "generational-box", + "longest-increasing-subsequence", + "rustc-hash 1.1.0", + "rustversion", + "serde", + "slab", + "slotmap", + "tracing", + "warnings", +] + +[[package]] +name = "dioxus-core-macro" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "105c954caaaedf8cd10f3d1ba576b01e18aa8d33ad435182125eefe488cf0064" +dependencies = [ + "convert_case 0.6.0", + "dioxus-rsx", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "dioxus-core-types" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91a82fccfa48574eb7aa183e297769540904694844598433a9eb55896ad9f93b" +dependencies = [ + "once_cell", +] + +[[package]] +name = "dioxus-desktop" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b0cca3e7a10a4a3df37ea52c4cc7a53e5c9233489e03ee3f2829471fc3099a" +dependencies = [ + "async-trait", + "base64 0.22.1", + "cocoa 0.25.0", + "core-foundation 0.9.4", + "dioxus-cli-config", + "dioxus-core", + "dioxus-devtools", + "dioxus-document", + "dioxus-history", + "dioxus-hooks", + "dioxus-html", + "dioxus-interpreter-js", + "dioxus-signals", + "dunce", + "futures-channel", + "futures-util", + "generational-box", + "global-hotkey", + "infer", + "jni", + "lazy-js-bundle", + "muda 0.11.5", + "ndk", + "ndk-context", + "ndk-sys", + "objc", + "objc_id", + "once_cell", + "rfd", + "rustc-hash 1.1.0", + "serde", + "serde_json", + "signal-hook", + "slab", + "tao", + "thiserror 1.0.69", + "tokio", + "tracing", + "tray-icon", + "urlencoding", + "webbrowser", + "wry", +] + +[[package]] +name = "dioxus-devtools" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712a7300f1e8181218187b03502044157eef04e0a25b518117c5ef9ae1096880" +dependencies = [ + "dioxus-core", + "dioxus-devtools-types", + "dioxus-signals", + "serde", + "serde_json", + "tracing", + "tungstenite", + "warnings", +] + +[[package]] +name = "dioxus-devtools-types" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62434973c0c9c5a3bc42e9cd5e7070401c2062a437fb5528f318c3e42ebf4ff" +dependencies = [ + "dioxus-core", + "serde", +] + +[[package]] +name = "dioxus-document" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "802a2014d1662b6615eec0a275745822ee4fc66aacd9d0f2fb33d6c8da79b8f2" +dependencies = [ + "dioxus-core", + "dioxus-core-macro", + "dioxus-core-types", + "dioxus-html", + "futures-channel", + "futures-util", + "generational-box", + "lazy-js-bundle", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "dioxus-fullstack" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe99b48a1348eec385b5c4bd3e80fd863b0d3b47257d34e2ddc58754dec5d128" +dependencies = [ + "base64 0.22.1", + "bytes", + "ciborium", + "dioxus-desktop", + "dioxus-devtools", + "dioxus-history", + "dioxus-lib", + "dioxus-web", + "dioxus_server_macro", + "futures-channel", + "futures-util", + "generational-box", + "once_cell", + "serde", + "server_fn", + "tracing", +] + +[[package]] +name = "dioxus-history" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ae4e22616c698f35b60727313134955d885de2d32e83689258e586ebc9b7909" +dependencies = [ + "dioxus-core", + "tracing", +] + +[[package]] +name = "dioxus-hooks" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "948e2b3f20d9d4b2c300aaa60281b1755f3298684448920b27106da5841896d0" +dependencies = [ + "dioxus-core", + "dioxus-signals", + "futures-channel", + "futures-util", + "generational-box", + "rustversion", + "slab", + "tracing", + "warnings", +] + +[[package]] +name = "dioxus-html" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59c9a40e6fee20ce7990095492dedb6a753eebe05e67d28271a249de74dc796d" +dependencies = [ + "async-trait", + "dioxus-core", + "dioxus-core-macro", + "dioxus-core-types", + "dioxus-hooks", + "dioxus-html-internal-macro", + "enumset", + "euclid", + "futures-channel", + "generational-box", + "keyboard-types", + "lazy-js-bundle", + "rustversion", + "serde", + "serde_json", + "serde_repr", + "tracing", +] + +[[package]] +name = "dioxus-html-internal-macro" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43ba87b53688a2c9f619ecdf4b3b955bc1f08bd0570a80a0d626c405f6d14a76" +dependencies = [ + "convert_case 0.6.0", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "dioxus-interpreter-js" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330707b10ca75cb0eb05f9e5f8d80217cd0d7e62116a8277ae363c1a09b57a22" +dependencies = [ + "dioxus-core", + "dioxus-core-types", + "dioxus-html", + "js-sys", + "lazy-js-bundle", + "rustc-hash 1.1.0", + "serde", + "sledgehammer_bindgen", + "sledgehammer_utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "dioxus-lib" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5405b71aa9b8b0c3e0d22728f12f34217ca5277792bd315878cc6ecab7301b72" +dependencies = [ + "dioxus-config-macro", + "dioxus-core", + "dioxus-core-macro", + "dioxus-document", + "dioxus-history", + "dioxus-hooks", + "dioxus-html", + "dioxus-rsx", + "dioxus-signals", + "warnings", +] + +[[package]] +name = "dioxus-logger" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545961e752f6c8bf59c274951b3c8b18a106db6ad2f9e2035b29e1f2a3e899b1" +dependencies = [ + "console_error_panic_hook", + "dioxus-cli-config", + "tracing", + "tracing-subscriber 0.3.20", + "tracing-wasm", +] + +[[package]] +name = "dioxus-router" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7266a76fc9e4a91f56499d1d1aecfff7168952b6627a6008b4e9748d6bf863e4" +dependencies = [ + "dioxus-cli-config", + "dioxus-history", + "dioxus-lib", + "dioxus-router-macro", + "rustversion", + "tracing", + "url", + "urlencoding", +] + +[[package]] +name = "dioxus-router-macro" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2743ffb79e9a7d33d779c87d6deea2a6c047d0736012f95d63b909b83f0a6fd2" +dependencies = [ + "proc-macro2", + "quote", + "slab", + "syn 2.0.108", +] + +[[package]] +name = "dioxus-rsx" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb588e05800b5a7eb90b2f40fca5bbd7626e823fb5e1ba21e011de649b45aa1" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "dioxus-signals" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10e032dbb3a2c0386ec8b8ee59bc20b5aeb67038147c855801237b45b13d72ac" +dependencies = [ + "dioxus-core", + "futures-channel", + "futures-util", + "generational-box", + "once_cell", + "parking_lot 0.12.5", + "rustc-hash 1.1.0", + "tracing", + "warnings", +] + +[[package]] +name = "dioxus-web" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7c12475c3d360058b8afe1b68eb6dfc9cbb7dcd760aed37c5f85c561c83ed1" +dependencies = [ + "async-trait", + "ciborium", + "dioxus-cli-config", + "dioxus-core", + "dioxus-core-types", + "dioxus-devtools", + "dioxus-document", + "dioxus-history", + "dioxus-html", + "dioxus-interpreter-js", + "dioxus-signals", + "futures-channel", + "futures-util", + "generational-box", + "js-sys", + "lazy-js-bundle", + "rustc-hash 1.1.0", + "serde", + "serde-wasm-bindgen", + "serde_json", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "dioxus_server_macro" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "371a5b21989a06b53c5092e977b3f75d0e60a65a4c15a2aa1d07014c3b2dda97" +dependencies = [ + "proc-macro2", + "quote", + "server_fn_macro", + "syn 2.0.108", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1930,9 +2915,38 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + [[package]] name = "ds" version = "1.0.0" @@ -1940,18 +2954,33 @@ dependencies = [ "anyhow", "bounded-vec-deque", "chrono", - "env_logger", "kameo", - "log", "serde", "serde_json", "thiserror 1.0.69", "tokio", + "tracing", + "tracing-subscriber 0.3.20", "uuid", "waku-bindings", "waku-sys", ] +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[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.5" @@ -1986,6 +3015,7 @@ dependencies = [ "digest 0.10.7", "elliptic-curve 0.13.8", "rfc6979 0.4.0", + "serdect", "signature 2.2.0", "spki 0.7.3", ] @@ -2001,7 +3031,7 @@ dependencies = [ "libsecp256k1", "once_cell", "openssl", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "rand_core 0.6.4", "sha2 0.10.9", "wasm-bindgen", @@ -2041,7 +3071,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -2049,6 +3079,9 @@ name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] [[package]] name = "elliptic-curve" @@ -2087,10 +3120,17 @@ dependencies = [ "pkcs8 0.10.2", "rand_core 0.6.4", "sec1 0.7.3", + "serdect", "subtle", "zeroize", ] +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + [[package]] name = "enr" version = "0.7.0" @@ -2113,45 +3153,64 @@ dependencies = [ [[package]] name = "enum-ordinalize" -version = "4.3.0" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" dependencies = [ "enum-ordinalize-derive", ] [[package]] name = "enum-ordinalize-derive" -version = "4.3.1" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] -name = "env_filter" -version = "0.1.3" +name = "enumflags2" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ - "log", - "regex", + "enumflags2_derive", + "serde", ] [[package]] -name = "env_logger" -version = "0.11.8" +name = "enumflags2_derive" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "enumset" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.108", ] [[package]] @@ -2162,12 +3221,43 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "euclid" +version = "0.22.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", ] [[package]] @@ -2198,6 +3288,15 @@ dependencies = [ "bytes", ] +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "ff" version = "0.12.1" @@ -2224,6 +3323,22 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version 0.4.1", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + [[package]] name = "fixed-hash" version = "0.8.0" @@ -2242,6 +3357,16 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2254,13 +3379,40 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared", + "foreign-types-shared 0.1.1", +] + +[[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 0.3.1", +] + +[[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.108", ] [[package]] @@ -2270,10 +3422,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] -name = "form_urlencoded" -version = "1.2.1" +name = "foreign-types-shared" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -2294,6 +3452,16 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[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" version = "0.3.31" @@ -2342,6 +3510,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -2350,7 +3531,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -2399,16 +3580,136 @@ dependencies = [ ] [[package]] -name = "generic-array" -version = "0.14.7" +name = "gdk" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generational-box" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a673cf4fb0ea6a91aa86c08695756dfe875277a912cdbf33db9a9f62d47ed82b" +dependencies = [ + "parking_lot 0.12.5", + "tracing", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", "zeroize", ] +[[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.16" @@ -2424,14 +3725,16 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", + "wasm-bindgen", ] [[package]] @@ -2445,10 +3748,83 @@ dependencies = [ ] [[package]] -name = "gimli" -version = "0.31.1" +name = "gio" +version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[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.10.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 1.0.69", +] + +[[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.108", +] + +[[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" @@ -2456,6 +3832,68 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "global-hotkey" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b436093d1598b05e3b7fddc097b2bad32763f53a1beb25ab6f9718c6a60acd09" +dependencies = [ + "bitflags 2.10.0", + "cocoa 0.25.0", + "crossbeam-channel", + "keyboard-types", + "objc", + "once_cell", + "thiserror 1.0.69", + "windows-sys 0.52.0", + "x11-dl", +] + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[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 = "group" version = "0.12.1" @@ -2478,6 +3916,75 @@ dependencies = [ "subtle", ] +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.1.3", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[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" @@ -2492,10 +3999,25 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +dependencies = [ + "foldhash 0.2.0", "serde", ] +[[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" @@ -2513,9 +4035,6 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -dependencies = [ - "serde", -] [[package]] name = "hex-conservative" @@ -2567,11 +4086,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2615,14 +4134,17 @@ dependencies = [ ] [[package]] -name = "http" -version = "0.2.12" +name = "html5ever" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" dependencies = [ - "bytes", - "fnv", - "itoa", + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -2633,18 +4155,7 @@ checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", + "itoa 1.0.15", ] [[package]] @@ -2654,7 +4165,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.3.1", + "http", ] [[package]] @@ -2665,52 +4176,17 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] -[[package]] -name = "http-range-header" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" - [[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "hyper" version = "1.7.0" @@ -2721,10 +4197,10 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "httparse", - "itoa", + "itoa 1.0.15", "pin-project-lite", "pin-utils", "smallvec", @@ -2733,40 +4209,41 @@ dependencies = [ ] [[package]] -name = "hyper-tls" -version = "0.6.0" +name = "hyper-rustls" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "bytes", - "http-body-util", - "hyper 1.7.0", + "http", + "hyper", "hyper-util", - "native-tls", + "rustls", + "rustls-pki-types", "tokio", - "tokio-native-tls", + "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", "futures-core", "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "hyper 1.7.0", + "http", + "http-body", + "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2", "tokio", "tower-service", "tracing", @@ -2774,9 +4251,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2784,7 +4261,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -2883,10 +4360,16 @@ dependencies = [ ] [[package]] -name = "idna" -version = "1.0.3" +name = "ident_case" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -2920,18 +4403,39 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] name = "indexmap" -version = "2.10.0" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.0", "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6c16b11a665b26aeeb9b1d7f954cdeb034be38dd00adab4f2ae921a8fee804" +dependencies = [ + "cfb", ] [[package]] @@ -2952,17 +4456,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "io-uring" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" -dependencies = [ - "bitflags 2.9.2", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" version = "2.11.0" @@ -2979,12 +4472,6 @@ dependencies = [ "serde", ] -[[package]] -name = "is_terminal_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" - [[package]] name = "itertools" version = "0.10.5" @@ -3012,6 +4499,12 @@ dependencies = [ "either", ] +[[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.15" @@ -3019,34 +4512,55 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] -name = "jiff" -version = "0.2.15" +name = "javascriptcore-rs" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde", + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", ] [[package]] -name = "jiff-static" -version = "0.2.15" +name = "javascriptcore-rs-sys" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", + "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 1.0.69", + "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.77" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -3074,6 +4588,7 @@ dependencies = [ "ecdsa 0.16.9", "elliptic-curve 0.13.8", "once_cell", + "serdect", "sha2 0.10.9", ] @@ -3100,10 +4615,10 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bbbd8e8d7b02bc67eae0dcbdb82c0a71cc7cc61734059ee3e7439a1ee1e0e85" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", "uuid", ] @@ -3126,6 +4641,36 @@ dependencies = [ "sha3-asm", ] +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.10.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-js-bundle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e49596223b9d9d4947a14a25c142a6e7d8ab3f27eb3ade269d238bb8b5c267e2" + [[package]] name = "lazy_static" version = "1.5.0" @@ -3139,19 +4684,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] -name = "libc" -version = "0.2.175" +name = "libappindicator" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +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 0.7.4", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" -version = "0.8.8" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ "cfg-if", - "windows-targets 0.53.3", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", ] [[package]] @@ -3160,6 +4739,16 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.10.0", + "libc", +] + [[package]] name = "libsecp256k1" version = "0.7.2" @@ -3208,6 +4797,25 @@ dependencies = [ "libsecp256k1-core", ] +[[package]] +name = "libxdo" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" +dependencies = [ + "libxdo-sys", +] + +[[package]] +name = "libxdo-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" +dependencies = [ + "libc", + "x11", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -3216,9 +4824,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" @@ -3228,19 +4836,24 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "longest-increasing-subsequence" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bd0dd2cd90571056fdb71f6275fada10131182f84899f4b2a916e565d81d86" [[package]] name = "lru" @@ -3251,6 +4864,18 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + [[package]] name = "macro-string" version = "0.1.4" @@ -3259,20 +4884,108 @@ checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] -name = "matchit" -version = "0.7.3" +name = "malloc_buf" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "manganis" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317af44b15e7605b85f04525449a3bb631753040156c9b318e6cba8a3ea4ef73" +dependencies = [ + "const-serialize", + "manganis-core", + "manganis-macro", +] + +[[package]] +name = "manganis-core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c38bee65cc725b2bba23b5dbb290f57c8be8fadbe2043fb7e2ce73022ea06519" +dependencies = [ + "const-serialize", + "dioxus-cli-config", + "dioxus-core-types", + "serde", +] + +[[package]] +name = "manganis-macro" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f4f71310913c40174d9f0cfcbcb127dad0329ecdb3945678a120db22d3d065" +dependencies = [ + "dunce", + "manganis-core", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[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 = "match-lookup" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1265724d8cb29dbbc2b0f06fffb8bf1a8c0cf73a78eede9ba73a4a66c52a981e" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[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.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] [[package]] name = "merlin" @@ -3292,6 +5005,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3305,17 +5028,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3331,6 +5055,44 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "muda" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c47e7625990fc1af2226ea4f34fb2412b03c12639fcb91868581eb3a6893453" +dependencies = [ + "cocoa 0.25.0", + "crossbeam-channel", + "gtk", + "keyboard-types", + "libxdo", + "objc", + "once_cell", + "png", + "thiserror 1.0.69", + "windows-sys 0.52.0", +] + +[[package]] +name = "muda" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdae9c00e61cc0579bcac625e8ad22104c60548a025bfc972dc83868a28e1484" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "libxdo", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "once_cell", + "png", + "thiserror 1.0.69", + "windows-sys 0.59.0", +] + [[package]] name = "multiaddr" version = "0.17.1" @@ -3352,11 +5114,12 @@ dependencies = [ [[package]] name = "multibase" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404" +checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" dependencies = [ "base-x", + "base256emoji", "data-encoding", "data-encoding-macro", ] @@ -3393,22 +5156,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] -name = "native-tls" -version = "0.2.14" +name = "ndk" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "libc", + "bitflags 2.10.0", + "jni-sys", "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", + "ndk-sys", + "num_enum", + "raw-window-handle 0.6.2", + "thiserror 1.0.69", ] +[[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.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +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 = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + [[package]] name = "nom" version = "7.1.3" @@ -3419,6 +5220,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -3429,6 +5239,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -3460,9 +5276,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ "num_enum_derive", "rustversion", @@ -3470,35 +5286,220 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ + "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] name = "nybbles" -version = "0.3.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8983bb634df7248924ee0c4c3a749609b5abcb082c28fffe3254b3eb3602b307" +checksum = "2c4b5ecbd0beec843101bffe848217f770e8b8da81d8355b7d6e226f2199b3dc" dependencies = [ "alloy-rlp", - "const-hex", + "cfg-if", "proptest", + "ruint", "serde", "smallvec", ] [[package]] -name = "object" -version = "0.36.7" +name = "objc" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ - "memchr", + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[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" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "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.10.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.10.0", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "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]] @@ -3511,12 +5512,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "once_cell_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" - [[package]] name = "opaque-debug" version = "0.3.1" @@ -3601,13 +5596,13 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.73" +version = "0.10.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", "cfg-if", - "foreign-types", + "foreign-types 0.3.2", "libc", "once_cell", "openssl-macros", @@ -3622,20 +5617,14 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - [[package]] name = "openssl-sys" -version = "0.9.109" +version = "0.9.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" dependencies = [ "cc", "libc", @@ -3643,6 +5632,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-float" version = "2.10.1" @@ -3652,6 +5647,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "p256" version = "0.13.2" @@ -3674,6 +5679,31 @@ dependencies = [ "primeorder", ] +[[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 = "parity-scale-codec" version = "3.7.5" @@ -3696,12 +5726,18 @@ version = "3.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" dependencies = [ - "proc-macro-crate 3.3.0", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.11.2" @@ -3715,12 +5751,12 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", - "parking_lot_core 0.9.11", + "parking_lot_core 0.9.12", ] [[package]] @@ -3739,15 +5775,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.17", + "redox_syscall 0.5.18", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -3773,18 +5809,17 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" dependencies = [ "memchr", - "thiserror 2.0.16", "ucd-trie", ] @@ -3795,7 +5830,118 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 2.12.0", +] + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros", + "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_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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "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_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.1", ] [[package]] @@ -3815,7 +5961,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -3830,6 +5976,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs8" version = "0.9.0" @@ -3856,6 +6013,39 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + [[package]] name = "poly1305" version = "0.8.0" @@ -3885,24 +6075,21 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" -[[package]] -name = "portable-atomic-util" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" -dependencies = [ - "portable-atomic", -] - [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" dependencies = [ "zerovec", ] +[[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.21" @@ -3912,6 +6099,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prettyplease" version = "0.2.37" @@ -3919,7 +6112,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -3949,16 +6142,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" dependencies = [ "thiserror 1.0.69", - "toml", + "toml 0.5.11", ] [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" dependencies = [ - "toml_edit", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.7", ] [[package]] @@ -4004,33 +6207,50 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] -name = "proc-macro2" -version = "1.0.101" +name = "proc-macro-hack" +version = "0.5.20+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] -name = "proptest" -version = "1.7.0" +name = "proc-macro2-diagnostics" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", + "version_check", +] + +[[package]] +name = "proptest" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.9.2", - "lazy_static", + "bitflags 2.10.0", "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", "rand_xorshift", - "regex-syntax 0.8.5", + "regex-syntax 0.8.8", "rusty-fork", "tempfile", "unarray", @@ -4062,7 +6282,7 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ - "heck", + "heck 0.5.0", "itertools 0.14.0", "log", "multimap", @@ -4072,7 +6292,7 @@ dependencies = [ "prost 0.13.5", "prost-types", "regex", - "syn 2.0.106", + "syn 2.0.108", "tempfile", ] @@ -4086,7 +6306,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -4099,7 +6319,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -4118,10 +6338,65 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] -name = "quote" -version = "1.0.40" +name = "quinn" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] @@ -4138,6 +6413,20 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[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" @@ -4158,6 +6447,17 @@ checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", + "serde", +] + +[[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]] @@ -4180,6 +6480,15 @@ dependencies = [ "rand_core 0.9.3", ] +[[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" @@ -4195,7 +6504,26 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", + "serde", +] + +[[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]] @@ -4207,6 +6535,18 @@ dependencies = [ "rand_core 0.9.3", ] +[[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 = "rayon" version = "1.11.0" @@ -4238,34 +6578,65 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.17", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", "regex-automata", - "regex-syntax 0.8.5", + "regex-syntax 0.8.8", ] [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax 0.8.8", ] [[package]] @@ -4276,44 +6647,50 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.23" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64 0.22.1", "bytes", "futures-core", - "http 1.3.1", - "http-body 1.0.1", + "futures-util", + "http", + "http-body", "http-body-util", - "hyper 1.7.0", - "hyper-tls", + "hyper", + "hyper-rustls", "hyper-util", "js-sys", "log", - "native-tls", + "mime_guess", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", - "tokio-native-tls", - "tower 0.5.2", - "tower-http 0.6.6", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", + "webpki-roots", ] [[package]] @@ -4337,10 +6714,47 @@ dependencies = [ "subtle", ] +[[package]] +name = "rfd" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a73a7337fc24366edfca76ec521f51877b114e42dab584008209cca6719251" +dependencies = [ + "ashpd", + "block", + "dispatch", + "js-sys", + "log", + "objc", + "objc-foundation", + "objc_id", + "pollster", + "raw-window-handle 0.6.2", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rln" -version = "0.8.0" -source = "git+https://github.com/vacp2p/zerokit.git?branch=master#9da80dd80727ab9df06bbd2af596aa23d952dc4e" +version = "0.9.0" +source = "git+https://github.com/vacp2p/zerokit.git?branch=master#7f6f66bb13abb82bd5a3bb760bb49338caea5f6f" dependencies = [ "ark-bn254", "ark-ec", @@ -4363,7 +6777,8 @@ dependencies = [ "ruint", "serde", "serde_json", - "thiserror 2.0.16", + "tempfile", + "thiserror 2.0.17", "tiny-keccak", "zeroize", "zerokit_utils", @@ -4381,13 +6796,14 @@ dependencies = [ [[package]] name = "ruint" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecb38f82477f20c5c3d62ef52d7c4e536e38ea9b73fb570a20c5cae0e14bcf6" +checksum = "a68df0380e5c9d20ce49534f292a36a7514ae21350726efe1865bdb1fa91d278" dependencies = [ "alloy-rlp", "ark-ff 0.3.0", "ark-ff 0.4.2", + "ark-ff 0.5.0", "bytes", "fastrlp 0.3.1", "fastrlp 0.4.0", @@ -4401,7 +6817,7 @@ dependencies = [ "rand 0.9.2", "rlp", "ruint-macro", - "serde", + "serde_core", "valuable", "zeroize", ] @@ -4412,12 +6828,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - [[package]] name = "rustc-hash" version = "1.1.0" @@ -4451,7 +6861,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.26", + "semver 1.0.27", ] [[package]] @@ -4460,7 +6870,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4469,26 +6879,52 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", ] [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ + "web-time", "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.103.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -4497,9 +6933,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rusty-fork" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" dependencies = [ "fnv", "quick-error", @@ -4514,12 +6950,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] -name = "schannel" -version = "0.1.27" +name = "same-file" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ - "windows-sys 0.59.0", + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", ] [[package]] @@ -4552,6 +7012,7 @@ dependencies = [ "der 0.7.10", "generic-array", "pkcs8 0.10.2", + "serdect", "subtle", "zeroize", ] @@ -4585,6 +7046,7 @@ dependencies = [ "bitcoin_hashes", "rand 0.8.5", "secp256k1-sys 0.10.1", + "serde", ] [[package]] @@ -4615,26 +7077,23 @@ dependencies = [ ] [[package]] -name = "security-framework" -version = "2.11.1" +name = "selectors" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" dependencies = [ - "bitflags 2.9.2", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" -dependencies = [ - "core-foundation-sys", - "libc", + "bitflags 1.3.2", + "cssparser", + "derive_more 0.99.20", + "fxhash", + "log", + "matches", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", ] [[package]] @@ -4648,9 +7107,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "semver-parser" @@ -4662,11 +7121,21 @@ dependencies = [ ] [[package]] -name = "serde" -version = "1.0.219" +name = "send_wrapper" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" dependencies = [ + "futures-core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", "serde_derive", ] @@ -4693,35 +7162,77 @@ dependencies = [ ] [[package]] -name = "serde_derive" -version = "1.0.219" +name = "serde-wasm-bindgen" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ - "itoa", + "itoa 1.0.15", "memchr", "ryu", "serde", + "serde_core", ] [[package]] -name = "serde_path_to_error" -version = "0.1.17" +name = "serde_qs" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ - "itoa", "serde", ] @@ -4732,11 +7243,115 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa", + "itoa 1.0.15", "ryu", "serde", ] +[[package]] +name = "serde_with" +version = "3.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.12.0", + "schemars 0.9.0", + "schemars 1.0.4", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct 0.2.0", + "serde", +] + +[[package]] +name = "server_fn" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fae7a3038a32e5a34ba32c6c45eb4852f8affaf8b794ebfcd4b1099e2d62ebe" +dependencies = [ + "bytes", + "const_format", + "dashmap 5.5.3", + "futures", + "gloo-net", + "http", + "js-sys", + "once_cell", + "reqwest", + "send_wrapper", + "serde", + "serde_json", + "serde_qs", + "server_fn_macro_default", + "thiserror 1.0.69", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faaaf648c6967aef78177c0610478abb5a3455811f401f3c62d10ae9bd3901a1" +dependencies = [ + "const_format", + "convert_case 0.6.0", + "proc-macro2", + "quote", + "syn 2.0.108", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro_default" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2aa8119b558a17992e0ac1fd07f080099564f24532858811ce04f742542440" +dependencies = [ + "server_fn_macro", + "syn 2.0.108", +] + +[[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 = "sha1" version = "0.10.6" @@ -4792,12 +7407,31 @@ dependencies = [ "cfg-if", ] +[[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 = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.6" @@ -4827,6 +7461,24 @@ dependencies = [ "rand_core 0.6.4", ] +[[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 = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.11" @@ -4849,6 +7501,45 @@ dependencies = [ "parking_lot 0.11.2", ] +[[package]] +name = "sledgehammer_bindgen" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e83e178d176459c92bc129cfd0958afac3ced925471b889b3a75546cfc4133" +dependencies = [ + "sledgehammer_bindgen_macro", + "wasm-bindgen", +] + +[[package]] +name = "sledgehammer_bindgen_macro" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62f06db0370222f7f498ef478fce9f8df5828848d1d3517e3331936d7074f55" +dependencies = [ + "quote", + "syn 2.0.108", +] + +[[package]] +name = "sledgehammer_utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdd4b83524961983cea3c55383b3910fd2f24fd13a188f5b091d2d504a61ae" +dependencies = [ + "rustc-hash 1.1.0", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "serde", + "version_check", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -4871,22 +7562,38 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.10" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] -name = "socket2" -version = "0.6.0" +name = "soup3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" dependencies = [ + "futures-channel", + "gio", + "glib", "libc", - "windows-sys 0.59.0", + "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]] @@ -4935,9 +7642,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -4945,6 +7652,37 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot 0.12.5", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "strum" version = "0.27.2" @@ -4960,10 +7698,10 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -4985,9 +7723,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" dependencies = [ "proc-macro2", "quote", @@ -4996,22 +7734,16 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "0.8.25" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4560533fbd6914b94a8fb5cc803ed6801c3455668db3b810702c57612bac9412" +checksum = "ff790eb176cc81bb8936aed0f7b9f14fc4670069a2d371b3e3b0ecce908b2cb3" dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.2" @@ -5041,7 +7773,71 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", +] + +[[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.30.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6682a07cf5bab0b8a2bd20d0a542917ab928b5edb75ebd4eda6b05cbaab872da" +dependencies = [ + "bitflags 2.10.0", + "cocoa 0.26.1", + "core-foundation 0.10.1", + "core-graphics 0.24.0", + "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 0.12.5", + "raw-window-handle 0.5.2", + "raw-window-handle 0.6.2", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.58.0", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", ] [[package]] @@ -5051,18 +7847,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] -name = "tempfile" -version = "3.21.0" +name = "target-lexicon" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", - "rustix 1.0.8", - "windows-sys 0.60.2", + "rustix 1.1.2", + "windows-sys 0.61.2", ] +[[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.69" @@ -5074,11 +7893,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.17", ] [[package]] @@ -5089,18 +7908,27 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", ] [[package]] @@ -5112,6 +7940,37 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa 1.0.15", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -5131,6 +7990,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +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 = "tls_codec" version = "0.4.2" @@ -5150,48 +8024,45 @@ checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project-lite", "signal-hook-registry", - "slab", - "socket2 0.6.0", + "socket2", "tokio-macros", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] -name = "tokio-native-tls" -version = "0.3.1" +name = "tokio-rustls" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "native-tls", + "rustls", "tokio", ] @@ -5207,18 +8078,6 @@ dependencies = [ "tokio-util", ] -[[package]] -name = "tokio-tungstenite" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite", -] - [[package]] name = "tokio-util" version = "0.7.16" @@ -5242,36 +8101,67 @@ dependencies = [ ] [[package]] -name = "toml_datetime" -version = "0.6.11" +name = "toml" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ - "indexmap", - "toml_datetime", - "winnow", + "serde", + "serde_spanned", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", ] [[package]] -name = "tower" -version = "0.4.13" +name = "toml_datetime" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", - "tracing", + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.12.0", + "serde", + "serde_spanned", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap 2.12.0", + "toml_datetime 0.7.3", + "toml_parser", + "winnow 0.7.13", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow 0.7.13", ] [[package]] @@ -5283,44 +8173,26 @@ dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tower-layer", "tower-service", ] -[[package]] -name = "tower-http" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" -dependencies = [ - "bitflags 2.9.2", - "bytes", - "futures-core", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "http-range-header", - "pin-project-lite", - "tower-layer", - "tower-service", -] - [[package]] name = "tower-http" version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", "bytes", "futures-util", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "iri-string", "pin-project-lite", - "tower 0.5.2", + "tower", "tower-layer", "tower-service", ] @@ -5343,12 +8215,23 @@ version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ - "log", "pin-project-lite", "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber 0.3.20", +] + [[package]] name = "tracing-attributes" version = "0.1.30" @@ -5357,7 +8240,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -5370,6 +8253,17 @@ dependencies = [ "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.2.25" @@ -5379,6 +8273,56 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber 0.3.20", + "wasm-bindgen", +] + +[[package]] +name = "tray-icon" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadd75f5002e2513eaa19b2365f533090cc3e93abd38788452d9ea85cff7b48a" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda 0.15.3", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "once_cell", + "png", + "thiserror 2.0.17", + "windows-sys 0.59.0", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -5387,28 +8331,27 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.20.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" dependencies = [ "byteorder", "bytes", "data-encoding", - "http 0.2.12", + "http", "httparse", "log", "rand 0.8.5", "sha1", "thiserror 1.0.69", - "url", "utf-8", ] [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "ucd-trie" @@ -5416,6 +8359,31 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "ui_bridge" +version = "0.1.0" +dependencies = [ + "anyhow", + "de_mls", + "de_mls_gateway", + "de_mls_ui_protocol", + "futures", + "tokio", + "tracing", + "uuid", +] + [[package]] name = "uint" version = "0.9.5" @@ -5435,10 +8403,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] -name = "unicode-ident" -version = "1.0.18" +name = "unicase" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-xid" @@ -5463,16 +8443,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" [[package]] -name = "url" -version = "2.5.4" +name = "untrusted" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -5485,34 +8478,29 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - [[package]] name = "uuid" -version = "1.18.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "js-sys", "rand 0.9.2", + "serde", "uuid-macro-internal", "wasm-bindgen", ] [[package]] name = "uuid-macro-internal" -version = "1.18.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22b7ad00068276db5fea436dba78daa7891b8d60db76e4f51cbdefbdecdab97e" +checksum = "d9384a660318abfbd7f8932c34d67e4d1ec511095f95972ddc01e19d7ba8413f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -5533,6 +8521,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[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.5" @@ -5585,6 +8579,16 @@ dependencies = [ "cc", ] +[[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" @@ -5594,6 +8598,34 @@ dependencies = [ "try-lock", ] +[[package]] +name = "warnings" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f68998838dab65727c9b30465595c6f7c953313559371ca8bf31759b3680ad" +dependencies = [ + "pin-project", + "tracing", + "warnings-macro", +] + +[[package]] +name = "warnings-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59195a1db0e95b920366d949ba5e0d3fc0e70b67c09be15ce5abb790106b0571" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[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.1+wasi-snapshot-preview1" @@ -5601,45 +8633,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ "cfg-if", "js-sys", @@ -5650,9 +8669,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5660,35 +8679,48 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 2.0.108", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] [[package]] -name = "wasmtimer" +name = "wasm-streams" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8d49b5d6c64e8558d9b1b065014426f35c18de636895d24893dbbd329743446" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmtimer" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" dependencies = [ "futures", "js-sys", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-utils", "slab", "wasm-bindgen", @@ -5696,14 +8728,130 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db67ae75a9405634f5882791678772c94ff5f16a66535aae186e26aa0841fc8b" +dependencies = [ + "core-foundation 0.9.4", + "home", + "jni", + "log", + "ndk-context", + "objc", + "raw-window-handle 0.5.2", + "url", + "web-sys", +] + +[[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 = "webpki-roots" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61ff3d9d0ee4efcb461b14eb3acfda2702d10dc329f339303fc3e57215ae2c" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.58.0", + "windows-implement 0.58.0", + "windows-interface 0.58.0", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "webview2-com-sys" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3a3e2eeb58f82361c93f9777014668eb3d07e7d174ee4c819575a9208011886" +dependencies = [ + "thiserror 1.0.69", + "windows", + "windows-core 0.58.0", +] + [[package]] name = "which" version = "4.4.2" @@ -5732,6 +8880,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -5739,64 +8896,146 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-core" -version = "0.61.2" +name = "windows" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" dependencies = [ - "windows-implement", - "windows-interface", + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link", - "windows-result", - "windows-strings", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", ] [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.3.4" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] +[[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" @@ -5821,7 +9060,46 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[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]] @@ -5842,21 +9120,42 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link", +] + +[[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" @@ -5865,9 +9164,21 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[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" @@ -5877,9 +9188,21 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[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" @@ -5889,9 +9212,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -5901,9 +9224,21 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[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" @@ -5913,9 +9248,21 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[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" @@ -5925,9 +9272,21 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[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" @@ -5937,9 +9296,21 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[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" @@ -5949,34 +9320,80 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.12" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "winnow" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ - "bitflags 2.9.2", + "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "writeable" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "wry" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac0099a336829fbf54c26b5f620c68980ebbe37196772aeaf6118df4931b5cb0" +dependencies = [ + "base64 0.22.1", + "block", + "cocoa 0.26.1", + "core-graphics 0.24.0", + "crossbeam-channel", + "dpi", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "ndk", + "objc", + "objc_id", + "once_cell", + "percent-encoding", + "raw-window-handle 0.6.2", + "sha2 0.10.9", + "soup3", + "tao-macros", + "thiserror 1.0.69", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.58.0", + "windows-version", + "x11-dl", +] + [[package]] name = "wyz" version = "0.5.1" @@ -5986,6 +9403,27 @@ dependencies = [ "tap", ] +[[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", +] + [[package]] name = "x25519-dalek" version = "2.0.1" @@ -5998,6 +9436,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + [[package]] name = "yoke" version = "0.8.0" @@ -6018,28 +9472,85 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", "synstructure 0.13.2", ] [[package]] -name = "zerocopy" -version = "0.8.26" +name = "zbus" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-process", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "ordered-stream", + "rand 0.8.5", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tokio", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.108", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -6059,15 +9570,15 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", "synstructure 0.13.2", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "zeroize_derive", ] @@ -6080,13 +9591,13 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] name = "zerokit_utils" -version = "0.6.0" -source = "git+https://github.com/vacp2p/zerokit.git?branch=master#9da80dd80727ab9df06bbd2af596aa23d952dc4e" +version = "0.7.0" +source = "git+https://github.com/vacp2p/zerokit.git?branch=master#7f6f66bb13abb82bd5a3bb760bb49338caea5f6f" dependencies = [ "ark-ff 0.5.0", "hex", @@ -6095,7 +9606,7 @@ dependencies = [ "rayon", "serde_json", "sled", - "thiserror 2.0.16", + "thiserror 2.0.17", "vacp2p_pmtree", ] @@ -6129,5 +9640,43 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", +] + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "url", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.108", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", ] diff --git a/Cargo.toml b/Cargo.toml index bbc58d0..478f9ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,21 +1,18 @@ [workspace] -members = ["ds", "mls_crypto"] -# [workspace.dependencies] -# foundry-contracts = { path = "crates/bindings" } +members = [ + "apps/de_mls_desktop_ui", + "crates/de_mls_gateway", + "crates/de_mls_ui_protocol", + "crates/ui_bridge", + "ds", + "mls_crypto", +] [package] -name = "de-mls" -version = "2.0.0" +name = "de_mls" +version = "2.1.0" edition = "2021" -[[bin]] -name = "de-mls" -path = "src/main.rs" -bench = false - -# [lib] -# bench = false - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] @@ -25,12 +22,11 @@ openmls_basic_credential = "0.3.0" openmls_rust_crypto = "0.3.0" openmls_traits = "0.3.0" -axum = { version = "0.6.10", features = ["ws"] } -futures = "0.3.26" -tower-http = { version = "0.4.0", features = ["cors"] } -tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread", "full"] } +futures = "0.3.31" +tower-http = { version = "0.6.6", features = ["cors"] } +tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread", "full"] } tokio-util = "0.7.13" -alloy = { version = "0.11.0", features = [ +alloy = { version = "1.0.37", features = [ "providers", "node-bindings", "network", @@ -62,13 +58,26 @@ anyhow = "1.0.81" thiserror = "1.0.39" uuid = "1.11.0" -env_logger = "0.11.5" -log = "0.4.22" +tracing = "0.1.41" ds = { path = "ds" } mls_crypto = { path = "mls_crypto" } prost = "0.13.5" bytes = "1.10.1" +tower-layer = "0.3.3" +http = "1.3.1" [build-dependencies] prost-build = "0.13.5" + +[profile] + +[profile.wasm-dev] +inherits = "dev" +opt-level = 1 + +[profile.server-dev] +inherits = "dev" + +[profile.android-dev] +inherits = "dev" diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 80b679a..0000000 --- a/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -#################################################################################################### -## Build image -#################################################################################################### -FROM rust:latest - -WORKDIR /app -RUN apt-get update && apt-get install -y libssl-dev pkg-config gcc clang - -# Cache build dependencies -RUN echo "fn main() {}" > dummy.rs -COPY ["Cargo.toml", "./Cargo.toml"] -COPY ["ds/", "./ds/"] -COPY ["mls_crypto/", "./mls_crypto/"] -RUN sed -i 's#src/main.rs#dummy.rs#' Cargo.toml -RUN cargo build -RUN sed -i 's#dummy.rs#src/main.rs#' Cargo.toml -# Build the actual app -COPY ["src/", "./src/"] -RUN cargo build - -CMD ["/app/target/debug/de-mls"] diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 261eeb9..0000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..36323a1 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,203 @@ +Copyright (c) 2022 Vac Research + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..36a7944 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2022 Vac Research + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE O THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 3f405ad..e0ed2ba 100644 --- a/README.md +++ b/README.md @@ -1,96 +1,120 @@ -# de-mls +# De-MLS -Decentralized MLS PoC using a smart contract for group coordination +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) +[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -> Note: The frontend implementation is based on [chatr](https://github.com/0xLaurens/chatr), -> a real-time chat application built with Rust and SvelteKit +Decentralized MLS proof-of-concept that coordinates secure group membership through +off-chain consensus and a Waku relay. +This repository now ships a native desktop client built with Dioxus that drives the MLS core directly. -## Run Test Waku Node +## What’s Included -This node is used to easially connect different instances of the app between each other. +- **de-mls** – core library that manages MLS groups, consensus, and Waku integration +- **crates/de_mls_gateway** – bridges UI commands (`AppCmd`) to the core runtime and streams `AppEvent`s back +- **crates/ui_bridge** – bootstrap glue that hosts the async command loop for desktop clients +- **apps/de_mls_desktop_ui** – Dioxus desktop UI with login, chat, stewardship, and voting flows +- **tests/** – integration tests that exercise the MLS state machine and consensus paths + +## Quick Start + +### 1. Launch a test Waku relay + +Run a lightweight `nwaku` node that your local clients can connect to: ```bash -docker run -p 8645:8645 -p 60000:60000 wakuorg/nwaku:v0.33.1 --cluster-id=15 --rest --relay --rln-relay=false --pubsub-topic=/waku/2/rs/15/1 +docker run \ + -p 8645:8645 \ + -p 60000:60000 \ + wakuorg/nwaku:v0.33.1 \ + --cluster-id=15 \ + --rest \ + --relay \ + --rln-relay=false \ + --pubsub-topic=/waku/2/rs/15/1 ``` -## Run User Instance +Take note of the node multiaddr printed in the logs (looks like `/ip4/127.0.0.1/tcp/60000/p2p/`). -Create a `.env` file in the `.env` folder for each client containing the following variables: +### 2. Set the runtime environment -```text -NAME=client1 -BACKEND_PORT=3000 -FRONTEND_PORT=4000 -NODE_PORT=60000 -PEER_ADDRESSES=[/ip4/x.x.x.x/tcp/60000/p2p/xxxx...xxxx] -``` - -Run docker compose up for the user instance +The desktop app reads the same environment variables the MLS core uses: ```bash -docker-compose --env-file ./.env/client1.env up --build +export NODE_PORT=60001 # UDP/TCP port the embedded Waku client will bind to +export PEER_ADDRESSES=/ip4/127.0.0.1/tcp/60000/p2p/ +export RUST_LOG=info,de_mls_gateway=info # optional; controls UI + gateway logging ``` -For each client, run the following command to start the frontend on the local host with the port specified in the `.env` file +Use a unique `NODE_PORT` per local client so the embedded Waku nodes do not collide. +`PEER_ADDRESSES` accepts a comma-separated list if you want to bootstrap from multiple relays. -Run from the frontend directory +### 3. Launch the desktop application ```bash -PUBLIC_API_URL=http://0.0.0.0:3000 PUBLIC_WEBSOCKET_URL=ws://localhost:3000 npm run dev +cargo run -p de_mls_desktop_ui ``` -Run from the root directory +The first run creates `apps/de_mls_desktop_ui/logs/de_mls_ui.log` and starts the event bridge +and embedded Waku client. +Repeat steps 2–3 in another terminal with a different `NODE_PORT` to simulate multiple users. -```bash -RUST_BACKTRACE=full RUST_LOG=info NODE_PORT=60001 PEER_ADDRESSES=/ip4/x.x.x.x/tcp/60000/p2p/xxxx...xxxx,/ip4/y.y.y.y/tcp/60000/p2p/yyyy...yyyy cargo run -- --nocapture -``` +## Using the Desktop UI -## Steward State Management +- **Login screen** – paste an Ethereum-compatible secp256k1 private key (hex, with or without `0x`) + and click `Enter`. + On success the app derives your wallet address, stores it in session state, + and navigates to the home layout. -The system implements a robust state machine for managing steward epochs with the following states: +- **Header bar** – shows the derived address and allows runtime log-level changes (`error`→`trace`). + Log files rotate daily under `apps/de_mls_desktop_ui/logs/`. -### States +- **Groups panel** – lists every MLS group returned by the gateway. + Use `Create` or `Join` to open a modal, enter the group name, + and the UI automatically refreshes the list and opens the group. -- **Working**: Normal operation where all users can send any message type freely -- **Waiting**: Steward epoch active, only steward can send BATCH_PROPOSALS_MESSAGE -- **Voting**: Consensus voting phase with only voting-related messages: - - Everyone: VOTE, USER_VOTE - - Steward only: VOTING_PROPOSAL, PROPOSAL - - All other messages blocked during voting +- **Chat panel** – displays live conversation messages for the active group. + Compose text messages at the bottom; the UI also offers: + - `Leave group` to request a self-ban (the backend fills in your address) + - `Request ban` to request ban for another user + Member lists are fetched automatically when a group is opened so you can + pick existing members from the ban modal. -### State Transitions +- **Consensus panel** – keeps stewards and members aligned: + - Shows whether you are a steward for the active group + - Lists pending steward requests collected during the current epoch + - Surfaces the proposal currently open for voting with `YES`/`NO` buttons + - Stores the latest proposal decisions with timestamps for quick auditing + +## Steward State Machine + +- **Working** – normal mode; all MLS messages are allowed +- **Waiting** – a steward epoch is active; only the steward may push `BATCH_PROPOSALS_MESSAGE` +- **Voting** – the consensus phase; everyone may submit `VOTE`/`USER_VOTE`, + the steward can still publish proposal metadata + +Transitions: ```text Working --start_steward_epoch()--> Waiting (if proposals exist) -Working --start_steward_epoch()--> Working (if no proposals - no state change) +Working --start_steward_epoch()--> Working (if no proposals) Waiting --start_voting()---------> Voting -Waiting --no_proposals_found()---> Working (edge case: proposals disappear) +Waiting --no_proposals_found()---> Working Voting --complete_voting(YES)----> Waiting --apply_proposals()--> Working Voting --complete_voting(NO)-----> Working ``` -### Steward Flow Scenarios +Stewards always return to `Working` after an epoch finishes; +edge cases such as missing proposals are handled defensively with detailed tracing. -1. **No Proposals**: Steward stays in Working state throughout epoch -2. **Successful Vote**: - - **Steward**: Working → Waiting → Voting → Waiting → Working - - **Non-Steward**: Working → Waiting → Voting → Working -3. **Failed Vote**: - - **Steward**: Working → Waiting → Voting → Working - - **Non-Steward**: Working → Waiting → Voting → Working -4. **Edge Case**: Working → Waiting → Working (if proposals disappear during voting) +## Development Tips -### Guarantees +- `cargo test` – runs the Rust unit + integration test suite +- `cargo fmt --all check` / `cargo clippy` – keep formatting and linting consistent with the codebase +- `RUST_BACKTRACE=full` – helpful when debugging state-machine transitions during development -- Steward always returns to Working state after epoch completion -- No infinite loops or stuck states -- All edge cases properly handled -- Robust error handling with detailed logging +Logs for the desktop UI live in `apps/de_mls_desktop_ui/logs/`; core logs are emitted to stdout as well. -### Example of ban user +## Contributing -In chat message block run ban command, note that user wallet address should be in the format without `0x` - -```bash -/ban f39555ce6ab55579cfffb922665825e726880af6 -``` +Issues and pull requests are welcome. Please include reproduction steps, relevant logs, +and test coverage where possible. diff --git a/apps/de_mls_desktop_ui/Cargo.toml b/apps/de_mls_desktop_ui/Cargo.toml new file mode 100644 index 0000000..c55575f --- /dev/null +++ b/apps/de_mls_desktop_ui/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "de_mls_desktop_ui" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +de_mls_ui_protocol = { path = "../../crates/de_mls_ui_protocol" } +de_mls_gateway = { path = "../../crates/de_mls_gateway" } +ui_bridge = { path = "../../crates/ui_bridge" } +de_mls = { path = "../../" } +mls_crypto = { path = "../../mls_crypto" } + +dioxus = { version = "0.6.2", features = ["signals", "router", "desktop"] } +dioxus-desktop = "0.6.3" +tokio = { version = "1.47.1", features = [ + "rt-multi-thread", + "macros", + "sync", + "time", +] } +futures = "0.3.31" +anyhow = "1.0.100" +thiserror = "2.0.17" +uuid = { version = "1.18.1", features = ["v4", "serde"] } +once_cell = "1.21.3" +parking_lot = "0.12.5" +tracing = "0.1.41" +tracing-subscriber = { version = "0.3.20", features = ["fmt", "env-filter"] } +tracing-appender = "0.2.3" +chrono = { version = "0.4", features = ["serde"] } +hex = "0.4" diff --git a/apps/de_mls_desktop_ui/assets/main.css b/apps/de_mls_desktop_ui/assets/main.css new file mode 100644 index 0000000..ec2258c --- /dev/null +++ b/apps/de_mls_desktop_ui/assets/main.css @@ -0,0 +1,619 @@ +:root { + --bg: #0b0d10; + --card: #14161c; + --text: #e5e7ec; + --muted: #9094a2; + --primary: #00b2ff; + --primary-2: #007ad9; + --border: #1c1e25; + --good: #00f5a0; + --bad: #ff005c; +} + +* { + box-sizing: border-box; +} + +html, +body, +#main { + height: 100%; + width: 100%; + margin: 0; + padding: 0; + background: var(--bg); + color: var(--text); + font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Noto Sans, Apple Color Emoji, Segoe UI Emoji; +} + +.page { + max-width: 1400px; + margin: 0 auto; +} + +h1 { + margin: 0 0 16px 0; + font-size: 22px; +} + +.header { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 12px; + border-bottom: 1px solid var(--border); + background: rgba(255, 255, 255, 0.03); + position: sticky; + top: 0; + z-index: 5; +} + +.header .brand { + font-weight: 700; + letter-spacing: .5px; +} + +.header .user-hint { + color: var(--muted); + font-size: 12px; + padding: 4px 8px; + border: 1px dashed var(--border); + border-radius: 8px; + max-width: 420px; +} + +.header .spacer { + flex: 1; +} + +.header .label { + color: var(--muted); +} + +.header .level { + padding: 6px 8px; + border-radius: 8px; + border: 1px solid var(--border); + background: var(--card); + color: var(--text); + outline: none; + font-size: 13px; +} + +.page.login { + max-width: 520px; + margin-top: 32px; +} + +.form-row { + display: flex; + flex-direction: column; + gap: 6px; + margin: 12px 0; +} + +input, +select { + padding: 10px 12px; + border-radius: 8px; + border: 1px solid var(--border); + background: var(--card); + color: var(--text); + outline: none; + font-size: 14px; +} + +.input-error { + color: var(--bad); + font-size: 12px; +} + +button { + border: 1px solid var(--border); + background: var(--card); + color: var(--text); + padding: 10px 14px; + border-radius: 10px; + cursor: pointer; +} + +button.primary { + background: var(--primary); + border-color: var(--primary); + color: white; +} + +button.primary:hover { + background: var(--primary-2); +} + +button.secondary { + background: transparent; +} + +button.ghost { + background: transparent; + border-color: var(--border); + color: var(--muted); +} + +button.icon { + width: 32px; + height: 32px; + border-radius: 8px; +} + +button.mini { + padding: 4px 8px; + border-radius: 8px; + font-size: 12px; +} + +.alerts { + position: fixed; + top: 64px; + right: 24px; + display: flex; + flex-direction: column; + gap: 8px; + z-index: 20; +} + +.alert { + display: flex; + align-items: flex-start; + gap: 10px; + min-width: 260px; + max-width: 420px; + padding: 12px 14px; + border-radius: 10px; + border: 1px solid var(--border); + background: var(--card); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +.alert.error { + border-color: rgba(255, 0, 92, 0.4); + background: rgba(255, 0, 92, 0.1); + color: var(--bad); +} + +.alert .message { + flex: 1; + font-size: 13px; + line-height: 1.4; +} + +.member-picker { + display: flex; + flex-direction: column; + gap: 10px; +} + +.member-picker .helper { + color: var(--muted); + font-size: 13px; +} + +.member-list { + display: flex; + flex-direction: column; + gap: 10px; + max-height: 260px; + overflow-y: auto; + padding-right: 4px; +} + +.member-item { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 10px; + background: transparent; + box-shadow: none; + border: none; + width: 100%; +} + +.member-item .member-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + width: 100%; +} + +.member-item .member-id { + font-size: 12px; + font-weight: 400; + letter-spacing: 0.02em; + color: var(--text); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + overflow-wrap: anywhere; + flex: 1; +} + +.member-item .member-choose { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: none; + padding: 6px 12px; + border-radius: 999px; + border: none; + background: var(--primary); + color: white; + cursor: pointer; + box-shadow: 0 6px 18px rgba(0, 178, 255, 0.35); +} + +.member-item .member-choose:hover { + background: var(--primary-2); + box-shadow: 0 6px 18px rgba(0, 122, 217, 0.45); +} + +/* Home layout */ +.page.home { + padding: 12px; +} + +.layout { + display: grid; + grid-template-columns: 280px 1fr 500px; + gap: 12px; +} + +.mono { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +.ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + + +.panel { + background: var(--card); + border: 1px solid var(--border); + border-radius: 12px; + padding: 12px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.panel h2 { + margin: 0 0 6px 0; + font-size: 18px; +} + +.hint { + color: var(--muted); + padding: 8px 0; +} + +.panel.groups .group-list { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 8px; +} + +/* Group list a bit wider rows to align with long names */ +.group-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: 10px; +} + +.group-row .title { + font-weight: 600; + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.panel.groups .footer { + margin-top: auto; + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.panel.groups .footer .primary { + flex: 1; +} + +/* Chat */ +.panel.chat .messages { + min-height: 360px; + height: 58vh; + overflow-y: auto; + border: 1px solid var(--border); + border-radius: 12px; + padding: 12px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.panel.chat .chat-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.msg { + display: flex; + flex-direction: column; + gap: 4px; + align-items: flex-start; +} + +.msg.me { + align-items: flex-end; +} + +.msg.me .body { + background: rgba(79, 140, 255, 0.15); + border: 1px solid rgba(79, 140, 255, 0.35); + padding: 8px 10px; + border-radius: 10px; +} + +.msg.system { + opacity: 0.9; +} + +.msg.system .body { + font-style: italic; + color: var(--muted); + background: transparent; + border: none; + padding: 0; +} + +.msg .from { + color: var(--muted); + font-size: 16px; +} + +.msg .body { + color: var(--text); + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--border); + padding: 8px 10px; + border-radius: 10px; +} + +.composer { + display: flex; + gap: 8px; + align-items: center; +} + +.composer input { + flex: 1; + min-width: 0; +} + +.composer button { + flex: 0 0 auto; +} + +/* Consensus panel */ +.panel.consensus .status { + display: flex; + align-items: center; + gap: 8px; +} + +.panel.consensus .status .good { + color: var(--good); + font-weight: 600; +} + +.panel.consensus .status .bad { + color: var(--bad); + font-weight: 600; +} + +.panel.consensus .proposal-item { + display: grid; + grid-template-columns: minmax(6rem, max-content) 1fr; + align-items: start; + gap: 8px; + padding: 6px 8px; + border-radius: 6px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border); +} + +.panel.consensus .proposal-item .action { + color: var(--primary); + font-size: 12px; + font-weight: 600; +} + +.panel.consensus .proposal-item .value { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + color: var(--text); + font-size: 12px; + overflow-wrap: anywhere; + word-break: break-word; +} + +.panel.consensus .proposal-item.proposal-id { + background: rgba(0, 178, 255, 0.08); + border-color: rgba(0, 178, 255, 0.45); + box-shadow: inset 0 0 0 1px rgba(0, 178, 255, 0.15); +} + +.panel.consensus .proposal-item.proposal-id .action { + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.panel.consensus .proposal-item.proposal-id .value { + font-weight: 700; + font-size: 13px; + letter-spacing: 0.03em; +} + +/* Consensus sections */ +.panel.consensus { + display: flex; + flex-direction: column; + gap: 12px; +} + +.panel.consensus .status { + flex-shrink: 0; + margin-bottom: 16px; +} + +.panel.consensus .consensus-section { + margin: 8px 0; + padding: 12px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.02); + border: 1px solid var(--border); + display: flex; + flex-direction: column; +} + +.panel.consensus .consensus-section h3 { + margin: 0 0 12px 0; + font-size: 14px; + color: var(--primary); + border-bottom: 1px solid var(--border); + padding-bottom: 8px; +} + +.panel.consensus .no-data { + color: var(--muted); + font-style: italic; + text-align: center; + padding: 20px; + font-size: 13px; +} + +.panel.consensus .proposals-window { + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 6px; +} + +.panel.consensus .vote-actions { + display: flex; + gap: 8px; + justify-content: flex-end; + margin-top: 12px; +} + +/* Consensus results window */ +.panel.consensus .results-window { + overflow-y: auto; + border: 1px solid var(--border); + border-radius: 8px; + padding: 8px; + background: rgba(255, 255, 255, 0.02); + max-height: 200px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.panel.consensus .result-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 8px; + border-radius: 6px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border); + font-size: 13px; +} + +.panel.consensus .result-item .proposal-id { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + color: var(--muted); + font-weight: 600; +} + +.panel.consensus .result-item .outcome { + font-weight: 600; + padding: 2px 6px; + border-radius: 4px; +} + +.panel.consensus .result-item .outcome.accepted { + color: var(--good); + background: rgba(23, 201, 100, 0.1); +} + +.panel.consensus .result-item .outcome.rejected { + color: var(--bad); + background: rgba(243, 18, 96, 0.1); +} + +.panel.consensus .result-item .outcome.unspecified { + color: var(--muted); + background: rgba(163, 167, 179, 0.1); +} + +.panel.consensus .result-item .timestamp { + color: var(--muted); + font-size: 11px; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +/* Modal */ +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, .45); + display: flex; + align-items: center; + justify-content: center; +} + +.modal { + width: 520px; + max-width: calc(100vw - 32px); + background: var(--card); + border: 1px solid var(--border); + border-radius: 14px; + box-shadow: 0 0 6px var(--primary); +} + +.modal-head { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 14px; + border-bottom: 1px solid var(--border); +} + +.modal-body { + padding: 14px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.actions { + display: flex; + gap: 8px; + justify-content: flex-end; + margin-top: 6px; +} diff --git a/apps/de_mls_desktop_ui/src/logging.rs b/apps/de_mls_desktop_ui/src/logging.rs new file mode 100644 index 0000000..82dd8ee --- /dev/null +++ b/apps/de_mls_desktop_ui/src/logging.rs @@ -0,0 +1,63 @@ +use once_cell::sync::OnceCell; +use std::sync::Mutex; + +use tracing_appender::rolling; +use tracing_subscriber::{ + fmt, + layer::SubscriberExt, + reload::{Handle, Layer as ReloadLayer}, + util::SubscriberInitExt, + EnvFilter, Registry, +}; + +/// Global reload handle so the UI can change the filter at runtime. +static RELOAD: OnceCell>> = OnceCell::new(); + +/// Initialize logging: console + rolling daily file ("logs/de_mls_ui.log"). +/// Returns the initial level string actually applied. +pub fn init_logging(default_level: &str) -> String { + // Use env var if present, else the provided default + let env_level = std::env::var("RUST_LOG").unwrap_or_else(|_| default_level.to_string()); + + // Build a reloadable EnvFilter + let filter = EnvFilter::try_new(&env_level).unwrap_or_else(|_| EnvFilter::new("info")); + let (reload_layer, handle) = ReloadLayer::new(filter); + + // File sink (non-blocking) + let file_appender = rolling::daily("logs", "de_mls_ui.log"); + let (file_writer, guard) = tracing_appender::non_blocking(file_appender); + // Keep guard alive for the whole process to flush on drop + Box::leak(Box::new(guard)); + + // Build the subscriber: registry + reloadable filter + console + file + tracing_subscriber::registry() + .with(reload_layer) + .with(fmt::layer().with_writer(std::io::stdout)) // console + .with(fmt::layer().with_writer(file_writer).with_ansi(false)) // file + .init(); + + RELOAD.set(Mutex::new(handle)).ok(); + + // Return the level we consider “active” for the UI dropdown + std::env::var("RUST_LOG").unwrap_or(env_level) +} + +/// Set the global log level dynamically, e.g. "error", "warn", "info", "debug", "trace", +/// or a full filter string like "info,de_mls_gateway=debug". +pub fn set_level(new_level: &str) -> Result<(), String> { + let handle = RELOAD + .get() + .ok_or_else(|| "logger not initialized".to_string())? + .lock() + .map_err(|_| "reload handle poisoned".to_string())?; + + let filter = EnvFilter::try_new(new_level) + .map_err(|e| format!("invalid level/filter '{new_level}': {e}"))?; + + // Replace the inner EnvFilter of the reloadable layer + handle + .modify(|inner: &mut EnvFilter| *inner = filter) + .map_err(|e| format!("failed to apply filter: {e}"))?; + + Ok(()) +} diff --git a/apps/de_mls_desktop_ui/src/main.rs b/apps/de_mls_desktop_ui/src/main.rs new file mode 100644 index 0000000..25da371 --- /dev/null +++ b/apps/de_mls_desktop_ui/src/main.rs @@ -0,0 +1,971 @@ +// apps/de_mls_desktop_ui/src/main.rs +#![allow(non_snake_case)] +use dioxus::prelude::*; +use dioxus_desktop::{launch::launch as desktop_launch, Config, LogicalSize, WindowBuilder}; +use std::sync::{ + atomic::{AtomicU64, Ordering}, + Arc, +}; + +use de_mls::{ + bootstrap_core_from_env, + message::convert_group_requests_to_display, + protos::{ + consensus::v1::{Outcome, ProposalResult, VotePayload}, + de_mls::messages::v1::ConversationMessage, + }, +}; +use de_mls_gateway::GATEWAY; +use de_mls_ui_protocol::v1::{AppCmd, AppEvent}; +use mls_crypto::normalize_wallet_address_str; + +mod logging; + +static CSS: Asset = asset!("/assets/main.css"); +static NEXT_ALERT_ID: AtomicU64 = AtomicU64::new(1); +const MAX_VISIBLE_ALERTS: usize = 5; + +// Helper function to format timestamps +fn format_timestamp(timestamp_ms: u64) -> String { + use std::time::UNIX_EPOCH; + + // Convert to SystemTime and format + let timestamp = UNIX_EPOCH + std::time::Duration::from_secs(timestamp_ms); + let datetime: chrono::DateTime = timestamp.into(); + datetime.format("%H:%M:%S").to_string() +} + +// ─────────────────────────── App state ─────────────────────────── + +#[derive(Clone, Debug, Default, PartialEq)] +struct SessionState { + address: String, + key: String, +} + +#[derive(Clone, Debug, Default, PartialEq)] +struct GroupsState { + items: Vec, // names only + loaded: bool, +} + +#[derive(Clone, Debug, Default, PartialEq)] +struct ChatState { + opened_group: Option, // which group is “Open” in the UI + messages: Vec, // all messages; filtered per view + members: Vec, // cached member addresses for opened group +} + +#[derive(Clone, Debug, Default, PartialEq)] +struct ConsensusState { + is_steward: bool, + pending: Option, // active/pending proposal for opened group + // Store results with timestamps for better display + latest_results: Vec<(u32, Outcome, u64)>, // (vote_id, result, timestamp_ms) + // Store current epoch proposals for stewards + current_epoch_proposals: Vec<(String, String)>, // (action, address) pairs +} + +#[derive(Clone, Debug, PartialEq)] +struct Alert { + id: u64, + message: String, +} + +#[derive(Clone, Debug, Default, PartialEq)] +struct AlertsState { + errors: Vec, +} + +fn record_error(alerts: &mut Signal, message: impl Into) { + let raw = message.into(); + let summary = summarize_error(&raw); + tracing::error!("ui error: {}", raw); + let id = NEXT_ALERT_ID.fetch_add(1, Ordering::Relaxed); + let mut state = alerts.write(); + state.errors.push(Alert { + id, + message: summary, + }); + if state.errors.len() > MAX_VISIBLE_ALERTS { + state.errors.remove(0); + } +} + +fn dismiss_error(alerts: &mut Signal, alert_id: u64) { + alerts.write().errors.retain(|alert| alert.id != alert_id); +} + +fn summarize_error(raw: &str) -> String { + let mut summary = raw + .lines() + .next() + .map(|line| line.trim().to_string()) + .unwrap_or_else(|| raw.trim().to_string()); + const MAX_LEN: usize = 160; + if summary.len() > MAX_LEN { + summary.truncate(MAX_LEN.saturating_sub(1)); + summary.push('…'); + } + if summary.is_empty() { + "Unexpected error".to_string() + } else { + summary + } +} + +// ─────────────────────────── Routing ─────────────────────────── + +#[derive(Routable, Clone, PartialEq)] +enum Route { + #[route("/")] + Login, + #[route("/home")] + Home, // unified page +} + +// ─────────────────────────── Entry ─────────────────────────── + +fn main() { + let initial_level = logging::init_logging("info"); + tracing::info!("🚀 DE-MLS Desktop UI starting… level={}", initial_level); + + // Build a small RT to run the async bootstrap before the UI + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("rt"); + + rt.block_on(async { + let boot = bootstrap_core_from_env() + .await + .expect("bootstrap_core_from_env failed"); + // hand CoreCtx to the gateway via the UI bridge + ui_bridge::start_ui_bridge(boot.core.clone()); + boot.core + }); + + let config = Config::new().with_window( + WindowBuilder::new() + .with_title("DE-MLS Desktop UI") + .with_inner_size(LogicalSize::new(1280, 820)) + .with_resizable(true), + ); + + tracing::info!("Launching desktop application"); + desktop_launch(App, vec![], vec![Box::new(config)]); +} + +fn App() -> Element { + use_context_provider(|| Signal::new(AlertsState::default())); + use_context_provider(|| Signal::new(SessionState::default())); + use_context_provider(|| Signal::new(GroupsState::default())); + use_context_provider(|| Signal::new(ChatState::default())); + use_context_provider(|| Signal::new(ConsensusState::default())); + + rsx! { + document::Stylesheet { href: CSS } + HeaderBar {} + AlertsCenter {} + Router:: {} + } +} + +fn HeaderBar() -> Element { + // local signal to reflect current level in the select + let mut level = use_signal(|| std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string())); + let session = use_context::>(); + let my_addr = session.read().address.clone(); + + let on_change = { + move |evt: FormEvent| { + let new_val = evt.value(); + if let Err(e) = crate::logging::set_level(&new_val) { + tracing::warn!("failed to set log level: {}", e); + } else { + level.set(new_val); + } + } + }; + + rsx! { + div { class: "header", + div { class: "brand", "DE-MLS" } + if !my_addr.is_empty() { + span { class: "user-hint mono ellipsis", title: "{my_addr}", "{my_addr}" } + } + div { class: "spacer" } + label { class: "label", "Log level" } + select { + class: "level", + value: "{level}", + oninput: on_change, + option { value: "error", "error" } + option { value: "warn", "warn" } + option { value: "info", "info" } + option { value: "debug", "debug" } + option { value: "trace", "trace" } + } + } + } +} + +// ─────────────────────────── Pages ─────────────────────────── + +fn Login() -> Element { + let nav = use_navigator(); + let mut session = use_context::>(); + let mut key = use_signal(String::new); + let mut alerts = use_context::>(); + + // Local single-consumer loop: only Login() steals LoggedIn events + use_future({ + move || async move { + loop { + match GATEWAY.next_event().await { + Some(AppEvent::LoggedIn(name)) => { + session.write().address = name; + nav.replace(Route::Home); + break; + } + Some(AppEvent::Error(error)) => { + record_error(&mut alerts, error); + } + Some(other) => { + tracing::debug!("login view ignored event: {:?}", other); + } + None => break, + } + } + } + }); + + let oninput_key = { move |e: FormEvent| key.set(e.value()) }; + + let mut on_submit = move |_| { + let k = key.read().trim().to_string(); + if k.is_empty() { + return; + } + session.write().key = k.clone(); + spawn(async move { + let _ = GATEWAY.send(AppCmd::Login { private_key: k }).await; + }); + }; + + rsx! { + div { class: "page login", + h1 { "DE-MLS — Login" } + div { class: "form-row", + label { "Private key" } + input { + r#type: "password", + value: "{key}", + oninput: oninput_key, + placeholder: "0x...", + } + } + button { class: "primary", onclick: move |_| { on_submit(()); }, "Enter" } + } + } +} + +fn Home() -> Element { + let mut groups = use_context::>(); + let mut chat = use_context::>(); + let mut cons = use_context::>(); + let mut alerts = use_context::>(); + + use_future({ + move || async move { + if !groups.read().loaded { + let _ = GATEWAY.send(AppCmd::ListGroups).await; + } + } + }); + + // Local event loop for handling events from the gateway + use_future({ + move || async move { + loop { + match GATEWAY.next_event().await { + Some(AppEvent::StewardStatus { + group_id, + is_steward, + }) => { + // only update if it is the currently opened group + if chat.read().opened_group.as_deref() == Some(group_id.as_str()) { + cons.write().is_steward = is_steward; + } + } + Some(AppEvent::CurrentEpochProposals { + group_id, + proposals, + }) => { + // only update if it is the currently opened group + if chat.read().opened_group.as_deref() == Some(group_id.as_str()) { + cons.write().current_epoch_proposals = proposals; + } + } + Some(AppEvent::GroupMembers { group_id, members }) => { + if chat.read().opened_group.as_deref() == Some(group_id.as_str()) { + chat.write().members = members; + } + } + Some(AppEvent::ProposalAdded { + group_id, + action, + address, + }) => { + // only update if it is the currently opened group + if chat.read().opened_group.as_deref() == Some(group_id.as_str()) { + // Avoid duplicates: do not enqueue if the same (action, address) already exists + let exists = { + cons.read().current_epoch_proposals.iter().any(|(a, addr)| { + a == &action && addr.eq_ignore_ascii_case(&address) + }) + }; + if !exists { + cons.write().current_epoch_proposals.push((action, address)); + } + } + } + Some(AppEvent::CurrentEpochProposalsCleared { group_id }) => { + // only update if it is the currently opened group + if chat.read().opened_group.as_deref() == Some(group_id.as_str()) { + cons.write().current_epoch_proposals.clear(); + } + } + Some(AppEvent::Groups(names)) => { + groups.write().items = names; + groups.write().loaded = true; + } + Some(AppEvent::ChatMessage(msg)) => { + chat.write().messages.push(msg); + } + Some(AppEvent::VoteRequested(vp)) => { + let opened = chat.read().opened_group.clone(); + if opened.as_deref() == Some(vp.group_id.as_str()) { + cons.write().pending = Some(vp); + } + } + Some(AppEvent::ProposalDecided(ProposalResult { + group_id, + proposal_id, + outcome, + decided_at_ms, + })) => { + if chat.read().opened_group.as_deref() == Some(group_id.as_str()) { + cons.write().latest_results.push(( + proposal_id, + Outcome::try_from(outcome).unwrap_or(Outcome::Unspecified), + decided_at_ms, + )); + } + cons.write().pending = None; + } + Some(AppEvent::GroupRemoved(name)) => { + let mut g = groups.write(); + g.items.retain(|n| n != &name); + if chat.read().opened_group.as_deref() == Some(name.as_str()) { + chat.write().opened_group = None; + chat.write().members.clear(); + } + } + Some(AppEvent::Error(error)) => { + record_error(&mut alerts, error); + } + Some(_) => {} + None => break, + } + } + } + }); + + rsx! { + div { class: "page home", + div { class: "layout", + GroupListSection {} + ChatSection {} + ConsensusSection {} + } + } + } +} + +fn AlertsCenter() -> Element { + let alerts = use_context::>(); + let items = alerts.read().errors.clone(); + rsx! { + div { class: "alerts", + for alert in items.iter() { + AlertItem { + key: "{alert.id}", + alert_id: alert.id, + message: alert.message.clone(), + } + } + } + } +} + +#[derive(Props, PartialEq, Clone)] +struct AlertItemProps { + alert_id: u64, + message: String, +} + +fn AlertItem(props: AlertItemProps) -> Element { + let mut alerts = use_context::>(); + let alert_id = props.alert_id; + let message = props.message.clone(); + let dismiss = move |_| { + dismiss_error(&mut alerts, alert_id); + }; + + rsx! { + div { class: "alert error", + span { class: "message", "{message}" } + button { class: "ghost icon", onclick: dismiss, "✕" } + } + } +} + +// ─────────────────────────── Sections ─────────────────────────── + +fn GroupListSection() -> Element { + let groups_state = use_context::>(); + let mut chat = use_context::>(); + let mut show_modal = use_signal(|| false); + let mut new_name = use_signal(String::new); + let mut create_mode = use_signal(|| true); // true=create, false=join + + let items_snapshot: Vec = groups_state.read().items.clone(); + let loaded = groups_state.read().loaded; + + let mut open_group = { + move |name: String| { + chat.write().opened_group = Some(name.clone()); + chat.write().members.clear(); + let group_id = name.clone(); + spawn(async move { + let _ = GATEWAY + .send(AppCmd::EnterGroup { + group_id: group_id.clone(), + }) + .await; + let _ = GATEWAY + .send(AppCmd::LoadHistory { + group_id: group_id.clone(), + }) + .await; + let _ = GATEWAY + .send(AppCmd::GetStewardStatus { + group_id: group_id.clone(), + }) + .await; + let _ = GATEWAY + .send(AppCmd::GetCurrentEpochProposals { + group_id: group_id.clone(), + }) + .await; + let _ = GATEWAY + .send(AppCmd::GetGroupMembers { + group_id: group_id.clone(), + }) + .await; + }); + } + }; + + let mut modal_submit = { + move |_| { + let name = new_name.read().trim().to_string(); + if name.is_empty() { + return; + } + let action_name = name.clone(); + if *create_mode.read() { + spawn(async move { + let _ = GATEWAY + .send(AppCmd::CreateGroup { + name: action_name.clone(), + }) + .await; + let _ = GATEWAY.send(AppCmd::ListGroups).await; + }); + } else { + spawn(async move { + let _ = GATEWAY + .send(AppCmd::JoinGroup { + name: action_name.clone(), + }) + .await; + let _ = GATEWAY.send(AppCmd::ListGroups).await; + }); + } + open_group(name); + new_name.set(String::new()); + show_modal.set(false); + } + }; + + rsx! { + div { class: "panel groups", + h2 { "Groups" } + + if !loaded { + div { class: "hint", "Loading groups…" } + } else if items_snapshot.is_empty() { + div { class: "hint", "No groups yet." } + } else { + ul { class: "group-list", + for name in items_snapshot.into_iter() { + li { + key: "{name}", + class: "group-row", + div { class: "title", "{name}" } + button { + class: "secondary", + onclick: move |_| { open_group(name.clone()); }, + "Open" + } + } + } + } + } + + div { class: "footer", + button { class: "primary", onclick: move |_| { create_mode.set(true); show_modal.set(true); }, "Create" } + button { class: "primary", onclick: move |_| { create_mode.set(false); show_modal.set(true); }, "Join" } + } + + if *show_modal.read() { + Modal { + title: if *create_mode.read() { "Create Group".to_string() } else { "Join Group".to_string() }, + on_close: move || { show_modal.set(false); }, + div { class: "form-row", + label { "Group name" } + input { + r#type: "text", + value: "{new_name}", + oninput: move |e| new_name.set(e.value()), + placeholder: "mls-devs", + } + } + + div { class: "actions", + button { class: "primary", onclick: move |_| { modal_submit(()); }, "Confirm" } + button { class: "ghost", onclick: move |_| { show_modal.set(false); }, "Cancel" } + } + } + } + } + } +} + +fn ChatSection() -> Element { + let chat = use_context::>(); + let session = use_context::>(); + let mut msg_input = use_signal(String::new); + let mut show_ban_modal = use_signal(|| false); + let mut ban_address = use_signal(String::new); + let mut ban_error = use_signal(|| Option::::None); + + let send_msg = { + move |_| { + let text = msg_input.read().trim().to_string(); + if text.is_empty() { + return; + } + let Some(gid) = chat.read().opened_group.clone() else { + return; + }; + + msg_input.set(String::new()); + spawn(async move { + let _ = GATEWAY + .send(AppCmd::SendMessage { + group_id: gid, + body: text, + }) + .await; + }); + } + }; + + let open_ban_modal = { + move |_| { + if let Some(gid) = chat.read().opened_group.clone() { + spawn(async move { + let _ = GATEWAY + .send(AppCmd::GetGroupMembers { + group_id: gid.clone(), + }) + .await; + }); + } + ban_error.set(None); + show_ban_modal.set(true); + } + }; + + let submit_ban_request = { + move |_| { + let raw = ban_address.read().to_string(); + let target = match normalize_wallet_address_str(&raw) { + Ok(addr) => addr, + Err(err) => { + ban_error.set(Some(err.to_string())); + return; + } + }; + + let opened = chat.read().opened_group.clone(); + let Some(group_id) = opened else { + return; + }; + + ban_error.set(None); + show_ban_modal.set(false); + ban_address.set(String::new()); + + let addr_to_ban = target.clone(); + spawn(async move { + let _ = GATEWAY + .send(AppCmd::SendBanRequest { + group_id: group_id.clone(), + user_to_ban: addr_to_ban, + }) + .await; + }); + } + }; + + let oninput_ban_address = { + move |e: FormEvent| { + ban_error.set(None); + ban_address.set(e.value()) + } + }; + + let close_ban_modal = { + move || { + ban_address.set(String::new()); + ban_error.set(None); + show_ban_modal.set(false); + } + }; + + let cancel_ban_modal = { + move |_| { + ban_address.set(String::new()); + ban_error.set(None); + show_ban_modal.set(false); + } + }; + + let msgs_for_group = { + let opened = chat.read().opened_group.clone(); + chat.read() + .messages + .iter() + .filter(|m| Some(m.group_name.as_str()) == opened.as_deref()) + .cloned() + .collect::>() + }; + + let my_name = Arc::new(session.read().address.clone()); + let my_name_for_leave = my_name.clone(); + + let members_snapshot = chat.read().members.clone(); + let my_address = (*my_name).clone(); + let selectable_members: Vec = members_snapshot + .into_iter() + .filter(|member| !member.eq_ignore_ascii_case(&my_address)) + .collect(); + + let pick_member_handler = { + move |member: String| { + move |_| { + ban_error.set(None); + ban_address.set(member.clone()); + } + } + }; + + rsx! { + div { class: "panel chat", + div { class: "chat-header", + h2 { "Chat" } + if let Some(gid) = chat.read().opened_group.clone() { + button { + class: "ghost mini", + onclick: move |_| { + let group_id = gid.clone(); + let addr = my_name_for_leave.clone(); + // Send a self-ban (leave) request: requester filled by backend + spawn(async move { + let _ = GATEWAY + .send(AppCmd::SendBanRequest { group_id: group_id.clone(), user_to_ban: (*addr).clone() }) + .await; + }); + }, + "Leave group" + } + button { + class: "ghost mini", + onclick: open_ban_modal, + "Request ban" + } + } + } + if chat.read().opened_group.is_none() { + div { class: "hint", "Pick a group to chat." } + } else { + div { class: "messages", + for (i, m) in msgs_for_group.iter().enumerate() { + if (*my_name).clone() == m.sender || m.sender.eq_ignore_ascii_case("me") { + div { key: "{i}", class: "msg me", + span { class: "from", "{m.sender}" } + span { class: "body", "{String::from_utf8_lossy(&m.message)}" } + } + } else if m.sender.eq_ignore_ascii_case("system") { + div { key: "{i}", class: "msg system", + span { class: "body", "{String::from_utf8_lossy(&m.message)}" } + } + } else { + div { key: "{i}", class: "msg", + span { class: "from", "{m.sender}" } + span { class: "body", "{String::from_utf8_lossy(&m.message)}" } + } + } + } + } + div { class: "composer", + input { + r#type: "text", + value: "{msg_input}", + oninput: move |e| msg_input.set(e.value()), + placeholder: "Type a message…", + } + button { class: "primary", onclick: send_msg, "Send" } + } + } + } + + if *show_ban_modal.read() { + Modal { + title: "Request user ban".to_string(), + on_close: close_ban_modal, + div { class: "form-row", + label { "User address" } + input { + r#type: "text", + value: "{ban_address}", + oninput: oninput_ban_address, + placeholder: "0x...", + } + if let Some(error) = &*ban_error.read() { + span { class: "input-error", "{error}" } + } + } + if selectable_members.is_empty() { + div { class: "hint muted", "No members loaded yet." } + } else { + div { class: "member-picker", + span { class: "helper", "Or pick a member:" } + div { class: "member-list", + for member in selectable_members.iter() { + div { + key: "{member}", + class: "member-item", + div { class: "member-actions", + span { class: "member-id mono", "{member}" } + button { + class: "member-choose", + onclick: pick_member_handler(member.clone()), + "Choose" + } + } + } + } + } + } + } + div { class: "actions", + button { class: "primary", onclick: submit_ban_request, "Submit" } + button { + class: "ghost", + onclick: cancel_ban_modal, + "Cancel" + } + } + } + } + } +} + +fn ConsensusSection() -> Element { + let chat = use_context::>(); + let mut cons = use_context::>(); + + let vote_yes = { + move |_| { + let pending_proposal = cons.read().pending.clone(); + if let Some(v) = pending_proposal { + // Clear the pending proposal immediately to close the vote window + cons.write().pending = None; + spawn(async move { + let _ = GATEWAY + .send(AppCmd::Vote { + group_id: v.group_id.clone(), + proposal_id: v.proposal_id, + choice: true, + }) + .await; + }); + } + } + }; + let vote_no = { + move |_| { + let pending_proposal = cons.read().pending.clone(); + if let Some(v) = pending_proposal { + // Clear the pending proposal immediately to close the vote window + cons.write().pending = None; + spawn(async move { + let _ = GATEWAY + .send(AppCmd::Vote { + group_id: v.group_id.clone(), + proposal_id: v.proposal_id, + choice: false, + }) + .await; + }); + } + } + }; + + let opened = chat.read().opened_group.clone(); + let pending = cons + .read() + .pending + .clone() + .filter(|p| Some(p.group_id.as_str()) == opened.as_deref()); + + rsx! { + div { class: "panel consensus", + h2 { "Consensus" } + + if let Some(_group) = opened { + // Steward status + div { class: "status", + span { class: "muted", "You are " } + if cons.read().is_steward { + span { class: "good", "a steward" } + } else { + span { class: "bad", "not a steward" } + } + } + + // Pending Requests section + div { class: "consensus-section", + h3 { "Pending Requests" } + if cons.read().is_steward && !cons.read().current_epoch_proposals.is_empty() { + div { class: "proposals-window", + for (action, address) in &cons.read().current_epoch_proposals { + div { class: "proposal-item", + span { class: "action", "{action}:" } + span { class: "value", "{address}" } + } + } + } + } else { + div { class: "no-data", "No pending requests" } + } + } + + // Proposal for Vote section + div { class: "consensus-section", + h3 { "Proposal for Vote" } + if let Some(v) = pending { + div { class: "proposals-window", + div { class: "proposal-item proposal-id", + span { class: "action", "Proposal ID:" } + span { class: "value", "{v.proposal_id}" } + } + for (action, id) in convert_group_requests_to_display(&v.group_requests) { + div { class: "proposal-item", + span { class: "action", "{action}:" } + span { class: "value", "{id}" } + } + } + } + div { class: "vote-actions", + button { class: "primary", onclick: vote_yes, "YES" } + button { class: "ghost", onclick: vote_no, "NO" } + } + } else { + div { class: "no-data", "No proposal for vote" } + } + } + + // Latest Decisions section + div { class: "consensus-section", + h3 { "Latest Decisions" } + if cons.read().latest_results.is_empty() { + div { class: "no-data", "No latest decisions" } + } else { + div { class: "results-window", + for (vid, res, timestamp_ms) in cons.read().latest_results.iter().rev() { + div { class: "result-item", + span { class: "proposal-id", "{vid}" } + span { + class: match res { + Outcome::Accepted => "outcome accepted", + Outcome::Rejected => "outcome rejected", + Outcome::Unspecified => "outcome unspecified", + }, + match res { + Outcome::Accepted => "Accepted", + Outcome::Rejected => "Rejected", + Outcome::Unspecified => "Unspecified", + } + } + span { class: "timestamp", + "{format_timestamp(*timestamp_ms)}" + } + } + } + } + } + } + } else { + div { class: "hint", "Open a group to see proposals & voting." } + } + } + } +} + +// ─────────────────────────── Modal ─────────────────────────── + +#[derive(Props, PartialEq, Clone)] +struct ModalProps { + title: String, + children: Element, + on_close: EventHandler, +} +fn Modal(props: ModalProps) -> Element { + rsx! { + div { class: "modal-backdrop", onclick: move |_| (props.on_close)(()), + div { class: "modal", onclick: move |e| e.stop_propagation(), + div { class: "modal-head", + h3 { "{props.title}" } + button { class: "icon", onclick: move |_| (props.on_close)(()), "✕" } + } + div { class: "modal-body", {props.children} } + } + } + } +} diff --git a/build.rs b/build.rs index 640ceea..3c9b506 100644 --- a/build.rs +++ b/build.rs @@ -1,9 +1,9 @@ fn main() -> Result<(), std::io::Error> { prost_build::compile_protos( &[ + "src/protos/messages/v1/consensus.proto", "src/protos/messages/v1/welcome.proto", "src/protos/messages/v1/application.proto", - "src/protos/messages/v1/consensus.proto", ], &["src/protos/"], )?; diff --git a/crates/de_mls_gateway/Cargo.toml b/crates/de_mls_gateway/Cargo.toml new file mode 100644 index 0000000..2660405 --- /dev/null +++ b/crates/de_mls_gateway/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "de_mls_gateway" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +tokio = { version = "1.43.0", features = [ + "macros", + "rt-multi-thread", + "sync", + "time", +] } +anyhow = "1.0.100" +kameo = "0.13.0" +futures = "0.3.31" +uuid = { version = "1.18.1", features = ["v4", "serde"] } + +de_mls_ui_protocol = { path = "../de_mls_ui_protocol" } +ds = { path = "../../ds" } +mls_crypto = { path = "../../mls_crypto" } +once_cell = "1.21.3" +parking_lot = "0.12.5" +de_mls = { path = "../../" } +tracing = "0.1.41" +tracing-subscriber = "0.3.20" +hex = "0.4" diff --git a/crates/de_mls_gateway/src/forwarder.rs b/crates/de_mls_gateway/src/forwarder.rs new file mode 100644 index 0000000..0d02684 --- /dev/null +++ b/crates/de_mls_gateway/src/forwarder.rs @@ -0,0 +1,151 @@ +use kameo::actor::ActorRef; + +use std::sync::{atomic::Ordering, Arc}; +use tracing::info; + +use de_mls::{ + message::MessageType, + protos::de_mls::messages::v1::{app_message, ConversationMessage}, + user::{User, UserAction}, + user_actor::LeaveGroupRequest, + user_app_instance::CoreCtx, +}; +use de_mls_ui_protocol::v1::AppEvent; + +use crate::Gateway; + +impl Gateway { + pub(crate) fn spawn_consensus_forwarder(&self, core: Arc) -> anyhow::Result<()> { + let evt_tx = self.evt_tx.clone(); + let mut rx = core.consensus.subscribe_decisions(); + + tokio::spawn(async move { + tracing::info!("gateway: consensus forwarder started"); + while let Ok(res) = rx.recv().await { + let _ = evt_tx.unbounded_send(AppEvent::ProposalDecided(res)); + } + tracing::info!("gateway: consensus forwarder ended"); + }); + Ok(()) + } + + /// Spawn the pubsub forwarder once, after first successful login. + pub(crate) fn spawn_waku_forwarder(&self, core: Arc, user: ActorRef) { + if self.started.swap(true, Ordering::SeqCst) { + return; + } + + let evt_tx = self.evt_tx.clone(); + + tokio::spawn(async move { + let mut rx = core.app_state.pubsub.subscribe(); + tracing::info!("gateway: pubsub forwarder started"); + + while let Ok(wmsg) = rx.recv().await { + let content_topic = wmsg.content_topic.clone(); + + // fast-topic filter + if !core.topics.contains(&content_topic).await { + continue; + } + + // hand over to user actor to decide action + let action = match user.ask(wmsg).await { + Ok(a) => a, + Err(e) => { + tracing::warn!("user.ask failed: {e}"); + continue; + } + }; + + // route the action + let res = match action { + UserAction::SendToWaku(msg) => core + .app_state + .waku_node + .send(msg) + .await + .map_err(|e| anyhow::anyhow!("error sending waku message: {e}")), + + UserAction::SendToApp(app_msg) => { + // voting + let res = match &app_msg.payload { + Some(app_message::Payload::VotePayload(vp)) => evt_tx + .unbounded_send(AppEvent::VoteRequested(vp.clone())) + .map_err(|e| { + anyhow::anyhow!("error sending vote requested event: {e}") + }) + .and_then(|_| { + // Also clear current epoch proposals when voting starts + evt_tx.unbounded_send(AppEvent::CurrentEpochProposalsCleared { + group_id: vp.group_id.clone(), + }) + .map_err(|e| { + anyhow::anyhow!("error sending clear current epoch proposals event: {e}") + }) + }), + Some(app_message::Payload::ProposalAdded(pa)) => evt_tx + .unbounded_send(AppEvent::from(pa.clone())) + .map_err(|e| { + anyhow::anyhow!("error sending proposal added event: {e}") + }), + Some(app_message::Payload::BanRequest(br)) => evt_tx + .unbounded_send(AppEvent::from(br.clone())) + .map_err(|e| { + anyhow::anyhow!("error sending proposal added event (ban request): {e}") + }), + Some(app_message::Payload::ConversationMessage(cm)) => evt_tx + .unbounded_send(AppEvent::ChatMessage(ConversationMessage { + message: cm.message.clone(), + sender: cm.sender.clone(), + group_name: cm.group_name.clone(), + })) + .map_err(|e| anyhow::anyhow!("error sending chat message: {e}")), + _ => { + AppEvent::Error(format!("Invalid app message: {:?}", app_msg.payload.unwrap().message_type())); + Ok::<(), anyhow::Error>(()) + } + }; + match res { + Ok(()) => Ok(()), + Err(e) => Err(anyhow::anyhow!("error sending app message: {e}")), + } + } + + UserAction::LeaveGroup(group_name) => { + let _ = user + .ask(LeaveGroupRequest { + group_name: group_name.clone(), + }) + .await + .map_err(|e| anyhow::anyhow!("error leaving group: {e}")); + + core.topics.remove_many(&group_name).await; + info!("Leave group: {:?}", &group_name); + + let _ = evt_tx + .unbounded_send(AppEvent::GroupRemoved(group_name.clone())) + .map_err(|e| anyhow::anyhow!("error sending group removed event: {e}")); + + let _ = evt_tx + .unbounded_send(AppEvent::ChatMessage(ConversationMessage { + message: format!("You're removed from the group {group_name}") + .into_bytes(), + sender: "system".to_string(), + group_name: group_name.clone(), + })) + .map_err(|e| anyhow::anyhow!("error sending chat message: {e}")); + Ok::<(), anyhow::Error>(()) + } + UserAction::DoNothing => Ok(()), + }; + + if let Err(e) = res { + tracing::warn!("error handling waku action: {e:?}"); + } + } + + tracing::info!("gateway: pubsub forwarder ended"); + }); + } +} diff --git a/crates/de_mls_gateway/src/group.rs b/crates/de_mls_gateway/src/group.rs new file mode 100644 index 0000000..81470d5 --- /dev/null +++ b/crates/de_mls_gateway/src/group.rs @@ -0,0 +1,281 @@ +use std::time::Duration; +use tracing::info; + +use de_mls::{ + protos::de_mls::messages::v1::{app_message, BanRequest}, + steward, + user::UserAction, + user_actor::{ + CreateGroupRequest, GetCurrentEpochProposalsRequest, GetGroupMembersRequest, + GetProposalsForStewardVotingRequest, IsStewardStatusRequest, SendGroupMessage, + StartStewardEpochRequest, StewardMessageRequest, UserVoteRequest, + }, + user_app_instance::STEWARD_EPOCH, +}; +use de_mls_ui_protocol::v1::AppEvent; + +use crate::Gateway; + +impl Gateway { + pub async fn create_group(&self, group_name: String) -> anyhow::Result<()> { + let core = self.core(); + let user = self.user()?; + user.ask(CreateGroupRequest { + group_name: group_name.clone(), + is_creation: true, + }) + .await?; + core.topics.add_many(&group_name).await; + core.groups.insert(group_name.clone()).await; + info!("User start sending steward message for group {group_name:?}"); + let user_clone = user.clone(); + let group_name_clone = group_name.clone(); + let evt_tx_clone = self.evt_tx.clone(); + tokio::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(STEWARD_EPOCH)); + loop { + interval.tick().await; + // Step 1: Start steward epoch - check for proposals and start epoch if needed + let proposals_count = match user_clone + .ask(StartStewardEpochRequest { + group_name: group_name.clone(), + }) + .await + { + Ok(count) => count, + Err(e) => { + tracing::warn!( + "start steward epoch request failed for group {group_name:?}: {e}" + ); + continue; + } + }; + + // Step 2: Send new steward key to the waku node for new epoch + let msg = match user_clone + .ask(StewardMessageRequest { + group_name: group_name.clone(), + }) + .await + { + Ok(msg) => msg, + Err(e) => { + tracing::warn!( + "steward message request failed for group {group_name:?}: {e}" + ); + continue; + } + }; + if let Err(e) = core.app_state.waku_node.send(msg).await { + tracing::warn!("failed to send steward message for group {group_name:?}: {e}"); + continue; + } + + if proposals_count == 0 { + info!("No proposals to vote on for group: {group_name}, completing epoch without voting"); + } else { + info!("Found {proposals_count} proposals to vote on for group: {group_name}"); + + // Step 3: Start voting process - steward gets proposals for voting + let action = match user_clone + .ask(GetProposalsForStewardVotingRequest { + group_name: group_name.clone(), + }) + .await + { + Ok(action) => action, + Err(e) => { + tracing::warn!( + "get proposals for steward voting failed for group {group_name:?}: {e}" + ); + continue; + } + }; + + // Step 4: Send proposals to ws to steward to vote or do nothing if no proposals + // After voting, steward sends vote and proposal to waku node and start consensus process + match action { + UserAction::SendToApp(app_msg) => { + if let Some(app_message::Payload::VotePayload(vp)) = &app_msg.payload { + if let Err(e) = + evt_tx_clone.unbounded_send(AppEvent::VoteRequested(vp.clone())) + { + tracing::warn!("failed to send vote requested event: {e}"); + } + + // Also clear current epoch proposals when voting starts + if let Err(e) = evt_tx_clone.unbounded_send( + AppEvent::CurrentEpochProposalsCleared { + group_id: group_name.clone(), + }, + ) { + tracing::warn!("failed to send proposals cleared event: {e}"); + } + } + if let Some(app_message::Payload::ProposalAdded(pa)) = &app_msg.payload + { + if let Err(e) = + evt_tx_clone.unbounded_send(AppEvent::from(pa.clone())) + { + tracing::warn!("failed to send proposal added event: {e}"); + } + } + } + UserAction::DoNothing => { + info!("No action to take for group: {group_name}"); + return Ok(()); + } + _ => { + return Err(anyhow::anyhow!("Invalid user action: {action}")); + } + } + } + } + }); + tracing::debug!("User started sending steward message for group {group_name_clone:?}"); + Ok(()) + } + + pub async fn join_group(&self, group_name: String) -> anyhow::Result<()> { + let core = self.core(); + let user = self.user()?; + user.ask(CreateGroupRequest { + group_name: group_name.clone(), + is_creation: false, + }) + .await?; + core.topics.add_many(&group_name).await; + core.groups.insert(group_name.clone()).await; + tracing::debug!("User joined group {group_name}"); + tracing::debug!( + "User have topic for group {:?}", + core.topics.snapshot().await + ); + Ok(()) + } + + pub async fn send_message(&self, group_name: String, message: String) -> anyhow::Result<()> { + let core = self.core(); + let user = self.user()?; + let pmt = user + .ask(SendGroupMessage { + message: message.clone().into_bytes(), + group_name: group_name.clone(), + }) + .await?; + core.app_state.waku_node.send(pmt).await?; + Ok(()) + } + + pub async fn send_ban_request( + &self, + group_name: String, + user_to_ban: String, + ) -> anyhow::Result<()> { + let core = self.core(); + let user = self.user()?; + + let ban_request = BanRequest { + user_to_ban: user_to_ban.clone(), + requester: String::new(), + group_name: group_name.clone(), + }; + + let msg = user + .ask(de_mls::user_actor::BuildBanMessage { + ban_request, + group_name: group_name.clone(), + }) + .await?; + match msg { + UserAction::SendToWaku(msg) => { + core.app_state.waku_node.send(msg).await?; + } + UserAction::SendToApp(app_msg) => { + let event = match app_msg.payload { + Some(app_message::Payload::ProposalAdded(ref proposal)) => { + AppEvent::from(proposal.clone()) + } + Some(app_message::Payload::BanRequest(ref ban_request)) => { + AppEvent::from(ban_request.clone()) + } + _ => return Err(anyhow::anyhow!("Invalid user action")), + }; + self.push_event(event); + } + _ => return Err(anyhow::anyhow!("Invalid user action")), + } + + Ok(()) + } + + pub async fn process_user_vote( + &self, + group_name: String, + proposal_id: u32, + vote: bool, + ) -> anyhow::Result<()> { + let user = self.user()?; + + let user_vote_result = user + .ask(UserVoteRequest { + group_name: group_name.clone(), + proposal_id, + vote, + }) + .await?; + if let Some(waku_msg) = user_vote_result { + self.core().app_state.waku_node.send(waku_msg).await?; + } + Ok(()) + } + + pub async fn group_list(&self) -> Vec { + let core = self.core(); + core.groups.all().await + } + + pub async fn get_steward_status(&self, group_name: String) -> anyhow::Result { + let user = self.user()?; + let is_steward = user.ask(IsStewardStatusRequest { group_name }).await?; + Ok(is_steward) + } + + /// Get current epoch proposals for the given group + pub async fn get_current_epoch_proposals( + &self, + group_name: String, + ) -> anyhow::Result> { + let user = self.user()?; + + let proposals = user + .ask(GetCurrentEpochProposalsRequest { group_name }) + .await?; + let display_proposals: Vec<(String, String)> = proposals + .iter() + .map(|proposal| match proposal { + steward::GroupUpdateRequest::AddMember(kp) => { + let address = format!( + "0x{}", + hex::encode(kp.leaf_node().credential().serialized_content()) + ); + ("Add Member".to_string(), address) + } + steward::GroupUpdateRequest::RemoveMember(id) => { + ("Remove Member".to_string(), id.clone()) + } + }) + .collect(); + Ok(display_proposals) + } + + pub async fn get_group_members(&self, group_name: String) -> anyhow::Result> { + let user = self.user()?; + let members = user + .ask(GetGroupMembersRequest { + group_name: group_name.clone(), + }) + .await?; + Ok(members) + } +} diff --git a/crates/de_mls_gateway/src/lib.rs b/crates/de_mls_gateway/src/lib.rs new file mode 100644 index 0000000..93abeac --- /dev/null +++ b/crates/de_mls_gateway/src/lib.rs @@ -0,0 +1,138 @@ +//! de_mls_gateway: a thin facade between UI (AppCmd/AppEvent) and the core runtime. +//! +//! Responsibilities: +//! - Own a single event pipe UI <- gateway (`AppEvent`) +//! - Provide a command entrypoint UI -> gateway (`send(AppCmd)`) +//! - Hold references to the core context (`CoreCtx`) and current user actor +//! - Offer small helper methods (login_with_private_key, etc.) +use futures::{ + channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}, + StreamExt, +}; +use kameo::actor::ActorRef; +use once_cell::sync::Lazy; +use parking_lot::RwLock; +use std::sync::{atomic::AtomicBool, Arc}; +use tokio::sync::Mutex; + +use de_mls::{ + user::User, + user_app_instance::{create_user_instance, CoreCtx}, +}; +use de_mls_ui_protocol::v1::{AppCmd, AppEvent}; + +mod forwarder; +mod group; +// Global, process-wide gateway instance +pub static GATEWAY: Lazy = Lazy::new(Gateway::new); + +/// Helper to set the core context once during startup (called by ui_bridge). +pub fn init_core(core: Arc) { + GATEWAY.set_core(core); +} + +pub struct Gateway { + // UI events (gateway -> UI) + // A channel that sends AppEvents to the UI. + evt_tx: UnboundedSender, + // A channel that receives AppEvents from the UI. + evt_rx: Mutex>, + + // UI commands (UI -> gateway) + // A channel that sends AppCommands to the gateway (ui_bridge registers the sender here). + // It gives the UI an async door to submit AppCmds back to the gateway (`Gateway::send(AppCmd)`). + cmd_tx: RwLock>>, + + // It anchors the shared references to consensus, topics, app_state, etc. + core: RwLock>>, // set once during startup + + // Current logged-in user actor + user: RwLock>>, + // Flag that guards against spawning the Waku forwarder more than once. + // It's initialized to false and set to true after the first successful login. + started: AtomicBool, +} + +impl Gateway { + fn new() -> Self { + let (evt_tx, evt_rx) = unbounded(); + Self { + evt_tx, + evt_rx: Mutex::new(evt_rx), + cmd_tx: RwLock::new(None), + core: RwLock::new(None), + user: RwLock::new(None), + started: AtomicBool::new(false), + } + } + + /// Called once by the bootstrap (ui_bridge) to provide the core context. + pub fn set_core(&self, core: Arc) { + *self.core.write() = Some(core); + } + + pub fn core(&self) -> Arc { + self.core + .read() + .as_ref() + .expect("Gateway core not initialized") + .clone() + } + + /// ui_bridge registers its command sender so `send` can work. + pub fn register_cmd_sink(&self, tx: UnboundedSender) { + *self.cmd_tx.write() = Some(tx); + } + + /// Push an event to the UI. + pub fn push_event(&self, evt: AppEvent) { + let _ = self.evt_tx.unbounded_send(evt); + } + + /// Await next event on the UI side. + pub async fn next_event(&self) -> Option { + let mut rx = self.evt_rx.lock().await; + rx.next().await + } + + /// UI convenience: enqueue a command (UI -> gateway). + pub async fn send(&self, cmd: AppCmd) -> anyhow::Result<()> { + if let Some(tx) = self.cmd_tx.read().clone() { + tx.unbounded_send(cmd) + .map_err(|e| anyhow::anyhow!("send cmd failed: {e}")) + } else { + Err(anyhow::anyhow!("cmd sink not registered")) + } + } + + // ─────────────────────────── High-level helpers ─────────────────────────── + + /// Create the user actor with a private key (no group yet). + /// Returns a derived display name (e.g., address string). + pub async fn login_with_private_key(&self, private_key: String) -> anyhow::Result { + let core = self.core(); + let consensus_service = core.consensus.as_ref().clone(); + + // Create user actor via core helper (you implement this inside your core) + let (user_ref, user_address) = create_user_instance( + private_key.clone(), + core.app_state.clone(), + &consensus_service, + ) + .await?; + + *self.user.write() = Some(user_ref.clone()); + + self.spawn_waku_forwarder(core.clone(), user_ref.clone()); + self.spawn_consensus_forwarder(core.clone())?; + Ok(user_address) + } + + /// Get a copy of the current user ref (if logged in). + pub fn user(&self) -> anyhow::Result> { + self.user + .read() + .clone() + .ok_or_else(|| anyhow::anyhow!("user not logged in")) + } +} diff --git a/crates/de_mls_ui_protocol/Cargo.toml b/crates/de_mls_ui_protocol/Cargo.toml new file mode 100644 index 0000000..cd1cf2f --- /dev/null +++ b/crates/de_mls_ui_protocol/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "de_mls_ui_protocol" +version = "0.1.0" +edition = "2021" +publish = false +license = "MIT OR Apache-2.0" + +[dependencies] +serde_json = "1.0" +serde = { version = "1.0.163", features = ["derive"] } +uuid = { version = "1.18.1", features = ["v4", "serde"] } +thiserror = "2.0.17" + +de_mls = { path = "../../" } +mls_crypto = { path = "../../mls_crypto" } \ No newline at end of file diff --git a/crates/de_mls_ui_protocol/src/lib.rs b/crates/de_mls_ui_protocol/src/lib.rs new file mode 100644 index 0000000..31fecc0 --- /dev/null +++ b/crates/de_mls_ui_protocol/src/lib.rs @@ -0,0 +1,127 @@ +//! UI <-> Gateway protocol (PoC). Keep it dependency-light (serde only). +// crates/de_mls_ui_protocol/src/lib.rs +pub mod v1 { + use de_mls::{ + message::MessageType, + protos::{ + consensus::v1::{ProposalResult, VotePayload}, + de_mls::messages::v1::{BanRequest, ConversationMessage, ProposalAdded}, + }, + }; + use mls_crypto::identity::normalize_wallet_address; + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Clone, Serialize, Deserialize)] + #[non_exhaustive] + pub enum AppCmd { + Login { + private_key: String, + }, + ListGroups, + CreateGroup { + name: String, + }, + JoinGroup { + name: String, + }, + EnterGroup { + group_id: String, + }, + SendMessage { + group_id: String, + body: String, + }, + LoadHistory { + group_id: String, + }, + Vote { + group_id: String, + proposal_id: u32, + choice: bool, + }, + LeaveGroup { + group_id: String, + }, + GetStewardStatus { + group_id: String, + }, + GetCurrentEpochProposals { + group_id: String, + }, + SendBanRequest { + group_id: String, + user_to_ban: String, + }, + GetGroupMembers { + group_id: String, + }, + } + + #[derive(Debug, Clone)] + #[non_exhaustive] + pub enum AppEvent { + LoggedIn(String), + Groups(Vec), + GroupCreated(String), + GroupRemoved(String), + EnteredGroup { + group_id: String, + }, + ChatMessage(ConversationMessage), + LeaveGroup { + group_id: String, + }, + + StewardStatus { + group_id: String, + is_steward: bool, + }, + + VoteRequested(VotePayload), + ProposalDecided(ProposalResult), + CurrentEpochProposals { + group_id: String, + proposals: Vec<(String, String)>, + }, + ProposalAdded { + group_id: String, + action: String, + address: String, + }, + CurrentEpochProposalsCleared { + group_id: String, + }, + GroupMembers { + group_id: String, + members: Vec, + }, + Error(String), + } + + impl From for AppEvent { + fn from(proposal_added: ProposalAdded) -> Self { + AppEvent::ProposalAdded { + group_id: proposal_added.group_id.clone(), + action: proposal_added + .request + .as_ref() + .unwrap() + .message_type() + .to_string(), + address: normalize_wallet_address( + &proposal_added.request.as_ref().unwrap().wallet_address, + ), + } + } + } + + impl From for AppEvent { + fn from(ban_request: BanRequest) -> Self { + AppEvent::ProposalAdded { + group_id: ban_request.group_name.clone(), + action: "Remove Member".to_string(), + address: ban_request.user_to_ban.clone(), + } + } + } +} diff --git a/crates/ui_bridge/Cargo.toml b/crates/ui_bridge/Cargo.toml new file mode 100644 index 0000000..5540e98 --- /dev/null +++ b/crates/ui_bridge/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ui_bridge" +version = "0.1.0" +edition = "2021" + +[dependencies] +futures = "0.3.31" +anyhow = "1.0.100" +uuid = { version = "1.18.1", features = ["v4", "serde"] } +tracing = "0.1.41" +tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] } + +de_mls_gateway = { path = "../de_mls_gateway" } +de_mls_ui_protocol = { path = "../de_mls_ui_protocol" } +de_mls = { path = "../../" } diff --git a/crates/ui_bridge/src/lib.rs b/crates/ui_bridge/src/lib.rs new file mode 100644 index 0000000..a16e95d --- /dev/null +++ b/crates/ui_bridge/src/lib.rs @@ -0,0 +1,201 @@ +//! ui_bridge +//! +//! Owns the command loop translating `AppCmd` -> core calls +//! and pushing `AppEvent` back to the UI via the Gateway. +//! +//! It ensures there is a Tokio runtime (desktop app may not have one yet). + +// crates/ui_bridge/src/lib.rs +use futures::channel::mpsc::{unbounded, UnboundedReceiver}; +use futures::StreamExt; +use std::sync::Arc; + +use de_mls::protos::de_mls::messages::v1::ConversationMessage; +use de_mls::user_app_instance::CoreCtx; +use de_mls_gateway::{init_core, GATEWAY}; +use de_mls_ui_protocol::v1::{AppCmd, AppEvent}; + +/// Call once during process startup (before launching the Dioxus UI). +pub fn start_ui_bridge(core: Arc) { + // 1) Give the gateway access to the core context. + init_core(core); + + // 2) Create a command channel UI -> gateway and register the sender. + let (cmd_tx, cmd_rx) = unbounded::(); + GATEWAY.register_cmd_sink(cmd_tx); + + // 3) Drive the dispatcher loop on a Tokio runtime + if let Ok(handle) = tokio::runtime::Handle::try_current() { + handle.spawn(async move { + if let Err(e) = ui_loop(cmd_rx).await { + tracing::error!("ui_loop crashed: {e}"); + } + }); + } else { + std::thread::Builder::new() + .name("ui-bridge".into()) + .spawn(move || { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("tokio runtime"); + rt.block_on(async move { + if let Err(e) = ui_loop(cmd_rx).await { + eprintln!("ui_loop crashed: {e:?}"); + } + }); + }) + .expect("spawn ui-bridge"); + } +} + +async fn ui_loop(mut cmd_rx: UnboundedReceiver) -> anyhow::Result<()> { + while let Some(cmd) = cmd_rx.next().await { + match cmd { + // ───────────── Authentication / session ───────────── + AppCmd::Login { private_key } => { + match GATEWAY.login_with_private_key(private_key).await { + Ok(derived_name) => GATEWAY.push_event(AppEvent::LoggedIn(derived_name)), + Err(e) => GATEWAY.push_event(AppEvent::Error(format!("Login failed: {e}"))), + } + } + + // ───────────── Groups ───────────── + AppCmd::ListGroups => { + let groups = GATEWAY.group_list().await; + GATEWAY.push_event(AppEvent::Groups(groups)); + } + + AppCmd::CreateGroup { name } => { + GATEWAY.create_group(name.clone()).await?; + + let groups = GATEWAY.group_list().await; + GATEWAY.push_event(AppEvent::Groups(groups)); + } + + AppCmd::JoinGroup { name } => { + GATEWAY.join_group(name.clone()).await?; + + let groups = GATEWAY.group_list().await; + GATEWAY.push_event(AppEvent::Groups(groups)); + } + + AppCmd::EnterGroup { group_id } => { + GATEWAY.push_event(AppEvent::EnteredGroup { group_id }); + } + + AppCmd::LeaveGroup { group_id } => { + GATEWAY.push_event(AppEvent::LeaveGroup { group_id }); + } + + AppCmd::GetGroupMembers { group_id } => { + match GATEWAY.get_group_members(group_id.clone()).await { + Ok(members) => { + GATEWAY.push_event(AppEvent::GroupMembers { group_id, members }); + } + Err(e) => { + GATEWAY + .push_event(AppEvent::Error(format!("Get group members failed: {e}"))); + } + } + } + + AppCmd::SendBanRequest { + group_id, + user_to_ban, + } => { + if let Err(e) = GATEWAY + .send_ban_request(group_id.clone(), user_to_ban.clone()) + .await + { + GATEWAY.push_event(AppEvent::Error(format!("Send ban request failed: {e}"))); + } else { + GATEWAY.push_event(AppEvent::ChatMessage(ConversationMessage { + message: "You requested to leave or ban user from the group" + .to_string() + .into_bytes(), + sender: "system".to_string(), + group_name: group_id.clone(), + })); + } + } + + // ───────────── Chat ───────────── + AppCmd::SendMessage { group_id, body } => { + GATEWAY.push_event(AppEvent::ChatMessage(ConversationMessage { + message: body.as_bytes().to_vec(), + sender: "me".to_string(), + group_name: group_id.clone(), + })); + + GATEWAY.send_message(group_id, body).await?; + } + + AppCmd::LoadHistory { group_id } => { + // TODO: load from storage; stub: + GATEWAY.push_event(AppEvent::ChatMessage(ConversationMessage { + message: "History loaded (stub)".as_bytes().to_vec(), + sender: "system".to_string(), + group_name: group_id.clone(), + })); + } + + // ───────────── Consensus ───────────── + AppCmd::Vote { + group_id, + proposal_id, + choice, + } => { + // Process the user vote: + // if it come from the user, send the vote result to Waku + // if it come from the steward, just process it and return None + GATEWAY + .process_user_vote(group_id.clone(), proposal_id, choice) + .await?; + + GATEWAY.push_event(AppEvent::ChatMessage(ConversationMessage { + message: format!( + "Your vote ({}) has been submitted for proposal {proposal_id}", + if choice { "YES" } else { "NO" } + ) + .as_bytes() + .to_vec(), + sender: "system".to_string(), + group_name: group_id.clone(), + })); + } + + AppCmd::GetCurrentEpochProposals { group_id } => { + match GATEWAY.get_current_epoch_proposals(group_id.clone()).await { + Ok(proposals) => { + GATEWAY.push_event(AppEvent::CurrentEpochProposals { + group_id, + proposals, + }); + } + Err(e) => GATEWAY.push_event(AppEvent::Error(format!( + "Get current epoch proposals failed: {e}" + ))), + } + } + + AppCmd::GetStewardStatus { group_id } => { + match GATEWAY.get_steward_status(group_id.clone()).await { + Ok(is_steward) => { + GATEWAY.push_event(AppEvent::StewardStatus { + group_id, + is_steward, + }); + } + Err(e) => GATEWAY + .push_event(AppEvent::Error(format!("Get steward status failed: {e}"))), + } + } + + other => { + tracing::warn!("unhandled AppCmd: {:?}", other); + } + } + } + Ok(()) +} diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 5aebfdf..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: ${NAME} -services: - frontend: - build: - context: frontend - dockerfile: Dockerfile - ports: - - ${FRONTEND_PORT}:5173 - environment: - - PUBLIC_API_URL=http://127.0.0.1:${BACKEND_PORT} - - PUBLIC_WEBSOCKET_URL=ws://127.0.0.1:${BACKEND_PORT} - depends_on: - - backend - - backend: - build: - context: . - dockerfile: Dockerfile - ports: - - ${BACKEND_PORT}:3000 - environment: - - RUST_LOG=info - - NODE_PORT=${NODE_PORT} - - PEER_ADDRESSES=${PEER_ADDRESSES} diff --git a/ds/Cargo.toml b/ds/Cargo.toml index 2ba79c9..44bccc3 100644 --- a/ds/Cargo.toml +++ b/ds/Cargo.toml @@ -26,5 +26,5 @@ thiserror = "1.0.39" serde_json = "1.0" serde = "1.0.163" -env_logger = "0.11.5" -log = "0.4.22" +tracing = "0.1.41" +tracing-subscriber = "0.3.20" diff --git a/ds/src/lib.rs b/ds/src/lib.rs index dccb737..5e33a3c 100644 --- a/ds/src/lib.rs +++ b/ds/src/lib.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use waku_bindings::{node::PubsubTopic, Encoding, WakuContentTopic}; +pub mod topic_filter; pub mod waku_actor; pub const GROUP_VERSION: &str = "1"; diff --git a/ds/src/topic_filter.rs b/ds/src/topic_filter.rs new file mode 100644 index 0000000..fc67e2d --- /dev/null +++ b/ds/src/topic_filter.rs @@ -0,0 +1,52 @@ +//! This module contains the topic filter for the Waku node +use tokio::sync::RwLock; +use waku_bindings::WakuContentTopic; + +use crate::build_content_topics; + +/// Fast allowlist for content topics without requiring Hash. +/// Internally uses a Vec and dedupes on insert. +#[derive(Default, Debug)] +pub struct TopicFilter { + list: RwLock>, +} + +impl TopicFilter { + pub fn new() -> Self { + Self::default() + } + + /// Build and add topics if not already present. + pub async fn add_many(&self, group_name: &str) { + let topics = build_content_topics(group_name); + self.list.write().await.extend(topics); + } + + /// Remove any matching topics. + pub async fn remove_many(&self, group_name: &str) { + let topics = build_content_topics(group_name); + self.list + .write() + .await + .retain(|x| !topics.iter().any(|t| t == x)); + } + + /// Membership test (first-stage filter). + #[inline] + pub async fn contains(&self, t: &WakuContentTopic) -> bool { + self.list.read().await.iter().any(|x| x == t) + } + + pub async fn snapshot(&self) -> Vec { + self.list.read().await.clone() + } + + pub async fn get_group_name(&self, t: &WakuContentTopic) -> Option { + self.list + .read() + .await + .iter() + .find(|x| x == &t) + .map(|x| x.application_name.clone().to_string()) + } +} diff --git a/ds/src/waku_actor.rs b/ds/src/waku_actor.rs index 5c9a378..ac4cb47 100644 --- a/ds/src/waku_actor.rs +++ b/ds/src/waku_actor.rs @@ -1,7 +1,7 @@ -use log::{debug, error, info}; use serde::{Deserialize, Serialize}; use std::{thread::sleep, time::Duration}; use tokio::sync::mpsc::{Receiver, Sender}; +use tracing::{debug, error, info}; use waku_bindings::{ node::{WakuNodeConfig, WakuNodeHandle}, waku_new, Initialized, LibwakuResponse, Multiaddr, Running, WakuEvent, WakuMessage, @@ -24,7 +24,7 @@ impl WakuNode { tcp_port: Some(port), cluster_id: Some(15), shards: vec![1], - log_level: Some("INFO"), // Supported: TRACE, DEBUG, INFO, NOTICE, WARN, ERROR or FATAL + log_level: Some("FATAL"), // Supported: TRACE, DEBUG, INFO, NOTICE, WARN, ERROR or FATAL ..Default::default() })) .await @@ -43,16 +43,16 @@ impl WakuNode { serde_json::from_str(v.unwrap().as_str()).expect("Parsing event to succeed"); match event { WakuEvent::WakuMessage(evt) => { - info!("WakuMessage event received: {:?}", evt.message_hash); + debug!("WakuMessage event received: {:?}", evt.message_hash); waku_sender .blocking_send(evt.waku_message.clone()) .expect("Failed to send message to waku"); } WakuEvent::RelayTopicHealthChange(evt) => { - info!("Relay topic change evt: {evt:?}"); + debug!("Relay topic change evt: {evt:?}"); } WakuEvent::ConnectionChange(evt) => { - info!("Conn change evt: {evt:?}"); + debug!("Conn change evt: {evt:?}"); } WakuEvent::Unrecognized(e) => panic!("Unrecognized waku event: {e:?}"), _ => panic!("event case not expected"), @@ -197,9 +197,9 @@ pub async fn run_waku_node( info!("Waiting for message to send to waku"); while let Some(msg) = receiver.recv().await { - info!("Received message to send to waku"); + debug!("Received message to send to waku"); let id = waku_node.send_message(msg).await?; - info!("Successfully publish message with id: {id:?}"); + debug!("Successfully publish message with id: {id:?}"); } Ok(()) diff --git a/ds/tests/ds_waku_test.rs b/ds/tests/ds_waku_test.rs index f3146ad..0872cf2 100644 --- a/ds/tests/ds_waku_test.rs +++ b/ds/tests/ds_waku_test.rs @@ -7,8 +7,8 @@ use kameo::{ message::{Context, Message}, Actor, }; -use log::info; use tokio::sync::mpsc::channel; +use tracing::info; use waku_bindings::WakuMessage; #[derive(Debug, Clone, Actor)] @@ -44,7 +44,7 @@ impl Message for Application { #[tokio::test(flavor = "multi_thread")] async fn test_waku_client() { - env_logger::init(); + tracing_subscriber::fmt::init(); let group_name = "new_group"; let mut pubsub = PubSub::::new(); diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index 6635cf5..0000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -.DS_Store -node_modules -/build -/.svelte-kit -/package -.env -.env.* -!.env.example -vite.config.js.timestamp-* -vite.config.ts.timestamp-* diff --git a/frontend/.netlify/functions-internal/render.json b/frontend/.netlify/functions-internal/render.json deleted file mode 100644 index 98d3d2e..0000000 --- a/frontend/.netlify/functions-internal/render.json +++ /dev/null @@ -1 +0,0 @@ -{"config":{"nodeModuleFormat":"esm"},"version":1} \ No newline at end of file diff --git a/frontend/.netlify/functions-internal/render.mjs b/frontend/.netlify/functions-internal/render.mjs deleted file mode 100644 index c9dfa1a..0000000 --- a/frontend/.netlify/functions-internal/render.mjs +++ /dev/null @@ -1,37 +0,0 @@ -import { init } from '../serverless.js'; - -export const handler = init({ - appDir: "_app", - appPath: "_app", - assets: new Set(["favicon.png"]), - mimeTypes: {".png":"image/png"}, - _: { - client: {"start":{"file":"_app/immutable/entry/start.530cd74f.js","imports":["_app/immutable/entry/start.530cd74f.js","_app/immutable/chunks/index.b5cfe40e.js","_app/immutable/chunks/singletons.995fdd8e.js","_app/immutable/chunks/index.0a9737cc.js"],"stylesheets":[],"fonts":[]},"app":{"file":"_app/immutable/entry/app.e73d9bc3.js","imports":["_app/immutable/entry/app.e73d9bc3.js","_app/immutable/chunks/index.b5cfe40e.js"],"stylesheets":[],"fonts":[]}}, - nodes: [ - () => import('../server/nodes/0.js'), - () => import('../server/nodes/1.js'), - () => import('../server/nodes/2.js'), - () => import('../server/nodes/3.js') - ], - routes: [ - { - id: "/", - pattern: /^\/$/, - params: [], - page: { layouts: [0], errors: [1], leaf: 2 }, - endpoint: null - }, - { - id: "/chat", - pattern: /^\/chat\/?$/, - params: [], - page: { layouts: [0], errors: [1], leaf: 3 }, - endpoint: null - } - ], - matchers: async () => { - - return { }; - } - } -}); diff --git a/frontend/.netlify/server/_app/immutable/assets/Toaster.d4bfa763.css b/frontend/.netlify/server/_app/immutable/assets/Toaster.d4bfa763.css deleted file mode 100644 index 2cb7b2a..0000000 --- a/frontend/.netlify/server/_app/immutable/assets/Toaster.d4bfa763.css +++ /dev/null @@ -1 +0,0 @@ -div.svelte-lzwg39{width:20px;opacity:0;height:20px;border-radius:10px;background:var(--primary, #61d345);position:relative;transform:rotate(45deg);animation:svelte-lzwg39-circleAnimation 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;animation-delay:100ms}div.svelte-lzwg39::after{content:'';box-sizing:border-box;animation:svelte-lzwg39-checkmarkAnimation 0.2s ease-out forwards;opacity:0;animation-delay:200ms;position:absolute;border-right:2px solid;border-bottom:2px solid;border-color:var(--secondary, #fff);bottom:6px;left:6px;height:10px;width:6px}@keyframes svelte-lzwg39-circleAnimation{from{transform:scale(0) rotate(45deg);opacity:0}to{transform:scale(1) rotate(45deg);opacity:1}}@keyframes svelte-lzwg39-checkmarkAnimation{0%{height:0;width:0;opacity:0}40%{height:0;width:6px;opacity:1}100%{opacity:1;height:10px}}div.svelte-10jnndo{width:20px;opacity:0;height:20px;border-radius:10px;background:var(--primary, #ff4b4b);position:relative;transform:rotate(45deg);animation:svelte-10jnndo-circleAnimation 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;animation-delay:100ms}div.svelte-10jnndo::after,div.svelte-10jnndo::before{content:'';animation:svelte-10jnndo-firstLineAnimation 0.15s ease-out forwards;animation-delay:150ms;position:absolute;border-radius:3px;opacity:0;background:var(--secondary, #fff);bottom:9px;left:4px;height:2px;width:12px}div.svelte-10jnndo:before{animation:svelte-10jnndo-secondLineAnimation 0.15s ease-out forwards;animation-delay:180ms;transform:rotate(90deg)}@keyframes svelte-10jnndo-circleAnimation{from{transform:scale(0) rotate(45deg);opacity:0}to{transform:scale(1) rotate(45deg);opacity:1}}@keyframes svelte-10jnndo-firstLineAnimation{from{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}@keyframes svelte-10jnndo-secondLineAnimation{from{transform:scale(0) rotate(90deg);opacity:0}to{transform:scale(1) rotate(90deg);opacity:1}}div.svelte-bj4lu8{width:12px;height:12px;box-sizing:border-box;border:2px solid;border-radius:100%;border-color:var(--secondary, #e0e0e0);border-right-color:var(--primary, #616161);animation:svelte-bj4lu8-rotate 1s linear infinite}@keyframes svelte-bj4lu8-rotate{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}.indicator.svelte-1c92bpz{position:relative;display:flex;justify-content:center;align-items:center;min-width:20px;min-height:20px}.status.svelte-1c92bpz{position:absolute}.animated.svelte-1c92bpz{position:relative;transform:scale(0.6);opacity:0.4;min-width:20px;animation:svelte-1c92bpz-enter 0.3s 0.12s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards}@keyframes svelte-1c92bpz-enter{from{transform:scale(0.6);opacity:0.4}to{transform:scale(1);opacity:1}}.message.svelte-o805t1{display:flex;justify-content:center;margin:4px 10px;color:inherit;flex:1 1 auto;white-space:pre-line}@keyframes svelte-15lyehg-enterAnimation{0%{transform:translate3d(0, calc(var(--factor) * -200%), 0) scale(0.6);opacity:0.5}100%{transform:translate3d(0, 0, 0) scale(1);opacity:1}}@keyframes svelte-15lyehg-exitAnimation{0%{transform:translate3d(0, 0, -1px) scale(1);opacity:1}100%{transform:translate3d(0, calc(var(--factor) * -150%), -1px) scale(0.6);opacity:0}}@keyframes svelte-15lyehg-fadeInAnimation{0%{opacity:0}100%{opacity:1}}@keyframes svelte-15lyehg-fadeOutAnimation{0%{opacity:1}100%{opacity:0}}.base.svelte-15lyehg{display:flex;align-items:center;background:#fff;color:#363636;line-height:1.3;will-change:transform;box-shadow:0 3px 10px rgba(0, 0, 0, 0.1), 0 3px 3px rgba(0, 0, 0, 0.05);max-width:350px;pointer-events:auto;padding:8px 10px;border-radius:8px}.transparent.svelte-15lyehg{opacity:0}.enter.svelte-15lyehg{animation:svelte-15lyehg-enterAnimation 0.35s cubic-bezier(0.21, 1.02, 0.73, 1) forwards}.exit.svelte-15lyehg{animation:svelte-15lyehg-exitAnimation 0.4s cubic-bezier(0.06, 0.71, 0.55, 1) forwards}.fadeIn.svelte-15lyehg{animation:svelte-15lyehg-fadeInAnimation 0.35s cubic-bezier(0.21, 1.02, 0.73, 1) forwards}.fadeOut.svelte-15lyehg{animation:svelte-15lyehg-fadeOutAnimation 0.4s cubic-bezier(0.06, 0.71, 0.55, 1) forwards}.wrapper.svelte-1pakgpd{left:0;right:0;display:flex;position:absolute;transform:translateY(calc(var(--offset, 16px) * var(--factor) * 1px))}.transition.svelte-1pakgpd{transition:all 230ms cubic-bezier(0.21, 1.02, 0.73, 1)}.active.svelte-1pakgpd{z-index:9999}.active.svelte-1pakgpd>*{pointer-events:auto}.toaster.svelte-jyff3d{--default-offset:16px;position:fixed;z-index:9999;top:var(--default-offset);left:var(--default-offset);right:var(--default-offset);bottom:var(--default-offset);pointer-events:none} \ No newline at end of file diff --git a/frontend/.netlify/server/_app/immutable/assets/_layout.ba8665a3.css b/frontend/.netlify/server/_app/immutable/assets/_layout.ba8665a3.css deleted file mode 100644 index 5546ebc..0000000 --- a/frontend/.netlify/server/_app/immutable/assets/_layout.ba8665a3.css +++ /dev/null @@ -1,3085 +0,0 @@ -/* -! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com -*//* -1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) -2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) -*/ - -*, -::before, -::after { - box-sizing: border-box; /* 1 */ - border-width: 0; /* 2 */ - border-style: solid; /* 2 */ - border-color: #e5e7eb; /* 2 */ -} - -::before, -::after { - --tw-content: ''; -} - -/* -1. Use a consistent sensible line-height in all browsers. -2. Prevent adjustments of font size after orientation changes in iOS. -3. Use a more readable tab size. -4. Use the user's configured `sans` font-family by default. -5. Use the user's configured `sans` font-feature-settings by default. -*/ - -html { - line-height: 1.5; /* 1 */ - -webkit-text-size-adjust: 100%; /* 2 */ - -moz-tab-size: 4; /* 3 */ - -o-tab-size: 4; - tab-size: 4; /* 3 */ - font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */ - font-feature-settings: normal; /* 5 */ -} - -/* -1. Remove the margin in all browsers. -2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. -*/ - -body { - margin: 0; /* 1 */ - line-height: inherit; /* 2 */ -} - -/* -1. Add the correct height in Firefox. -2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) -3. Ensure horizontal rules are visible by default. -*/ - -hr { - height: 0; /* 1 */ - color: inherit; /* 2 */ - border-top-width: 1px; /* 3 */ -} - -/* -Add the correct text decoration in Chrome, Edge, and Safari. -*/ - -abbr:where([title]) { - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; -} - -/* -Remove the default font size and weight for headings. -*/ - -h1, -h2, -h3, -h4, -h5, -h6 { - font-size: inherit; - font-weight: inherit; -} - -/* -Reset links to optimize for opt-in styling instead of opt-out. -*/ - -a { - color: inherit; - text-decoration: inherit; -} - -/* -Add the correct font weight in Edge and Safari. -*/ - -b, -strong { - font-weight: bolder; -} - -/* -1. Use the user's configured `mono` font family by default. -2. Correct the odd `em` font sizing in all browsers. -*/ - -code, -kbd, -samp, -pre { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/* -Add the correct font size in all browsers. -*/ - -small { - font-size: 80%; -} - -/* -Prevent `sub` and `sup` elements from affecting the line height in all browsers. -*/ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -/* -1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) -2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) -3. Remove gaps between table borders by default. -*/ - -table { - text-indent: 0; /* 1 */ - border-color: inherit; /* 2 */ - border-collapse: collapse; /* 3 */ -} - -/* -1. Change the font styles in all browsers. -2. Remove the margin in Firefox and Safari. -3. Remove default padding in all browsers. -*/ - -button, -input, -optgroup, -select, -textarea { - font-family: inherit; /* 1 */ - font-size: 100%; /* 1 */ - font-weight: inherit; /* 1 */ - line-height: inherit; /* 1 */ - color: inherit; /* 1 */ - margin: 0; /* 2 */ - padding: 0; /* 3 */ -} - -/* -Remove the inheritance of text transform in Edge and Firefox. -*/ - -button, -select { - text-transform: none; -} - -/* -1. Correct the inability to style clickable types in iOS and Safari. -2. Remove default button styles. -*/ - -button, -[type='button'], -[type='reset'], -[type='submit'] { - -webkit-appearance: button; /* 1 */ - background-color: transparent; /* 2 */ - background-image: none; /* 2 */ -} - -/* -Use the modern Firefox focus style for all focusable elements. -*/ - -:-moz-focusring { - outline: auto; -} - -/* -Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) -*/ - -:-moz-ui-invalid { - box-shadow: none; -} - -/* -Add the correct vertical alignment in Chrome and Firefox. -*/ - -progress { - vertical-align: baseline; -} - -/* -Correct the cursor style of increment and decrement buttons in Safari. -*/ - -::-webkit-inner-spin-button, -::-webkit-outer-spin-button { - height: auto; -} - -/* -1. Correct the odd appearance in Chrome and Safari. -2. Correct the outline style in Safari. -*/ - -[type='search'] { - -webkit-appearance: textfield; /* 1 */ - outline-offset: -2px; /* 2 */ -} - -/* -Remove the inner padding in Chrome and Safari on macOS. -*/ - -::-webkit-search-decoration { - -webkit-appearance: none; -} - -/* -1. Correct the inability to style clickable types in iOS and Safari. -2. Change font properties to `inherit` in Safari. -*/ - -::-webkit-file-upload-button { - -webkit-appearance: button; /* 1 */ - font: inherit; /* 2 */ -} - -/* -Add the correct display in Chrome and Safari. -*/ - -summary { - display: list-item; -} - -/* -Removes the default spacing and border for appropriate elements. -*/ - -blockquote, -dl, -dd, -h1, -h2, -h3, -h4, -h5, -h6, -hr, -figure, -p, -pre { - margin: 0; -} - -fieldset { - margin: 0; - padding: 0; -} - -legend { - padding: 0; -} - -ol, -ul, -menu { - list-style: none; - margin: 0; - padding: 0; -} - -/* -Prevent resizing textareas horizontally by default. -*/ - -textarea { - resize: vertical; -} - -/* -1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) -2. Set the default placeholder color to the user's configured gray 400 color. -*/ - -input::-moz-placeholder, textarea::-moz-placeholder { - opacity: 1; /* 1 */ - color: #9ca3af; /* 2 */ -} - -input::placeholder, -textarea::placeholder { - opacity: 1; /* 1 */ - color: #9ca3af; /* 2 */ -} - -/* -Set the default cursor for buttons. -*/ - -button, -[role="button"] { - cursor: pointer; -} - -/* -Make sure disabled buttons don't get the pointer cursor. -*/ -:disabled { - cursor: default; -} - -/* -1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) -2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) - This can trigger a poorly considered lint error in some tools but is included by design. -*/ - -img, -svg, -video, -canvas, -audio, -iframe, -embed, -object { - display: block; /* 1 */ - vertical-align: middle; /* 2 */ -} - -/* -Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) -*/ - -img, -video { - max-width: 100%; - height: auto; -} - -/* Make elements with the HTML hidden attribute stay hidden by default */ -[hidden] { - display: none; -} - -:root, -[data-theme] { - background-color: hsla(var(--b1) / var(--tw-bg-opacity, 1)); - color: hsla(var(--bc) / var(--tw-text-opacity, 1)); -} - -html { - -webkit-tap-highlight-color: transparent; -} - -:root { - color-scheme: light; - --pf: 258.89 94.378% 40.941%; - --sf: 314 100% 37.647%; - --af: 174 60% 40.784%; - --nf: 219 14.085% 22.275%; - --in: 198 93% 60%; - --su: 158 64% 52%; - --wa: 43 96% 56%; - --er: 0 91% 71%; - --inc: 198 100% 12%; - --suc: 158 100% 10%; - --wac: 43 100% 11%; - --erc: 0 100% 14%; - --rounded-box: 1rem; - --rounded-btn: 0.5rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 258.89 94.378% 51.176%; - --pc: 0 0% 100%; - --s: 314 100% 47.059%; - --sc: 0 0% 100%; - --a: 174 60% 50.98%; - --ac: 174.71 43.59% 15.294%; - --n: 219 14.085% 27.843%; - --nc: 0 0% 100%; - --b1: 0 0% 100%; - --b2: 0 0% 94.902%; - --b3: 180 1.9608% 90%; - --bc: 215 27.907% 16.863%; -} - -@media (prefers-color-scheme: dark) { - - :root { - color-scheme: dark; - --pf: 262.35 80.315% 40.157%; - --sf: 315.75 70.196% 40%; - --af: 174.69 70.335% 32.784%; - --in: 198 93% 60%; - --su: 158 64% 52%; - --wa: 43 96% 56%; - --er: 0 91% 71%; - --inc: 198 100% 12%; - --suc: 158 100% 10%; - --wac: 43 100% 11%; - --erc: 0 100% 14%; - --rounded-box: 1rem; - --rounded-btn: 0.5rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 262.35 80.315% 50.196%; - --pc: 0 0% 100%; - --s: 315.75 70.196% 50%; - --sc: 0 0% 100%; - --a: 174.69 70.335% 40.98%; - --ac: 0 0% 100%; - --n: 218.18 18.033% 11.961%; - --nf: 222.86 17.073% 8.0392%; - --nc: 220 13.376% 69.216%; - --b1: 220 17.647% 20%; - --b2: 220 17.241% 17.059%; - --b3: 218.57 17.949% 15.294%; - --bc: 220 13.376% 69.216%; - } -} - -[data-theme=light] { - color-scheme: light; - --pf: 258.89 94.378% 40.941%; - --sf: 314 100% 37.647%; - --af: 174 60% 40.784%; - --nf: 219 14.085% 22.275%; - --in: 198 93% 60%; - --su: 158 64% 52%; - --wa: 43 96% 56%; - --er: 0 91% 71%; - --inc: 198 100% 12%; - --suc: 158 100% 10%; - --wac: 43 100% 11%; - --erc: 0 100% 14%; - --rounded-box: 1rem; - --rounded-btn: 0.5rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 258.89 94.378% 51.176%; - --pc: 0 0% 100%; - --s: 314 100% 47.059%; - --sc: 0 0% 100%; - --a: 174 60% 50.98%; - --ac: 174.71 43.59% 15.294%; - --n: 219 14.085% 27.843%; - --nc: 0 0% 100%; - --b1: 0 0% 100%; - --b2: 0 0% 94.902%; - --b3: 180 1.9608% 90%; - --bc: 215 27.907% 16.863%; -} - -[data-theme=dark] { - color-scheme: dark; - --pf: 262.35 80.315% 40.157%; - --sf: 315.75 70.196% 40%; - --af: 174.69 70.335% 32.784%; - --in: 198 93% 60%; - --su: 158 64% 52%; - --wa: 43 96% 56%; - --er: 0 91% 71%; - --inc: 198 100% 12%; - --suc: 158 100% 10%; - --wac: 43 100% 11%; - --erc: 0 100% 14%; - --rounded-box: 1rem; - --rounded-btn: 0.5rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 262.35 80.315% 50.196%; - --pc: 0 0% 100%; - --s: 315.75 70.196% 50%; - --sc: 0 0% 100%; - --a: 174.69 70.335% 40.98%; - --ac: 0 0% 100%; - --n: 218.18 18.033% 11.961%; - --nf: 222.86 17.073% 8.0392%; - --nc: 220 13.376% 69.216%; - --b1: 220 17.647% 20%; - --b2: 220 17.241% 17.059%; - --b3: 218.57 17.949% 15.294%; - --bc: 220 13.376% 69.216%; -} - -[data-theme=cupcake] { - color-scheme: light; - --pf: 183.03 47.368% 47.216%; - --sf: 338.25 71.429% 62.431%; - --af: 39 84.112% 46.431%; - --nf: 280 46.479% 11.137%; - --in: 198 93% 60%; - --su: 158 64% 52%; - --wa: 43 96% 56%; - --er: 0 91% 71%; - --pc: 183.03 100% 11.804%; - --sc: 338.25 100% 15.608%; - --ac: 39 100% 11.608%; - --nc: 280 82.688% 82.784%; - --inc: 198 100% 12%; - --suc: 158 100% 10%; - --wac: 43 100% 11%; - --erc: 0 100% 14%; - --rounded-box: 1rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --p: 183.03 47.368% 59.02%; - --s: 338.25 71.429% 78.039%; - --a: 39 84.112% 58.039%; - --n: 280 46.479% 13.922%; - --b1: 24 33.333% 97.059%; - --b2: 26.667 21.951% 91.961%; - --b3: 22.5 14.286% 89.02%; - --bc: 280 46.479% 13.922%; - --rounded-btn: 1.9rem; - --tab-border: 2px; - --tab-radius: .5rem; -} - -[data-theme=bumblebee] { - color-scheme: light; - --pf: 41.124 74.167% 42.353%; - --sf: 49.901 94.393% 46.431%; - --af: 240 33.333% 11.294%; - --nf: 240 33.333% 11.294%; - --b2: 0 0% 90%; - --b3: 0 0% 81%; - --in: 198 93% 60%; - --su: 158 64% 52%; - --wa: 43 96% 56%; - --er: 0 91% 71%; - --bc: 0 0% 20%; - --ac: 240 60.274% 82.824%; - --nc: 240 60.274% 82.824%; - --inc: 198 100% 12%; - --suc: 158 100% 10%; - --wac: 43 100% 11%; - --erc: 0 100% 14%; - --rounded-box: 1rem; - --rounded-btn: 0.5rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 41.124 74.167% 52.941%; - --pc: 240 33.333% 14.118%; - --s: 49.901 94.393% 58.039%; - --sc: 240 33.333% 14.118%; - --a: 240 33.333% 14.118%; - --n: 240 33.333% 14.118%; - --b1: 0 0% 100%; -} - -[data-theme=emerald] { - color-scheme: light; - --pf: 141.18 50% 48%; - --sf: 218.88 96.078% 48%; - --af: 9.8901 81.25% 44.863%; - --nf: 219.23 20.312% 20.078%; - --b2: 0 0% 90%; - --b3: 0 0% 81%; - --in: 198 93% 60%; - --su: 158 64% 52%; - --wa: 43 96% 56%; - --er: 0 91% 71%; - --inc: 198 100% 12%; - --suc: 158 100% 10%; - --wac: 43 100% 11%; - --erc: 0 100% 14%; - --rounded-box: 1rem; - --rounded-btn: 0.5rem; - --rounded-badge: 1.9rem; - --btn-text-case: uppercase; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 141.18 50% 60%; - --pc: 151.11 28.421% 18.627%; - --s: 218.88 96.078% 60%; - --sc: 210 20% 98.039%; - --a: 9.8901 81.25% 56.078%; - --ac: 210 20% 98.039%; - --n: 219.23 20.312% 25.098%; - --nc: 210 20% 98.039%; - --b1: 0 0% 100%; - --bc: 219.23 20.312% 25.098%; - --animation-btn: 0; - --animation-input: 0; - --btn-focus-scale: 1; -} - -[data-theme=corporate] { - color-scheme: light; - --pf: 229.09 95.652% 51.137%; - --sf: 214.91 26.316% 47.216%; - --af: 154.2 49.02% 48%; - --nf: 233.33 27.273% 10.353%; - --b2: 0 0% 90%; - --b3: 0 0% 81%; - --in: 198 93% 60%; - --su: 158 64% 52%; - --wa: 43 96% 56%; - --er: 0 91% 71%; - --pc: 229.09 100% 92.784%; - --sc: 214.91 100% 11.804%; - --ac: 154.2 100% 12%; - --inc: 198 100% 12%; - --suc: 158 100% 10%; - --wac: 43 100% 11%; - --erc: 0 100% 14%; - --btn-text-case: uppercase; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 229.09 95.652% 63.922%; - --s: 214.91 26.316% 59.02%; - --a: 154.2 49.02% 60%; - --n: 233.33 27.273% 12.941%; - --nc: 210 38.462% 94.902%; - --b1: 0 0% 100%; - --bc: 233.33 27.273% 12.941%; - --rounded-box: 0.25rem; - --rounded-btn: .125rem; - --rounded-badge: .125rem; - --animation-btn: 0; - --animation-input: 0; - --btn-focus-scale: 1; -} - -[data-theme=synthwave] { - color-scheme: dark; - --pf: 320.73 69.62% 55.216%; - --sf: 197.03 86.592% 51.922%; - --af: 48 89.041% 45.647%; - --nf: 253.22 60.825% 15.216%; - --b2: 253.85 59.091% 23.294%; - --b3: 253.85 59.091% 20.965%; - --pc: 320.73 100% 13.804%; - --sc: 197.03 100% 12.98%; - --ac: 48 100% 11.412%; - --rounded-box: 1rem; - --rounded-btn: 0.5rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 320.73 69.62% 69.02%; - --s: 197.03 86.592% 64.902%; - --a: 48 89.041% 57.059%; - --n: 253.22 60.825% 19.02%; - --nc: 260 60% 98.039%; - --b1: 253.85 59.091% 25.882%; - --bc: 260 60% 98.039%; - --in: 199.13 86.957% 63.922%; - --inc: 257.45 63.218% 17.059%; - --su: 168.1 74.233% 68.039%; - --suc: 257.45 63.218% 17.059%; - --wa: 48 89.041% 57.059%; - --wac: 257.45 63.218% 17.059%; - --er: 351.85 73.636% 56.863%; - --erc: 260 60% 98.039%; -} - -[data-theme=retro] { - color-scheme: light; - --pf: 2.6667 73.77% 60.863%; - --sf: 144.62 27.273% 57.569%; - --af: 49.024 67.213% 60.863%; - --nf: 41.667 16.822% 33.569%; - --inc: 221.21 100% 90.667%; - --suc: 142.13 100% 87.255%; - --wac: 32.133 100% 8.7451%; - --erc: 0 100% 90.118%; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 2.6667 73.77% 76.078%; - --pc: 345 5.2632% 14.902%; - --s: 144.62 27.273% 71.961%; - --sc: 345 5.2632% 14.902%; - --a: 49.024 67.213% 76.078%; - --ac: 345 5.2632% 14.902%; - --n: 41.667 16.822% 41.961%; - --nc: 45 47.059% 80%; - --b1: 45 47.059% 80%; - --b2: 45.283 37.063% 71.961%; - --b3: 42.188 35.955% 65.098%; - --bc: 345 5.2632% 14.902%; - --in: 221.21 83.193% 53.333%; - --su: 142.13 76.216% 36.275%; - --wa: 32.133 94.619% 43.725%; - --er: 0 72.222% 50.588%; - --rounded-box: 0.4rem; - --rounded-btn: 0.4rem; - --rounded-badge: 0.4rem; -} - -[data-theme=cyberpunk] { - color-scheme: light; - --pf: 344.78 100% 58.353%; - --sf: 195.12 80.392% 56%; - --af: 276 74.324% 56.784%; - --nf: 57.273 100% 10.353%; - --b2: 56 100% 45%; - --b3: 56 100% 40.5%; - --in: 198 93% 60%; - --su: 158 64% 52%; - --wa: 43 96% 56%; - --er: 0 91% 71%; - --bc: 56 100% 10%; - --pc: 344.78 100% 14.588%; - --sc: 195.12 100% 14%; - --ac: 276 100% 14.196%; - --inc: 198 100% 12%; - --suc: 158 100% 10%; - --wac: 43 100% 11%; - --erc: 0 100% 14%; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace; - --p: 344.78 100% 72.941%; - --s: 195.12 80.392% 70%; - --a: 276 74.324% 70.98%; - --n: 57.273 100% 12.941%; - --nc: 56 100% 50%; - --b1: 56 100% 50%; - --rounded-box: 0; - --rounded-btn: 0; - --rounded-badge: 0; - --tab-radius: 0; -} - -[data-theme=valentine] { - color-scheme: light; - --pf: 353.23 73.81% 53.647%; - --sf: 254.12 86.441% 61.49%; - --af: 181.41 55.556% 56%; - --nf: 336 42.857% 38.431%; - --b2: 318.46 46.429% 80.118%; - --b3: 318.46 46.429% 72.106%; - --pc: 353.23 100% 13.412%; - --sc: 254.12 100% 15.373%; - --ac: 181.41 100% 14%; - --inc: 221.21 100% 90.667%; - --suc: 142.13 100% 87.255%; - --wac: 32.133 100% 8.7451%; - --erc: 0 100% 90.118%; - --rounded-box: 1rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 353.23 73.81% 67.059%; - --s: 254.12 86.441% 76.863%; - --a: 181.41 55.556% 70%; - --n: 336 42.857% 48.039%; - --nc: 318.46 46.429% 89.02%; - --b1: 318.46 46.429% 89.02%; - --bc: 343.64 38.462% 28.039%; - --in: 221.21 83.193% 53.333%; - --su: 142.13 76.216% 36.275%; - --wa: 32.133 94.619% 43.725%; - --er: 0 72.222% 50.588%; - --rounded-btn: 1.9rem; -} - -[data-theme=halloween] { - color-scheme: dark; - --pf: 31.927 89.344% 41.725%; - --sf: 271.22 45.794% 33.569%; - --af: 91.071 100% 26.353%; - --nf: 180 3.5714% 8.7843%; - --b2: 0 0% 11.647%; - --b3: 0 0% 10.482%; - --bc: 0 0% 82.588%; - --sc: 271.22 100% 88.392%; - --ac: 91.071 100% 6.5882%; - --nc: 180 4.8458% 82.196%; - --inc: 221.21 100% 90.667%; - --suc: 142.13 100% 87.255%; - --wac: 32.133 100% 8.7451%; - --erc: 0 100% 90.118%; - --rounded-box: 1rem; - --rounded-btn: 0.5rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 31.927 89.344% 52.157%; - --pc: 180 7.3171% 8.0392%; - --s: 271.22 45.794% 41.961%; - --a: 91.071 100% 32.941%; - --n: 180 3.5714% 10.98%; - --b1: 0 0% 12.941%; - --in: 221.21 83.193% 53.333%; - --su: 142.13 76.216% 36.275%; - --wa: 32.133 94.619% 43.725%; - --er: 0 72.222% 50.588%; -} - -[data-theme=garden] { - color-scheme: light; - --pf: 138.86 15.982% 34.353%; - --sf: 96.923 37.143% 74.51%; - --af: 0 67.742% 75.137%; - --nf: 0 3.9106% 28.078%; - --b2: 0 4.3478% 81.882%; - --b3: 0 4.3478% 73.694%; - --in: 198 93% 60%; - --su: 158 64% 52%; - --wa: 43 96% 56%; - --er: 0 91% 71%; - --pc: 138.86 100% 88.588%; - --inc: 198 100% 12%; - --suc: 158 100% 10%; - --wac: 43 100% 11%; - --erc: 0 100% 14%; - --rounded-box: 1rem; - --rounded-btn: 0.5rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 138.86 15.982% 42.941%; - --s: 96.923 37.143% 93.137%; - --sc: 96 32.468% 15.098%; - --a: 0 67.742% 93.922%; - --ac: 0 21.951% 16.078%; - --n: 0 3.9106% 35.098%; - --nc: 0 4.3478% 90.98%; - --b1: 0 4.3478% 90.98%; - --bc: 0 3.2258% 6.0784%; -} - -[data-theme=forest] { - color-scheme: dark; - --pf: 141.04 71.963% 33.569%; - --sf: 140.98 74.694% 38.431%; - --af: 35.148 68.98% 41.569%; - --nf: 0 9.6774% 4.8627%; - --b2: 0 12.195% 7.2353%; - --b3: 0 12.195% 6.5118%; - --in: 198 93% 60%; - --su: 158 64% 52%; - --wa: 43 96% 56%; - --er: 0 91% 71%; - --bc: 0 11.727% 81.608%; - --sc: 140.98 100% 9.6078%; - --ac: 35.148 100% 10.392%; - --nc: 0 6.8894% 81.216%; - --inc: 198 100% 12%; - --suc: 158 100% 10%; - --wac: 43 100% 11%; - --erc: 0 100% 14%; - --rounded-box: 1rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 141.04 71.963% 41.961%; - --pc: 140.66 100% 88.039%; - --s: 140.98 74.694% 48.039%; - --a: 35.148 68.98% 51.961%; - --n: 0 9.6774% 6.0784%; - --b1: 0 12.195% 8.0392%; - --rounded-btn: 1.9rem; -} - -[data-theme=aqua] { - color-scheme: dark; - --pf: 181.79 92.857% 39.529%; - --sf: 274.41 30.909% 45.49%; - --af: 47.059 100% 64%; - --nf: 205.4 53.725% 40%; - --b2: 218.61 52.511% 38.647%; - --b3: 218.61 52.511% 34.782%; - --bc: 218.61 100% 88.588%; - --sc: 274.41 100% 91.373%; - --ac: 47.059 100% 16%; - --nc: 205.4 100% 90%; - --inc: 221.21 100% 90.667%; - --suc: 142.13 100% 87.255%; - --wac: 32.133 100% 8.7451%; - --erc: 0 100% 90.118%; - --rounded-box: 1rem; - --rounded-btn: 0.5rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 181.79 92.857% 49.412%; - --pc: 181.41 100% 16.667%; - --s: 274.41 30.909% 56.863%; - --a: 47.059 100% 80%; - --n: 205.4 53.725% 50%; - --b1: 218.61 52.511% 42.941%; - --in: 221.21 83.193% 53.333%; - --su: 142.13 76.216% 36.275%; - --wa: 32.133 94.619% 43.725%; - --er: 0 72.222% 50.588%; -} - -[data-theme=lofi] { - color-scheme: light; - --pf: 0 0% 4.0784%; - --sf: 0 1.9608% 8%; - --af: 0 0% 11.922%; - --nf: 0 0% 0%; - --btn-text-case: uppercase; - --border-btn: 1px; - --tab-border: 1px; - --p: 0 0% 5.098%; - --pc: 0 0% 100%; - --s: 0 1.9608% 10%; - --sc: 0 0% 100%; - --a: 0 0% 14.902%; - --ac: 0 0% 100%; - --n: 0 0% 0%; - --nc: 0 0% 100%; - --b1: 0 0% 100%; - --b2: 0 0% 94.902%; - --b3: 0 1.9608% 90%; - --bc: 0 0% 0%; - --in: 212.35 100% 47.647%; - --inc: 0 0% 100%; - --su: 136.84 72.152% 46.471%; - --suc: 0 0% 100%; - --wa: 4.5614 100% 66.471%; - --wac: 0 0% 100%; - --er: 325.05 77.6% 49.02%; - --erc: 0 0% 100%; - --rounded-box: 0.25rem; - --rounded-btn: 0.125rem; - --rounded-badge: 0.125rem; - --animation-btn: 0; - --animation-input: 0; - --btn-focus-scale: 1; - --tab-radius: 0; -} - -[data-theme=pastel] { - color-scheme: light; - --pf: 283.64 21.569% 64%; - --sf: 351.63 70.492% 70.431%; - --af: 158.49 54.639% 64.784%; - --nf: 198.62 43.719% 48.784%; - --in: 198 93% 60%; - --su: 158 64% 52%; - --wa: 43 96% 56%; - --er: 0 91% 71%; - --bc: 0 0% 20%; - --pc: 283.64 59.314% 16%; - --sc: 351.63 100% 17.608%; - --ac: 158.49 100% 16.196%; - --nc: 198.62 100% 12.196%; - --inc: 198 100% 12%; - --suc: 158 100% 10%; - --wac: 43 100% 11%; - --erc: 0 100% 14%; - --rounded-box: 1rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 283.64 21.569% 80%; - --s: 351.63 70.492% 88.039%; - --a: 158.49 54.639% 80.98%; - --n: 198.62 43.719% 60.98%; - --b1: 0 0% 100%; - --b2: 210 20% 98.039%; - --b3: 216 12.195% 83.922%; - --rounded-btn: 1.9rem; -} - -[data-theme=fantasy] { - color-scheme: light; - --pf: 296.04 82.813% 20.078%; - --sf: 200 100% 29.647%; - --af: 30.894 94.378% 40.941%; - --nf: 215 27.907% 13.49%; - --b2: 0 0% 90%; - --b3: 0 0% 81%; - --in: 198 93% 60%; - --su: 158 64% 52%; - --wa: 43 96% 56%; - --er: 0 91% 71%; - --pc: 296.04 100% 85.02%; - --sc: 200 100% 87.412%; - --ac: 30.894 100% 10.235%; - --nc: 215 62.264% 83.373%; - --inc: 198 100% 12%; - --suc: 158 100% 10%; - --wac: 43 100% 11%; - --erc: 0 100% 14%; - --rounded-box: 1rem; - --rounded-btn: 0.5rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 296.04 82.813% 25.098%; - --s: 200 100% 37.059%; - --a: 30.894 94.378% 51.176%; - --n: 215 27.907% 16.863%; - --b1: 0 0% 100%; - --bc: 215 27.907% 16.863%; -} - -[data-theme=wireframe] { - color-scheme: light; - --pf: 0 0% 57.725%; - --sf: 0 0% 57.725%; - --af: 0 0% 57.725%; - --nf: 0 0% 73.725%; - --bc: 0 0% 20%; - --pc: 0 0% 14.431%; - --sc: 0 0% 14.431%; - --ac: 0 0% 14.431%; - --nc: 0 0% 18.431%; - --inc: 240 100% 90%; - --suc: 120 100% 85.02%; - --wac: 60 100% 10%; - --erc: 0 100% 90%; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - font-family: Chalkboard,comic sans ms,"sanssecondaryerif"; - --p: 0 0% 72.157%; - --s: 0 0% 72.157%; - --a: 0 0% 72.157%; - --n: 0 0% 92.157%; - --b1: 0 0% 100%; - --b2: 0 0% 93.333%; - --b3: 0 0% 86.667%; - --in: 240 100% 50%; - --su: 120 100% 25.098%; - --wa: 60 30.196% 50%; - --er: 0 100% 50%; - --rounded-box: 0.2rem; - --rounded-btn: 0.2rem; - --rounded-badge: 0.2rem; - --tab-radius: 0.2rem; -} - -[data-theme=black] { - color-scheme: dark; - --pf: 0 1.9608% 16%; - --sf: 0 1.9608% 16%; - --af: 0 1.9608% 16%; - --bc: 0 0% 80%; - --pc: 0 5.3922% 84%; - --sc: 0 5.3922% 84%; - --ac: 0 5.3922% 84%; - --nc: 0 2.5404% 83.02%; - --inc: 240 100% 90%; - --suc: 120 100% 85.02%; - --wac: 60 100% 10%; - --erc: 0 100% 90%; - --border-btn: 1px; - --tab-border: 1px; - --p: 0 1.9608% 20%; - --s: 0 1.9608% 20%; - --a: 0 1.9608% 20%; - --b1: 0 0% 0%; - --b2: 0 0% 5.098%; - --b3: 0 1.9608% 10%; - --n: 0 1.2987% 15.098%; - --nf: 0 1.9608% 20%; - --in: 240 100% 50%; - --su: 120 100% 25.098%; - --wa: 60 100% 50%; - --er: 0 100% 50%; - --rounded-box: 0; - --rounded-btn: 0; - --rounded-badge: 0; - --animation-btn: 0; - --animation-input: 0; - --btn-text-case: lowercase; - --btn-focus-scale: 1; - --tab-radius: 0; -} - -[data-theme=luxury] { - color-scheme: dark; - --pf: 0 0% 80%; - --sf: 218.4 54.348% 14.431%; - --af: 318.62 21.805% 20.863%; - --nf: 270 4.3478% 7.2157%; - --pc: 0 0% 20%; - --sc: 218.4 100% 83.608%; - --ac: 318.62 84.615% 85.216%; - --inc: 202.35 100% 14%; - --suc: 89.007 100% 10.392%; - --wac: 53.906 100% 12.706%; - --erc: 0 100% 14.353%; - --rounded-box: 1rem; - --rounded-btn: 0.5rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 0 0% 100%; - --s: 218.4 54.348% 18.039%; - --a: 318.62 21.805% 26.078%; - --n: 270 4.3478% 9.0196%; - --nc: 37.083 67.29% 58.039%; - --b1: 240 10% 3.9216%; - --b2: 270 4.3478% 9.0196%; - --b3: 270 2.1739% 18.039%; - --bc: 37.083 67.29% 58.039%; - --in: 202.35 100% 70%; - --su: 89.007 61.633% 51.961%; - --wa: 53.906 68.817% 63.529%; - --er: 0 100% 71.765%; -} - -[data-theme=dracula] { - color-scheme: dark; - --pf: 325.52 100% 58.98%; - --sf: 264.71 89.474% 62.118%; - --af: 31.02 100% 56.941%; - --nf: 229.57 15.033% 24%; - --b2: 231.43 14.894% 16.588%; - --b3: 231.43 14.894% 14.929%; - --pc: 325.52 100% 14.745%; - --sc: 264.71 100% 15.529%; - --ac: 31.02 100% 14.235%; - --nc: 229.57 70.868% 86%; - --inc: 190.53 100% 15.373%; - --suc: 135.18 100% 12.941%; - --wac: 64.909 100% 15.294%; - --erc: 0 100% 93.333%; - --rounded-box: 1rem; - --rounded-btn: 0.5rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 325.52 100% 73.725%; - --s: 264.71 89.474% 77.647%; - --a: 31.02 100% 71.176%; - --n: 229.57 15.033% 30%; - --b1: 231.43 14.894% 18.431%; - --bc: 60 30% 96.078%; - --in: 190.53 96.61% 76.863%; - --su: 135.18 94.444% 64.706%; - --wa: 64.909 91.667% 76.471%; - --er: 0 100% 66.667%; -} - -[data-theme=cmyk] { - color-scheme: light; - --pf: 202.72 83.251% 48.157%; - --sf: 335.25 77.67% 47.686%; - --af: 56.195 100% 47.843%; - --nf: 0 0% 8.1569%; - --b2: 0 0% 90%; - --b3: 0 0% 81%; - --bc: 0 0% 20%; - --pc: 202.72 100% 12.039%; - --sc: 335.25 100% 91.922%; - --ac: 56.195 100% 11.961%; - --nc: 0 0% 82.039%; - --inc: 192.2 100% 10.431%; - --suc: 291.06 100% 87.608%; - --wac: 25.027 100% 11.333%; - --erc: 3.956 100% 91.137%; - --rounded-box: 1rem; - --rounded-btn: 0.5rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 202.72 83.251% 60.196%; - --s: 335.25 77.67% 59.608%; - --a: 56.195 100% 59.804%; - --n: 0 0% 10.196%; - --b1: 0 0% 100%; - --in: 192.2 48.361% 52.157%; - --su: 291.06 48.454% 38.039%; - --wa: 25.027 84.615% 56.667%; - --er: 3.956 80.531% 55.686%; -} - -[data-theme=autumn] { - color-scheme: light; - --pf: 344.23 95.804% 22.431%; - --sf: 0.44444 63.38% 46.588%; - --af: 27.477 56.021% 50.039%; - --nf: 22.105 17.117% 34.824%; - --b2: 0 0% 85.059%; - --b3: 0 0% 76.553%; - --bc: 0 0% 18.902%; - --pc: 344.23 100% 85.608%; - --sc: 0.44444 100% 91.647%; - --ac: 27.477 100% 12.51%; - --nc: 22.105 100% 88.706%; - --inc: 186.94 100% 9.9216%; - --suc: 164.59 100% 8.6275%; - --wac: 30.141 100% 9.9216%; - --erc: 353.6 100% 89.765%; - --rounded-box: 1rem; - --rounded-btn: 0.5rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 344.23 95.804% 28.039%; - --s: 0.44444 63.38% 58.235%; - --a: 27.477 56.021% 62.549%; - --n: 22.105 17.117% 43.529%; - --b1: 0 0% 94.51%; - --in: 186.94 47.826% 49.608%; - --su: 164.59 33.636% 43.137%; - --wa: 30.141 84.19% 49.608%; - --er: 353.6 79.116% 48.824%; -} - -[data-theme=business] { - color-scheme: dark; - --pf: 210 64.103% 24.471%; - --sf: 200 12.931% 43.608%; - --af: 12.515 79.512% 47.843%; - --nf: 212.73 13.58% 12.706%; - --b2: 0 0% 11.294%; - --b3: 0 0% 10.165%; - --bc: 0 0% 82.51%; - --pc: 210 100% 86.118%; - --sc: 200 100% 10.902%; - --ac: 12.515 100% 11.961%; - --nc: 212.73 28.205% 83.176%; - --inc: 199.15 100% 88.353%; - --suc: 144 100% 11.137%; - --wac: 39.231 100% 12.078%; - --erc: 6.3415 100% 88.667%; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 210 64.103% 30.588%; - --s: 200 12.931% 54.51%; - --a: 12.515 79.512% 59.804%; - --n: 212.73 13.58% 15.882%; - --b1: 0 0% 12.549%; - --in: 199.15 100% 41.765%; - --su: 144 30.973% 55.686%; - --wa: 39.231 64.356% 60.392%; - --er: 6.3415 55.656% 43.333%; - --rounded-box: 0.25rem; - --rounded-btn: .125rem; - --rounded-badge: .125rem; -} - -[data-theme=acid] { - color-scheme: light; - --pf: 302.59 100% 40%; - --sf: 27.294 100% 40%; - --af: 72 98.425% 40.157%; - --nf: 238.42 43.182% 13.804%; - --b2: 0 0% 88.235%; - --b3: 0 0% 79.412%; - --bc: 0 0% 19.608%; - --pc: 302.59 100% 90%; - --sc: 27.294 100% 10%; - --ac: 72 100% 10.039%; - --nc: 238.42 99.052% 83.451%; - --inc: 209.85 100% 11.569%; - --suc: 148.87 100% 11.608%; - --wac: 52.574 100% 11.451%; - --erc: 0.78261 100% 89.02%; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 302.59 100% 50%; - --s: 27.294 100% 50%; - --a: 72 98.425% 50.196%; - --n: 238.42 43.182% 17.255%; - --b1: 0 0% 98.039%; - --in: 209.85 91.628% 57.843%; - --su: 148.87 49.533% 58.039%; - --wa: 52.574 92.661% 57.255%; - --er: 0.78261 100% 45.098%; - --rounded-box: 1.25rem; - --rounded-btn: 1rem; - --rounded-badge: 1rem; -} - -[data-theme=lemonade] { - color-scheme: light; - --pf: 88.8 96.154% 24.471%; - --sf: 60 80.952% 43.765%; - --af: 62.553 79.661% 70.745%; - --nf: 238.42 43.182% 13.804%; - --b2: 0 0% 90%; - --b3: 0 0% 81%; - --bc: 0 0% 20%; - --pc: 88.8 100% 86.118%; - --sc: 60 100% 10.941%; - --ac: 62.553 100% 17.686%; - --nc: 238.42 99.052% 83.451%; - --inc: 191.61 79.118% 16.902%; - --suc: 74.458 100% 15.725%; - --wac: 50.182 100% 15.059%; - --erc: 0.98361 100% 16.588%; - --rounded-box: 1rem; - --rounded-btn: 0.5rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 88.8 96.154% 30.588%; - --s: 60 80.952% 54.706%; - --a: 62.553 79.661% 88.431%; - --n: 238.42 43.182% 17.255%; - --b1: 0 0% 100%; - --in: 191.61 39.241% 84.51%; - --su: 74.458 76.147% 78.627%; - --wa: 50.182 87.302% 75.294%; - --er: 0.98361 70.115% 82.941%; -} - -[data-theme=night] { - color-scheme: dark; - --pf: 198.44 93.204% 47.686%; - --sf: 234.45 89.474% 59.137%; - --af: 328.85 85.621% 56%; - --b2: 222.22 47.368% 10.059%; - --b3: 222.22 47.368% 9.0529%; - --bc: 222.22 65.563% 82.235%; - --pc: 198.44 100% 11.922%; - --sc: 234.45 100% 14.784%; - --ac: 328.85 100% 14%; - --nc: 217.24 75.772% 83.49%; - --inc: 198.46 100% 9.6078%; - --suc: 172.46 100% 10.078%; - --wac: 40.61 100% 12.706%; - --erc: 350.94 100% 14.235%; - --rounded-box: 1rem; - --rounded-btn: 0.5rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 198.44 93.204% 59.608%; - --s: 234.45 89.474% 73.922%; - --a: 328.85 85.621% 70%; - --n: 217.24 32.584% 17.451%; - --nf: 217.06 30.357% 21.961%; - --b1: 222.22 47.368% 11.176%; - --in: 198.46 90.204% 48.039%; - --su: 172.46 66.008% 50.392%; - --wa: 40.61 88.172% 63.529%; - --er: 350.94 94.558% 71.176%; -} - -[data-theme=coffee] { - color-scheme: dark; - --pf: 29.583 66.667% 46.118%; - --sf: 182.4 24.752% 15.843%; - --af: 194.19 74.4% 19.608%; - --nf: 300 20% 4.7059%; - --b2: 306 18.519% 9.5294%; - --b3: 306 18.519% 8.5765%; - --pc: 29.583 100% 11.529%; - --sc: 182.4 67.237% 83.961%; - --ac: 194.19 100% 84.902%; - --nc: 300 13.75% 81.176%; - --inc: 171.15 100% 13.451%; - --suc: 92.5 100% 12.471%; - --wac: 43.125 100% 13.725%; - --erc: 9.7561 100% 14.941%; - --rounded-box: 1rem; - --rounded-btn: 0.5rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 29.583 66.667% 57.647%; - --s: 182.4 24.752% 19.804%; - --a: 194.19 74.4% 24.51%; - --n: 300 20% 5.8824%; - --b1: 306 18.519% 10.588%; - --bc: 36.667 8.3333% 42.353%; - --in: 171.15 36.527% 67.255%; - --su: 92.5 25% 62.353%; - --wa: 43.125 100% 68.627%; - --er: 9.7561 95.349% 74.706%; -} - -[data-theme=winter] { - color-scheme: light; - --pf: 211.79 100% 40.627%; - --sf: 246.92 47.273% 34.51%; - --af: 310.41 49.388% 41.569%; - --nf: 217.02 92.157% 8%; - --pc: 211.79 100% 90.157%; - --sc: 246.92 100% 88.627%; - --ac: 310.41 100% 90.392%; - --nc: 217.02 100% 82%; - --inc: 191.54 100% 15.608%; - --suc: 181.5 100% 13.255%; - --wac: 32.308 100% 16.706%; - --erc: 0 100% 14.431%; - --rounded-box: 1rem; - --rounded-btn: 0.5rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --p: 211.79 100% 50.784%; - --s: 246.92 47.273% 43.137%; - --a: 310.41 49.388% 51.961%; - --n: 217.02 92.157% 10%; - --b1: 0 0% 100%; - --b2: 216.92 100% 97.451%; - --b3: 218.82 43.59% 92.353%; - --bc: 214.29 30.061% 31.961%; - --in: 191.54 92.857% 78.039%; - --su: 181.5 46.512% 66.275%; - --wa: 32.308 61.905% 83.529%; - --er: 0 63.38% 72.157%; -} - -*, ::before, ::after { - --tw-border-spacing-x: 0; - --tw-border-spacing-y: 0; - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-rotate: 0; - --tw-skew-x: 0; - --tw-skew-y: 0; - --tw-scale-x: 1; - --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; - --tw-scroll-snap-strictness: proximity; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-color: rgb(59 130 246 / 0.5); - --tw-ring-offset-shadow: 0 0 #0000; - --tw-ring-shadow: 0 0 #0000; - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; -} - -::backdrop { - --tw-border-spacing-x: 0; - --tw-border-spacing-y: 0; - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-rotate: 0; - --tw-skew-x: 0; - --tw-skew-y: 0; - --tw-scale-x: 1; - --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; - --tw-scroll-snap-strictness: proximity; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-color: rgb(59 130 246 / 0.5); - --tw-ring-offset-shadow: 0 0 #0000; - --tw-ring-shadow: 0 0 #0000; - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; -} -.container { - width: 100%; -} -@media (min-width: 640px) { - - .container { - max-width: 640px; - } -} -@media (min-width: 768px) { - - .container { - max-width: 768px; - } -} -@media (min-width: 1024px) { - - .container { - max-width: 1024px; - } -} -@media (min-width: 1280px) { - - .container { - max-width: 1280px; - } -} -@media (min-width: 1536px) { - - .container { - max-width: 1536px; - } -} -.avatar.placeholder > div { - display: flex; - align-items: center; - justify-content: center; -} -.btn { - display: inline-flex; - flex-shrink: 0; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - flex-wrap: wrap; - align-items: center; - justify-content: center; - border-color: transparent; - border-color: hsl(var(--n) / var(--tw-border-opacity)); - text-align: center; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; - transition-duration: 200ms; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - border-radius: var(--rounded-btn, 0.5rem); - height: 3rem; - padding-left: 1rem; - padding-right: 1rem; - font-size: 0.875rem; - line-height: 1.25rem; - line-height: 1em; - min-height: 3rem; - font-weight: 600; - text-transform: uppercase; - text-transform: var(--btn-text-case, uppercase); - text-decoration-line: none; - border-width: var(--border-btn, 1px); - animation: button-pop var(--animation-btn, 0.25s) ease-out; - --tw-border-opacity: 1; - --tw-bg-opacity: 1; - background-color: hsl(var(--n) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--nc) / var(--tw-text-opacity)); -} -.btn-disabled, - .btn[disabled] { - pointer-events: none; -} -.btn-square { - height: 3rem; - width: 3rem; - padding: 0px; -} -.btn.loading, - .btn.loading:hover { - pointer-events: none; -} -.btn.loading:before { - margin-right: 0.5rem; - height: 1rem; - width: 1rem; - border-radius: 9999px; - border-width: 2px; - animation: spin 2s linear infinite; - content: ""; - border-top-color: transparent; - border-left-color: transparent; - border-bottom-color: currentColor; - border-right-color: currentColor; -} -@media (prefers-reduced-motion: reduce) { - - .btn.loading:before { - animation: spin 10s linear infinite; - } -} -@keyframes spin { - - from { - transform: rotate(0deg); - } - - to { - transform: rotate(360deg); - } -} -.btn-group > input[type="radio"].btn { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} -.btn-group > input[type="radio"].btn:before { - content: attr(data-title); -} -.card { - position: relative; - display: flex; - flex-direction: column; - border-radius: var(--rounded-box, 1rem); -} -.card:focus { - outline: 2px solid transparent; - outline-offset: 2px; -} -.card-body { - display: flex; - flex: 1 1 auto; - flex-direction: column; - padding: var(--padding-card, 2rem); - gap: 0.5rem; -} -.card-body :where(p) { - flex-grow: 1; -} -.card-actions { - display: flex; - flex-wrap: wrap; - align-items: flex-start; - gap: 0.5rem; -} -.card figure { - display: flex; - align-items: center; - justify-content: center; -} -.card.image-full { - display: grid; -} -.card.image-full:before { - position: relative; - content: ""; - z-index: 10; - --tw-bg-opacity: 1; - background-color: hsl(var(--n) / var(--tw-bg-opacity)); - opacity: 0.75; - border-radius: var(--rounded-box, 1rem); -} -.card.image-full:before, - .card.image-full > * { - grid-column-start: 1; - grid-row-start: 1; -} -.card.image-full > figure img { - height: 100%; - -o-object-fit: cover; - object-fit: cover; -} -.card.image-full > .card-body { - position: relative; - z-index: 20; - --tw-text-opacity: 1; - color: hsl(var(--nc) / var(--tw-text-opacity)); -} -.chat { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - -moz-column-gap: 0.75rem; - column-gap: 0.75rem; - padding-top: 0.25rem; - padding-bottom: 0.25rem; -} -.checkbox { - flex-shrink: 0; - --chkbg: var(--bc); - --chkfg: var(--b1); - height: 1.5rem; - width: 1.5rem; - cursor: pointer; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - border-width: 1px; - border-color: hsl(var(--bc) / var(--tw-border-opacity)); - --tw-border-opacity: 0.2; - border-radius: var(--rounded-btn, 0.5rem); -} -.form-control { - display: flex; - flex-direction: column; -} -.label { - display: flex; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - align-items: center; - justify-content: space-between; - padding-left: 0.25rem; - padding-right: 0.25rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} -.input { - flex-shrink: 1; - height: 3rem; - padding-left: 1rem; - padding-right: 1rem; - font-size: 1rem; - line-height: 2; - line-height: 1.5rem; - border-width: 1px; - border-color: hsl(var(--bc) / var(--tw-border-opacity)); - --tw-border-opacity: 0; - --tw-bg-opacity: 1; - background-color: hsl(var(--b1) / var(--tw-bg-opacity)); - border-radius: var(--rounded-btn, 0.5rem); -} -.input-group > .input { - isolation: isolate; -} -.input-group > *, - .input-group > .input, - .input-group > .textarea, - .input-group > .select { - border-radius: 0px; -} -.link { - cursor: pointer; - text-decoration-line: underline; -} -.menu > :where(li.disabled > *:not(ul):focus) { - cursor: auto; -} -.toast { - position: fixed; - display: flex; - min-width: -moz-fit-content; - min-width: fit-content; - flex-direction: column; - gap: 0.5rem; - padding: 1rem; -} -.tooltip { - position: relative; - display: inline-block; - --tooltip-offset: calc(100% + 1px + var(--tooltip-tail, 0px)); - text-align: center; - --tooltip-tail: 3px; - --tooltip-color: hsl(var(--n)); - --tooltip-text-color: hsl(var(--nc)); - --tooltip-tail-offset: calc(100% + 1px - var(--tooltip-tail)); -} -.tooltip:before { - position: absolute; - pointer-events: none; - z-index: 1; - content: var(--tw-content); - --tw-content: attr(data-tip); - max-width: 20rem; - border-radius: 0.25rem; - padding-left: 0.5rem; - padding-right: 0.5rem; - padding-top: 0.25rem; - padding-bottom: 0.25rem; - font-size: 0.875rem; - line-height: 1.25rem; - background-color: var(--tooltip-color); - color: var(--tooltip-text-color); - width: -moz-max-content; - width: max-content; -} -.tooltip:before, .tooltip-top:before { - transform: translateX(-50%); - top: auto; - left: 50%; - right: auto; - bottom: var(--tooltip-offset); -} -.btn-outline .badge { - --tw-border-opacity: 1; - border-color: hsl(var(--nf, var(--n)) / var(--tw-border-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--nc) / var(--tw-text-opacity)); -} -.btn-outline.btn-primary .badge { - --tw-border-opacity: 1; - border-color: hsl(var(--p) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--p) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--pc) / var(--tw-text-opacity)); -} -.btn-outline.btn-secondary .badge { - --tw-border-opacity: 1; - border-color: hsl(var(--s) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--s) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--sc) / var(--tw-text-opacity)); -} -.btn-outline.btn-accent .badge { - --tw-border-opacity: 1; - border-color: hsl(var(--a) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--a) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--ac) / var(--tw-text-opacity)); -} -.btn-outline .badge.outline { - --tw-border-opacity: 1; - border-color: hsl(var(--nf, var(--n)) / var(--tw-border-opacity)); - background-color: transparent; -} -.btn-outline.btn-primary .badge-outline { - --tw-border-opacity: 1; - border-color: hsl(var(--p) / var(--tw-border-opacity)); - background-color: transparent; - --tw-text-opacity: 1; - color: hsl(var(--p) / var(--tw-text-opacity)); -} -.btn-outline.btn-secondary .badge-outline { - --tw-border-opacity: 1; - border-color: hsl(var(--s) / var(--tw-border-opacity)); - background-color: transparent; - --tw-text-opacity: 1; - color: hsl(var(--s) / var(--tw-text-opacity)); -} -.btn-outline.btn-accent .badge-outline { - --tw-border-opacity: 1; - border-color: hsl(var(--a) / var(--tw-border-opacity)); - background-color: transparent; - --tw-text-opacity: 1; - color: hsl(var(--a) / var(--tw-text-opacity)); -} -.btn-outline.btn-info .badge-outline { - --tw-border-opacity: 1; - border-color: hsl(var(--in) / var(--tw-border-opacity)); - background-color: transparent; - --tw-text-opacity: 1; - color: hsl(var(--in) / var(--tw-text-opacity)); -} -.btn-outline.btn-success .badge-outline { - --tw-border-opacity: 1; - border-color: hsl(var(--su) / var(--tw-border-opacity)); - background-color: transparent; - --tw-text-opacity: 1; - color: hsl(var(--su) / var(--tw-text-opacity)); -} -.btn-outline.btn-warning .badge-outline { - --tw-border-opacity: 1; - border-color: hsl(var(--wa) / var(--tw-border-opacity)); - background-color: transparent; - --tw-text-opacity: 1; - color: hsl(var(--wa) / var(--tw-text-opacity)); -} -.btn-outline.btn-error .badge-outline { - --tw-border-opacity: 1; - border-color: hsl(var(--er) / var(--tw-border-opacity)); - background-color: transparent; - --tw-text-opacity: 1; - color: hsl(var(--er) / var(--tw-text-opacity)); -} -.btn-outline:hover .badge { - --tw-border-opacity: 1; - border-color: hsl(var(--b2, var(--b1)) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--b2, var(--b1)) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--bc) / var(--tw-text-opacity)); -} -.btn-outline:hover .badge.outline { - --tw-border-opacity: 1; - border-color: hsl(var(--b2, var(--b1)) / var(--tw-border-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--nc) / var(--tw-text-opacity)); -} -.btn-outline.btn-primary:hover .badge { - --tw-border-opacity: 1; - border-color: hsl(var(--pc) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--pc) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--p) / var(--tw-text-opacity)); -} -.btn-outline.btn-primary:hover .badge.outline { - --tw-border-opacity: 1; - border-color: hsl(var(--pc) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--pf, var(--p)) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--pc) / var(--tw-text-opacity)); -} -.btn-outline.btn-secondary:hover .badge { - --tw-border-opacity: 1; - border-color: hsl(var(--sc) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--sc) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--s) / var(--tw-text-opacity)); -} -.btn-outline.btn-secondary:hover .badge.outline { - --tw-border-opacity: 1; - border-color: hsl(var(--sc) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--sf, var(--s)) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--sc) / var(--tw-text-opacity)); -} -.btn-outline.btn-accent:hover .badge { - --tw-border-opacity: 1; - border-color: hsl(var(--ac) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--ac) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--a) / var(--tw-text-opacity)); -} -.btn-outline.btn-accent:hover .badge.outline { - --tw-border-opacity: 1; - border-color: hsl(var(--ac) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--af, var(--a)) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--ac) / var(--tw-text-opacity)); -} -.btm-nav>*:where(.active) { - border-top-width: 2px; - --tw-bg-opacity: 1; - background-color: hsl(var(--b1) / var(--tw-bg-opacity)); -} -.btm-nav>*.disabled, - .btm-nav>*.disabled:hover, - .btm-nav>*[disabled], - .btm-nav>*[disabled]:hover { - pointer-events: none; - --tw-border-opacity: 0; - background-color: hsl(var(--n) / var(--tw-bg-opacity)); - --tw-bg-opacity: 0.1; - color: hsl(var(--bc) / var(--tw-text-opacity)); - --tw-text-opacity: 0.2; -} -.btm-nav>* .label { - font-size: 1rem; - line-height: 1.5rem; -} -.btn:active:hover, - .btn:active:focus { - animation: none; - transform: scale(var(--btn-focus-scale, 0.95)); -} -.btn:hover, - .btn-active { - --tw-border-opacity: 1; - border-color: hsl(var(--nf, var(--n)) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--nf, var(--n)) / var(--tw-bg-opacity)); -} -.btn:focus-visible { - outline: 2px solid hsl(var(--nf)); - outline-offset: 2px; -} -.btn-primary { - --tw-border-opacity: 1; - border-color: hsl(var(--p) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--p) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--pc) / var(--tw-text-opacity)); -} -.btn-primary:hover, - .btn-primary.btn-active { - --tw-border-opacity: 1; - border-color: hsl(var(--pf, var(--p)) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--pf, var(--p)) / var(--tw-bg-opacity)); -} -.btn-primary:focus-visible { - outline: 2px solid hsl(var(--p)); -} -.btn-accent { - --tw-border-opacity: 1; - border-color: hsl(var(--a) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--a) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--ac) / var(--tw-text-opacity)); -} -.btn-accent:hover, - .btn-accent.btn-active { - --tw-border-opacity: 1; - border-color: hsl(var(--af, var(--a)) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--af, var(--a)) / var(--tw-bg-opacity)); -} -.btn-accent:focus-visible { - outline: 2px solid hsl(var(--a)); -} -.btn-success { - --tw-border-opacity: 1; - border-color: hsl(var(--su) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--su) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--suc, var(--nc)) / var(--tw-text-opacity)); -} -.btn-success:hover, - .btn-success.btn-active { - --tw-border-opacity: 1; - border-color: hsl(var(--su) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--su) / var(--tw-bg-opacity)); -} -.btn-success:focus-visible { - outline: 2px solid hsl(var(--su)); -} -.btn-warning { - --tw-border-opacity: 1; - border-color: hsl(var(--wa) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--wa) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--wac, var(--nc)) / var(--tw-text-opacity)); -} -.btn-warning:hover, - .btn-warning.btn-active { - --tw-border-opacity: 1; - border-color: hsl(var(--wa) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--wa) / var(--tw-bg-opacity)); -} -.btn-warning:focus-visible { - outline: 2px solid hsl(var(--wa)); -} -.btn-error { - --tw-border-opacity: 1; - border-color: hsl(var(--er) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--er) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--erc, var(--nc)) / var(--tw-text-opacity)); -} -.btn-error:hover, - .btn-error.btn-active { - --tw-border-opacity: 1; - border-color: hsl(var(--er) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--er) / var(--tw-bg-opacity)); -} -.btn-error:focus-visible { - outline: 2px solid hsl(var(--er)); -} -.btn.glass:hover, - .btn.glass.btn-active { - --glass-opacity: 25%; - --glass-border-opacity: 15%; -} -.btn.glass:focus-visible { - outline: 2px solid currentColor; -} -.btn-ghost { - border-width: 1px; - border-color: transparent; - background-color: transparent; - color: currentColor; -} -.btn-ghost:hover, - .btn-ghost.btn-active { - --tw-border-opacity: 0; - background-color: hsl(var(--bc) / var(--tw-bg-opacity)); - --tw-bg-opacity: 0.2; -} -.btn-ghost:focus-visible { - outline: 2px solid currentColor; -} -.btn-outline { - border-color: currentColor; - background-color: transparent; - --tw-text-opacity: 1; - color: hsl(var(--bc) / var(--tw-text-opacity)); -} -.btn-outline:hover, - .btn-outline.btn-active { - --tw-border-opacity: 1; - border-color: hsl(var(--bc) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--bc) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--b1) / var(--tw-text-opacity)); -} -.btn-outline.btn-primary { - --tw-text-opacity: 1; - color: hsl(var(--p) / var(--tw-text-opacity)); -} -.btn-outline.btn-primary:hover, - .btn-outline.btn-primary.btn-active { - --tw-border-opacity: 1; - border-color: hsl(var(--pf, var(--p)) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--pf, var(--p)) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--pc) / var(--tw-text-opacity)); -} -.btn-outline.btn-secondary { - --tw-text-opacity: 1; - color: hsl(var(--s) / var(--tw-text-opacity)); -} -.btn-outline.btn-secondary:hover, - .btn-outline.btn-secondary.btn-active { - --tw-border-opacity: 1; - border-color: hsl(var(--sf, var(--s)) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--sf, var(--s)) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--sc) / var(--tw-text-opacity)); -} -.btn-outline.btn-accent { - --tw-text-opacity: 1; - color: hsl(var(--a) / var(--tw-text-opacity)); -} -.btn-outline.btn-accent:hover, - .btn-outline.btn-accent.btn-active { - --tw-border-opacity: 1; - border-color: hsl(var(--af, var(--a)) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--af, var(--a)) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--ac) / var(--tw-text-opacity)); -} -.btn-outline.btn-success { - --tw-text-opacity: 1; - color: hsl(var(--su) / var(--tw-text-opacity)); -} -.btn-outline.btn-success:hover, - .btn-outline.btn-success.btn-active { - --tw-border-opacity: 1; - border-color: hsl(var(--su) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--su) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--suc, var(--nc)) / var(--tw-text-opacity)); -} -.btn-outline.btn-info { - --tw-text-opacity: 1; - color: hsl(var(--in) / var(--tw-text-opacity)); -} -.btn-outline.btn-info:hover, - .btn-outline.btn-info.btn-active { - --tw-border-opacity: 1; - border-color: hsl(var(--in) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--in) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--inc, var(--nc)) / var(--tw-text-opacity)); -} -.btn-outline.btn-warning { - --tw-text-opacity: 1; - color: hsl(var(--wa) / var(--tw-text-opacity)); -} -.btn-outline.btn-warning:hover, - .btn-outline.btn-warning.btn-active { - --tw-border-opacity: 1; - border-color: hsl(var(--wa) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--wa) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--wac, var(--nc)) / var(--tw-text-opacity)); -} -.btn-outline.btn-error { - --tw-text-opacity: 1; - color: hsl(var(--er) / var(--tw-text-opacity)); -} -.btn-outline.btn-error:hover, - .btn-outline.btn-error.btn-active { - --tw-border-opacity: 1; - border-color: hsl(var(--er) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--er) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--erc, var(--nc)) / var(--tw-text-opacity)); -} -.btn-disabled, - .btn-disabled:hover, - .btn[disabled], - .btn[disabled]:hover { - --tw-border-opacity: 0; - background-color: hsl(var(--n) / var(--tw-bg-opacity)); - --tw-bg-opacity: 0.2; - color: hsl(var(--bc) / var(--tw-text-opacity)); - --tw-text-opacity: 0.2; -} -.btn.loading.btn-square:before, - .btn.loading.btn-circle:before { - margin-right: 0px; -} -.btn.loading.btn-xl:before, - .btn.loading.btn-lg:before { - height: 1.25rem; - width: 1.25rem; -} -.btn.loading.btn-sm:before, - .btn.loading.btn-xs:before { - height: 0.75rem; - width: 0.75rem; -} -.btn-group > input[type="radio"]:checked.btn, - .btn-group > .btn-active { - --tw-border-opacity: 1; - border-color: hsl(var(--p) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--p) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--pc) / var(--tw-text-opacity)); -} -.btn-group > input[type="radio"]:checked.btn:focus-visible, .btn-group > .btn-active:focus-visible { - outline: 2px solid hsl(var(--p)); -} -@keyframes button-pop { - - 0% { - transform: scale(var(--btn-focus-scale, 0.95)); - } - - 40% { - transform: scale(1.02); - } - - 100% { - transform: scale(1); - } -} -.card :where(figure:first-child) { - overflow: hidden; - border-start-start-radius: inherit; - border-start-end-radius: inherit; - border-end-start-radius: unset; - border-end-end-radius: unset; -} -.card :where(figure:last-child) { - overflow: hidden; - border-start-start-radius: unset; - border-start-end-radius: unset; - border-end-start-radius: inherit; - border-end-end-radius: inherit; -} -.card:focus-visible { - outline: 2px solid currentColor; - outline-offset: 2px; -} -.card.bordered { - border-width: 1px; - --tw-border-opacity: 1; - border-color: hsl(var(--b2, var(--b1)) / var(--tw-border-opacity)); -} -.card.compact .card-body { - padding: 1rem; - font-size: 0.875rem; - line-height: 1.25rem; -} -.card-title { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 1.25rem; - line-height: 1.75rem; - font-weight: 600; -} -.card.image-full :where(figure) { - overflow: hidden; - border-radius: inherit; -} -.checkbox:focus-visible { - outline: 2px solid hsl(var(--bc)); - outline-offset: 2px; -} -.checkbox:checked, - .checkbox[checked="true"], - .checkbox[aria-checked="true"] { - --tw-bg-opacity: 1; - background-color: hsl(var(--bc) / var(--tw-bg-opacity)); - background-repeat: no-repeat; - animation: checkmark var(--animation-input, 0.2s) ease-in-out; - background-image: linear-gradient(-45deg, transparent 65%, hsl(var(--chkbg)) 65.99%), linear-gradient(45deg, transparent 75%, hsl(var(--chkbg)) 75.99%), linear-gradient(-45deg, hsl(var(--chkbg)) 40%, transparent 40.99%), linear-gradient(45deg, hsl(var(--chkbg)) 30%, hsl(var(--chkfg)) 30.99%, hsl(var(--chkfg)) 40%, transparent 40.99%), linear-gradient(-45deg, hsl(var(--chkfg)) 50%, hsl(var(--chkbg)) 50.99%); -} -.checkbox:indeterminate { - --tw-bg-opacity: 1; - background-color: hsl(var(--bc) / var(--tw-bg-opacity)); - background-repeat: no-repeat; - animation: checkmark var(--animation-input, 0.2s) ease-in-out; - background-image: linear-gradient(90deg, transparent 80%, hsl(var(--chkbg)) 80%), linear-gradient(-90deg, transparent 80%, hsl(var(--chkbg)) 80%), linear-gradient(0deg, hsl(var(--chkbg)) 43%, hsl(var(--chkfg)) 43%, hsl(var(--chkfg)) 57%, hsl(var(--chkbg)) 57%); -} -.checkbox-primary { - --chkbg: var(--p); - --chkfg: var(--pc); - --tw-border-opacity: 1; - border-color: hsl(var(--p) / var(--tw-border-opacity)); -} -.checkbox-primary:hover { - --tw-border-opacity: 1; - border-color: hsl(var(--p) / var(--tw-border-opacity)); -} -.checkbox-primary:focus-visible { - outline: 2px solid hsl(var(--p)); -} -.checkbox-primary:checked, - .checkbox-primary[checked="true"], - .checkbox-primary[aria-checked="true"] { - --tw-border-opacity: 1; - border-color: hsl(var(--p) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--p) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--pc) / var(--tw-text-opacity)); -} -.checkbox:disabled { - cursor: not-allowed; - border-color: transparent; - --tw-bg-opacity: 1; - background-color: hsl(var(--bc) / var(--tw-bg-opacity)); - opacity: 0.2; -} -@keyframes checkmark { - - 0% { - background-position-y: 5px; - } - - 50% { - background-position-y: -2px; - } - - 100% { - background-position-y: 0; - } -} -[dir="rtl"] .checkbox:checked, - [dir="rtl"] .checkbox[checked="true"], - [dir="rtl"] .checkbox[aria-checked="true"] { - background-image: linear-gradient(45deg, transparent 65%, hsl(var(--chkbg)) 65.99%), linear-gradient(-45deg, transparent 75%, hsl(var(--chkbg)) 75.99%), linear-gradient(45deg, hsl(var(--chkbg)) 40%, transparent 40.99%), linear-gradient(-45deg, hsl(var(--chkbg)) 30%, hsl(var(--chkfg)) 30.99%, hsl(var(--chkfg)) 40%, transparent 40.99%), linear-gradient(45deg, hsl(var(--chkfg)) 50%, hsl(var(--chkbg)) 50.99%); -} -.drawer-toggle:focus-visible ~ .drawer-content .drawer-button.btn-primary { - outline: 2px solid hsl(var(--p)); -} -.drawer-toggle:focus-visible ~ .drawer-content .drawer-button.btn-accent { - outline: 2px solid hsl(var(--a)); -} -.drawer-toggle:focus-visible ~ .drawer-content .drawer-button.btn-success { - outline: 2px solid hsl(var(--su)); -} -.drawer-toggle:focus-visible ~ .drawer-content .drawer-button.btn-warning { - outline: 2px solid hsl(var(--wa)); -} -.drawer-toggle:focus-visible ~ .drawer-content .drawer-button.btn-error { - outline: 2px solid hsl(var(--er)); -} -.drawer-toggle:focus-visible ~ .drawer-content .drawer-button.btn-ghost { - outline: 2px solid currentColor; -} -.label-text { - font-size: 0.875rem; - line-height: 1.25rem; - --tw-text-opacity: 1; - color: hsl(var(--bc) / var(--tw-text-opacity)); -} -.label a:hover { - --tw-text-opacity: 1; - color: hsl(var(--bc) / var(--tw-text-opacity)); -} -.input[list]::-webkit-calendar-picker-indicator { - line-height: 1em; -} -.input-bordered { - --tw-border-opacity: 0.2; -} -.input:focus { - outline: 2px solid hsla(var(--bc) / 0.2); - outline-offset: 2px; -} -.input-primary { - --tw-border-opacity: 1; - border-color: hsl(var(--p) / var(--tw-border-opacity)); -} -.input-primary:focus { - outline: 2px solid hsl(var(--p)); -} -.input-disabled, - .input[disabled] { - cursor: not-allowed; - --tw-border-opacity: 1; - border-color: hsl(var(--b2, var(--b1)) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--b2, var(--b1)) / var(--tw-bg-opacity)); - --tw-text-opacity: 0.2; -} -.input-disabled::-moz-placeholder, .input[disabled]::-moz-placeholder { - color: hsl(var(--bc) / var(--tw-placeholder-opacity)); - --tw-placeholder-opacity: 0.2; -} -.input-disabled::placeholder, - .input[disabled]::placeholder { - color: hsl(var(--bc) / var(--tw-placeholder-opacity)); - --tw-placeholder-opacity: 0.2; -} -.link-accent { - --tw-text-opacity: 1; - color: hsl(var(--a) / var(--tw-text-opacity)); -} -.link-accent:hover { - --tw-text-opacity: 1; - color: hsl(var(--af, var(--a)) / var(--tw-text-opacity)); -} -.link:focus { - outline: 2px solid transparent; - outline-offset: 2px; -} -.link:focus-visible { - outline: 2px solid currentColor; - outline-offset: 2px; -} -.menu :where(li:not(.menu-title):not(:empty)) > :where(:not(ul).active), - .menu :where(li:not(.menu-title):not(:empty)) > :where(*:not(ul):active) { - --tw-bg-opacity: 1; - background-color: hsl(var(--p) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--pc) / var(--tw-text-opacity)); -} -.menu li.disabled > * { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - color: hsl(var(--bc) / var(--tw-text-opacity)); - --tw-text-opacity: 0.2; -} -.menu li.disabled > *:hover { - background-color: transparent; -} -.mockup-phone .display { - overflow: hidden; - border-radius: 40px; - margin-top: -25px; -} -@keyframes progress-loading { - - 50% { - left: 107%; - } -} -@keyframes radiomark { - - 0% { - box-shadow: 0 0 0 12px hsl(var(--b1)) inset, 0 0 0 12px hsl(var(--b1)) inset; - } - - 50% { - box-shadow: 0 0 0 3px hsl(var(--b1)) inset, 0 0 0 3px hsl(var(--b1)) inset; - } - - 100% { - box-shadow: 0 0 0 4px hsl(var(--b1)) inset, 0 0 0 4px hsl(var(--b1)) inset; - } -} -@keyframes rating-pop { - - 0% { - transform: translateY(-0.125em); - } - - 40% { - transform: translateY(-0.125em); - } - - 100% { - transform: translateY(0); - } -} -.table tr.active th, - .table tr.active td, - .table tr.active:nth-child(even) th, - .table tr.active:nth-child(even) td { - --tw-bg-opacity: 1; - background-color: hsl(var(--b3, var(--b2)) / var(--tw-bg-opacity)); -} -.table tr.hover:hover th, - .table tr.hover:hover td, - .table tr.hover:nth-child(even):hover th, - .table tr.hover:nth-child(even):hover td { - --tw-bg-opacity: 1; - background-color: hsl(var(--b3, var(--b2)) / var(--tw-bg-opacity)); -} -.toast>* { - animation: toast-pop 0.25s ease-out; -} -@keyframes toast-pop { - - 0% { - transform: scale(0.9); - opacity: 0; - } - - 100% { - transform: scale(1); - opacity: 1; - } -} -.tooltip:before, -.tooltip:after { - opacity: 0; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; - transition-delay: 100ms; - transition-duration: 200ms; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); -} -.tooltip:after { - position: absolute; - content: ""; - border-style: solid; - border-width: var(--tooltip-tail, 0); - width: 0; - height: 0; - display: block; -} -.tooltip.tooltip-open:before, -.tooltip.tooltip-open:after, -.tooltip:hover:before, -.tooltip:hover:after { - opacity: 1; - transition-delay: 75ms; -} -.tooltip:not([data-tip]):hover:before, -.tooltip:not([data-tip]):hover:after { - visibility: hidden; - opacity: 0; -} -.tooltip:after, .tooltip-top:after { - transform: translateX(-50%); - border-color: var(--tooltip-color) transparent transparent transparent; - top: auto; - left: 50%; - right: auto; - bottom: var(--tooltip-tail-offset); -} -.btm-nav-xs>*:where(.active) { - border-top-width: 1px; -} -.btm-nav-sm>*:where(.active) { - border-top-width: 2px; -} -.btm-nav-md>*:where(.active) { - border-top-width: 2px; -} -.btm-nav-lg>*:where(.active) { - border-top-width: 4px; -} -.btn-sm { - height: 2rem; - padding-left: 0.75rem; - padding-right: 0.75rem; - min-height: 2rem; - font-size: 0.875rem; -} -.btn-md { - height: 3rem; - padding-left: 1rem; - padding-right: 1rem; - min-height: 3rem; - font-size: 0.875rem; -} -.btn-wide { - width: 16rem; -} -.btn-square:where(.btn-xs) { - height: 1.5rem; - width: 1.5rem; - padding: 0px; -} -.btn-square:where(.btn-sm) { - height: 2rem; - width: 2rem; - padding: 0px; -} -.btn-square:where(.btn-md) { - height: 3rem; - width: 3rem; - padding: 0px; -} -.btn-square:where(.btn-lg) { - height: 4rem; - width: 4rem; - padding: 0px; -} -.btn-circle:where(.btn-sm) { - height: 2rem; - width: 2rem; - border-radius: 9999px; - padding: 0px; -} -.btn-circle:where(.btn-md) { - height: 3rem; - width: 3rem; - border-radius: 9999px; - padding: 0px; -} -:where(.toast) { - right: 0px; - left: auto; - top: auto; - bottom: 0px; - --tw-translate-x: 0px; - --tw-translate-y: 0px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} -.toast:where(.toast-start) { - right: auto; - left: 0px; - --tw-translate-x: 0px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} -.toast:where(.toast-center) { - right: 50%; - left: 50%; - --tw-translate-x: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} -.toast:where(.toast-end) { - right: 0px; - left: auto; - --tw-translate-x: 0px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} -.toast:where(.toast-bottom) { - top: auto; - bottom: 0px; - --tw-translate-y: 0px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} -.toast:where(.toast-middle) { - top: 50%; - bottom: auto; - --tw-translate-y: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} -.toast:where(.toast-top) { - top: 0px; - bottom: auto; - --tw-translate-y: 0px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} -.avatar.offline:before { - content: ""; - position: absolute; - z-index: 10; - display: block; - border-radius: 9999px; - --tw-bg-opacity: 1; - background-color: hsl(var(--b3, var(--b2)) / var(--tw-bg-opacity)); - width: 15%; - height: 15%; - top: 7%; - right: 7%; - box-shadow: 0 0 0 2px hsl(var(--b1)); -} -.btn-group .btn:not(:first-child):not(:last-child), .btn-group.btn-group-horizontal .btn:not(:first-child):not(:last-child) { - border-top-left-radius: 0; - border-top-right-radius: 0; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} -.btn-group .btn:first-child:not(:last-child), .btn-group.btn-group-horizontal .btn:first-child:not(:last-child) { - margin-left: -1px; - margin-top: -0px; - border-top-left-radius: var(--rounded-btn, 0.5rem); - border-top-right-radius: 0; - border-bottom-left-radius: var(--rounded-btn, 0.5rem); - border-bottom-right-radius: 0; -} -.btn-group .btn:last-child:not(:first-child), .btn-group.btn-group-horizontal .btn:last-child:not(:first-child) { - border-top-left-radius: 0; - border-top-right-radius: var(--rounded-btn, 0.5rem); - border-bottom-left-radius: 0; - border-bottom-right-radius: var(--rounded-btn, 0.5rem); -} -.btn-group.btn-group-vertical .btn:first-child:not(:last-child) { - margin-left: -0px; - margin-top: -1px; - border-top-left-radius: var(--rounded-btn, 0.5rem); - border-top-right-radius: var(--rounded-btn, 0.5rem); - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} -.btn-group.btn-group-vertical .btn:last-child:not(:first-child) { - border-top-left-radius: 0; - border-top-right-radius: 0; - border-bottom-left-radius: var(--rounded-btn, 0.5rem); - border-bottom-right-radius: var(--rounded-btn, 0.5rem); -} -.card-compact .card-body { - padding: 1rem; - font-size: 0.875rem; - line-height: 1.25rem; -} -.card-compact .card-title { - margin-bottom: 0.25rem; -} -.card-normal .card-body { - padding: var(--padding-card, 2rem); - font-size: 1rem; - line-height: 1.5rem; -} -.card-normal .card-title { - margin-bottom: 0.75rem; -} -.mx-auto { - margin-left: auto; - margin-right: auto; -} -.my-10 { - margin-top: 2.5rem; - margin-bottom: 2.5rem; -} -.my-2 { - margin-top: 0.5rem; - margin-bottom: 0.5rem; -} -.my-3 { - margin-top: 0.75rem; - margin-bottom: 0.75rem; -} -.my-5 { - margin-top: 1.25rem; - margin-bottom: 1.25rem; -} -.mb-2 { - margin-bottom: 0.5rem; -} -.mb-4 { - margin-bottom: 1rem; -} -.mb-8 { - margin-bottom: 2rem; -} -.ml-4 { - margin-left: 1rem; -} -.mr-3 { - margin-right: 0.75rem; -} -.mt-4 { - margin-top: 1rem; -} -.flex { - display: flex; -} -.contents { - display: contents; -} -.h-96 { - height: 24rem; -} -.h-screen { - height: 100vh; -} -.max-h-80 { - max-height: 20rem; -} -.w-96 { - width: 24rem; -} -.w-\[40rem\] { - width: 40rem; -} -.w-\[51rem\] { - width: 51rem; -} -.w-full { - width: 100%; -} -.max-w-4xl { - max-width: 56rem; -} -.flex-grow { - flex-grow: 1; -} -.cursor-pointer { - cursor: pointer; -} -.flex-col { - flex-direction: column; -} -.items-center { - align-items: center; -} -.justify-end { - justify-content: flex-end; -} -.justify-center { - justify-content: center; -} -.justify-between { - justify-content: space-between; -} -.self-center { - align-self: center; -} -.overflow-y-auto { - overflow-y: auto; -} -.scroll-smooth { - scroll-behavior: smooth; -} -.whitespace-pre-line { - white-space: pre-line; -} -.bg-base-200 { - --tw-bg-opacity: 1; - background-color: hsl(var(--b2, var(--b1)) / var(--tw-bg-opacity)); -} -.bg-base-300 { - --tw-bg-opacity: 1; - background-color: hsl(var(--b3, var(--b2)) / var(--tw-bg-opacity)); -} -.bg-warning { - --tw-bg-opacity: 1; - background-color: hsl(var(--wa) / var(--tw-bg-opacity)); -} -.p-4 { - padding: 1rem; -} -.px-3 { - padding-left: 0.75rem; - padding-right: 0.75rem; -} -.py-2 { - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} -.pt-24 { - padding-top: 6rem; -} -.text-center { - text-align: center; -} -.text-3xl { - font-size: 1.875rem; - line-height: 2.25rem; -} -.text-xl { - font-size: 1.25rem; - line-height: 1.75rem; -} -.font-bold { - font-weight: 700; -} -.text-warning-content { - --tw-text-opacity: 1; - color: hsl(var(--wac, var(--nc)) / var(--tw-text-opacity)); -} -.shadow-xl { - --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); - --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} -@media (min-width: 640px) { - - .sm\:w-auto { - width: auto; - } - - .sm\:w-full { - width: 100%; - } - - .sm\:items-center { - align-items: center; - } - - .sm\:justify-center { - justify-content: center; - } - - .sm\:pt-0 { - padding-top: 0px; - } -} -@media (min-width: 768px) { - - .md\:w-4\/5 { - width: 80%; - } - - .md\:px-0 { - padding-left: 0px; - padding-right: 0px; - } -} -@media (min-width: 1024px) { - - .lg\:w-3\/5 { - width: 60%; - } -} \ No newline at end of file diff --git a/frontend/.netlify/server/chunks/Toaster.svelte_svelte_type_style_lang.js b/frontend/.netlify/server/chunks/Toaster.svelte_svelte_type_style_lang.js deleted file mode 100644 index 10604d0..0000000 --- a/frontend/.netlify/server/chunks/Toaster.svelte_svelte_type_style_lang.js +++ /dev/null @@ -1,242 +0,0 @@ -import { d as derived, w as writable } from "./index2.js"; -import { k as get_store_value } from "./index3.js"; -function writableDerived(origins, derive, reflect, initial) { - var childDerivedSetter, originValues, blockNextDerive = false; - var reflectOldValues = reflect.length >= 2; - var wrappedDerive = (got, set) => { - childDerivedSetter = set; - if (reflectOldValues) { - originValues = got; - } - if (!blockNextDerive) { - let returned = derive(got, set); - if (derive.length < 2) { - set(returned); - } else { - return returned; - } - } - blockNextDerive = false; - }; - var childDerived = derived(origins, wrappedDerive, initial); - var singleOrigin = !Array.isArray(origins); - function doReflect(reflecting) { - var setWith = reflect(reflecting, originValues); - if (singleOrigin) { - blockNextDerive = true; - origins.set(setWith); - } else { - setWith.forEach((value, i) => { - blockNextDerive = true; - origins[i].set(value); - }); - } - blockNextDerive = false; - } - var tryingSet = false; - function update2(fn) { - var isUpdated, mutatedBySubscriptions, oldValue, newValue; - if (tryingSet) { - newValue = fn(get_store_value(childDerived)); - childDerivedSetter(newValue); - return; - } - var unsubscribe = childDerived.subscribe((value) => { - if (!tryingSet) { - oldValue = value; - } else if (!isUpdated) { - isUpdated = true; - } else { - mutatedBySubscriptions = true; - } - }); - newValue = fn(oldValue); - tryingSet = true; - childDerivedSetter(newValue); - unsubscribe(); - tryingSet = false; - if (mutatedBySubscriptions) { - newValue = get_store_value(childDerived); - } - if (isUpdated) { - doReflect(newValue); - } - } - return { - subscribe: childDerived.subscribe, - set(value) { - update2(() => value); - }, - update: update2 - }; -} -const TOAST_LIMIT = 20; -const toasts = writable([]); -const pausedAt = writable(null); -const toastTimeouts = /* @__PURE__ */ new Map(); -const addToRemoveQueue = (toastId) => { - if (toastTimeouts.has(toastId)) { - return; - } - const timeout = setTimeout(() => { - toastTimeouts.delete(toastId); - remove(toastId); - }, 1e3); - toastTimeouts.set(toastId, timeout); -}; -const clearFromRemoveQueue = (toastId) => { - const timeout = toastTimeouts.get(toastId); - if (timeout) { - clearTimeout(timeout); - } -}; -function update(toast2) { - if (toast2.id) { - clearFromRemoveQueue(toast2.id); - } - toasts.update(($toasts) => $toasts.map((t) => t.id === toast2.id ? { ...t, ...toast2 } : t)); -} -function add(toast2) { - toasts.update(($toasts) => [toast2, ...$toasts].slice(0, TOAST_LIMIT)); -} -function upsert(toast2) { - if (get_store_value(toasts).find((t) => t.id === toast2.id)) { - update(toast2); - } else { - add(toast2); - } -} -function dismiss(toastId) { - toasts.update(($toasts) => { - if (toastId) { - addToRemoveQueue(toastId); - } else { - $toasts.forEach((toast2) => { - addToRemoveQueue(toast2.id); - }); - } - return $toasts.map((t) => t.id === toastId || toastId === void 0 ? { ...t, visible: false } : t); - }); -} -function remove(toastId) { - toasts.update(($toasts) => { - if (toastId === void 0) { - return []; - } - return $toasts.filter((t) => t.id !== toastId); - }); -} -function startPause(time) { - pausedAt.set(time); -} -function endPause(time) { - let diff; - pausedAt.update(($pausedAt) => { - diff = time - ($pausedAt || 0); - return null; - }); - toasts.update(($toasts) => $toasts.map((t) => ({ - ...t, - pauseDuration: t.pauseDuration + diff - }))); -} -const defaultTimeouts = { - blank: 4e3, - error: 4e3, - success: 2e3, - loading: Infinity, - custom: 4e3 -}; -function useToasterStore(toastOptions = {}) { - const mergedToasts = writableDerived(toasts, ($toasts) => $toasts.map((t) => ({ - ...toastOptions, - ...toastOptions[t.type], - ...t, - duration: t.duration || toastOptions[t.type]?.duration || toastOptions?.duration || defaultTimeouts[t.type], - style: [toastOptions.style, toastOptions[t.type]?.style, t.style].join(";") - })), ($toasts) => $toasts); - return { - toasts: mergedToasts, - pausedAt - }; -} -const isFunction = (valOrFunction) => typeof valOrFunction === "function"; -const resolveValue = (valOrFunction, arg) => isFunction(valOrFunction) ? valOrFunction(arg) : valOrFunction; -const genId = (() => { - let count = 0; - return () => { - count += 1; - return count.toString(); - }; -})(); -const prefersReducedMotion = (() => { - let shouldReduceMotion; - return () => { - if (shouldReduceMotion === void 0 && typeof window !== "undefined") { - const mediaQuery = matchMedia("(prefers-reduced-motion: reduce)"); - shouldReduceMotion = !mediaQuery || mediaQuery.matches; - } - return shouldReduceMotion; - }; -})(); -const createToast = (message, type = "blank", opts) => ({ - createdAt: Date.now(), - visible: true, - type, - ariaProps: { - role: "status", - "aria-live": "polite" - }, - message, - pauseDuration: 0, - ...opts, - id: opts?.id || genId() -}); -const createHandler = (type) => (message, options) => { - const toast2 = createToast(message, type, options); - upsert(toast2); - return toast2.id; -}; -const toast = (message, opts) => createHandler("blank")(message, opts); -toast.error = createHandler("error"); -toast.success = createHandler("success"); -toast.loading = createHandler("loading"); -toast.custom = createHandler("custom"); -toast.dismiss = (toastId) => { - dismiss(toastId); -}; -toast.remove = (toastId) => remove(toastId); -toast.promise = (promise, msgs, opts) => { - const id = toast.loading(msgs.loading, { ...opts, ...opts?.loading }); - promise.then((p) => { - toast.success(resolveValue(msgs.success, p), { - id, - ...opts, - ...opts?.success - }); - return p; - }).catch((e) => { - toast.error(resolveValue(msgs.error, e), { - id, - ...opts, - ...opts?.error - }); - }); - return promise; -}; -const CheckmarkIcon_svelte_svelte_type_style_lang = ""; -const ErrorIcon_svelte_svelte_type_style_lang = ""; -const LoaderIcon_svelte_svelte_type_style_lang = ""; -const ToastIcon_svelte_svelte_type_style_lang = ""; -const ToastMessage_svelte_svelte_type_style_lang = ""; -const ToastBar_svelte_svelte_type_style_lang = ""; -const ToastWrapper_svelte_svelte_type_style_lang = ""; -const Toaster_svelte_svelte_type_style_lang = ""; -export { - update as a, - endPause as e, - prefersReducedMotion as p, - startPause as s, - toast as t, - useToasterStore as u -}; diff --git a/frontend/.netlify/server/chunks/index.js b/frontend/.netlify/server/chunks/index.js deleted file mode 100644 index c63b566..0000000 --- a/frontend/.netlify/server/chunks/index.js +++ /dev/null @@ -1,78 +0,0 @@ -let HttpError = class HttpError2 { - /** - * @param {number} status - * @param {{message: string} extends App.Error ? (App.Error | string | undefined) : App.Error} body - */ - constructor(status, body) { - this.status = status; - if (typeof body === "string") { - this.body = { message: body }; - } else if (body) { - this.body = body; - } else { - this.body = { message: `Error: ${status}` }; - } - } - toString() { - return JSON.stringify(this.body); - } -}; -let Redirect = class Redirect2 { - /** - * @param {300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308} status - * @param {string} location - */ - constructor(status, location) { - this.status = status; - this.location = location; - } -}; -let ActionFailure = class ActionFailure2 { - /** - * @param {number} status - * @param {T} [data] - */ - constructor(status, data) { - this.status = status; - this.data = data; - } -}; -function error(status, message) { - if (isNaN(status) || status < 400 || status > 599) { - throw new Error(`HTTP error status codes must be between 400 and 599 — ${status} is invalid`); - } - return new HttpError(status, message); -} -function json(data, init) { - const body = JSON.stringify(data); - const headers = new Headers(init?.headers); - if (!headers.has("content-length")) { - headers.set("content-length", encoder.encode(body).byteLength.toString()); - } - if (!headers.has("content-type")) { - headers.set("content-type", "application/json"); - } - return new Response(body, { - ...init, - headers - }); -} -const encoder = new TextEncoder(); -function text(body, init) { - const headers = new Headers(init?.headers); - if (!headers.has("content-length")) { - headers.set("content-length", encoder.encode(body).byteLength.toString()); - } - return new Response(body, { - ...init, - headers - }); -} -export { - ActionFailure as A, - HttpError as H, - Redirect as R, - error as e, - json as j, - text as t -}; diff --git a/frontend/.netlify/server/chunks/index2.js b/frontend/.netlify/server/chunks/index2.js deleted file mode 100644 index 5663a88..0000000 --- a/frontend/.netlify/server/chunks/index2.js +++ /dev/null @@ -1,92 +0,0 @@ -import { n as noop, l as safe_not_equal, h as subscribe, r as run_all, p as is_function } from "./index3.js"; -const subscriber_queue = []; -function readable(value, start) { - return { - subscribe: writable(value, start).subscribe - }; -} -function writable(value, start = noop) { - let stop; - const subscribers = /* @__PURE__ */ new Set(); - function set(new_value) { - if (safe_not_equal(value, new_value)) { - value = new_value; - if (stop) { - const run_queue = !subscriber_queue.length; - for (const subscriber of subscribers) { - subscriber[1](); - subscriber_queue.push(subscriber, value); - } - if (run_queue) { - for (let i = 0; i < subscriber_queue.length; i += 2) { - subscriber_queue[i][0](subscriber_queue[i + 1]); - } - subscriber_queue.length = 0; - } - } - } - } - function update(fn) { - set(fn(value)); - } - function subscribe2(run, invalidate = noop) { - const subscriber = [run, invalidate]; - subscribers.add(subscriber); - if (subscribers.size === 1) { - stop = start(set) || noop; - } - run(value); - return () => { - subscribers.delete(subscriber); - if (subscribers.size === 0 && stop) { - stop(); - stop = null; - } - }; - } - return { set, update, subscribe: subscribe2 }; -} -function derived(stores, fn, initial_value) { - const single = !Array.isArray(stores); - const stores_array = single ? [stores] : stores; - const auto = fn.length < 2; - return readable(initial_value, (set) => { - let started = false; - const values = []; - let pending = 0; - let cleanup = noop; - const sync = () => { - if (pending) { - return; - } - cleanup(); - const result = fn(single ? values[0] : values, set); - if (auto) { - set(result); - } else { - cleanup = is_function(result) ? result : noop; - } - }; - const unsubscribers = stores_array.map((store, i) => subscribe(store, (value) => { - values[i] = value; - pending &= ~(1 << i); - if (started) { - sync(); - } - }, () => { - pending |= 1 << i; - })); - started = true; - sync(); - return function stop() { - run_all(unsubscribers); - cleanup(); - started = false; - }; - }); -} -export { - derived as d, - readable as r, - writable as w -}; diff --git a/frontend/.netlify/server/chunks/index3.js b/frontend/.netlify/server/chunks/index3.js deleted file mode 100644 index b120979..0000000 --- a/frontend/.netlify/server/chunks/index3.js +++ /dev/null @@ -1,250 +0,0 @@ -function noop() { -} -function run(fn) { - return fn(); -} -function blank_object() { - return /* @__PURE__ */ Object.create(null); -} -function run_all(fns) { - fns.forEach(run); -} -function is_function(thing) { - return typeof thing === "function"; -} -function safe_not_equal(a, b) { - return a != a ? b == b : a !== b || (a && typeof a === "object" || typeof a === "function"); -} -function subscribe(store, ...callbacks) { - if (store == null) { - return noop; - } - const unsub = store.subscribe(...callbacks); - return unsub.unsubscribe ? () => unsub.unsubscribe() : unsub; -} -function get_store_value(store) { - let value; - subscribe(store, (_) => value = _)(); - return value; -} -let current_component; -function set_current_component(component) { - current_component = component; -} -function get_current_component() { - if (!current_component) - throw new Error("Function called outside component initialization"); - return current_component; -} -function onDestroy(fn) { - get_current_component().$$.on_destroy.push(fn); -} -function setContext(key, context) { - get_current_component().$$.context.set(key, context); - return context; -} -function getContext(key) { - return get_current_component().$$.context.get(key); -} -const _boolean_attributes = [ - "allowfullscreen", - "allowpaymentrequest", - "async", - "autofocus", - "autoplay", - "checked", - "controls", - "default", - "defer", - "disabled", - "formnovalidate", - "hidden", - "inert", - "ismap", - "itemscope", - "loop", - "multiple", - "muted", - "nomodule", - "novalidate", - "open", - "playsinline", - "readonly", - "required", - "reversed", - "selected" -]; -const boolean_attributes = /* @__PURE__ */ new Set([..._boolean_attributes]); -const invalid_attribute_name_character = /[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u; -function spread(args, attrs_to_add) { - const attributes = Object.assign({}, ...args); - if (attrs_to_add) { - const classes_to_add = attrs_to_add.classes; - const styles_to_add = attrs_to_add.styles; - if (classes_to_add) { - if (attributes.class == null) { - attributes.class = classes_to_add; - } else { - attributes.class += " " + classes_to_add; - } - } - if (styles_to_add) { - if (attributes.style == null) { - attributes.style = style_object_to_string(styles_to_add); - } else { - attributes.style = style_object_to_string(merge_ssr_styles(attributes.style, styles_to_add)); - } - } - } - let str = ""; - Object.keys(attributes).forEach((name) => { - if (invalid_attribute_name_character.test(name)) - return; - const value = attributes[name]; - if (value === true) - str += " " + name; - else if (boolean_attributes.has(name.toLowerCase())) { - if (value) - str += " " + name; - } else if (value != null) { - str += ` ${name}="${value}"`; - } - }); - return str; -} -function merge_ssr_styles(style_attribute, style_directive) { - const style_object = {}; - for (const individual_style of style_attribute.split(";")) { - const colon_index = individual_style.indexOf(":"); - const name = individual_style.slice(0, colon_index).trim(); - const value = individual_style.slice(colon_index + 1).trim(); - if (!name) - continue; - style_object[name] = value; - } - for (const name in style_directive) { - const value = style_directive[name]; - if (value) { - style_object[name] = value; - } else { - delete style_object[name]; - } - } - return style_object; -} -const ATTR_REGEX = /[&"]/g; -const CONTENT_REGEX = /[&<]/g; -function escape(value, is_attr = false) { - const str = String(value); - const pattern = is_attr ? ATTR_REGEX : CONTENT_REGEX; - pattern.lastIndex = 0; - let escaped = ""; - let last = 0; - while (pattern.test(str)) { - const i = pattern.lastIndex - 1; - const ch = str[i]; - escaped += str.substring(last, i) + (ch === "&" ? "&" : ch === '"' ? """ : "<"); - last = i + 1; - } - return escaped + str.substring(last); -} -function escape_attribute_value(value) { - const should_escape = typeof value === "string" || value && typeof value === "object"; - return should_escape ? escape(value, true) : value; -} -function escape_object(obj) { - const result = {}; - for (const key in obj) { - result[key] = escape_attribute_value(obj[key]); - } - return result; -} -function each(items, fn) { - let str = ""; - for (let i = 0; i < items.length; i += 1) { - str += fn(items[i], i); - } - return str; -} -const missing_component = { - $$render: () => "" -}; -function validate_component(component, name) { - if (!component || !component.$$render) { - if (name === "svelte:component") - name += " this={...}"; - throw new Error(`<${name}> is not a valid SSR component. You may need to review your build config to ensure that dependencies are compiled, rather than imported as pre-compiled modules. Otherwise you may need to fix a <${name}>.`); - } - return component; -} -let on_destroy; -function create_ssr_component(fn) { - function $$render(result, props, bindings, slots, context) { - const parent_component = current_component; - const $$ = { - on_destroy, - context: new Map(context || (parent_component ? parent_component.$$.context : [])), - // these will be immediately discarded - on_mount: [], - before_update: [], - after_update: [], - callbacks: blank_object() - }; - set_current_component({ $$ }); - const html = fn(result, props, bindings, slots); - set_current_component(parent_component); - return html; - } - return { - render: (props = {}, { $$slots = {}, context = /* @__PURE__ */ new Map() } = {}) => { - on_destroy = []; - const result = { title: "", head: "", css: /* @__PURE__ */ new Set() }; - const html = $$render(result, props, {}, $$slots, context); - run_all(on_destroy); - return { - html, - css: { - code: Array.from(result.css).map((css) => css.code).join("\n"), - map: null - // TODO - }, - head: result.title + result.head - }; - }, - $$render - }; -} -function add_attribute(name, value, boolean) { - if (value == null || boolean && !value) - return ""; - const assignment = boolean && value === true ? "" : `="${escape(value, true)}"`; - return ` ${name}${assignment}`; -} -function style_object_to_string(style_object) { - return Object.keys(style_object).filter((key) => style_object[key]).map((key) => `${key}: ${escape_attribute_value(style_object[key])};`).join(" "); -} -function add_styles(style_object) { - const styles = style_object_to_string(style_object); - return styles ? ` style="${styles}"` : ""; -} -export { - add_styles as a, - spread as b, - create_ssr_component as c, - escape_object as d, - escape as e, - merge_ssr_styles as f, - add_attribute as g, - subscribe as h, - each as i, - getContext as j, - get_store_value as k, - safe_not_equal as l, - missing_component as m, - noop as n, - onDestroy as o, - is_function as p, - run_all as r, - setContext as s, - validate_component as v -}; diff --git a/frontend/.netlify/server/chunks/internal.js b/frontend/.netlify/server/chunks/internal.js deleted file mode 100644 index 89831b1..0000000 --- a/frontend/.netlify/server/chunks/internal.js +++ /dev/null @@ -1,179 +0,0 @@ -import { c as create_ssr_component, s as setContext, v as validate_component, m as missing_component } from "./index3.js"; -import "./shared-server.js"; -let base = ""; -let assets = base; -const initial = { base, assets }; -function reset() { - base = initial.base; - assets = initial.assets; -} -function set_assets(path) { - assets = initial.assets = path; -} -function afterUpdate() { -} -function set_building() { -} -const Root = create_ssr_component(($$result, $$props, $$bindings, slots) => { - let { stores } = $$props; - let { page } = $$props; - let { constructors } = $$props; - let { components = [] } = $$props; - let { form } = $$props; - let { data_0 = null } = $$props; - let { data_1 = null } = $$props; - { - setContext("__svelte__", stores); - } - afterUpdate(stores.page.notify); - if ($$props.stores === void 0 && $$bindings.stores && stores !== void 0) - $$bindings.stores(stores); - if ($$props.page === void 0 && $$bindings.page && page !== void 0) - $$bindings.page(page); - if ($$props.constructors === void 0 && $$bindings.constructors && constructors !== void 0) - $$bindings.constructors(constructors); - if ($$props.components === void 0 && $$bindings.components && components !== void 0) - $$bindings.components(components); - if ($$props.form === void 0 && $$bindings.form && form !== void 0) - $$bindings.form(form); - if ($$props.data_0 === void 0 && $$bindings.data_0 && data_0 !== void 0) - $$bindings.data_0(data_0); - if ($$props.data_1 === void 0 && $$bindings.data_1 && data_1 !== void 0) - $$bindings.data_1(data_1); - let $$settled; - let $$rendered; - do { - $$settled = true; - { - stores.page.set(page); - } - $$rendered = ` - - -${constructors[1] ? `${validate_component(constructors[0] || missing_component, "svelte:component").$$render( - $$result, - { data: data_0, this: components[0] }, - { - this: ($$value) => { - components[0] = $$value; - $$settled = false; - } - }, - { - default: () => { - return `${validate_component(constructors[1] || missing_component, "svelte:component").$$render( - $$result, - { data: data_1, form, this: components[1] }, - { - this: ($$value) => { - components[1] = $$value; - $$settled = false; - } - }, - {} - )}`; - } - } - )}` : `${validate_component(constructors[0] || missing_component, "svelte:component").$$render( - $$result, - { data: data_0, form, this: components[0] }, - { - this: ($$value) => { - components[0] = $$value; - $$settled = false; - } - }, - {} - )}`} - -${``}`; - } while (!$$settled); - return $$rendered; -}); -const options = { - app_template_contains_nonce: false, - csp: { "mode": "auto", "directives": { "upgrade-insecure-requests": false, "block-all-mixed-content": false }, "reportOnly": { "upgrade-insecure-requests": false, "block-all-mixed-content": false } }, - csrf_check_origin: true, - embedded: false, - env_public_prefix: "PUBLIC_", - hooks: null, - // added lazily, via `get_hooks` - preload_strategy: "modulepreload", - root: Root, - service_worker: false, - templates: { - app: ({ head, body, assets: assets2, nonce, env }) => '\n\n \n \n \n \n ' + head + '\n \n \n
' + body + "
\n \n\n", - error: ({ status, message }) => '\n\n \n \n ' + message + ` - - - - -
- ` + status + '\n
\n

' + message + "

\n
\n
\n \n\n" - }, - version_hash: "1vmmy0u" -}; -function get_hooks() { - return {}; -} -export { - assets as a, - base as b, - set_building as c, - get_hooks as g, - options as o, - reset as r, - set_assets as s -}; diff --git a/frontend/.netlify/server/chunks/shared-server.js b/frontend/.netlify/server/chunks/shared-server.js deleted file mode 100644 index 13941ee..0000000 --- a/frontend/.netlify/server/chunks/shared-server.js +++ /dev/null @@ -1,11 +0,0 @@ -let public_env = {}; -function set_private_env(environment) { -} -function set_public_env(environment) { - public_env = environment; -} -export { - set_private_env as a, - public_env as p, - set_public_env as s -}; diff --git a/frontend/.netlify/server/entries/fallbacks/error.svelte.js b/frontend/.netlify/server/entries/fallbacks/error.svelte.js deleted file mode 100644 index aa7ae7c..0000000 --- a/frontend/.netlify/server/entries/fallbacks/error.svelte.js +++ /dev/null @@ -1,30 +0,0 @@ -import { j as getContext, c as create_ssr_component, h as subscribe, e as escape } from "../../chunks/index3.js"; -const getStores = () => { - const stores = getContext("__svelte__"); - return { - page: { - subscribe: stores.page.subscribe - }, - navigating: { - subscribe: stores.navigating.subscribe - }, - updated: stores.updated - }; -}; -const page = { - /** @param {(value: any) => void} fn */ - subscribe(fn) { - const store = getStores().page; - return store.subscribe(fn); - } -}; -const Error$1 = create_ssr_component(($$result, $$props, $$bindings, slots) => { - let $page, $$unsubscribe_page; - $$unsubscribe_page = subscribe(page, (value) => $page = value); - $$unsubscribe_page(); - return `

${escape($page.status)}

-

${escape($page.error?.message)}

`; -}); -export { - Error$1 as default -}; diff --git a/frontend/.netlify/server/entries/pages/_layout.svelte.js b/frontend/.netlify/server/entries/pages/_layout.svelte.js deleted file mode 100644 index 758258e..0000000 --- a/frontend/.netlify/server/entries/pages/_layout.svelte.js +++ /dev/null @@ -1,289 +0,0 @@ -import { o as onDestroy, c as create_ssr_component, a as add_styles, e as escape, v as validate_component, m as missing_component, b as spread, d as escape_object, f as merge_ssr_styles, g as add_attribute, h as subscribe, i as each } from "../../chunks/index3.js"; -import { u as useToasterStore, t as toast, s as startPause, e as endPause, a as update, p as prefersReducedMotion } from "../../chunks/Toaster.svelte_svelte_type_style_lang.js"; -const app = ""; -function calculateOffset(toast2, $toasts, opts) { - const { reverseOrder, gutter = 8, defaultPosition } = opts || {}; - const relevantToasts = $toasts.filter((t) => (t.position || defaultPosition) === (toast2.position || defaultPosition) && t.height); - const toastIndex = relevantToasts.findIndex((t) => t.id === toast2.id); - const toastsBefore = relevantToasts.filter((toast3, i) => i < toastIndex && toast3.visible).length; - const offset = relevantToasts.filter((t) => t.visible).slice(...reverseOrder ? [toastsBefore + 1] : [0, toastsBefore]).reduce((acc, t) => acc + (t.height || 0) + gutter, 0); - return offset; -} -const handlers = { - startPause() { - startPause(Date.now()); - }, - endPause() { - endPause(Date.now()); - }, - updateHeight: (toastId, height) => { - update({ id: toastId, height }); - }, - calculateOffset -}; -function useToaster(toastOptions) { - const { toasts, pausedAt } = useToasterStore(toastOptions); - const timeouts = /* @__PURE__ */ new Map(); - let _pausedAt; - const unsubscribes = [ - pausedAt.subscribe(($pausedAt) => { - if ($pausedAt) { - for (const [, timeoutId] of timeouts) { - clearTimeout(timeoutId); - } - timeouts.clear(); - } - _pausedAt = $pausedAt; - }), - toasts.subscribe(($toasts) => { - if (_pausedAt) { - return; - } - const now = Date.now(); - for (const t of $toasts) { - if (timeouts.has(t.id)) { - continue; - } - if (t.duration === Infinity) { - continue; - } - const durationLeft = (t.duration || 0) + t.pauseDuration - (now - t.createdAt); - if (durationLeft < 0) { - if (t.visible) { - toast.dismiss(t.id); - } - return null; - } - timeouts.set(t.id, setTimeout(() => toast.dismiss(t.id), durationLeft)); - } - }) - ]; - onDestroy(() => { - for (const unsubscribe of unsubscribes) { - unsubscribe(); - } - }); - return { toasts, handlers }; -} -const css$7 = { - code: "div.svelte-lzwg39{width:20px;opacity:0;height:20px;border-radius:10px;background:var(--primary, #61d345);position:relative;transform:rotate(45deg);animation:svelte-lzwg39-circleAnimation 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;animation-delay:100ms}div.svelte-lzwg39::after{content:'';box-sizing:border-box;animation:svelte-lzwg39-checkmarkAnimation 0.2s ease-out forwards;opacity:0;animation-delay:200ms;position:absolute;border-right:2px solid;border-bottom:2px solid;border-color:var(--secondary, #fff);bottom:6px;left:6px;height:10px;width:6px}@keyframes svelte-lzwg39-circleAnimation{from{transform:scale(0) rotate(45deg);opacity:0}to{transform:scale(1) rotate(45deg);opacity:1}}@keyframes svelte-lzwg39-checkmarkAnimation{0%{height:0;width:0;opacity:0}40%{height:0;width:6px;opacity:1}100%{opacity:1;height:10px}}", - map: null -}; -const CheckmarkIcon = create_ssr_component(($$result, $$props, $$bindings, slots) => { - let { primary = "#61d345" } = $$props; - let { secondary = "#fff" } = $$props; - if ($$props.primary === void 0 && $$bindings.primary && primary !== void 0) - $$bindings.primary(primary); - if ($$props.secondary === void 0 && $$bindings.secondary && secondary !== void 0) - $$bindings.secondary(secondary); - $$result.css.add(css$7); - return ` - - -
`; -}); -const css$6 = { - code: "div.svelte-10jnndo{width:20px;opacity:0;height:20px;border-radius:10px;background:var(--primary, #ff4b4b);position:relative;transform:rotate(45deg);animation:svelte-10jnndo-circleAnimation 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;animation-delay:100ms}div.svelte-10jnndo::after,div.svelte-10jnndo::before{content:'';animation:svelte-10jnndo-firstLineAnimation 0.15s ease-out forwards;animation-delay:150ms;position:absolute;border-radius:3px;opacity:0;background:var(--secondary, #fff);bottom:9px;left:4px;height:2px;width:12px}div.svelte-10jnndo:before{animation:svelte-10jnndo-secondLineAnimation 0.15s ease-out forwards;animation-delay:180ms;transform:rotate(90deg)}@keyframes svelte-10jnndo-circleAnimation{from{transform:scale(0) rotate(45deg);opacity:0}to{transform:scale(1) rotate(45deg);opacity:1}}@keyframes svelte-10jnndo-firstLineAnimation{from{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}@keyframes svelte-10jnndo-secondLineAnimation{from{transform:scale(0) rotate(90deg);opacity:0}to{transform:scale(1) rotate(90deg);opacity:1}}", - map: null -}; -const ErrorIcon = create_ssr_component(($$result, $$props, $$bindings, slots) => { - let { primary = "#ff4b4b" } = $$props; - let { secondary = "#fff" } = $$props; - if ($$props.primary === void 0 && $$bindings.primary && primary !== void 0) - $$bindings.primary(primary); - if ($$props.secondary === void 0 && $$bindings.secondary && secondary !== void 0) - $$bindings.secondary(secondary); - $$result.css.add(css$6); - return ` - - -
`; -}); -const css$5 = { - code: "div.svelte-bj4lu8{width:12px;height:12px;box-sizing:border-box;border:2px solid;border-radius:100%;border-color:var(--secondary, #e0e0e0);border-right-color:var(--primary, #616161);animation:svelte-bj4lu8-rotate 1s linear infinite}@keyframes svelte-bj4lu8-rotate{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}", - map: null -}; -const LoaderIcon = create_ssr_component(($$result, $$props, $$bindings, slots) => { - let { primary = "#616161" } = $$props; - let { secondary = "#e0e0e0" } = $$props; - if ($$props.primary === void 0 && $$bindings.primary && primary !== void 0) - $$bindings.primary(primary); - if ($$props.secondary === void 0 && $$bindings.secondary && secondary !== void 0) - $$bindings.secondary(secondary); - $$result.css.add(css$5); - return ` - - -
`; -}); -const css$4 = { - code: ".indicator.svelte-1c92bpz{position:relative;display:flex;justify-content:center;align-items:center;min-width:20px;min-height:20px}.status.svelte-1c92bpz{position:absolute}.animated.svelte-1c92bpz{position:relative;transform:scale(0.6);opacity:0.4;min-width:20px;animation:svelte-1c92bpz-enter 0.3s 0.12s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards}@keyframes svelte-1c92bpz-enter{from{transform:scale(0.6);opacity:0.4}to{transform:scale(1);opacity:1}}", - map: null -}; -const ToastIcon = create_ssr_component(($$result, $$props, $$bindings, slots) => { - let type; - let icon; - let iconTheme; - let { toast: toast2 } = $$props; - if ($$props.toast === void 0 && $$bindings.toast && toast2 !== void 0) - $$bindings.toast(toast2); - $$result.css.add(css$4); - ({ type, icon, iconTheme } = toast2); - return `${typeof icon === "string" ? `
${escape(icon)}
` : `${typeof icon !== "undefined" ? `${validate_component(icon || missing_component, "svelte:component").$$render($$result, {}, {}, {})}` : `${type !== "blank" ? `
${validate_component(LoaderIcon, "LoaderIcon").$$render($$result, Object.assign({}, iconTheme), {}, {})} - ${type !== "loading" ? `
${type === "error" ? `${validate_component(ErrorIcon, "ErrorIcon").$$render($$result, Object.assign({}, iconTheme), {}, {})}` : `${validate_component(CheckmarkIcon, "CheckmarkIcon").$$render($$result, Object.assign({}, iconTheme), {}, {})}`}
` : ``}
` : ``}`}`}`; -}); -const css$3 = { - code: ".message.svelte-o805t1{display:flex;justify-content:center;margin:4px 10px;color:inherit;flex:1 1 auto;white-space:pre-line}", - map: null -}; -const ToastMessage = create_ssr_component(($$result, $$props, $$bindings, slots) => { - let { toast: toast2 } = $$props; - if ($$props.toast === void 0 && $$bindings.toast && toast2 !== void 0) - $$bindings.toast(toast2); - $$result.css.add(css$3); - return `${typeof toast2.message === "string" ? `${escape(toast2.message)}` : `${validate_component(toast2.message || missing_component, "svelte:component").$$render($$result, { toast: toast2 }, {}, {})}`} -`; -}); -const css$2 = { - code: "@keyframes svelte-15lyehg-enterAnimation{0%{transform:translate3d(0, calc(var(--factor) * -200%), 0) scale(0.6);opacity:0.5}100%{transform:translate3d(0, 0, 0) scale(1);opacity:1}}@keyframes svelte-15lyehg-exitAnimation{0%{transform:translate3d(0, 0, -1px) scale(1);opacity:1}100%{transform:translate3d(0, calc(var(--factor) * -150%), -1px) scale(0.6);opacity:0}}@keyframes svelte-15lyehg-fadeInAnimation{0%{opacity:0}100%{opacity:1}}@keyframes svelte-15lyehg-fadeOutAnimation{0%{opacity:1}100%{opacity:0}}.base.svelte-15lyehg{display:flex;align-items:center;background:#fff;color:#363636;line-height:1.3;will-change:transform;box-shadow:0 3px 10px rgba(0, 0, 0, 0.1), 0 3px 3px rgba(0, 0, 0, 0.05);max-width:350px;pointer-events:auto;padding:8px 10px;border-radius:8px}.transparent.svelte-15lyehg{opacity:0}.enter.svelte-15lyehg{animation:svelte-15lyehg-enterAnimation 0.35s cubic-bezier(0.21, 1.02, 0.73, 1) forwards}.exit.svelte-15lyehg{animation:svelte-15lyehg-exitAnimation 0.4s cubic-bezier(0.06, 0.71, 0.55, 1) forwards}.fadeIn.svelte-15lyehg{animation:svelte-15lyehg-fadeInAnimation 0.35s cubic-bezier(0.21, 1.02, 0.73, 1) forwards}.fadeOut.svelte-15lyehg{animation:svelte-15lyehg-fadeOutAnimation 0.4s cubic-bezier(0.06, 0.71, 0.55, 1) forwards}", - map: null -}; -const ToastBar = create_ssr_component(($$result, $$props, $$bindings, slots) => { - let { toast: toast2 } = $$props; - let { position = void 0 } = $$props; - let { style = "" } = $$props; - let { Component = void 0 } = $$props; - let factor; - let animation; - if ($$props.toast === void 0 && $$bindings.toast && toast2 !== void 0) - $$bindings.toast(toast2); - if ($$props.position === void 0 && $$bindings.position && position !== void 0) - $$bindings.position(position); - if ($$props.style === void 0 && $$bindings.style && style !== void 0) - $$bindings.style(style); - if ($$props.Component === void 0 && $$bindings.Component && Component !== void 0) - $$bindings.Component(Component); - $$result.css.add(css$2); - { - { - const top = (toast2.position || position || "top-center").includes("top"); - factor = top ? 1 : -1; - const [enter, exit] = prefersReducedMotion() ? ["fadeIn", "fadeOut"] : ["enter", "exit"]; - animation = toast2.visible ? enter : exit; - } - } - return `
${Component ? `${validate_component(Component || missing_component, "svelte:component").$$render($$result, {}, {}, { - message: () => { - return `${validate_component(ToastMessage, "ToastMessage").$$render($$result, { toast: toast2, slot: "message" }, {}, {})}`; - }, - icon: () => { - return `${validate_component(ToastIcon, "ToastIcon").$$render($$result, { toast: toast2, slot: "icon" }, {}, {})}`; - } - })}` : `${slots.default ? slots.default({ ToastIcon, ToastMessage, toast: toast2 }) : ` - ${validate_component(ToastIcon, "ToastIcon").$$render($$result, { toast: toast2 }, {}, {})} - ${validate_component(ToastMessage, "ToastMessage").$$render($$result, { toast: toast2 }, {}, {})} - `}`} -
`; -}); -const css$1 = { - code: ".wrapper.svelte-1pakgpd{left:0;right:0;display:flex;position:absolute;transform:translateY(calc(var(--offset, 16px) * var(--factor) * 1px))}.transition.svelte-1pakgpd{transition:all 230ms cubic-bezier(0.21, 1.02, 0.73, 1)}.active.svelte-1pakgpd{z-index:9999}.active.svelte-1pakgpd>*{pointer-events:auto}", - map: null -}; -const ToastWrapper = create_ssr_component(($$result, $$props, $$bindings, slots) => { - let top; - let bottom; - let factor; - let justifyContent; - let { toast: toast2 } = $$props; - let { setHeight } = $$props; - let wrapperEl; - if ($$props.toast === void 0 && $$bindings.toast && toast2 !== void 0) - $$bindings.toast(toast2); - if ($$props.setHeight === void 0 && $$bindings.setHeight && setHeight !== void 0) - $$bindings.setHeight(setHeight); - $$result.css.add(css$1); - top = toast2.position?.includes("top") ? 0 : null; - bottom = toast2.position?.includes("bottom") ? 0 : null; - factor = toast2.position?.includes("top") ? 1 : -1; - justifyContent = toast2.position?.includes("center") && "center" || toast2.position?.includes("right") && "flex-end" || null; - return `
${toast2.type === "custom" ? `${validate_component(ToastMessage, "ToastMessage").$$render($$result, { toast: toast2 }, {}, {})}` : `${slots.default ? slots.default({ toast: toast2 }) : ` - ${validate_component(ToastBar, "ToastBar").$$render($$result, { toast: toast2, position: toast2.position }, {}, {})} - `}`} -
`; -}); -const css = { - code: ".toaster.svelte-jyff3d{--default-offset:16px;position:fixed;z-index:9999;top:var(--default-offset);left:var(--default-offset);right:var(--default-offset);bottom:var(--default-offset);pointer-events:none}", - map: null -}; -const Toaster = create_ssr_component(($$result, $$props, $$bindings, slots) => { - let $toasts, $$unsubscribe_toasts; - let { reverseOrder = false } = $$props; - let { position = "top-center" } = $$props; - let { toastOptions = void 0 } = $$props; - let { gutter = 8 } = $$props; - let { containerStyle = void 0 } = $$props; - let { containerClassName = void 0 } = $$props; - const { toasts, handlers: handlers2 } = useToaster(toastOptions); - $$unsubscribe_toasts = subscribe(toasts, (value) => $toasts = value); - let _toasts; - if ($$props.reverseOrder === void 0 && $$bindings.reverseOrder && reverseOrder !== void 0) - $$bindings.reverseOrder(reverseOrder); - if ($$props.position === void 0 && $$bindings.position && position !== void 0) - $$bindings.position(position); - if ($$props.toastOptions === void 0 && $$bindings.toastOptions && toastOptions !== void 0) - $$bindings.toastOptions(toastOptions); - if ($$props.gutter === void 0 && $$bindings.gutter && gutter !== void 0) - $$bindings.gutter(gutter); - if ($$props.containerStyle === void 0 && $$bindings.containerStyle && containerStyle !== void 0) - $$bindings.containerStyle(containerStyle); - if ($$props.containerClassName === void 0 && $$bindings.containerClassName && containerClassName !== void 0) - $$bindings.containerClassName(containerClassName); - $$result.css.add(css); - _toasts = $toasts.map((toast2) => ({ - ...toast2, - position: toast2.position || position, - offset: handlers2.calculateOffset(toast2, $toasts, { - reverseOrder, - gutter, - defaultPosition: position - }) - })); - $$unsubscribe_toasts(); - return `
${each(_toasts, (toast2) => { - return `${validate_component(ToastWrapper, "ToastWrapper").$$render( - $$result, - { - toast: toast2, - setHeight: (height) => handlers2.updateHeight(toast2.id, height) - }, - {}, - {} - )}`; - })} -
`; -}); -const Layout = create_ssr_component(($$result, $$props, $$bindings, slots) => { - return `${validate_component(Toaster, "Toaster").$$render($$result, {}, {}, {})} -
${slots.default ? slots.default({}) : ``}
`; -}); -export { - Layout as default -}; diff --git a/frontend/.netlify/server/entries/pages/_page.svelte.js b/frontend/.netlify/server/entries/pages/_page.svelte.js deleted file mode 100644 index 5fe697b..0000000 --- a/frontend/.netlify/server/entries/pages/_page.svelte.js +++ /dev/null @@ -1,39 +0,0 @@ -import { c as create_ssr_component, e as escape, i as each, g as add_attribute } from "../../chunks/index3.js"; -import "../../chunks/Toaster.svelte_svelte_type_style_lang.js"; -const Page = create_ssr_component(($$result, $$props, $$bindings, slots) => { - let status, rooms; - let { data } = $$props; - let eth_pk = ""; - let room = ""; - let create_new_room = false; - const filled_in = () => { - return !(eth_pk.length > 0 && room.length > 0); - }; - if ($$props.data === void 0 && $$bindings.data && data !== void 0) - $$bindings.data(data); - ({ status, rooms } = data); - return `

Chatr: a Websocket chatroom

-
-

List of active chatroom's -

-
- ${status && rooms.length < 1 ? `

${escape(status)}

` : ``} - ${rooms ? `${each(rooms, (room2) => { - return ` -
- `; - })}` : ``} -
-
-
-
-
-
-

Check out Chatr, to view the source code! -

`; -}); -export { - Page as default -}; diff --git a/frontend/.netlify/server/entries/pages/_page.ts.js b/frontend/.netlify/server/entries/pages/_page.ts.js deleted file mode 100644 index 80ee122..0000000 --- a/frontend/.netlify/server/entries/pages/_page.ts.js +++ /dev/null @@ -1,19 +0,0 @@ -import { p as public_env } from "../../chunks/shared-server.js"; -const load = async ({ fetch }) => { - try { - let url = `${public_env.PUBLIC_API_URL}`; - if (url.endsWith("/")) { - url = url.slice(0, -1); - } - const res = await fetch(`${url}/rooms`); - return await res.json(); - } catch (e) { - return { - status: "API offline (try again in a min)", - rooms: [] - }; - } -}; -export { - load -}; diff --git a/frontend/.netlify/server/entries/pages/chat/_page.svelte.js b/frontend/.netlify/server/entries/pages/chat/_page.svelte.js deleted file mode 100644 index c282505..0000000 --- a/frontend/.netlify/server/entries/pages/chat/_page.svelte.js +++ /dev/null @@ -1,128 +0,0 @@ -import { c as create_ssr_component, h as subscribe, o as onDestroy, g as add_attribute, e as escape, i as each } from "../../../chunks/index3.js"; -import { w as writable } from "../../../chunks/index2.js"; -import { p as public_env } from "../../../chunks/shared-server.js"; -import { t as toast } from "../../../chunks/Toaster.svelte_svelte_type_style_lang.js"; -import "../../../chunks/index.js"; -const eth_private_key = writable(""); -const group = writable(""); -const createNewRoom = writable(false); -function guard(name) { - return () => { - throw new Error(`Cannot call ${name}(...) on the server`); - }; -} -const goto = guard("goto"); -const Page = create_ssr_component(($$result, $$props, $$bindings, slots) => { - let $group, $$unsubscribe_group; - let $eth_private_key, $$unsubscribe_eth_private_key; - let $createNewRoom, $$unsubscribe_createNewRoom; - $$unsubscribe_group = subscribe(group, (value) => $group = value); - $$unsubscribe_eth_private_key = subscribe(eth_private_key, (value) => $eth_private_key = value); - $$unsubscribe_createNewRoom = subscribe(createNewRoom, (value) => $createNewRoom = value); - let status = "🔴"; - let statusTip = "Disconnected"; - let message = ""; - let messages = []; - let socket; - let interval; - let delay = 2e3; - let timeout = false; - let currentVotingProposal = null; - let showVotingUI = false; - function connect() { - socket = new WebSocket(`${public_env.PUBLIC_WEBSOCKET_URL}/ws`); - socket.addEventListener("open", () => { - status = "🟢"; - statusTip = "Connected"; - timeout = false; - socket.send(JSON.stringify({ - eth_private_key: $eth_private_key, - group_id: $group, - should_create: $createNewRoom - })); - }); - socket.addEventListener("close", () => { - status = "🔴"; - statusTip = "Disconnected"; - if (timeout == false) { - delay = 2e3; - timeout = true; - } - }); - socket.addEventListener("message", function(event) { - if (event.data == "Username already taken.") { - toast.error(event.data); - goto("/"); - } else { - try { - const data = JSON.parse(event.data); - if (data.type === "voting_proposal") { - currentVotingProposal = data.proposal; - showVotingUI = true; - toast.success("New voting proposal received!"); - } else { - messages = [...messages, event.data]; - } - } catch (e) { - messages = [...messages, event.data]; - } - } - }); - } - onDestroy(() => { - if (socket) { - socket.close(); - } - if (interval) { - clearInterval(interval); - } - timeout = false; - }); - { - { - if (interval || !timeout && interval) { - clearInterval(interval); - } - if (timeout == true) { - interval = setInterval( - () => { - if (delay < 3e4) - delay = delay * 2; - console.log("reconnecting in:", delay); - connect(); - }, - delay - ); - } - } - } - $$unsubscribe_group(); - $$unsubscribe_eth_private_key(); - $$unsubscribe_createNewRoom(); - return `

MLS Chat ${escape(status)}

-
- - -
- - - ${showVotingUI && currentVotingProposal ? `

Voting Proposal

-

Group Name: ${escape(currentVotingProposal.group_name)}

-

Proposal ID: ${escape(currentVotingProposal.proposal_id)}

-

Proposal Payload:

-
${escape(currentVotingProposal.payload)}
-
- -
` : ``} - -
${each(messages, (msg) => { - return `
${escape(msg)}
`; - })}
- -
-
`; -}); -export { - Page as default -}; diff --git a/frontend/.netlify/server/index.js b/frontend/.netlify/server/index.js deleted file mode 100644 index 7607bf7..0000000 --- a/frontend/.netlify/server/index.js +++ /dev/null @@ -1,2674 +0,0 @@ -import { b as base, a as assets, r as reset, o as options, g as get_hooks } from "./chunks/internal.js"; -import { H as HttpError, j as json, t as text, R as Redirect, e as error, A as ActionFailure } from "./chunks/index.js"; -import * as devalue from "devalue"; -import { w as writable, r as readable } from "./chunks/index2.js"; -import { p as public_env, s as set_public_env } from "./chunks/shared-server.js"; -import { parse, serialize } from "cookie"; -import * as set_cookie_parser from "set-cookie-parser"; -const DEV = false; -function negotiate(accept, types) { - const parts = []; - accept.split(",").forEach((str, i) => { - const match = /([^/]+)\/([^;]+)(?:;q=([0-9.]+))?/.exec(str); - if (match) { - const [, type, subtype, q = "1"] = match; - parts.push({ type, subtype, q: +q, i }); - } - }); - parts.sort((a, b) => { - if (a.q !== b.q) { - return b.q - a.q; - } - if (a.subtype === "*" !== (b.subtype === "*")) { - return a.subtype === "*" ? 1 : -1; - } - if (a.type === "*" !== (b.type === "*")) { - return a.type === "*" ? 1 : -1; - } - return a.i - b.i; - }); - let accepted; - let min_priority = Infinity; - for (const mimetype of types) { - const [type, subtype] = mimetype.split("/"); - const priority = parts.findIndex( - (part) => (part.type === type || part.type === "*") && (part.subtype === subtype || part.subtype === "*") - ); - if (priority !== -1 && priority < min_priority) { - accepted = mimetype; - min_priority = priority; - } - } - return accepted; -} -function is_content_type(request, ...types) { - const type = request.headers.get("content-type")?.split(";", 1)[0].trim() ?? ""; - return types.includes(type.toLowerCase()); -} -function is_form_content_type(request) { - return is_content_type( - request, - "application/x-www-form-urlencoded", - "multipart/form-data", - "text/plain" - ); -} -function coalesce_to_error(err) { - return err instanceof Error || err && /** @type {any} */ - err.name && /** @type {any} */ - err.message ? ( - /** @type {Error} */ - err - ) : new Error(JSON.stringify(err)); -} -function normalize_error(error2) { - return ( - /** @type {Redirect | HttpError | Error} */ - error2 - ); -} -function method_not_allowed(mod, method) { - return text(`${method} method not allowed`, { - status: 405, - headers: { - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405 - // "The server must generate an Allow header field in a 405 status code response" - allow: allowed_methods(mod).join(", ") - } - }); -} -function allowed_methods(mod) { - const allowed = []; - for (const method in ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]) { - if (method in mod) - allowed.push(method); - } - if (mod.GET || mod.HEAD) - allowed.push("HEAD"); - return allowed; -} -function static_error_page(options2, status, message) { - let page = options2.templates.error({ status, message }); - return text(page, { - headers: { "content-type": "text/html; charset=utf-8" }, - status - }); -} -async function handle_fatal_error(event, options2, error2) { - error2 = error2 instanceof HttpError ? error2 : coalesce_to_error(error2); - const status = error2 instanceof HttpError ? error2.status : 500; - const body = await handle_error_and_jsonify(event, options2, error2); - const type = negotiate(event.request.headers.get("accept") || "text/html", [ - "application/json", - "text/html" - ]); - if (event.isDataRequest || type === "application/json") { - return json(body, { - status - }); - } - return static_error_page(options2, status, body.message); -} -async function handle_error_and_jsonify(event, options2, error2) { - if (error2 instanceof HttpError) { - return error2.body; - } else { - return await options2.hooks.handleError({ error: error2, event }) ?? { - message: event.route.id != null ? "Internal Error" : "Not Found" - }; - } -} -function redirect_response(status, location) { - const response = new Response(void 0, { - status, - headers: { location } - }); - return response; -} -function clarify_devalue_error(event, error2) { - if (error2.path) { - return `Data returned from \`load\` while rendering ${event.route.id} is not serializable: ${error2.message} (data${error2.path})`; - } - if (error2.path === "") { - return `Data returned from \`load\` while rendering ${event.route.id} is not a plain object`; - } - return error2.message; -} -function stringify_uses(node) { - const uses = []; - if (node.uses && node.uses.dependencies.size > 0) { - uses.push(`"dependencies":${JSON.stringify(Array.from(node.uses.dependencies))}`); - } - if (node.uses && node.uses.params.size > 0) { - uses.push(`"params":${JSON.stringify(Array.from(node.uses.params))}`); - } - if (node.uses?.parent) - uses.push(`"parent":1`); - if (node.uses?.route) - uses.push(`"route":1`); - if (node.uses?.url) - uses.push(`"url":1`); - return `"uses":{${uses.join(",")}}`; -} -async function render_endpoint(event, mod, state) { - const method = ( - /** @type {import('types').HttpMethod} */ - event.request.method - ); - let handler = mod[method]; - if (!handler && method === "HEAD") { - handler = mod.GET; - } - if (!handler) { - return method_not_allowed(mod, method); - } - const prerender = mod.prerender ?? state.prerender_default; - if (prerender && (mod.POST || mod.PATCH || mod.PUT || mod.DELETE)) { - throw new Error("Cannot prerender endpoints that have mutative methods"); - } - if (state.prerendering && !prerender) { - if (state.depth > 0) { - throw new Error(`${event.route.id} is not prerenderable`); - } else { - return new Response(void 0, { status: 204 }); - } - } - try { - const response = await handler( - /** @type {import('types').RequestEvent>} */ - event - ); - if (!(response instanceof Response)) { - throw new Error( - `Invalid response from route ${event.url.pathname}: handler should return a Response object` - ); - } - if (state.prerendering) { - response.headers.set("x-sveltekit-prerender", String(prerender)); - } - return response; - } catch (e) { - if (e instanceof Redirect) { - return new Response(void 0, { - status: e.status, - headers: { location: e.location } - }); - } - throw e; - } -} -function is_endpoint_request(event) { - const { method, headers } = event.request; - if (method === "PUT" || method === "PATCH" || method === "DELETE" || method === "OPTIONS") { - return true; - } - if (method === "POST" && headers.get("x-sveltekit-action") === "true") - return false; - const accept = event.request.headers.get("accept") ?? "*/*"; - return negotiate(accept, ["*", "text/html"]) !== "text/html"; -} -function compact(arr) { - return arr.filter( - /** @returns {val is NonNullable} */ - (val) => val != null - ); -} -function normalize_path(path, trailing_slash) { - if (path === "/" || trailing_slash === "ignore") - return path; - if (trailing_slash === "never") { - return path.endsWith("/") ? path.slice(0, -1) : path; - } else if (trailing_slash === "always" && !path.endsWith("/")) { - return path + "/"; - } - return path; -} -function decode_pathname(pathname) { - return pathname.split("%25").map(decodeURI).join("%25"); -} -function decode_params(params) { - for (const key2 in params) { - params[key2] = decodeURIComponent(params[key2]); - } - return params; -} -const tracked_url_properties = ["href", "pathname", "search", "searchParams", "toString", "toJSON"]; -function make_trackable(url, callback) { - const tracked = new URL(url); - for (const property of tracked_url_properties) { - let value = tracked[property]; - Object.defineProperty(tracked, property, { - get() { - callback(); - return value; - }, - enumerable: true, - configurable: true - }); - } - { - tracked[Symbol.for("nodejs.util.inspect.custom")] = (depth, opts, inspect) => { - return inspect(url, opts); - }; - } - disable_hash(tracked); - return tracked; -} -function disable_hash(url) { - Object.defineProperty(url, "hash", { - get() { - throw new Error( - "Cannot access event.url.hash. Consider using `$page.url.hash` inside a component instead" - ); - } - }); -} -function disable_search(url) { - for (const property of ["search", "searchParams"]) { - Object.defineProperty(url, property, { - get() { - throw new Error(`Cannot access url.${property} on a page with prerendering enabled`); - } - }); - } -} -const DATA_SUFFIX = "/__data.json"; -function has_data_suffix(pathname) { - return pathname.endsWith(DATA_SUFFIX); -} -function add_data_suffix(pathname) { - return pathname.replace(/\/$/, "") + DATA_SUFFIX; -} -function strip_data_suffix(pathname) { - return pathname.slice(0, -DATA_SUFFIX.length); -} -function is_action_json_request(event) { - const accept = negotiate(event.request.headers.get("accept") ?? "*/*", [ - "application/json", - "text/html" - ]); - return accept === "application/json" && event.request.method === "POST"; -} -async function handle_action_json_request(event, options2, server) { - const actions = server?.actions; - if (!actions) { - const no_actions_error = error(405, "POST method not allowed. No actions exist for this page"); - return action_json( - { - type: "error", - error: await handle_error_and_jsonify(event, options2, no_actions_error) - }, - { - status: no_actions_error.status, - headers: { - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405 - // "The server must generate an Allow header field in a 405 status code response" - allow: "GET" - } - } - ); - } - check_named_default_separate(actions); - try { - const data = await call_action(event, actions); - if (false) - ; - if (data instanceof ActionFailure) { - return action_json({ - type: "failure", - status: data.status, - // @ts-expect-error we assign a string to what is supposed to be an object. That's ok - // because we don't use the object outside, and this way we have better code navigation - // through knowing where the related interface is used. - data: stringify_action_response( - data.data, - /** @type {string} */ - event.route.id - ) - }); - } else { - return action_json({ - type: "success", - status: data ? 200 : 204, - // @ts-expect-error see comment above - data: stringify_action_response( - data, - /** @type {string} */ - event.route.id - ) - }); - } - } catch (e) { - const err = normalize_error(e); - if (err instanceof Redirect) { - return action_json({ - type: "redirect", - status: err.status, - location: err.location - }); - } - return action_json( - { - type: "error", - error: await handle_error_and_jsonify(event, options2, check_incorrect_fail_use(err)) - }, - { - status: err instanceof HttpError ? err.status : 500 - } - ); - } -} -function check_incorrect_fail_use(error2) { - return error2 instanceof ActionFailure ? new Error(`Cannot "throw fail()". Use "return fail()"`) : error2; -} -function action_json(data, init2) { - return json(data, init2); -} -function is_action_request(event) { - return event.request.method === "POST"; -} -async function handle_action_request(event, server) { - const actions = server?.actions; - if (!actions) { - event.setHeaders({ - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405 - // "The server must generate an Allow header field in a 405 status code response" - allow: "GET" - }); - return { - type: "error", - error: error(405, "POST method not allowed. No actions exist for this page") - }; - } - check_named_default_separate(actions); - try { - const data = await call_action(event, actions); - if (false) - ; - if (data instanceof ActionFailure) { - return { - type: "failure", - status: data.status, - data: data.data - }; - } else { - return { - type: "success", - status: 200, - // @ts-expect-error this will be removed upon serialization, so `undefined` is the same as omission - data - }; - } - } catch (e) { - const err = normalize_error(e); - if (err instanceof Redirect) { - return { - type: "redirect", - status: err.status, - location: err.location - }; - } - return { - type: "error", - error: check_incorrect_fail_use(err) - }; - } -} -function check_named_default_separate(actions) { - if (actions.default && Object.keys(actions).length > 1) { - throw new Error( - `When using named actions, the default action cannot be used. See the docs for more info: https://kit.svelte.dev/docs/form-actions#named-actions` - ); - } -} -async function call_action(event, actions) { - const url = new URL(event.request.url); - let name = "default"; - for (const param of url.searchParams) { - if (param[0].startsWith("/")) { - name = param[0].slice(1); - if (name === "default") { - throw new Error('Cannot use reserved action name "default"'); - } - break; - } - } - const action = actions[name]; - if (!action) { - throw new Error(`No action with name '${name}' found`); - } - if (!is_form_content_type(event.request)) { - throw new Error( - `Actions expect form-encoded data (received ${event.request.headers.get("content-type")})` - ); - } - return action(event); -} -function validate_action_return(data) { - if (data instanceof Redirect) { - throw new Error(`Cannot \`return redirect(...)\` — use \`throw redirect(...)\` instead`); - } - if (data instanceof HttpError) { - throw new Error( - `Cannot \`return error(...)\` — use \`throw error(...)\` or \`return fail(...)\` instead` - ); - } -} -function uneval_action_response(data, route_id) { - return try_deserialize(data, devalue.uneval, route_id); -} -function stringify_action_response(data, route_id) { - return try_deserialize(data, devalue.stringify, route_id); -} -function try_deserialize(data, fn, route_id) { - try { - return fn(data); - } catch (e) { - const error2 = ( - /** @type {any} */ - e - ); - if ("path" in error2) { - let message = `Data returned from action inside ${route_id} is not serializable: ${error2.message}`; - if (error2.path !== "") - message += ` (data.${error2.path})`; - throw new Error(message); - } - throw error2; - } -} -async function unwrap_promises(object) { - for (const key2 in object) { - if (typeof object[key2]?.then === "function") { - return Object.fromEntries( - await Promise.all(Object.entries(object).map(async ([key3, value]) => [key3, await value])) - ); - } - } - return object; -} -async function load_server_data({ event, state, node, parent }) { - if (!node?.server) - return null; - const uses = { - dependencies: /* @__PURE__ */ new Set(), - params: /* @__PURE__ */ new Set(), - parent: false, - route: false, - url: false - }; - const url = make_trackable(event.url, () => { - uses.url = true; - }); - if (state.prerendering) { - disable_search(url); - } - const result = await node.server.load?.call(null, { - ...event, - fetch: (info, init2) => { - const url2 = new URL(info instanceof Request ? info.url : info, event.url); - uses.dependencies.add(url2.href); - return event.fetch(info, init2); - }, - /** @param {string[]} deps */ - depends: (...deps) => { - for (const dep of deps) { - const { href } = new URL(dep, event.url); - uses.dependencies.add(href); - } - }, - params: new Proxy(event.params, { - get: (target, key2) => { - uses.params.add(key2); - return target[ - /** @type {string} */ - key2 - ]; - } - }), - parent: async () => { - uses.parent = true; - return parent(); - }, - route: new Proxy(event.route, { - get: (target, key2) => { - uses.route = true; - return target[ - /** @type {'id'} */ - key2 - ]; - } - }), - url - }); - const data = result ? await unwrap_promises(result) : null; - return { - type: "data", - data, - uses, - slash: node.server.trailingSlash - }; -} -async function load_data({ - event, - fetched, - node, - parent, - server_data_promise, - state, - resolve_opts, - csr -}) { - const server_data_node = await server_data_promise; - if (!node?.universal?.load) { - return server_data_node?.data ?? null; - } - const result = await node.universal.load.call(null, { - url: event.url, - params: event.params, - data: server_data_node?.data ?? null, - route: event.route, - fetch: create_universal_fetch(event, state, fetched, csr, resolve_opts), - setHeaders: event.setHeaders, - depends: () => { - }, - parent - }); - const data = result ? await unwrap_promises(result) : null; - return data; -} -function create_universal_fetch(event, state, fetched, csr, resolve_opts) { - return async (input, init2) => { - const cloned_body = input instanceof Request && input.body ? input.clone().body : null; - let response = await event.fetch(input, init2); - const url = new URL(input instanceof Request ? input.url : input, event.url); - const same_origin = url.origin === event.url.origin; - let dependency; - if (same_origin) { - if (state.prerendering) { - dependency = { response, body: null }; - state.prerendering.dependencies.set(url.pathname, dependency); - } - } else { - const mode = input instanceof Request ? input.mode : init2?.mode ?? "cors"; - if (mode === "no-cors") { - response = new Response("", { - status: response.status, - statusText: response.statusText, - headers: response.headers - }); - } else { - const acao = response.headers.get("access-control-allow-origin"); - if (!acao || acao !== event.url.origin && acao !== "*") { - throw new Error( - `CORS error: ${acao ? "Incorrect" : "No"} 'Access-Control-Allow-Origin' header is present on the requested resource` - ); - } - } - } - const proxy = new Proxy(response, { - get(response2, key2, _receiver) { - async function text2() { - const body = await response2.text(); - if (!body || typeof body === "string") { - const status_number = Number(response2.status); - if (isNaN(status_number)) { - throw new Error( - `response.status is not a number. value: "${response2.status}" type: ${typeof response2.status}` - ); - } - fetched.push({ - url: same_origin ? url.href.slice(event.url.origin.length) : url.href, - method: event.request.method, - request_body: ( - /** @type {string | ArrayBufferView | undefined} */ - input instanceof Request && cloned_body ? await stream_to_string(cloned_body) : init2?.body - ), - request_headers: init2?.headers, - response_body: body, - response: response2 - }); - } - if (dependency) { - dependency.body = body; - } - return body; - } - if (key2 === "arrayBuffer") { - return async () => { - const buffer = await response2.arrayBuffer(); - if (dependency) { - dependency.body = new Uint8Array(buffer); - } - return buffer; - }; - } - if (key2 === "text") { - return text2; - } - if (key2 === "json") { - return async () => { - return JSON.parse(await text2()); - }; - } - return Reflect.get(response2, key2, response2); - } - }); - if (csr) { - const get = response.headers.get; - response.headers.get = (key2) => { - const lower = key2.toLowerCase(); - const value = get.call(response.headers, lower); - if (value && !lower.startsWith("x-sveltekit-")) { - const included = resolve_opts.filterSerializedResponseHeaders(lower, value); - if (!included) { - throw new Error( - `Failed to get response header "${lower}" — it must be included by the \`filterSerializedResponseHeaders\` option: https://kit.svelte.dev/docs/hooks#server-hooks-handle (at ${event.route.id})` - ); - } - } - return value; - }; - } - return proxy; - }; -} -async function stream_to_string(stream) { - let result = ""; - const reader = stream.getReader(); - const decoder = new TextDecoder(); - while (true) { - const { done, value } = await reader.read(); - if (done) { - break; - } - result += decoder.decode(value); - } - return result; -} -function hash(...values) { - let hash2 = 5381; - for (const value of values) { - if (typeof value === "string") { - let i = value.length; - while (i) - hash2 = hash2 * 33 ^ value.charCodeAt(--i); - } else if (ArrayBuffer.isView(value)) { - const buffer = new Uint8Array(value.buffer, value.byteOffset, value.byteLength); - let i = buffer.length; - while (i) - hash2 = hash2 * 33 ^ buffer[--i]; - } else { - throw new TypeError("value must be a string or TypedArray"); - } - } - return (hash2 >>> 0).toString(36); -} -const escape_html_attr_dict = { - "&": "&", - '"': """ -}; -const escape_html_attr_regex = new RegExp( - // special characters - `[${Object.keys(escape_html_attr_dict).join("")}]|[\\ud800-\\udbff](?![\\udc00-\\udfff])|[\\ud800-\\udbff][\\udc00-\\udfff]|[\\udc00-\\udfff]`, - "g" -); -function escape_html_attr(str) { - const escaped_str = str.replace(escape_html_attr_regex, (match) => { - if (match.length === 2) { - return match; - } - return escape_html_attr_dict[match] ?? `&#${match.charCodeAt(0)};`; - }); - return `"${escaped_str}"`; -} -const replacements = { - "<": "\\u003C", - "\u2028": "\\u2028", - "\u2029": "\\u2029" -}; -const pattern = new RegExp(`[${Object.keys(replacements).join("")}]`, "g"); -function serialize_data(fetched, filter, prerendering = false) { - const headers = {}; - let cache_control = null; - let age = null; - let vary = false; - for (const [key2, value] of fetched.response.headers) { - if (filter(key2, value)) { - headers[key2] = value; - } - if (key2 === "cache-control") - cache_control = value; - if (key2 === "age") - age = value; - if (key2 === "vary") - vary = true; - } - const payload = { - status: fetched.response.status, - statusText: fetched.response.statusText, - headers, - body: fetched.response_body - }; - const safe_payload = JSON.stringify(payload).replace(pattern, (match) => replacements[match]); - const attrs = [ - 'type="application/json"', - "data-sveltekit-fetched", - `data-url=${escape_html_attr(fetched.url)}` - ]; - if (fetched.request_headers || fetched.request_body) { - const values = []; - if (fetched.request_headers) { - values.push([...new Headers(fetched.request_headers)].join(",")); - } - if (fetched.request_body) { - values.push(fetched.request_body); - } - attrs.push(`data-hash="${hash(...values)}"`); - } - if (!prerendering && fetched.method === "GET" && cache_control && !vary) { - const match = /s-maxage=(\d+)/g.exec(cache_control) ?? /max-age=(\d+)/g.exec(cache_control); - if (match) { - const ttl = +match[1] - +(age ?? "0"); - attrs.push(`data-ttl="${ttl}"`); - } - } - return ` - - -
-
- -
-
\ No newline at end of file diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte deleted file mode 100644 index d439145..0000000 --- a/frontend/src/routes/+page.svelte +++ /dev/null @@ -1,99 +0,0 @@ - - -
-
-

Chatr: a Websocket chatroom

-
-
-
-
-
-

- List of active chatroom's -

- -
- {#if status && rooms.length < 1} -
-
-

{status}

-
-
- {/if} - {#if rooms} - {#each rooms as room} - -
-
- - {/each} - {/if} - -
-
- - -
-
- - -
-
- -
- -
-
-

- Check out Chatr, to view the source code! -

-
- diff --git a/frontend/src/routes/+page.ts b/frontend/src/routes/+page.ts deleted file mode 100644 index b73fb26..0000000 --- a/frontend/src/routes/+page.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type {PageLoad} from './$types'; -import {env} from "$env/dynamic/public"; - -export const load: PageLoad = async ({fetch}) => { - try { - let url = `${env.PUBLIC_API_URL}`; - if (url.endsWith("/")) { - url = url.slice(0, -1); - } - const res = await fetch(`${url}/rooms`); - return await res.json(); - } catch (e) { - return { - status: "API offline (try again in a min)", - rooms: [] - } - } -} \ No newline at end of file diff --git a/frontend/src/routes/chat/+page.svelte b/frontend/src/routes/chat/+page.svelte deleted file mode 100644 index 52754ac..0000000 --- a/frontend/src/routes/chat/+page.svelte +++ /dev/null @@ -1,197 +0,0 @@ - - -
-

- Chat Room {status} -

- -
- - -{#if showVotingUI && currentVotingProposal} -
-
-

Voting Proposal

-
-

Group Name: {currentVotingProposal.group_name}

-

Proposal ID: {currentVotingProposal.proposal_id}

-

Proposal Payload:

-
- {currentVotingProposal.payload} -
-
-
- - - -
-
-
-{/if} - -
-
-
- {#each messages as msg} -
{msg}
- {/each} -
-
-
- -
-
- - -
-
diff --git a/frontend/static/favicon.png b/frontend/static/favicon.png deleted file mode 100644 index 825b9e65af7c104cfb07089bb28659393b4f2097..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1571 zcmV+;2Hg3HP)Px)-AP12RCwC$UE6KzI1p6{F2N z1VK2vi|pOpn{~#djwYcWXTI_im_u^TJgMZ4JMOsSj!0ma>B?-(Hr@X&W@|R-$}W@Z zgj#$x=!~7LGqHW?IO8+*oE1MyDp!G=L0#^lUx?;!fXv@l^6SvTnf^ac{5OurzC#ZMYc20lI%HhX816AYVs1T3heS1*WaWH z%;x>)-J}YB5#CLzU@GBR6sXYrD>Vw(Fmt#|JP;+}<#6b63Ike{Fuo!?M{yEffez;| zp!PfsuaC)>h>-AdbnwN13g*1LowNjT5?+lFVd#9$!8Z9HA|$*6dQ8EHLu}U|obW6f z2%uGv?vr=KNq7YYa2Roj;|zooo<)lf=&2yxM@e`kM$CmCR#x>gI>I|*Ubr({5Y^rb zghxQU22N}F51}^yfDSt786oMTc!W&V;d?76)9KXX1 z+6Okem(d}YXmmOiZq$!IPk5t8nnS{%?+vDFz3BevmFNgpIod~R{>@#@5x9zJKEHLHv!gHeK~n)Ld!M8DB|Kfe%~123&Hz1Z(86nU7*G5chmyDe ziV7$pB7pJ=96hpxHv9rCR29%bLOXlKU<_13_M8x)6;P8E1Kz6G<&P?$P^%c!M5`2` zfY2zg;VK5~^>TJGQzc+33-n~gKt{{of8GzUkWmU110IgI0DLxRIM>0US|TsM=L|@F z0Bun8U!cRB7-2apz=y-7*UxOxz@Z0)@QM)9wSGki1AZ38ceG7Q72z5`i;i=J`ILzL z@iUO?SBBG-0cQuo+an4TsLy-g-x;8P4UVwk|D8{W@U1Zi z!M)+jqy@nQ$p?5tsHp-6J304Q={v-B>66$P0IDx&YT(`IcZ~bZfmn11#rXd7<5s}y zBi9eim&zQc0Dk|2>$bs0PnLmDfMP5lcXRY&cvJ=zKxI^f0%-d$tD!`LBf9^jMSYUA zI8U?CWdY@}cRq6{5~y+)#h1!*-HcGW@+gZ4B};0OnC~`xQOyH19z*TA!!BJ%9s0V3F?CAJ{hTd#*tf+ur-W9MOURF-@B77_-OshsY}6 zOXRY=5%C^*26z?l)1=$bz30!so5tfABdSYzO+H=CpV~aaUefmjvfZ3Ttu9W&W3Iu6 zROlh0MFA5h;my}8lB0tAV-Rvc2Zs_CCSJnx@d`**$idgy-iMob4dJWWw|21b4NB=LfsYp0Aeh{Ov)yztQi;eL4y5 zMi>8^SzKqk8~k?UiQK^^-5d8c%bV?$F8%X~czyiaKCI2=UH Result { let id = Identity::new(CIPHERSUITE, &crypto, user_address.as_slice())?; Ok(id) } + +/// Validates and normalizes Ethereum-style wallet addresses. +/// +/// Accepts either `0x`-prefixed or raw 40-character hex strings, returning a lowercase, +/// `0x`-prefixed representation on success. +pub fn normalize_wallet_address_str(address: &str) -> Result { + parse_wallet_address(address).map(|addr| addr.to_string()) +} + +/// Parses an Ethereum wallet address into an [`Address`] after validation. +/// +/// This ensures the address is 20 bytes / 40 hex chars and contains only hexadecimal digits. +pub fn parse_wallet_address(address: &str) -> Result { + let trimmed = address.trim(); + if trimmed.is_empty() { + return Err(IdentityError::InvalidWalletAddress(address.to_string())); + } + + let hex_part = trimmed + .strip_prefix("0x") + .or_else(|| trimmed.strip_prefix("0X")) + .unwrap_or(trimmed); + + if hex_part.len() != 40 || !hex_part.chars().all(|c| c.is_ascii_hexdigit()) { + return Err(IdentityError::InvalidWalletAddress(trimmed.to_string())); + } + + let normalized = format!("0x{}", hex_part.to_ascii_lowercase()); + Address::from_str(&normalized) + .map_err(|_| IdentityError::InvalidWalletAddress(trimmed.to_string())) +} + +fn is_prefixed_hex(input: &str) -> bool { + let rest = input + .strip_prefix("0x") + .or_else(|| input.strip_prefix("0X")); + match rest { + Some(hex_part) if !hex_part.is_empty() => hex_part.chars().all(|c| c.is_ascii_hexdigit()), + _ => false, + } +} + +fn is_raw_hex(input: &str) -> bool { + !input.is_empty() && input.chars().all(|c| c.is_ascii_hexdigit()) +} + +pub fn normalize_wallet_address(raw: &[u8]) -> String { + let as_utf8 = std::str::from_utf8(raw) + .map(|s| s.trim()) + .unwrap_or_default(); + + if is_prefixed_hex(as_utf8) { + return as_utf8.to_string(); + } + + if is_raw_hex(as_utf8) { + return format!("0x{}", as_utf8); + } + + if raw.is_empty() { + String::new() + } else { + format!("0x{}", hex::encode(raw)) + } +} + +#[cfg(test)] +mod tests { + use super::{is_prefixed_hex, normalize_wallet_address}; + + #[test] + fn keeps_prefixed_hex() { + let addr = normalize_wallet_address(b"0xAbCd1234"); + assert_eq!(addr, "0xAbCd1234"); + } + + #[test] + fn prefixes_raw_hex() { + let addr = normalize_wallet_address(b"ABCD1234"); + assert_eq!(addr, "0xABCD1234"); + } + + #[test] + fn encodes_binary_bytes() { + let addr = normalize_wallet_address(&[0x11, 0x22, 0x33]); + assert_eq!(addr, "0x112233"); + } + + #[test] + fn trims_ascii_input() { + let addr = normalize_wallet_address(b" 0x1F "); + assert_eq!(addr, "0x1F"); + } + + #[test] + fn prefixed_hex_helper() { + assert!(is_prefixed_hex("0xabc")); + assert!(is_prefixed_hex("0XABC")); + assert!(!is_prefixed_hex("abc")); + assert!(!is_prefixed_hex("0x")); + } +} diff --git a/mls_crypto/src/lib.rs b/mls_crypto/src/lib.rs index 4fc1f39..d5ec149 100644 --- a/mls_crypto/src/lib.rs +++ b/mls_crypto/src/lib.rs @@ -1,3 +1,5 @@ pub mod error; pub mod identity; pub mod openmls_provider; + +pub use identity::{normalize_wallet_address_str, parse_wallet_address}; diff --git a/src/action_handlers.rs b/src/action_handlers.rs deleted file mode 100644 index cbe9102..0000000 --- a/src/action_handlers.rs +++ /dev/null @@ -1,157 +0,0 @@ -use kameo::actor::ActorRef; -use log::info; -use std::sync::Arc; -use tokio::sync::mpsc::Sender; -use tokio_util::sync::CancellationToken; -use waku_bindings::WakuMessage; - -use crate::{ - protos::messages::v1::{AppMessage, BanRequest, ConversationMessage}, - user::{User, UserAction}, - user_actor::{BuildBanMessage, LeaveGroupRequest, SendGroupMessage, UserVoteRequest}, - ws_actor::{RawWsMessage, WsAction, WsActor}, - AppState, -}; -use ds::waku_actor::WakuMessageToSend; - -pub async fn handle_user_actions( - msg: WakuMessage, - waku_node: Sender, - ws_actor: ActorRef, - user_actor: ActorRef, - app_state: Arc, - cancel_token: CancellationToken, -) -> Result<(), Box> { - let action = user_actor.ask(msg).await?; - match action { - UserAction::SendToWaku(msg) => { - waku_node.send(msg).await?; - } - UserAction::SendToApp(msg) => { - ws_actor.ask(msg).await?; - } - UserAction::LeaveGroup(group_name) => { - user_actor - .ask(LeaveGroupRequest { - group_name: group_name.clone(), - }) - .await?; - - app_state - .content_topics - .write() - .await - .retain(|topic| topic.application_name != group_name); - info!("Leave group: {:?}", &group_name); - let app_message: AppMessage = ConversationMessage { - message: format!("You're removed from the group {group_name}").into_bytes(), - sender: "SYSTEM".to_string(), - group_name: group_name.clone(), - } - .into(); - ws_actor.ask(app_message).await?; - cancel_token.cancel(); - } - UserAction::DoNothing => {} - } - Ok(()) -} - -pub async fn handle_ws_action( - msg: RawWsMessage, - ws_actor: ActorRef, - user_actor: ActorRef, - waku_node: Sender, -) -> Result<(), Box> { - let action = ws_actor.ask(msg).await?; - match action { - WsAction::Connect(connect) => { - info!("Got unexpected connect: {:?}", &connect); - } - WsAction::UserMessage(msg) => { - let app_message: AppMessage = ConversationMessage { - message: msg.message.clone(), - sender: "me".to_string(), - group_name: msg.group_id.clone(), - } - .into(); - ws_actor.ask(app_message).await?; - - let pmt = user_actor - .ask(SendGroupMessage { - message: msg.message.clone(), - group_name: msg.group_id, - }) - .await?; - waku_node.send(pmt).await?; - } - WsAction::RemoveUser(user_to_ban, group_name) => { - info!("Got remove user: {:?}", &user_to_ban); - - // Create a ban request message to send to the group - let ban_request_msg = BanRequest { - user_to_ban: user_to_ban.clone(), - requester: "someone".to_string(), // The current user is the requester - group_name: group_name.clone(), - }; - - // Send the ban request directly via Waku if the user is not the steward - // If steward, need to add remove proposal to the group and sent notification to the group - let waku_msg = user_actor - .ask(BuildBanMessage { - ban_request: ban_request_msg, - group_name: group_name.clone(), - }) - .await?; - waku_node.send(waku_msg).await?; - - // Send a local confirmation message - let app_message: AppMessage = ConversationMessage { - message: format!("Ban request for user {user_to_ban} sent to group").into_bytes(), - sender: "system".to_string(), - group_name: group_name.clone(), - } - .into(); - ws_actor.ask(app_message).await?; - } - WsAction::UserVote { - proposal_id, - vote, - group_id, - } => { - info!("Got user vote: proposal_id={proposal_id}, vote={vote}, group={group_id}"); - - // Process the user vote: - // if it come from the user, send the vote result to Waku - // if it come from the steward, just process it and return None - let user_vote_result = user_actor - .ask(UserVoteRequest { - group_name: group_id.clone(), - proposal_id, - vote, - }) - .await?; - - // Send a local confirmation message - let app_message: AppMessage = ConversationMessage { - message: format!( - "Your vote ({}) has been submitted for proposal {proposal_id}", - if vote { "YES" } else { "NO" }, - ) - .into_bytes(), - sender: "SYSTEM".to_string(), - group_name: group_id.clone(), - } - .into(); - ws_actor.ask(app_message).await?; - - // Send the vote result to Waku - if let Some(waku_msg) = user_vote_result { - waku_node.send(waku_msg).await?; - } - } - WsAction::DoNothing => {} - } - - Ok(()) -} diff --git a/src/bootstrap.rs b/src/bootstrap.rs new file mode 100644 index 0000000..d862331 --- /dev/null +++ b/src/bootstrap.rs @@ -0,0 +1,108 @@ +// de_mls/src/bootstrap.rs +use std::sync::Arc; +use tokio::sync::{broadcast, mpsc}; +use tokio_util::sync::CancellationToken; +use tracing::{error, info}; +use waku_bindings::{Multiaddr, WakuMessage}; + +use ds::waku_actor::{run_waku_node, WakuMessageToSend}; + +use crate::user_app_instance::{AppState, CoreCtx}; + +#[derive(Clone, Debug)] +pub struct BootstrapConfig { + /// TCP/UDP port for the embedded Waku node + pub node_port: String, + /// Comma-separated peer multiaddrs parsed into a vec + pub peers: Vec, +} + +pub struct Bootstrap { + pub core: Arc, + /// Cancels the Waku→broadcast forwarder task + pub cancel: CancellationToken, + /// The thread running the Waku node runtime; join on shutdown if you want + pub waku_thread: std::thread::JoinHandle<()>, +} + +/// Same wiring you previously did in `main.rs`, now reusable for server & desktop. +pub async fn bootstrap_core(cfg: BootstrapConfig) -> anyhow::Result { + // Channels used by AppState and Waku runtime + let (waku_in_tx, mut waku_in_rx) = mpsc::channel::(100); + let (to_waku_tx, mut to_waku_rx) = mpsc::channel::(100); + let (pubsub_tx, _) = broadcast::channel::(100); + + let app_state = Arc::new(AppState { + waku_node: to_waku_tx.clone(), + pubsub: pubsub_tx.clone(), + }); + + let core = Arc::new(CoreCtx::new(app_state.clone())); + + // Forward Waku messages into broadcast + let forward_cancel = CancellationToken::new(); + { + let forward_cancel = forward_cancel.clone(); + tokio::spawn(async move { + info!("Forwarding Waku → broadcast started"); + loop { + tokio::select! { + _ = forward_cancel.cancelled() => break, + maybe = waku_in_rx.recv() => { + if let Some(msg) = maybe { + let _ = pubsub_tx.send(msg); + } else { + break; + } + } + } + } + info!("Forwarding Waku → broadcast stopped"); + }); + } + + // Start Waku node on a dedicated thread with its own Tokio runtime + let node_port = cfg.node_port.clone(); + let peers = cfg.peers.clone(); + let waku_thread = std::thread::Builder::new() + .name("waku-node".into()) + .spawn(move || { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("waku tokio runtime"); + rt.block_on(async move { + if let Err(e) = + run_waku_node(node_port, Some(peers), waku_in_tx, &mut to_waku_rx).await + { + error!("run_waku_node failed: {e}"); + } + }); + })?; + + Ok(Bootstrap { + core, + cancel: forward_cancel, + waku_thread, + }) +} + +/// Helper that exactly mirrors your current env usage: +/// - requires NODE_PORT +/// - requires PEER_ADDRESSES (comma-separated multiaddrs) +pub async fn bootstrap_core_from_env() -> anyhow::Result { + use anyhow::Context; + + let node_port = std::env::var("NODE_PORT").context("NODE_PORT is not set")?; + let peer_addresses = std::env::var("PEER_ADDRESSES").context("PEER_ADDRESSES is not set")?; + let peers = peer_addresses + .split(',') + .filter(|s| !s.trim().is_empty()) + .map(|s| { + s.parse::() + .context(format!("Failed to parse peer address: {s}")) + }) + .collect::, _>>()?; + + bootstrap_core(BootstrapConfig { node_port, peers }).await +} diff --git a/src/consensus/mod.rs b/src/consensus/mod.rs index 9954f95..f09d4b6 100644 --- a/src/consensus/mod.rs +++ b/src/consensus/mod.rs @@ -6,25 +6,19 @@ //! - Proposal management //! - Vote collection and validation //! - Consensus reached detection - -use crate::error::ConsensusError; -use crate::protos::messages::v1::consensus::v1::{Proposal, Vote}; -use crate::LocalSigner; -use log::info; use prost::Message; use sha2::{Digest, Sha256}; use std::collections::HashMap; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::sync::broadcast; +use tracing::info; use uuid::Uuid; +use crate::error::ConsensusError; +use crate::protos::consensus::v1::{Outcome, Proposal, ProposalResult, Vote}; +use crate::LocalSigner; + pub mod service; - -// Re-export protobuf types for compatibility with generated code -pub mod v1 { - pub use crate::protos::messages::v1::consensus::v1::{Proposal, Vote}; -} - pub use service::ConsensusService; /// Consensus events emitted when consensus state changes @@ -81,7 +75,8 @@ pub struct ConsensusSession { pub votes: HashMap, Vote>, // vote_owner -> Vote pub created_at: u64, pub config: ConsensusConfig, - pub event_sender: Option>, + pub event_sender: broadcast::Sender<(String, ConsensusEvent)>, + pub decisions_tx: broadcast::Sender, pub group_name: String, } @@ -89,7 +84,8 @@ impl ConsensusSession { pub fn new( proposal: Proposal, config: ConsensusConfig, - event_sender: Option>, + event_sender: broadcast::Sender<(String, ConsensusEvent)>, + decisions_tx: broadcast::Sender, group_name: &str, ) -> Self { let now = SystemTime::now() @@ -104,6 +100,7 @@ impl ConsensusSession { created_at: now, config, event_sender, + decisions_tx, group_name: group_name.to_string(), } } @@ -131,7 +128,7 @@ impl ConsensusSession { } ConsensusState::ConsensusReached(_) => { info!( - "[consensus::mod::add_vote]: Consensus already reached for proposal {}, skipping vote", + "[mod::add_vote]: Consensus already reached for proposal {}, skipping vote", self.proposal.proposal_id ); Ok(()) @@ -173,7 +170,7 @@ impl ConsensusSession { if yes_votes > no_votes { self.state = ConsensusState::ConsensusReached(true); info!( - "[consensus::mod::check_consensus]: Enough votes received {yes_votes}-{no_votes} - consensus reached: YES" + "[mod::check_consensus]: Enough votes received {yes_votes}-{no_votes} - consensus reached: YES" ); self.emit_consensus_event(ConsensusEvent::ConsensusReached { proposal_id: self.proposal.proposal_id, @@ -182,7 +179,7 @@ impl ConsensusSession { } else if no_votes > yes_votes { self.state = ConsensusState::ConsensusReached(false); info!( - "[consensus::mod::check_consensus]: Enough votes received {yes_votes}-{no_votes} - consensus reached: NO" + "[mod::check_consensus]: Enough votes received {yes_votes}-{no_votes} - consensus reached: NO" ); self.emit_consensus_event(ConsensusEvent::ConsensusReached { proposal_id: self.proposal.proposal_id, @@ -193,7 +190,7 @@ impl ConsensusSession { if total_votes == expected_voters { self.state = ConsensusState::ConsensusReached(false); info!( - "[consensus::mod::check_consensus]: All votes received and tie - consensus not reached" + "[mod::check_consensus]: All votes received and tie - consensus not reached" ); self.emit_consensus_event(ConsensusEvent::ConsensusReached { proposal_id: self.proposal.proposal_id, @@ -203,7 +200,7 @@ impl ConsensusSession { // Tie - if it's not all votes, we wait for more votes self.state = ConsensusState::Active; info!( - "[consensus::mod::check_consensus]: Not enough votes received - consensus not reached" + "[mod::check_consensus]: Not enough votes received - consensus not reached" ); } } @@ -212,13 +209,19 @@ impl ConsensusSession { /// Emit a consensus event fn emit_consensus_event(&self, event: ConsensusEvent) { - if let Some(sender) = &self.event_sender { - info!( - "[consensus::mod::emit_consensus_event]: Emitting consensus event: {event:?} for proposal {}", - self.proposal.proposal_id - ); - let _ = sender.send((self.group_name.clone(), event)); - } + info!("[mod::emit_consensus_event]: Emitting consensus event: {event:?}"); + let _ = self + .event_sender + .send((self.group_name.clone(), event.clone())); + let _ = self.decisions_tx.send(ProposalResult { + group_id: self.group_name.clone(), + proposal_id: self.proposal.proposal_id, + outcome: Outcome::from(event) as i32, + decided_at_ms: SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Failed to get current time") + .as_secs(), + }); } /// Check if the session is still active diff --git a/src/consensus/service.rs b/src/consensus/service.rs index 5c6d858..7adcef7 100644 --- a/src/consensus/service.rs +++ b/src/consensus/service.rs @@ -1,22 +1,22 @@ //! Consensus service for managing consensus sessions and HashGraph integration +use prost::Message; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; +use tokio::sync::{broadcast, RwLock}; +use tracing::info; +use uuid::Uuid; use crate::consensus::{ compute_vote_hash, create_vote_for_proposal, ConsensusConfig, ConsensusEvent, ConsensusSession, ConsensusState, ConsensusStats, }; use crate::error::ConsensusError; -use crate::protos::messages::v1::consensus::v1::{Proposal, Vote}; +use crate::protos::consensus::v1::{Proposal, ProposalResult, UpdateRequest, Vote}; use crate::{verify_vote_hash, LocalSigner}; -use log::info; -use prost::Message; -use std::collections::HashMap; -use std::sync::Arc; -use std::time::{SystemTime, UNIX_EPOCH}; -use tokio::sync::{broadcast, RwLock}; -use uuid::Uuid; /// Consensus service that manages multiple consensus sessions for multiple groups -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct ConsensusService { /// Active consensus sessions organized by group: group_name -> proposal_id -> session sessions: Arc>>>, @@ -24,26 +24,32 @@ pub struct ConsensusService { max_sessions_per_group: usize, /// Event sender for consensus events event_sender: broadcast::Sender<(String, ConsensusEvent)>, + /// Event sender for consensus results for UI + decisions_tx: broadcast::Sender, } impl ConsensusService { /// Create a new consensus service pub fn new() -> Self { let (event_sender, _) = broadcast::channel(1000); + let (decisions_tx, _) = broadcast::channel(128); Self { sessions: Arc::new(RwLock::new(HashMap::new())), max_sessions_per_group: 10, event_sender, + decisions_tx, } } /// Create a new consensus service with custom max sessions per group pub fn new_with_max_sessions(max_sessions_per_group: usize) -> Self { let (event_sender, _) = broadcast::channel(1000); + let (decisions_tx, _) = broadcast::channel(128); Self { sessions: Arc::new(RwLock::new(HashMap::new())), max_sessions_per_group, event_sender, + decisions_tx, } } @@ -52,6 +58,16 @@ impl ConsensusService { self.event_sender.subscribe() } + /// Subscribe to consensus decisions + pub fn subscribe_decisions(&self) -> broadcast::Receiver { + self.decisions_tx.subscribe() + } + + // /// Send consensus decision to UI + // pub fn send_decision(&self, res: ProposalResult) { + // let _ = self.decisions_tx.send(res); + // } + pub async fn set_consensus_threshold_for_group_session( &mut self, group_name: &str, @@ -76,7 +92,7 @@ impl ConsensusService { &self, group_name: &str, name: String, - payload: String, + group_requests: Vec, proposal_owner: Vec, expected_voters_count: u32, expiration_time: u64, @@ -91,7 +107,7 @@ impl ConsensusService { // Create proposal with steward's vote let proposal = Proposal { name, - payload, + group_requests, proposal_id, proposal_owner, votes: vec![], @@ -107,7 +123,8 @@ impl ConsensusService { let session = ConsensusSession::new( proposal.clone(), config.clone(), - Some(self.event_sender.clone()), + self.event_sender.clone(), + self.decisions_tx.clone(), group_name, ); @@ -120,22 +137,7 @@ impl ConsensusService { let group_sessions = sessions .entry(group_name.to_string()) .or_insert_with(HashMap::new); - group_sessions.insert(proposal_id, session); - - // Clean up old sessions if we exceed the limit (within the same lock) - if group_sessions.len() > self.max_sessions_per_group { - // Sort sessions by creation time and keep the most recent ones - let mut session_entries: Vec<_> = group_sessions.drain().collect(); - session_entries.sort_by(|a, b| b.1.created_at.cmp(&a.1.created_at)); - - // Keep only the most recent sessions - for (proposal_id, session) in session_entries - .into_iter() - .take(self.max_sessions_per_group) - { - group_sessions.insert(proposal_id, session); - } - } + self.insert_session(group_sessions, proposal_id, session); } // Start automatic timeout handling for this proposal using session config @@ -151,7 +153,7 @@ impl ConsensusService { .is_some() { info!( - "Consensus result already exists for proposal {proposal_id}, skipping timeout" + "[create_proposal]:Consensus result already exists for proposal {proposal_id}, skipping timeout" ); return; } @@ -163,7 +165,7 @@ impl ConsensusService { .is_ok() { info!( - "Automatic timeout applied for proposal {proposal_id} after {timeout_seconds}s" + "[create_proposal]: Automatic timeout applied for proposal {proposal_id} after {timeout_seconds}s" ); } }); @@ -308,6 +310,32 @@ impl ConsensusService { Ok(()) } + fn insert_session( + &self, + group_sessions: &mut HashMap, + proposal_id: u32, + session: ConsensusSession, + ) { + group_sessions.insert(proposal_id, session); + self.prune_sessions(group_sessions); + } + + fn prune_sessions(&self, group_sessions: &mut HashMap) { + if group_sessions.len() <= self.max_sessions_per_group { + return; + } + + let mut session_entries: Vec<_> = group_sessions.drain().collect(); + session_entries.sort_by(|a, b| b.1.created_at.cmp(&a.1.created_at)); + + for (proposal_id, session) in session_entries + .into_iter() + .take(self.max_sessions_per_group) + { + group_sessions.insert(proposal_id, session); + } + } + /// Process incoming proposal message pub async fn process_incoming_proposal( &self, @@ -315,7 +343,7 @@ impl ConsensusService { proposal: Proposal, ) -> Result<(), ConsensusError> { info!( - "[consensus::service::process_incoming_proposal]: Processing incoming proposal for group {group_name}" + "[service::process_incoming_proposal]: Processing incoming proposal for group {group_name}" ); let mut sessions = self.sessions.write().await; let group_sessions = sessions @@ -334,29 +362,16 @@ impl ConsensusService { let mut session = ConsensusSession::new( proposal.clone(), ConsensusConfig::default(), - Some(self.event_sender.clone()), + self.event_sender.clone(), + self.decisions_tx.clone(), group_name, ); session.add_vote(proposal.votes[0].clone())?; - group_sessions.insert(proposal.proposal_id, session); + self.insert_session(group_sessions, proposal.proposal_id, session); - // Clean up old sessions if we exceed the limit (within the same lock) - if group_sessions.len() > self.max_sessions_per_group { - // Sort sessions by creation time and keep the most recent ones - let mut session_entries: Vec<_> = group_sessions.drain().collect(); - session_entries.sort_by(|a, b| b.1.created_at.cmp(&a.1.created_at)); + info!("[service::process_incoming_proposal]: Proposal stored, waiting for user vote"); - // Keep only the most recent sessions - for (proposal_id, session) in session_entries - .into_iter() - .take(self.max_sessions_per_group) - { - group_sessions.insert(proposal_id, session); - } - } - - info!("[consensus::service::process_incoming_proposal]: Proposal stored, waiting for user vote"); Ok(()) } @@ -397,9 +412,7 @@ impl ConsensusService { group_name: &str, vote: Vote, ) -> Result<(), ConsensusError> { - info!( - "[consensus::service::process_incoming_vote]: Processing incoming vote for group {group_name}" - ); + info!("[service::process_incoming_vote]: Processing incoming vote for group {group_name}"); let mut sessions = self.sessions.write().await; let group_sessions = sessions .get_mut(group_name) @@ -600,7 +613,7 @@ impl ConsensusService { // Check if consensus was already reached match session.state { crate::consensus::ConsensusState::ConsensusReached(result) => { - info!("Consensus already reached for proposal {proposal_id}, skipping timeout"); + info!("[handle_consensus_timeout]: Consensus already reached for proposal {proposal_id}, skipping timeout"); Ok(result) } _ => { @@ -624,7 +637,7 @@ impl ConsensusService { // Apply timeout consensus session.state = crate::consensus::ConsensusState::ConsensusReached(result); - info!("Timeout consensus applied for proposal {proposal_id}: {result} (liveness criteria)"); + info!("[handle_consensus_timeout]: Timeout consensus applied for proposal {proposal_id}: {result} (liveness criteria)"); // Emit consensus event session.emit_consensus_event( @@ -663,7 +676,7 @@ impl ConsensusService { ) -> bool { let required_votes = self.calculate_required_votes(expected_voters, consensus_threshold); println!( - "[consensus::service::check_sufficient_votes]: Total votes: {total_votes}, Expected voters: {expected_voters}, Consensus threshold: {consensus_threshold}, Required votes: {required_votes}" + "[service::check_sufficient_votes]: Total votes: {total_votes}, Expected voters: {expected_voters}, Consensus threshold: {consensus_threshold}, Required votes: {required_votes}" ); total_votes >= required_votes } diff --git a/src/error.rs b/src/error.rs index bbd2178..b98a3f7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -11,7 +11,7 @@ use openmls::{ use openmls_rust_crypto::MemoryStorageError; use std::string::FromUtf8Error; -use ds::{waku_actor::WakuMessageToSend, DeliveryServiceError}; +use ds::DeliveryServiceError; use mls_crypto::error::IdentityError; #[derive(Debug, thiserror::Error)] @@ -78,6 +78,8 @@ pub enum MessageError { pub enum GroupError { #[error(transparent)] MessageError(#[from] MessageError), + #[error(transparent)] + IdentityError(#[from] IdentityError), #[error("Steward not set")] StewardNotSet, @@ -145,22 +147,10 @@ pub enum UserError { FailedToExtractWelcomeMessage, #[error("Message verification failed")] MessageVerificationFailed, - #[error("Failed to create group: {0}")] - UnableToCreateGroup(String), - #[error("Failed to send message to user: {0}")] - UnableToHandleStewardEpoch(String), - #[error("Failed to process steward message: {0}")] - ProcessStewardMessageError(String), - #[error("Failed to get group update requests: {0}")] - GetGroupUpdateRequestsError(String), #[error("Invalid user action: {0}")] InvalidUserAction(String), - #[error("Failed to start voting: {0}")] - UnableToStartVoting(String), #[error("Unknown content topic type: {0}")] UnknownContentTopicType(String), - #[error("Failed to send message to ws: {0}")] - UnableToSendMessageToWs(String), #[error("Invalid group state: {0}")] InvalidGroupState(String), #[error("No proposals found")] @@ -180,6 +170,6 @@ pub enum UserError { MlsMessageInDeserializeError(#[from] openmls::prelude::Error), #[error("Failed to try into protocol message: {0}")] TryIntoProtocolMessageError(#[from] openmls::framing::errors::ProtocolMessageError), - #[error("Failed to send message to waku: {0}")] - WakuSendMessageError(#[from] tokio::sync::mpsc::error::SendError), + #[error("Failed to get current time")] + FailedToGetCurrentTime(#[from] std::time::SystemTimeError), } diff --git a/src/group.rs b/src/group.rs index 2ce9cb4..b5d6f92 100644 --- a/src/group.rs +++ b/src/group.rs @@ -1,6 +1,4 @@ use alloy::hex; -use kameo::Actor; -use log::{error, info}; use openmls::{ group::{GroupEpoch, GroupId, MlsGroup, MlsGroupCreateConfig}, prelude::{ @@ -12,18 +10,21 @@ use openmls_basic_credential::SignatureKeyPair; use prost::Message; use std::{fmt::Display, sync::Arc}; use tokio::sync::{Mutex, RwLock}; +use tracing::{error, info}; use uuid; use crate::{ - consensus::v1::{Proposal, Vote}, error::GroupError, message::{message_types, MessageType}, - protos::messages::v1::{app_message, AppMessage, BatchProposalsMessage, WelcomeMessage}, + protos::{ + consensus::v1::{Proposal, RequestType, UpdateRequest, Vote}, + de_mls::messages::v1::{app_message, AppMessage, BatchProposalsMessage, WelcomeMessage}, + }, state_machine::{GroupState, GroupStateMachine}, steward::GroupUpdateRequest, }; use ds::{waku_actor::WakuMessageToSend, APP_MSG_SUBTOPIC, WELCOME_SUBTOPIC}; -use mls_crypto::openmls_provider::MlsProvider; +use mls_crypto::{identity::normalize_wallet_address_str, openmls_provider::MlsProvider}; /// Represents the action to take after processing a group message or event. /// @@ -62,7 +63,7 @@ impl Display for GroupAction { /// - State machine integration for proper workflow enforcement /// - Member addition/removal through proposals /// - Message validation and permission checking -#[derive(Clone, Debug, Actor)] +#[derive(Clone, Debug)] pub struct Group { group_name: String, mls_group: Option>>, @@ -304,15 +305,26 @@ impl Group { /// ## Effects: /// - Adds an AddMember proposal to the current epoch /// - Proposal will be processed in the next steward epoch + /// - Returns a serialized `UiUpdateRequest` for UI notification pub async fn store_invite_proposal( &mut self, key_package: Box, - ) -> Result<(), GroupError> { + ) -> Result { let mut state_machine = self.state_machine.write().await; state_machine - .add_proposal(GroupUpdateRequest::AddMember(key_package)) + .add_proposal(GroupUpdateRequest::AddMember(key_package.clone())) .await; - Ok(()) + + let wallet_bytes = key_package + .leaf_node() + .credential() + .serialized_content() + .to_vec(); + + Ok(UpdateRequest { + request_type: RequestType::AddMember as i32, + wallet_address: wallet_bytes, + }) } /// Store a remove proposal in the steward queue for the current epoch. @@ -320,15 +332,31 @@ impl Group { /// ## Parameters: /// - `identity`: The identity string of the member to remove /// - /// ## Effects: - /// - Adds a RemoveMember proposal to the current epoch - /// - Proposal will be processed in the next steward epoch - pub async fn store_remove_proposal(&mut self, identity: String) -> Result<(), GroupError> { + /// ## Returns: + /// - Returns a serialized `UiUpdateRequest` for UI notification + /// - `GroupError::InvalidIdentity` if the identity is invalid + pub async fn store_remove_proposal( + &mut self, + identity: String, + ) -> Result { + let normalized_identity = normalize_wallet_address_str(&identity)?; let mut state_machine = self.state_machine.write().await; state_machine - .add_proposal(GroupUpdateRequest::RemoveMember(identity)) + .add_proposal(GroupUpdateRequest::RemoveMember( + normalized_identity.clone(), + )) .await; - Ok(()) + + let wallet_bytes = hex::decode( + normalized_identity + .strip_prefix("0x") + .unwrap_or(&normalized_identity), + )?; + + Ok(UpdateRequest { + request_type: RequestType::RemoveMember as i32, + wallet_address: wallet_bytes, + }) } /// Process an application message and determine the appropriate action. @@ -355,30 +383,31 @@ impl Group { let app_msg = AppMessage::decode(message.into_bytes().as_slice())?; match app_msg.payload { Some(app_message::Payload::ConversationMessage(conversation_message)) => { - info!("[group::process_application_message]: Processing conversation message"); + info!("[process_application_message]: Processing conversation message"); Ok(GroupAction::GroupAppMsg(conversation_message.into())) } Some(app_message::Payload::Proposal(proposal)) => { - info!("[group::process_application_message]: Processing proposal message"); + info!("[process_application_message]: Processing proposal message"); Ok(GroupAction::GroupProposal(proposal)) } Some(app_message::Payload::Vote(vote)) => { - info!("[group::process_application_message]: Processing vote message"); + info!("[process_application_message]: Processing vote message"); Ok(GroupAction::GroupVote(vote)) } Some(app_message::Payload::BanRequest(ban_request)) => { - info!("[group::process_application_message]: Processing ban request message"); + info!("[process_application_message]: Processing ban request message"); if self.is_steward().await { info!( - "[group::process_application_message]: Steward adding remove proposal for user {}", + "[process_application_message]: Steward adding remove proposal for user {}", ban_request.user_to_ban.clone() ); - self.store_remove_proposal(ban_request.user_to_ban.clone()) + let _ = self + .store_remove_proposal(ban_request.user_to_ban.clone()) .await?; } else { info!( - "[group::process_application_message]: Non-steward received ban request message" + "[process_application_message]: Non-steward received ban request message" ); } @@ -534,6 +563,15 @@ impl Group { .await } + /// Get the current epoch proposals for UI display. + pub async fn get_current_epoch_proposals(&self) -> Vec { + self.state_machine + .read() + .await + .get_current_epoch_proposals() + .await + } + /// Get the number of pending proposals for the voting epoch pub async fn get_voting_proposals_count(&self) -> usize { self.state_machine @@ -552,6 +590,14 @@ impl Group { .await } + pub async fn get_proposals_for_voting_epoch_as_ui_update_requests(&self) -> Vec { + self.get_proposals_for_voting_epoch() + .await + .iter() + .map(|p| p.clone().into()) + .collect() + } + /// Start voting on proposals for the current epoch pub async fn start_voting(&mut self) -> Result<(), GroupError> { self.state_machine.write().await.start_voting() @@ -575,14 +621,6 @@ impl Group { self.state_machine.write().await.start_consensus_reached(); } - /// Recover from consensus failure by transitioning back to Working state - pub async fn recover_from_consensus_failure(&mut self) -> Result<(), GroupError> { - self.state_machine - .write() - .await - .recover_from_consensus_failure() - } - /// Start waiting state (for non-steward peers after consensus or edge case recovery) pub async fn start_waiting(&mut self) { self.state_machine.write().await.start_waiting(); diff --git a/src/group_registry.rs b/src/group_registry.rs new file mode 100644 index 0000000..1f53f4d --- /dev/null +++ b/src/group_registry.rs @@ -0,0 +1,31 @@ +use std::collections::HashSet; +use tokio::sync::RwLock; + +// src/group_registry.rs +#[derive(Default, Debug)] +pub struct GroupRegistry { + names: RwLock>, +} + +impl GroupRegistry { + pub fn new() -> Self { + Self::default() + } + + pub async fn exists(&self, name: &str) -> bool { + self.names.read().await.contains(name) + } + + pub async fn insert(&self, name: String) -> bool { + let mut g = self.names.write().await; + if g.contains(&name) { + return false; + } + g.insert(name); + true + } + + pub async fn all(&self) -> Vec { + self.names.read().await.iter().cloned().collect() + } +} diff --git a/src/lib.rs b/src/lib.rs index 0707ee1..4079007 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,7 +33,7 @@ //! //! 1. **Working State**: Normal operation, all users can send any message freely //! 2. **Waiting State**: Steward epoch active, only steward can send BATCH_PROPOSALS_MESSAGE -//! 3. **Voting State**: Consensus voting, restricted message types (VOTE/USER_VOTE for all, VOTING_PROPOSAL/PROPOSAL for steward only) +//! 3. **Voting State**: Consensus voting, restricted message types (VOTE/USER_VOTE for all, VOTE_PAYLOAD/PROPOSAL for steward only) //! //! ### Complete State Transitions //! @@ -92,57 +92,42 @@ //! - **Waku**: Decentralized messaging protocol //! - **Alloy**: Ethereum wallet and signing -use alloy::primitives::{Address, PrimitiveSignature}; +use alloy::primitives::{Address, Signature}; use ecies::{decrypt, encrypt}; use libsecp256k1::{sign, verify, Message, PublicKey, SecretKey, Signature as libSignature}; use rand::thread_rng; use secp256k1::hashes::{sha256, Hash}; -use std::{ - collections::HashSet, - sync::{Arc, Mutex}, -}; -use tokio::sync::{mpsc::Sender, RwLock}; -use waku_bindings::{WakuContentTopic, WakuMessage}; -use ds::waku_actor::WakuMessageToSend; use error::{GroupError, MessageError}; -pub mod action_handlers; +pub mod bootstrap; +pub use bootstrap::{bootstrap_core, bootstrap_core_from_env, Bootstrap, BootstrapConfig}; + pub mod consensus; pub mod error; pub mod group; +pub mod group_registry; pub mod message; pub mod state_machine; pub mod steward; pub mod user; pub mod user_actor; pub mod user_app_instance; -pub mod ws_actor; pub mod protos { - pub mod messages { + pub mod consensus { pub mod v1 { - pub mod consensus { - pub mod v1 { - include!(concat!(env!("OUT_DIR"), "/consensus.v1.rs")); - } - } - include!(concat!(env!("OUT_DIR"), "/de_mls.messages.v1.rs")); + include!(concat!(env!("OUT_DIR"), "/consensus.v1.rs")); } } -} -pub struct AppState { - pub waku_node: Sender, - pub rooms: Mutex>, - pub content_topics: Arc>>, - pub pubsub: tokio::sync::broadcast::Sender, -} -#[derive(Debug, Clone)] -pub struct Connection { - pub eth_private_key: String, - pub group_id: String, - pub should_create_group: bool, + pub mod de_mls { + pub mod messages { + pub mod v1 { + include!(concat!(env!("OUT_DIR"), "/de_mls.messages.v1.rs")); + } + } + } } pub fn generate_keypair() -> (PublicKey, SecretKey) { @@ -186,15 +171,6 @@ pub fn decrypt_message(message: &[u8], secret_key: SecretKey) -> Result, Ok(decrypted) } -/// Check if a content topic exists in a list of topics or if the list is empty -pub async fn match_content_topic( - content_topics: &Arc>>, - topic: &WakuContentTopic, -) -> bool { - let locked_topics = content_topics.read().await; - locked_topics.is_empty() || locked_topics.iter().any(|t| t == topic) -} - pub trait LocalSigner { fn local_sign_message( &self, @@ -218,7 +194,7 @@ pub fn verify_vote_hash( expect: 65, actual: signature.len(), })?; - let signature = PrimitiveSignature::from_raw_array(&signature_bytes)?; + let signature = Signature::from_raw_array(&signature_bytes)?; let address = signature.recover_address_from_msg(message)?; let address_bytes = address.as_slice().to_vec(); Ok(address_bytes == public_key) diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 261b9ac..0000000 --- a/src/main.rs +++ /dev/null @@ -1,267 +0,0 @@ -use axum::{ - extract::ws::{Message, WebSocket, WebSocketUpgrade}, - extract::State, - http::Method, - response::IntoResponse, - routing::get, - Router, -}; -use futures::StreamExt; -use log::{error, info}; -use serde_json::json; -use std::{ - collections::HashSet, - net::SocketAddr, - sync::{Arc, Mutex}, -}; -use tokio::sync::{mpsc::channel, RwLock}; -use tokio_util::sync::CancellationToken; -use tower_http::cors::{Any, CorsLayer}; -use waku_bindings::{Multiaddr, WakuMessage}; - -use de_mls::{ - action_handlers::{handle_user_actions, handle_ws_action}, - match_content_topic, - user_app_instance::create_user_instance, - ws_actor::{RawWsMessage, WsAction, WsActor}, - AppState, Connection, -}; -use ds::waku_actor::{run_waku_node, WakuMessageToSend}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - env_logger::init(); - let port = std::env::var("PORT") - .map(|val| val.parse::()) - .unwrap_or(Ok(3000))?; - let addr = SocketAddr::from(([0, 0, 0, 0], port)); - let node_port = std::env::var("NODE_PORT").expect("NODE_PORT is not set"); - let peer_addresses = std::env::var("PEER_ADDRESSES") - .map(|val| { - val.split(",") - .map(|addr| { - addr.parse::() - .expect("Failed to parse peer address") - }) - .collect() - }) - .expect("PEER_ADDRESSES is not set"); - - let content_topics = Arc::new(RwLock::new(Vec::new())); - - let (waku_sender, mut waku_receiver) = channel::(100); - let (sender, mut receiver) = channel::(100); - let (tx, _) = tokio::sync::broadcast::channel(100); - - let app_state = Arc::new(AppState { - waku_node: sender, - rooms: Mutex::new(HashSet::new()), - content_topics, - pubsub: tx.clone(), - }); - info!("App state initialized"); - - let recv_messages = tokio::spawn(async move { - info!("Running recv messages from waku"); - while let Some(msg) = waku_receiver.recv().await { - let _ = tx.send(msg); - } - }); - info!("Waku receiver initialized"); - - let server_task = tokio::spawn(async move { - info!("Running server"); - run_server(app_state, addr) - .await - .expect("Failed to run server") - }); - - info!("Starting waku node"); - tokio::task::block_in_place(move || { - tokio::runtime::Handle::current().block_on(async move { - run_waku_node(node_port, Some(peer_addresses), waku_sender, &mut receiver).await - }) - })?; - - tokio::select! { - result = recv_messages => { - if let Err(w) = result { - error!("Error receiving messages from waku: {w}"); - } - } - result = server_task => { - if let Err(e) = result { - error!("Error hosting server: {e}"); - } - } - } - Ok(()) -} - -async fn run_server( - app_state: Arc, - addr: SocketAddr, -) -> Result<(), Box> { - let cors = CorsLayer::new() - .allow_origin(Any) - .allow_methods(vec![Method::GET]); - - let app = Router::new() - .route("/", get(|| async { "Hello World!" })) - .route("/ws", get(handler)) - .route("/rooms", get(get_rooms)) - .with_state(app_state) - .layer(cors); - - info!("Hosted on {addr:?}"); - - axum::Server::bind(&addr) - .serve(app.into_make_service()) - .await?; - Ok(()) -} - -async fn handler(ws: WebSocketUpgrade, State(state): State>) -> impl IntoResponse { - ws.on_upgrade(|socket| handle_socket(socket, state)) -} - -async fn handle_socket(socket: WebSocket, state: Arc) { - info!("Handling socket"); - let (ws_sender, mut ws_receiver) = socket.split(); - let ws_actor = kameo::spawn(WsActor::new(ws_sender)); - let mut main_loop_connection = None::; - let cancel_token = CancellationToken::new(); - while let Some(Ok(Message::Text(data))) = ws_receiver.next().await { - let res = ws_actor.ask(RawWsMessage { message: data }).await; - match res { - Ok(WsAction::Connect(connect)) => { - info!("Got connect: {:?}", &connect); - main_loop_connection = Some(Connection { - eth_private_key: connect.eth_private_key.clone(), - group_id: connect.group_id.clone(), - should_create_group: connect.should_create, - }); - let mut rooms = match state.rooms.lock() { - Ok(rooms) => rooms, - Err(e) => { - log::error!("Failed to acquire rooms lock: {e}"); - continue; - } - }; - if !rooms.contains(&connect.group_id.clone()) { - rooms.insert(connect.group_id.clone()); - } - info!("Prepare info for main loop: {main_loop_connection:?}"); - break; - } - Ok(_) => { - info!("Got chat message for non-existent user"); - } - - Err(e) => error!("Error handling message: {e}"), - } - } - - let user_actor = create_user_instance( - main_loop_connection.unwrap().clone(), - state.clone(), - ws_actor.clone(), - ) - .await - .expect("Failed to create user instance"); - - let user_actor_clone = user_actor.clone(); - let state_clone = state.clone(); - let ws_actor_clone = ws_actor.clone(); - let cancel_token_clone = cancel_token.clone(); - - let mut user_waku_receiver = state.pubsub.subscribe(); - let mut recv_messages_waku = tokio::spawn(async move { - info!("Running recv messages from waku for current user"); - while let Ok(msg) = user_waku_receiver.recv().await { - let content_topic = msg.content_topic.clone(); - // Check if message belongs to a relevant topic - if !match_content_topic(&state_clone.content_topics, &content_topic).await { - error!("Content topic not match: {content_topic:?}"); - return; - }; - info!("[handle_socket]: Received message from waku that matches content topic"); - let res = handle_user_actions( - msg, - state_clone.waku_node.clone(), - ws_actor_clone.clone(), - user_actor_clone.clone(), - state_clone.clone(), - cancel_token_clone.clone(), - ) - .await; - if let Err(e) = res { - error!("Error handling waku message: {e}"); - } - } - }); - - let user_ref_clone = user_actor.clone(); - let mut recv_messages_ws = { - tokio::spawn(async move { - info!("Running receive messages from websocket"); - while let Some(Ok(Message::Text(text))) = ws_receiver.next().await { - let res = handle_ws_action( - RawWsMessage { message: text }, - ws_actor.clone(), - user_ref_clone.clone(), - state.waku_node.clone(), - ) - .await; - if let Err(e) = res { - error!("Error handling websocket message: {e}"); - } - } - }) - }; - - tokio::select! { - _ = (&mut recv_messages_waku) => { - info!("recv messages from waku finished"); - recv_messages_ws.abort(); - } - _ = (&mut recv_messages_ws) => { - info!("receive messages from websocket finished"); - recv_messages_ws.abort(); - } - _ = cancel_token.cancelled() => { - info!("Cancel token cancelled"); - recv_messages_ws.abort(); - recv_messages_waku.abort(); - } - }; - - info!("Main loop finished"); -} - -async fn get_rooms(State(state): State>) -> String { - let rooms = match state.rooms.lock() { - Ok(rooms) => rooms, - Err(e) => { - log::error!("Failed to acquire rooms lock: {e}"); - return json!({ - "status": "Error acquiring rooms lock", - "rooms": [] - }) - .to_string(); - } - }; - let vec = rooms.iter().collect::>(); - match vec.len() { - 0 => json!({ - "status": "No rooms found yet!", - "rooms": [] - }) - .to_string(), - _ => json!({ - "status": "Success!", - "rooms": vec - }) - .to_string(), - } -} diff --git a/src/message.rs b/src/message.rs index f523dce..52bfabf 100644 --- a/src/message.rs +++ b/src/message.rs @@ -17,24 +17,28 @@ //! - [`ConversationMessage`] //! - [`BatchProposalsMessage`] //! - [`BanRequest`] -//! - [`VotingProposal`] +//! - [`VotePayload`] //! - [`UserVote`] //! +use alloy::hex; +use mls_crypto::identity::normalize_wallet_address; +use openmls::prelude::{KeyPackage, MlsMessageOut}; +use std::convert::TryFrom; use crate::{ - consensus::v1::{Proposal, Vote}, + consensus::ConsensusEvent, encrypt_message, - protos::messages::v1::{app_message, UserKeyPackage, UserVote, VotingProposal}, + protos::{ + consensus::v1::{Outcome, Proposal, RequestType, UpdateRequest, Vote, VotePayload}, + de_mls::messages::v1::{ + app_message, welcome_message, AppMessage, BanRequest, BatchProposalsMessage, + ConversationMessage, GroupAnnouncement, InvitationToJoin, ProposalAdded, + UserKeyPackage, UserVote, WelcomeMessage, + }, + }, + steward::GroupUpdateRequest, verify_message, MessageError, }; -use openmls::prelude::{KeyPackage, MlsMessageOut}; -use serde::{Deserialize, Serialize}; -use std::fmt::Display; - -use crate::protos::messages::v1::{ - welcome_message, AppMessage, BanRequest, BatchProposalsMessage, ConversationMessage, - GroupAnnouncement, InvitationToJoin, WelcomeMessage, -}; // Message type constants for consistency and type safety pub mod message_types { @@ -43,8 +47,9 @@ pub mod message_types { pub const BAN_REQUEST: &str = "BanRequest"; pub const PROPOSAL: &str = "Proposal"; pub const VOTE: &str = "Vote"; - pub const VOTING_PROPOSAL: &str = "VotingProposal"; + pub const VOTE_PAYLOAD: &str = "VotePayload"; pub const USER_VOTE: &str = "UserVote"; + pub const PROPOSAL_ADDED: &str = "ProposalAdded"; pub const UNKNOWN: &str = "Unknown"; } @@ -62,8 +67,19 @@ impl MessageType for app_message::Payload { app_message::Payload::BanRequest(_) => BAN_REQUEST, app_message::Payload::Proposal(_) => PROPOSAL, app_message::Payload::Vote(_) => VOTE, - app_message::Payload::VotingProposal(_) => VOTING_PROPOSAL, + app_message::Payload::VotePayload(_) => VOTE_PAYLOAD, app_message::Payload::UserVote(_) => USER_VOTE, + app_message::Payload::ProposalAdded(_) => PROPOSAL_ADDED, + } + } +} + +impl MessageType for UpdateRequest { + fn message_type(&self) -> &'static str { + match RequestType::try_from(self.request_type) { + Ok(RequestType::AddMember) => "Add Member", + Ok(RequestType::RemoveMember) => "Remove Member", + _ => "Unknown", } } } @@ -121,76 +137,10 @@ impl From for WelcomeMessage { } } -// APP MESSAGE SUBTOPIC -impl Display for AppMessage { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.payload { - Some(app_message::Payload::ConversationMessage(conversation_message)) => { - write!( - f, - "{}: {}", - conversation_message.sender, - String::from_utf8_lossy(&conversation_message.message) - ) - } - Some(app_message::Payload::BatchProposalsMessage(batch_msg)) => { - write!( - f, - "BatchProposalsMessage: {} proposals for group {}", - batch_msg.mls_proposals.len(), - String::from_utf8_lossy(&batch_msg.group_name) - ) - } - Some(app_message::Payload::BanRequest(ban_request)) => { - write!( - f, - "SYSTEM: {} wants to ban {}", - ban_request.requester, ban_request.user_to_ban - ) - } - Some(app_message::Payload::Proposal(proposal)) => { - write!( - f, - "Proposal: ID {} with {} votes for {} voters", - proposal.proposal_id, - proposal.votes.len(), - proposal.expected_voters_count - ) - } - Some(app_message::Payload::Vote(vote)) => { - write!( - f, - "Vote: {} for proposal {} ({})", - if vote.vote { "YES" } else { "NO" }, - vote.proposal_id, - vote.vote_id - ) - } - Some(app_message::Payload::VotingProposal(voting_proposal)) => { - write!( - f, - "VotingProposal: ID {} for group {}", - voting_proposal.proposal_id, voting_proposal.group_name - ) - } - Some(app_message::Payload::UserVote(user_vote)) => { - write!( - f, - "UserVote: {} for proposal {} in group {}", - if user_vote.vote { "YES" } else { "NO" }, - user_vote.proposal_id, - user_vote.group_name - ) - } - None => write!(f, "Empty message"), - } - } -} - -impl From for AppMessage { - fn from(voting_proposal: VotingProposal) -> Self { +impl From for AppMessage { + fn from(vote_payload: VotePayload) -> Self { AppMessage { - payload: Some(app_message::Payload::VotingProposal(voting_proposal)), + payload: Some(app_message::Payload::VotePayload(vote_payload)), } } } @@ -246,20 +196,75 @@ impl From for AppMessage { } } } -/// This struct is used to represent the message from the user that we got from web socket -#[derive(Deserialize, Debug, PartialEq, Serialize)] -pub struct UserMessage { - pub message: Vec, - pub group_id: String, + +impl From for AppMessage { + fn from(proposal_added: ProposalAdded) -> Self { + AppMessage { + payload: Some(app_message::Payload::ProposalAdded(proposal_added)), + } + } } -/// This struct is used to represent the connection data that web socket sends to the user -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct ConnectMessage { - /// This is the private key of the user that we will use to authenticate the user - pub eth_private_key: String, - /// This is the id of the group that the user is joining - pub group_id: String, - /// This is the flag that indicates if the user should create a new group or subscribe to an existing one - pub should_create: bool, +impl From for Outcome { + fn from(consensus_event: ConsensusEvent) -> Self { + match consensus_event { + ConsensusEvent::ConsensusReached { + proposal_id: _, + result: true, + } => Outcome::Accepted, + ConsensusEvent::ConsensusReached { + proposal_id: _, + result: false, + } => Outcome::Rejected, + ConsensusEvent::ConsensusFailed { + proposal_id: _, + reason: _, + } => Outcome::Unspecified, + } + } +} + +impl From for UpdateRequest { + fn from(group_update_request: GroupUpdateRequest) -> Self { + match group_update_request { + GroupUpdateRequest::AddMember(kp) => UpdateRequest { + request_type: RequestType::AddMember as i32, + wallet_address: kp.leaf_node().credential().serialized_content().to_vec(), + }, + GroupUpdateRequest::RemoveMember(id) => UpdateRequest { + request_type: RequestType::RemoveMember as i32, + wallet_address: hex::decode(id.strip_prefix("0x").unwrap_or(&id)) + .unwrap_or_else(|_| id.into_bytes()), + }, + } + } +} + +// Helper function to convert protobuf UpdateRequest to display format +pub fn convert_group_requests_to_display( + group_requests: &[UpdateRequest], +) -> Vec<(String, String)> { + let mut results = Vec::new(); + + for req in group_requests { + match RequestType::try_from(req.request_type) { + Ok(RequestType::AddMember) => { + results.push(( + "Add Member".to_string(), + normalize_wallet_address(&req.wallet_address), + )); + } + Ok(RequestType::RemoveMember) => { + results.push(( + "Remove Member".to_string(), + normalize_wallet_address(&req.wallet_address), + )); + } + _ => { + results.push(("Unknown".to_string(), "Invalid request".to_string())); + } + } + } + + results } diff --git a/src/protos/messages/v1/application.proto b/src/protos/messages/v1/application.proto index 4f946e4..e6da023 100644 --- a/src/protos/messages/v1/application.proto +++ b/src/protos/messages/v1/application.proto @@ -13,8 +13,9 @@ message AppMessage { BanRequest ban_request = 3; consensus.v1.Proposal proposal = 4; consensus.v1.Vote vote = 5; - VotingProposal voting_proposal = 6; + consensus.v1.VotePayload vote_payload = 6; UserVote user_vote = 7; + ProposalAdded proposal_added = 8; } } @@ -36,15 +37,15 @@ message BatchProposalsMessage { bytes commit_message = 3; // MLS commit message } -// New message types for voting -message VotingProposal { - uint32 proposal_id = 1; - string group_name = 2; - string payload = 3; -} - +// Yes/No vote for a given proposal. Based on the result, the `consensus.v1.Vote` will be created. message UserVote { uint32 proposal_id = 1; bool vote = 2; string group_name = 3; } + +// Proposal added message is sent to the UI when a new proposal is added to the group. +message ProposalAdded { + string group_id = 1; + consensus.v1.UpdateRequest request = 2; +} diff --git a/src/protos/messages/v1/consensus.proto b/src/protos/messages/v1/consensus.proto index 56519f2..c7b07ca 100644 --- a/src/protos/messages/v1/consensus.proto +++ b/src/protos/messages/v1/consensus.proto @@ -2,10 +2,16 @@ syntax = "proto3"; package consensus.v1; +enum RequestType { + REQUEST_TYPE_UNSPECIFIED = 0; + REQUEST_TYPE_ADD_MEMBER = 1; + REQUEST_TYPE_REMOVE_MEMBER = 2; +} + // Proposal represents a consensus proposal that needs voting message Proposal { string name = 10; // Proposal name - string payload = 11; // Payload of the proposal + repeated UpdateRequest group_requests = 11; // Structured group update requests uint32 proposal_id = 12; // Unique identifier of the proposal bytes proposal_owner = 13; // Public key of the creator repeated Vote votes = 14; // Vote list in the proposal @@ -28,3 +34,28 @@ message Vote { bytes vote_hash = 27; // Hash of all previously defined fields in Vote bytes signature = 28; // Signature of vote_hash } + +enum Outcome { + OUTCOME_UNSPECIFIED = 0; + OUTCOME_ACCEPTED = 1; + OUTCOME_REJECTED = 2; +} + +message ProposalResult { + string group_id = 31; + uint32 proposal_id = 32; + Outcome outcome = 33; + uint64 decided_at_ms = 34; +} + +message VotePayload { + string group_id = 41; + uint32 proposal_id = 42; + repeated UpdateRequest group_requests = 43; // Structured group update requests + uint64 timestamp = 44; +} + +message UpdateRequest { + RequestType request_type = 51; + bytes wallet_address = 52; +} diff --git a/src/state_machine.rs b/src/state_machine.rs index ff2bf27..d35ac4e 100644 --- a/src/state_machine.rs +++ b/src/state_machine.rs @@ -8,7 +8,7 @@ //! //! - **Working**: Normal operation state where users can send any message freely //! - **Waiting**: Steward epoch state where only steward can send BATCH_PROPOSALS_MESSAGE (if proposals exist) -//! - **Voting**: Voting state where everyone can send VOTE/USER_VOTE, only steward can send VOTING_PROPOSAL/PROPOSAL +//! - **Voting**: Voting state where everyone can send VOTE/USER_VOTE, only steward can send VOTE_PAYLOAD/PROPOSAL //! - **ConsensusReached**: Consensus achieved, waiting for steward to send batch proposals //! - **ConsensusFailed**: Consensus failed due to timeout or other reasons //! @@ -36,7 +36,7 @@ //! //! ## Voting State //! - **All users**: Can send VOTE and USER_VOTE -//! - **Steward only**: Can send VOTING_PROPOSAL and PROPOSAL +//! - **Steward only**: Can send VOTE_PAYLOAD and PROPOSAL //! - **All users**: All other message types blocked //! //! ## ConsensusReached State @@ -96,8 +96,7 @@ //! - Failed votes result in proposals being discarded and return to working state use std::fmt::Display; - -use log::info; +use tracing::info; use crate::message::message_types; use crate::steward::Steward; @@ -110,12 +109,10 @@ pub enum GroupState { Working, /// Waiting state during steward epoch - only steward can send BATCH_PROPOSALS_MESSAGE Waiting, - /// Voting state - everyone can send VOTE/USER_VOTE, only steward can send VOTING_PROPOSAL/PROPOSAL + /// Voting state - everyone can send VOTE/USER_VOTE, only steward can send VOTE_PAYLOAD/PROPOSAL Voting, /// Consensus reached state - consensus achieved, waiting for steward to send batch proposals ConsensusReached, - /// Consensus failed state - consensus failed due to timeout or other reasons - ConsensusFailed, } impl Display for GroupState { @@ -125,7 +122,6 @@ impl Display for GroupState { GroupState::Waiting => "Waiting", GroupState::Voting => "Voting", GroupState::ConsensusReached => "ConsensusReached", - GroupState::ConsensusFailed => "ConsensusFailed", }; write!(f, "{state}") } @@ -193,10 +189,10 @@ impl GroupStateMachine { GroupState::Voting => { // In voting state, only voting-related messages allowed match message_type { - message_types::VOTE => true, // Everyone can send votes - message_types::USER_VOTE => true, // Everyone can send user votes - message_types::VOTING_PROPOSAL => is_steward, // Only steward can send voting proposals - message_types::PROPOSAL => is_steward, // Only steward can send proposals + message_types::VOTE => true, // Everyone can send votes + message_types::USER_VOTE => true, // Everyone can send user votes + message_types::VOTE_PAYLOAD => is_steward, // Only steward can send voting proposals + message_types::PROPOSAL => is_steward, // Only steward can send proposals _ => false, // All other messages blocked during voting } } @@ -207,10 +203,6 @@ impl GroupStateMachine { _ => false, // All other messages blocked during ConsensusReached } } - GroupState::ConsensusFailed => { - // In ConsensusFailed state, no messages are allowed - false - } } } @@ -280,33 +272,6 @@ impl GroupStateMachine { info!("[start_consensus_reached] Transitioning to ConsensusReached state"); } - /// Start consensus failed state (for peers after consensus failure). - /// - /// ## State Transition: - /// Any State → ConsensusFailed - /// - /// ## Usage: - /// Called when consensus fails due to timeout or other reasons. - /// This state blocks all message types until recovery is initiated. - pub fn start_consensus_failed(&mut self) { - self.state = GroupState::ConsensusFailed; - info!("[start_consensus_failed] Transitioning to ConsensusFailed state"); - } - - /// Recover from consensus failure by transitioning back to Working state - pub fn recover_from_consensus_failure(&mut self) -> Result<(), GroupError> { - if self.state != GroupState::ConsensusFailed { - return Err(GroupError::InvalidStateTransition { - from: self.state.to_string(), - to: "Working".to_string(), - }); - } - - self.state = GroupState::Working; - info!("[recover_from_consensus_failure] Recovering from consensus failure, transitioning to Working state"); - Ok(()) - } - /// Start working state (for non-steward peers after consensus or edge case recovery). /// /// ## State Transition: @@ -354,6 +319,21 @@ impl GroupStateMachine { } } + /// Get the current epoch proposals for UI display. + /// + /// ## Returns: + /// - Vector of proposals currently collected for the next steward epoch + /// + /// ## Usage: + /// Used to display current proposals in the UI for stewards. + pub async fn get_current_epoch_proposals(&self) -> Vec { + if let Some(steward) = &self.steward { + steward.get_current_epoch_proposals().await + } else { + Vec::new() + } + } + /// Get the count of proposals in the voting epoch. /// /// ## Returns: @@ -670,7 +650,7 @@ mod tests { )); assert!(!state_machine.can_send_message_type(false, false, message_types::VOTE)); assert!(!state_machine.can_send_message_type(false, false, message_types::USER_VOTE)); - assert!(!state_machine.can_send_message_type(false, false, message_types::VOTING_PROPOSAL)); + assert!(!state_machine.can_send_message_type(false, false, message_types::VOTE_PAYLOAD)); assert!(!state_machine.can_send_message_type(false, false, message_types::PROPOSAL)); // BatchProposalsMessage should only be allowed from steward with proposals @@ -701,8 +681,8 @@ mod tests { assert!(state_machine.can_send_message_type(false, false, message_types::USER_VOTE)); // Only steward can send voting proposals and proposals - assert!(!state_machine.can_send_message_type(false, false, message_types::VOTING_PROPOSAL)); - assert!(state_machine.can_send_message_type(true, false, message_types::VOTING_PROPOSAL)); + assert!(!state_machine.can_send_message_type(false, false, message_types::VOTE_PAYLOAD)); + assert!(state_machine.can_send_message_type(true, false, message_types::VOTE_PAYLOAD)); assert!(!state_machine.can_send_message_type(false, false, message_types::PROPOSAL)); assert!(state_machine.can_send_message_type(true, false, message_types::PROPOSAL)); diff --git a/src/steward.rs b/src/steward.rs index cdeb680..08e602b 100644 --- a/src/steward.rs +++ b/src/steward.rs @@ -4,7 +4,10 @@ use openmls::prelude::KeyPackage; use std::{fmt::Display, str::FromStr, sync::Arc}; use tokio::sync::Mutex; -use crate::{protos::messages::v1::GroupAnnouncement, *}; +use crate::{ + decrypt_message, error::MessageError, generate_keypair, + protos::de_mls::messages::v1::GroupAnnouncement, sign_message, +}; #[derive(Clone, Debug)] pub struct Steward { @@ -101,6 +104,11 @@ impl Steward { self.voting_epoch_proposals.lock().await.len() } + /// Get the current epoch proposals for UI display. + pub async fn get_current_epoch_proposals(&self) -> Vec { + self.current_epoch_proposals.lock().await.clone() + } + /// Apply proposals for the current epoch (called after successful voting). pub async fn empty_voting_epoch_proposals(&mut self) { self.voting_epoch_proposals.lock().await.clear(); diff --git a/src/user.rs b/src/user.rs deleted file mode 100644 index 99f3d44..0000000 --- a/src/user.rs +++ /dev/null @@ -1,1666 +0,0 @@ -use alloy::{ - primitives::Address, - signers::{local::PrivateKeySigner, Signer}, -}; -use kameo::Actor; -use log::{debug, error, info}; -use openmls::{ - group::MlsGroupJoinConfig, - prelude::{DeserializeBytes, MlsMessageBodyIn, MlsMessageIn, StagedWelcome, Welcome}, -}; -use prost::Message; -use std::{collections::HashMap, fmt::Display, str::FromStr, sync::Arc}; -use tokio::sync::{broadcast, RwLock}; -use waku_bindings::WakuMessage; - -use ds::{waku_actor::WakuMessageToSend, APP_MSG_SUBTOPIC, WELCOME_SUBTOPIC}; -use mls_crypto::{ - identity::Identity, - openmls_provider::{MlsProvider, CIPHERSUITE}, -}; - -use crate::{ - consensus::{v1::Vote, ConsensusEvent, ConsensusService}, - error::UserError, - group::{Group, GroupAction}, - protos::messages::v1::{ - app_message, consensus::v1::Proposal, welcome_message, AppMessage, BanRequest, - BatchProposalsMessage, ConversationMessage, UserKeyPackage, VotingProposal, WelcomeMessage, - }, - state_machine::GroupState, - LocalSigner, -}; - -/// Represents the action to take after processing a user message or event. -/// -/// This enum defines the possible outcomes when processing user-related operations, -/// allowing the caller to determine the appropriate next steps for message handling, -/// group management, and network communication. -#[derive(Debug, Clone, PartialEq)] -pub enum UserAction { - SendToWaku(WakuMessageToSend), - SendToApp(AppMessage), - LeaveGroup(String), - DoNothing, -} - -impl Display for UserAction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - UserAction::SendToWaku(_) => write!(f, "SendToWaku"), - UserAction::SendToApp(_) => write!(f, "SendToApp"), - UserAction::LeaveGroup(group_name) => write!(f, "LeaveGroup({group_name})"), - UserAction::DoNothing => write!(f, "DoNothing"), - } - } -} - -/// Represents a user in the MLS-based messaging system. -/// -/// The User struct manages the lifecycle of multiple groups, handles consensus operations, -/// and coordinates communication between the application layer and the Waku network. -/// It integrates with the consensus service for proposal management and voting. -/// -/// ## Key Features: -/// - Multi-group management and coordination -/// - Consensus service integration for proposal handling -/// - Waku message processing and routing -/// - Steward epoch coordination -/// - Member management through proposals -#[derive(Actor)] -pub struct User { - identity: Identity, - // Each group has its own lock for better concurrency - groups: Arc>>>>, - provider: MlsProvider, - consensus_service: ConsensusService, - eth_signer: PrivateKeySigner, - // Queue for batch proposals that arrive before consensus is reached - pending_batch_proposals: Arc>>, -} - -impl User { - /// Create a new user instance with the specified Ethereum private key. - /// - /// ## Parameters: - /// - `user_eth_priv_key`: The user's Ethereum private key as a hex string - /// - /// ## Returns: - /// - New User instance with initialized identity and services - /// - /// ## Errors: - /// - `UserError` if private key parsing or identity creation fails - pub fn new(user_eth_priv_key: &str) -> Result { - let signer = PrivateKeySigner::from_str(user_eth_priv_key)?; - let user_address = signer.address(); - - let crypto = MlsProvider::default(); - let id = Identity::new(CIPHERSUITE, &crypto, user_address.as_slice())?; - - let user = User { - groups: Arc::new(RwLock::new(HashMap::new())), - identity: id, - eth_signer: signer, - provider: crypto, - consensus_service: ConsensusService::new(), - pending_batch_proposals: Arc::new(RwLock::new(HashMap::new())), - }; - Ok(user) - } - - /// Get a subscription to consensus events - pub fn subscribe_to_consensus_events(&self) -> broadcast::Receiver<(String, ConsensusEvent)> { - self.consensus_service.subscribe_to_events() - } - - pub async fn set_up_consensus_threshold_for_group( - &mut self, - group_name: &str, - proposal_id: u32, - consensus_threshold: f64, - ) -> Result<(), UserError> { - self.consensus_service - .set_consensus_threshold_for_group_session(group_name, proposal_id, consensus_threshold) - .await?; - Ok(()) - } - - /// Create a new group for this user. - /// - /// ## Parameters: - /// - `group_name`: The name of the group to create - /// - `is_creation`: Whether this is a group creation (true) or joining (false) - /// - /// ## Effects: - /// - If `is_creation` is true: Creates MLS group with steward capabilities - /// - If `is_creation` is false: Creates empty group for later joining - /// - Adds group to user's groups map - /// - /// ## Errors: - /// - `UserError::GroupAlreadyExistsError` if group already exists - /// - Various MLS group creation errors - pub async fn create_group( - &mut self, - group_name: &str, - is_creation: bool, - ) -> Result<(), UserError> { - let mut groups = self.groups.write().await; - if groups.contains_key(group_name) { - return Err(UserError::GroupAlreadyExistsError); - } - let group = if is_creation { - Group::new( - group_name, - true, - Some(&self.provider), - Some(self.identity.signer()), - Some(&self.identity.credential_with_key()), - )? - } else { - Group::new(group_name, false, None, None, None)? - }; - - groups.insert(group_name.to_string(), Arc::new(RwLock::new(group))); - Ok(()) - } - - /// Check if a group exists for this user. - /// - /// ## Parameters: - /// - `group_name`: The name of the group to check - /// - /// ## Returns: - /// - `true` if group exists, `false` otherwise - pub async fn if_group_exists(&self, group_name: &str) -> bool { - let groups = self.groups.read().await; - groups.contains_key(group_name) - } - - /// Get the state of a group. - /// - /// ## Parameters: - /// - `group_name`: The name of the group to get the state of - /// - /// ## Returns: - /// - `GroupState` of the group - pub async fn get_group_state(&self, group_name: &str) -> Result { - let groups = self.groups.read().await; - let state = groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - .read() - .await - .get_state() - .await; - - Ok(state) - } - - /// Get the number of members in a group. - /// - /// ## Parameters: - /// - `group_name`: The name of the group to get the number of members of - /// - /// ## Returns: - /// - The number of members in the group - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - pub async fn get_group_number_of_members(&self, group_name: &str) -> Result { - let groups = self.groups.read().await; - let members = groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - .read() - .await - .members_identity() - .await?; - Ok(members.len()) - } - - /// Get the MLS epoch of a group. - /// - /// ## Parameters: - /// - `group_name`: The name of the group to get the MLS epoch of - /// - /// ## Returns: - /// - The MLS epoch of the group - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - pub async fn get_group_mls_epoch(&self, group_name: &str) -> Result { - let groups = self.groups.read().await; - let epoch = groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - .read() - .await - .epoch() - .await?; - Ok(epoch.as_u64()) - } - - /// Check if a user is in a group. - /// - /// ## Parameters: - /// - `group_name`: The name of the group to check if the user is in - /// - `user_address`: The address of the user to check if they are in the group - /// - /// ## Returns: - /// - `true` if the user is in the group, `false` otherwise - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - pub async fn check_if_user_in_group( - &self, - group_name: &str, - user_address: &str, - ) -> Result { - let groups = self.groups.read().await; - let members = groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - .read() - .await - .members_identity() - .await?; - Ok(members.contains(&user_address.as_bytes().to_vec())) - } - - /// Process messages from the welcome subtopic. - /// - /// ## Parameters: - /// - `msg`: The Waku message to process - /// - `group_name`: The name of the group this message is for - /// - /// ## Returns: - /// - `UserAction` indicating what action should be taken - /// - /// ## Message Types Handled: - /// - **GroupAnnouncement**: Steward announcements for group joining - /// - **UserKeyPackage**: Encrypted key packages from new members - /// - **InvitationToJoin**: MLS welcome messages for group joining - /// - /// ## Effects: - /// - For group announcements: Generates and sends key package - /// - For user key packages: Decrypts and stores invite proposals (steward only) - /// - For invitations: Processes MLS welcome and joins group - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - /// - `UserError::MessageVerificationFailed` if announcement verification fails - /// - Various MLS and encryption errors - pub async fn process_welcome_subtopic( - &mut self, - msg: WakuMessage, - group_name: &str, - ) -> Result { - // Get the group lock first - let group = { - let groups = self.groups.read().await; - groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - }; - - let is_steward = { - let group = group.read().await; - group.is_steward().await - }; - let is_kp_shared = { - let group = group.read().await; - group.is_kp_shared() - }; - let is_mls_group_initialized = { - let group = group.read().await; - group.is_mls_group_initialized() - }; - - let received_msg = WelcomeMessage::decode(msg.payload())?; - if let Some(payload) = &received_msg.payload { - match payload { - welcome_message::Payload::GroupAnnouncement(group_announcement) => { - if is_steward || is_kp_shared { - Ok(UserAction::DoNothing) - } else { - info!( - "[user::process_welcome_subtopic]: User received group announcement message for group {group_name}" - ); - if !group_announcement.verify()? { - return Err(UserError::MessageVerificationFailed); - } - - let new_kp = self.identity.generate_key_package(&self.provider)?; - let encrypted_key_package = group_announcement.encrypt(new_kp)?; - group.write().await.set_kp_shared(true); - - let welcome_msg: WelcomeMessage = UserKeyPackage { - encrypt_kp: encrypted_key_package, - } - .into(); - Ok(UserAction::SendToWaku(WakuMessageToSend::new( - welcome_msg.encode_to_vec(), - WELCOME_SUBTOPIC, - group_name, - group.read().await.app_id(), - ))) - } - } - welcome_message::Payload::UserKeyPackage(user_key_package) => { - if is_steward { - info!( - "[user::process_welcome_subtopic]: Steward received key package for the group {group_name}" - ); - let key_package = group - .write() - .await - .decrypt_steward_msg(user_key_package.encrypt_kp.clone()) - .await?; - - group - .write() - .await - .store_invite_proposal(Box::new(key_package)) - .await?; - Ok(UserAction::DoNothing) - } else { - Ok(UserAction::DoNothing) - } - } - welcome_message::Payload::InvitationToJoin(invitation_to_join) => { - if is_steward || is_mls_group_initialized { - Ok(UserAction::DoNothing) - } else { - // Release the lock before calling join_group - drop(group); - - // Parse the MLS message to get the welcome - let (mls_in, _) = MlsMessageIn::tls_deserialize_bytes( - &invitation_to_join.mls_message_out_bytes, - )?; - - let welcome = match mls_in.extract() { - MlsMessageBodyIn::Welcome(welcome) => welcome, - _ => return Err(UserError::FailedToExtractWelcomeMessage), - }; - - if welcome.secrets().iter().any(|egs| { - let hash_ref = egs.new_member().as_slice().to_vec(); - self.identity.is_key_package_exists(&hash_ref) - }) { - self.join_group(welcome).await?; - let msg = self - .build_group_message( - "User joined to the group".as_bytes().to_vec(), - group_name, - ) - .await?; - Ok(UserAction::SendToWaku(msg)) - } else { - Ok(UserAction::DoNothing) - } - } - } - } - } else { - Err(UserError::EmptyWelcomeMessageError) - } - } - - /// Process messages from the application message subtopic. - /// - /// ## Parameters: - /// - `msg`: The Waku message to process - /// - `group_name`: The name of the group this message is for - /// - /// ## Returns: - /// - `UserAction` indicating what action should be taken - /// - /// ## Message Types Handled: - /// - **BatchProposalsMessage**: Batch proposals from steward - /// - **MLS Protocol Messages**: Encrypted group messages - /// - **Application Messages**: Various app-level messages - /// - /// ## Effects: - /// - Processes batch proposals and applies them to the group - /// - Handles MLS protocol messages through the group - /// - Routes consensus proposals and votes to appropriate handlers - /// - /// ## Preconditions: - /// - Group must be initialized with MLS group - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - /// - Various MLS processing errors - pub async fn process_app_subtopic( - &mut self, - msg: WakuMessage, - group_name: &str, - ) -> Result { - let group = { - let groups = self.groups.read().await; - groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - }; - - if !group.read().await.is_mls_group_initialized() { - return Ok(UserAction::DoNothing); - } - - // Try to parse as AppMessage first - // This one required for commit messages as they are sent as AppMessage - // without group encryption - if let Ok(app_message) = AppMessage::decode(msg.payload()) { - match app_message.payload { - Some(app_message::Payload::BatchProposalsMessage(batch_msg)) => { - info!( - "[user::process_app_subtopic]: Processing batch proposals message for group {group_name}" - ); - // Release the lock before calling self methods - return self - .process_batch_proposals_message(batch_msg, group_name) - .await; - } - _ => { - error!( - "[user::process_app_subtopic]: Cannot process another app message here: {:?}", - app_message.to_string() - ); - return Err(UserError::InvalidAppMessageType); - } - } - } - - // Fall back to MLS protocol message - let (mls_message_in, _) = MlsMessageIn::tls_deserialize_bytes(msg.payload())?; - let mls_message = mls_message_in.try_into_protocol_message()?; - - let res = { - info!("[user::process_app_subtopic]: processing encrypted protocol message"); - let groups = self.groups.read().await; - groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - .write() - .await - .process_protocol_msg(mls_message, &self.provider) - .await - }?; - - // Handle the result outside of any lock scope - match res { - GroupAction::GroupAppMsg(msg) => { - info!("[user::process_app_subtopic]: sending to app"); - Ok(UserAction::SendToApp(msg)) - } - GroupAction::LeaveGroup => { - info!("[user::process_app_subtopic]: leaving group"); - Ok(UserAction::LeaveGroup(group_name.to_string())) - } - GroupAction::DoNothing => { - info!("[user::process_app_subtopic]: doing nothing"); - Ok(UserAction::DoNothing) - } - GroupAction::GroupProposal(proposal) => { - info!("[user::process_app_subtopic]: processing consensus proposal"); - self.process_consensus_proposal(proposal, group_name).await - } - GroupAction::GroupVote(vote) => { - info!("[user::process_app_subtopic]: processing consensus vote"); - self.process_consensus_vote(vote, group_name).await - } - } - } - - /// Process batch proposals message from the steward. - /// - /// ## Parameters: - /// - `batch_msg`: The batch proposals message to process - /// - `group_name`: The name of the group these proposals are for - /// - /// ## Returns: - /// - `UserAction` indicating what action should be taken - /// - /// ## Effects: - /// - Processes all MLS proposals in the batch - /// - Applies the commit message to complete the group update - /// - Transitions group to Working state after successful processing - /// - /// ## State Requirements: - /// - Group must be in Waiting state to process batch proposals - /// - If not in correct state, stores proposals for later processing - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - /// - Various MLS processing errors - async fn process_batch_proposals_message( - &mut self, - batch_msg: BatchProposalsMessage, - group_name: &str, - ) -> Result { - // Get the group lock - let group = { - let groups = self.groups.read().await; - groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - }; - - let initial_state = group.read().await.get_state().await; - if initial_state != GroupState::Waiting { - info!( - "[user::process_batch_proposals_message]: Cannot process batch proposals in {initial_state} state, storing for later processing" - ); - // Store the batch proposals for later processing - self.store_pending_batch_proposals(group_name, batch_msg) - .await; - return Ok(UserAction::DoNothing); - } - - // Process all proposals before the commit - for proposal_bytes in batch_msg.mls_proposals { - let (mls_message_in, _) = MlsMessageIn::tls_deserialize_bytes(&proposal_bytes)?; - let protocol_message = mls_message_in.try_into_protocol_message()?; - - let _res = group - .write() - .await - .process_protocol_msg(protocol_message, &self.provider) - .await?; - } - - // Then process the commit message - let (mls_message_in, _) = MlsMessageIn::tls_deserialize_bytes(&batch_msg.commit_message)?; - let protocol_message = mls_message_in.try_into_protocol_message()?; - - let res = group - .write() - .await - .process_protocol_msg(protocol_message, &self.provider) - .await?; - - group.write().await.start_working().await; - - match res { - GroupAction::GroupAppMsg(msg) => Ok(UserAction::SendToApp(msg)), - GroupAction::LeaveGroup => Ok(UserAction::LeaveGroup(group_name.to_string())), - GroupAction::DoNothing => Ok(UserAction::DoNothing), - GroupAction::GroupProposal(proposal) => { - self.process_consensus_proposal(proposal, group_name).await - } - GroupAction::GroupVote(vote) => self.process_consensus_vote(vote, group_name).await, - } - } - - /// Process incoming Waku messages and route them to appropriate handlers. - /// - /// ## Parameters: - /// - `msg`: The Waku message to process - /// - /// ## Returns: - /// - `UserAction` indicating what action should be taken - /// - /// ## Message Routing: - /// - **Welcome Subtopic**: Routes to `process_welcome_subtopic()` - /// - **App Message Subtopic**: Routes to `process_app_subtopic()` - /// - **Unknown Topics**: Returns error - /// - /// ## Effects: - /// - Processes messages based on content topic - /// - Skips messages from the same app instance - /// - Routes to appropriate subtopic handlers - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - /// - `UserError::UnknownContentTopicType` for unsupported topics - /// - Various processing errors from subtopic handlers - pub async fn process_waku_message( - &mut self, - msg: WakuMessage, - ) -> Result { - let group_name = msg.content_topic.application_name.to_string(); - let group = { - let groups = self.groups.read().await; - groups - .get(&group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - }; - - if msg.meta == group.read().await.app_id() { - debug!("Message is from the same app, skipping"); - return Ok(UserAction::DoNothing); - } - - let ct_name = msg.content_topic.content_topic_name.to_string(); - match ct_name.as_str() { - WELCOME_SUBTOPIC => self.process_welcome_subtopic(msg, &group_name).await, - APP_MSG_SUBTOPIC => self.process_app_subtopic(msg, &group_name).await, - _ => Err(UserError::UnknownContentTopicType(ct_name)), - } - } - - /// Join a group after receiving a welcome message. - /// - /// ## Parameters: - /// - `welcome`: The MLS welcome message containing group information - /// - /// ## Effects: - /// - Creates new MLS group from welcome message - /// - Sets the MLS group in the user's group instance - /// - Updates group state to reflect successful joining - /// - /// ## Preconditions: - /// - Group must already exist in user's groups map - /// - Welcome message must be valid and contain proper group data - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - /// - Various MLS group creation errors - async fn join_group(&mut self, welcome: Welcome) -> Result<(), UserError> { - let group_config = MlsGroupJoinConfig::builder().build(); - let mls_group = - StagedWelcome::new_from_welcome(&self.provider, &group_config, welcome, None)? - .into_group(&self.provider)?; - - let group_id = mls_group.group_id().to_vec(); - let group_name = String::from_utf8(group_id)?; - - let groups = self.groups.read().await; - groups - .get(&group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - .write() - .await - .set_mls_group(mls_group)?; - - info!("[user::join_group]: User joined group {group_name}"); - Ok(()) - } - - /// Leave a group and clean up associated resources. - /// - /// ## Parameters: - /// - `group_name`: The name of the group to leave - /// - /// ## Effects: - /// - Removes group from user's groups map - /// - Cleans up all group-related resources - /// - /// ## Preconditions: - /// - Group must exist in user's groups map - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - pub async fn leave_group(&mut self, group_name: &str) -> Result<(), UserError> { - info!("[user::leave_group]: Leaving group {group_name}"); - if !self.if_group_exists(group_name).await { - return Err(UserError::GroupNotFoundError); - } - let mut groups = self.groups.write().await; - groups.remove(group_name); - Ok(()) - } - - /// Prepare a steward announcement message for a group. - /// - /// ## Parameters: - /// - `group_name`: The name of the group to prepare the message for - /// - /// ## Returns: - /// - Waku message containing the steward announcement - /// - /// ## Preconditions: - /// - Group must exist and be initialized - /// - /// ## Effects: - /// - Generates new steward announcement with refreshed keys - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - /// - Various steward message generation errors - pub async fn prepare_steward_msg( - &mut self, - group_name: &str, - ) -> Result { - let group = { - let groups = self.groups.read().await; - groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - }; - - let msg_to_send = group.write().await.generate_steward_message().await?; - Ok(msg_to_send) - } - - /// Build a group message for sending to the group. - /// - /// ## Parameters: - /// - `msg`: The message content as bytes - /// - `group_name`: The name of the group to send the message to - /// - /// ## Returns: - /// - Waku message ready for transmission - /// - /// ## Preconditions: - /// - Group must be initialized with MLS group - /// - /// ## Effects: - /// - Creates conversation message with sender identity - /// - Builds MLS message through the group - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - /// - `UserError::MlsGroupNotInitialized` if MLS group not initialized - /// - Various MLS message building errors - pub async fn build_group_message( - &mut self, - msg: Vec, - group_name: &str, - ) -> Result { - let group = { - let groups = self.groups.read().await; - groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - }; - - if !group.read().await.is_mls_group_initialized() { - return Err(UserError::MlsGroupNotInitialized); - } - - let app_msg = ConversationMessage { - message: msg, - sender: self.identity.identity_string(), - group_name: group_name.to_string(), - } - .into(); - - let msg_to_send = group - .write() - .await - .build_message(&self.provider, self.identity.signer(), &app_msg) - .await?; - - Ok(msg_to_send) - } - - /// Build a system message for sending to the group. - /// - /// ## Parameters: - /// - `system_message`: The message content as bytes - /// - `group_name`: The name of the group to send the message to - /// - /// ## Returns: - /// - Waku message ready for transmission - /// - /// ## Preconditions: - /// - Group must be initialized with MLS group - /// - /// ## Effects: - /// - Creates encrypted system message - /// - Builds MLS message through the group - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - /// - `UserError::MlsGroupNotInitialized` if MLS group not initialized - /// - Various MLS message building errors - pub async fn build_system_message( - &mut self, - system_message: Vec, - group_name: &str, - ) -> Result { - let group = { - let groups = self.groups.read().await; - groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - }; - - if !group.read().await.is_mls_group_initialized() { - return Err(UserError::MlsGroupNotInitialized); - } - - let app_msg = ConversationMessage { - message: system_message, - sender: "SYSTEM".to_string(), - group_name: group_name.to_string(), - } - .into(); - - let msg_to_send = group - .write() - .await - .build_message(&self.provider, self.identity.signer(), &app_msg) - .await?; - - Ok(msg_to_send) - } - - /// Build a special message (ban request/vote) preserving the original AppMessage structure. - /// - /// ## Parameters: - /// - `app_message`: The application message to build - /// - `group_name`: The name of the group to send the message to - /// - /// ## Returns: - /// - Waku message ready for transmission - /// - /// ## Preconditions: - /// - Group must be initialized with MLS group - /// - /// ## Effects: - /// - Preserves original AppMessage structure without wrapping - /// - Builds MLS message through the group - /// - /// ## Usage: - /// Used for consensus-related messages like proposals and votes - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - /// - `UserError::MlsGroupNotInitialized` if MLS group not initialized - /// - Various MLS message building errors - pub async fn build_changer_message( - &mut self, - app_message: AppMessage, - group_name: &str, - ) -> Result { - let group = { - let groups = self.groups.read().await; - groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - }; - - if !group.read().await.is_mls_group_initialized() { - return Err(UserError::MlsGroupNotInitialized); - } - - let msg_to_send = group - .write() - .await - .build_message(&self.provider, self.identity.signer(), &app_message) - .await?; - - Ok(msg_to_send) - } - - /// Get the identity string for debugging and identification purposes. - /// - /// ## Returns: - /// - String representation of the user's identity - /// - /// ## Usage: - /// Primarily used for debugging, logging, and user identification - pub fn identity_string(&self) -> String { - self.identity.identity_string() - } - - /// Apply proposals for the given group, returning the batch message(s). - /// - /// ## Parameters: - /// - `group_name`: The name of the group to apply proposals for - /// - /// ## Returns: - /// - Vector of Waku messages containing batch proposals and welcome messages - /// - /// ## Preconditions: - /// - Group must be initialized with MLS group - /// - User must be steward for the group - /// - /// ## Effects: - /// - Creates MLS proposals for all pending group updates - /// - Commits all proposals to the MLS group - /// - Generates batch proposals message and welcome message if needed - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - /// - `UserError::MlsGroupNotInitialized` if MLS group not initialized - /// - Various MLS proposal creation errors - pub async fn apply_proposals( - &mut self, - group_name: &str, - ) -> Result, UserError> { - let group = { - let groups = self.groups.read().await; - groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - }; - - if !group.read().await.is_mls_group_initialized() { - return Err(UserError::MlsGroupNotInitialized); - } - - let messages = group - .write() - .await - .create_batch_proposals_message(&self.provider, self.identity.signer()) - .await?; - info!("[user::apply_proposals]: Applied proposals for group {group_name}"); - Ok(messages) - } - - /// Start a new steward epoch for the given group. - /// - /// ## Parameters: - /// - `group_name`: The name of the group to start steward epoch for - /// - /// ## Returns: - /// - Number of proposals that will be voted on (0 if no proposals) - /// - /// ## Effects: - /// - Starts steward epoch through the group's state machine - /// - Collects proposals for voting - /// - Transitions group to appropriate state based on proposal count - /// - /// ## State Transitions: - /// - **With proposals**: Working → Waiting (returns proposal count) - /// - **No proposals**: Working → Working (stays in Working, returns 0) - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - /// - Various state machine errors - pub async fn start_steward_epoch(&mut self, group_name: &str) -> Result { - let groups = self.groups.read().await; - let proposal_count = groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - .write() - .await - .start_steward_epoch_with_validation() - .await?; - - if proposal_count == 0 { - info!("[user::start_steward_epoch]: No proposals to vote on, skipping steward epoch"); - } else { - info!("[user::start_steward_epoch]: Started steward epoch with {proposal_count} proposals"); - } - - Ok(proposal_count) - } - - /// Start voting for the given group, returning the proposal ID. - /// - /// ## Parameters: - /// - `group_name`: The name of the group to start voting for - /// - /// ## Returns: - /// - Tuple of (proposal_id, UserAction) for steward actions - /// - /// ## Effects: - /// - Starts voting phase in the group - /// - Creates consensus proposal for voting - /// - Sends voting proposal to frontend - /// - /// ## State Transitions: - /// - **Waiting → Voting**: If proposals found and steward starts voting - /// - **Waiting → Working**: If no proposals found (edge case fix) - /// - /// ## Edge Case Handling: - /// If no proposals are found during voting phase (rare edge case where proposals - /// disappear between epoch start and voting), transitions back to Working state - /// to prevent getting stuck in Waiting state. - /// - /// ## Preconditions: - /// - User must be steward for the group - /// - Group must have proposals in voting epoch - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - /// - `UserError::NoProposalsFound` if no proposals exist - /// - Various consensus service errors - pub async fn get_proposals_for_steward_voting( - &mut self, - group_name: &str, - ) -> Result<(u32, UserAction), UserError> { - info!( - "[user::get_proposals_for_steward_voting]: Getting proposals for steward voting in group {group_name}" - ); - - let group = { - let groups = self.groups.read().await; - groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - }; - - // If this is the steward, create proposal with vote and send to group - if group.read().await.is_steward().await { - let proposals = group.read().await.get_proposals_for_voting_epoch().await; - if !proposals.is_empty() { - group.write().await.start_voting().await?; - - // Get group members for expected voters count - let members = group.read().await.members_identity().await?; - let participant_ids: Vec> = members.into_iter().collect(); - let expected_voters_count = participant_ids.len() as u32; - - // Create consensus proposal - let proposal = self - .consensus_service - .create_proposal( - group_name, - "Group Update Proposal".to_string(), - proposals.iter().map(|p| p.to_string()).collect(), - self.identity.identity_string().into(), - expected_voters_count, - 3600, // 1 hour expiration - true, // liveness criteria - ) - .await?; - - info!( - "[user::get_proposals_for_steward_voting]: Created consensus proposal with ID {} and {} expected voters", - proposal.proposal_id, expected_voters_count - ); - - // Send voting proposal to frontend - let voting_proposal: AppMessage = VotingProposal { - proposal_id: proposal.proposal_id, - payload: proposal.payload, - group_name: group_name.to_string(), - } - .into(); - - Ok((proposal.proposal_id, UserAction::SendToApp(voting_proposal))) - } else { - error!("[user::get_proposals_for_steward_voting]: No proposals found"); - Err(UserError::NoProposalsFound) - } - } else { - // Not steward, do nothing - info!("[user::get_proposals_for_steward_voting]: Not steward, doing nothing"); - Ok((0, UserAction::DoNothing)) - } - } - - /// Add a remove proposal to the steward for the given group. - /// - /// ## Parameters: - /// - `group_name`: The name of the group to add the proposal to - /// - `identity`: The identity string of the member to remove - /// - /// ## Effects: - /// - Stores remove proposal in the group's steward queue - /// - Proposal will be processed in the next steward epoch - /// - /// ## Preconditions: - /// - Group must exist - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - /// - Various proposal storage errors - pub async fn add_remove_proposal( - &mut self, - group_name: &str, - identity: String, - ) -> Result<(), UserError> { - let groups = self.groups.read().await; - groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - .write() - .await - .store_remove_proposal(identity) - .await?; - Ok(()) - } - - /// Handle consensus result after it's determined. - /// - /// ## Parameters: - /// - `group_name`: The name of the group the consensus is for - /// - `vote_result`: Whether the consensus passed (true) or failed (false) - /// - /// ## Returns: - /// - Vector of Waku messages to send (if any) - /// - /// ## State Transitions: - /// **Steward:** - /// - **Vote YES**: Voting → ConsensusReached → Waiting → Working (creates and sends batch proposals, then applies them) - /// - **Vote NO**: Voting → Working (discards proposals) - /// - /// **Non-Steward:** - /// - **Vote YES**: Voting → ConsensusReached → Waiting → Working (waits for consensus + batch proposals, then applies them) - /// - **Vote NO**: Voting → Working (no proposals to apply) - /// - /// ## Effects: - /// - Completes voting in the group - /// - Handles proposal application or cleanup based on result - /// - Manages state transitions for both steward and non-steward users - /// - Processes pending batch proposals if available - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - /// - Various state machine and proposal processing errors - async fn handle_consensus_result( - &mut self, - group_name: &str, - vote_result: bool, - ) -> Result, UserError> { - let group = { - let groups = self.groups.read().await; - groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - }; - - group.write().await.complete_voting(vote_result).await?; - - // Handle vote result based on steward status - if group.read().await.is_steward().await { - if vote_result { - // Vote YES: Apply proposals and send commit messages - info!("[user::complete_voting_for_steward]: Vote YES, sending commit message"); - - // Apply proposals and complete (state must be ConsensusReached for this) - let messages = self.apply_proposals(group_name).await?; - group.write().await.handle_yes_vote().await?; - Ok(messages) - } else { - // Vote NO: Empty proposal queue without applying, no commit messages - info!("[user::complete_voting_for_steward]: Vote NO, emptying proposal queue without applying"); - - // Empty proposals without state requirement (direct steward call) - group.write().await.handle_no_vote().await?; - - Ok(vec![]) - } - } else if vote_result { - // Vote YES: Transition to ConsensusReached state to await batch proposals - group.write().await.start_consensus_reached().await; - info!("[user::handle_consensus_result]: Non-steward user transitioning to ConsensusReached state to await batch proposals"); - - // Now transition to Waiting state to follow complete state machine flow - group.write().await.start_waiting().await; - info!("[user::handle_consensus_result]: Non-steward user transitioning to Waiting state to await batch proposals"); - - // Check if there are pending batch proposals that can now be processed - if self.has_pending_batch_proposals(group_name).await { - info!("[user::handle_consensus_result]: Non-steward user has pending batch proposals, processing them now"); - let action = self.process_pending_batch_proposals(group_name).await?; - info!("[user::handle_consensus_result]: Successfully processed pending batch proposals"); - if let Some(action) = action { - match action { - UserAction::SendToWaku(waku_message) => { - info!( - "[user::handle_consensus_result]: Sending waku message to backend" - ); - Ok(vec![waku_message]) - } - UserAction::LeaveGroup(group_name) => { - self.leave_group(group_name.as_str()).await?; - info!("[user::handle_consensus_result]: Non-steward user left group {group_name}"); - Ok(vec![]) - } - UserAction::DoNothing => { - info!("[user::handle_consensus_result]: No action to process"); - Ok(vec![]) - } - _ => { - error!("[user::handle_consensus_result]: Invalid action to process"); - Err(UserError::InvalidUserAction(action.to_string())) - } - } - } else { - info!("[user::handle_consensus_result]: No action to process"); - Ok(vec![]) - } - } else { - info!("[user::handle_consensus_result]: No pending batch proposals to process"); - Ok(vec![]) - } - } else { - // Vote NO: Transition to Working state - group.write().await.start_working().await; - info!("[user::handle_consensus_result]: Non-steward user transitioning to Working state after failed vote"); - Ok(vec![]) - } - } - - /// Handle incoming consensus events and return commit messages if needed. - /// - /// ## Parameters: - /// - `group_name`: The name of the group the consensus event is for - /// - `event`: The consensus event to handle - /// - /// ## Returns: - /// - Vector of Waku messages to send (if any) - /// - /// ## Event Types Handled: - /// - **ConsensusReached**: Handles successful consensus with result - /// - **ConsensusFailed**: Handles consensus failure with liveness criteria - /// - /// ## Effects: - /// - Routes consensus events to appropriate handlers - /// - Manages state transitions based on consensus results - /// - Applies liveness criteria for failed consensus - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - /// - `UserError::InvalidGroupState` if group is in invalid state - /// - Various consensus handling errors - pub async fn handle_consensus_event( - &mut self, - group_name: &str, - event: ConsensusEvent, - ) -> Result, UserError> { - match event { - ConsensusEvent::ConsensusReached { - proposal_id, - result, - } => { - info!( - "[user::handle_consensus_event]: Consensus reached for proposal {proposal_id} in group {group_name}: {result}" - ); - - let group = { - let groups = self.groups.read().await; - groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - }; - - let current_state = group.read().await.get_state().await; - info!("[user::handle_consensus_event]: Current state: {:?} for proposal {proposal_id}", current_state); - - // Handle the consensus result and return commit messages - let messages = self.handle_consensus_result(group_name, result).await?; - Ok(messages) - } - ConsensusEvent::ConsensusFailed { - proposal_id, - reason, - } => { - info!( - "[user::handle_consensus_event]: Consensus failed for proposal {proposal_id} in group {group_name}: {reason}" - ); - - let group = { - let groups = self.groups.read().await; - groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - }; - - let current_state = group.read().await.get_state().await; - - info!("[user::handle_consensus_event]: Handling consensus failure in {:?} state for proposal {proposal_id}", current_state); - - // Handle consensus failure based on current state - match current_state { - GroupState::Voting => { - // If we're in Voting state, complete voting with liveness criteria - // Get liveness criteria from the actual proposal - let liveness_result = self - .consensus_service - .get_proposal_liveness_criteria(group_name, proposal_id) - .await - .unwrap_or(false); // Default to false if proposal not found - - info!("Applying liveness criteria for failed proposal {proposal_id}: {liveness_result}"); - let messages = self - .handle_consensus_result(group_name, liveness_result) - .await?; - Ok(messages) - } - _ => Err(UserError::InvalidGroupState(current_state.to_string())), - } - } - } - } - - /// Process incoming consensus proposal. - /// - /// ## Parameters: - /// - `proposal`: The consensus proposal to process - /// - `group_name`: The name of the group the proposal is for - /// - /// ## Returns: - /// - `UserAction` indicating what action should be taken - /// - /// ## Effects: - /// - Stores proposal in consensus service - /// - Starts voting phase in the group - /// - Creates voting proposal for frontend - /// - /// ## State Transitions: - /// - Any state → Voting (starts voting phase) - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - /// - Various consensus service errors - pub async fn process_consensus_proposal( - &mut self, - proposal: Proposal, - group_name: &str, - ) -> Result { - self.consensus_service - .process_incoming_proposal(group_name, proposal.clone()) - .await?; - - let group = { - let groups = self.groups.read().await; - groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - }; - - group.write().await.start_voting().await?; - info!( - "[user::process_consensus_proposal]: Starting voting for proposal {}", - proposal.proposal_id - ); - - // Send voting proposal to frontend - let voting_proposal: AppMessage = VotingProposal { - proposal_id: proposal.proposal_id, - payload: proposal.payload.clone(), - group_name: group_name.to_string(), - } - .into(); - - Ok(UserAction::SendToApp(voting_proposal)) - } - - /// Process user vote from frontend. - /// - /// ## Parameters: - /// - `proposal_id`: The ID of the proposal to vote on - /// - `user_vote`: The user's vote (true for yes, false for no) - /// - `group_name`: The name of the group the vote is for - /// - /// ## Returns: - /// - `UserAction` indicating what action should be taken - /// - /// ## Effects: - /// - For stewards: Creates consensus vote and sends to group - /// - For regular users: Processes user vote in consensus service - /// - Builds and sends appropriate message to group - /// - /// ## Message Types: - /// - **Steward**: Sends consensus vote message - /// - **Regular User**: Sends user vote message - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - /// - Various consensus service and message building errors - pub async fn process_user_vote( - &mut self, - proposal_id: u32, - user_vote: bool, - group_name: &str, - ) -> Result { - let group = { - let groups = self.groups.read().await; - groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - }; - let app_message = if group.read().await.is_steward().await { - info!( - "[user::process_user_vote]: Steward voting for proposal {proposal_id} in group {group_name}" - ); - let proposal = self - .consensus_service - .vote_on_proposal(group_name, proposal_id, user_vote, self.eth_signer.clone()) - .await?; - proposal.into() - } else { - info!( - "[user::process_user_vote]: User voting for proposal {proposal_id} in group {group_name}" - ); - let vote = self - .consensus_service - .process_user_vote(group_name, proposal_id, user_vote, self.eth_signer.clone()) - .await?; - vote.into() - }; - - let waku_msg = self.build_changer_message(app_message, group_name).await?; - - Ok(UserAction::SendToWaku(waku_msg)) - } - - /// Process incoming consensus vote and handle immediate state transitions. - /// - /// ## Parameters: - /// - `vote`: The consensus vote to process - /// - `group_name`: The name of the group the vote is for - /// - /// ## Returns: - /// - `UserAction` indicating what action should be taken - /// - /// ## Effects: - /// - Stores vote in consensus service - /// - Handles immediate state transitions if consensus is reached - /// - /// ## State Transitions: - /// When consensus is reached immediately after processing a vote: - /// - **Vote YES**: Non-steward transitions to Waiting state to await batch proposals - /// - **Vote NO**: Non-steward transitions to Working state immediately - /// - **Steward**: Relies on event-driven system for full proposal management - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - /// - Various consensus service errors - async fn process_consensus_vote( - &mut self, - vote: Vote, - group_name: &str, - ) -> Result { - self.consensus_service - .process_incoming_vote(group_name, vote.clone()) - .await?; - - Ok(UserAction::DoNothing) - } - - /// Process incoming ban request. - /// - /// ## Parameters: - /// - `ban_request`: The ban request to process - /// - `group_name`: The name of the group the ban request is for - /// - /// ## Returns: - /// - Waku message to send to the group - /// - /// ## Effects: - /// - **For stewards**: Adds remove proposal to steward queue and sends system message - /// - **For regular users**: Forwards ban request to the group - /// - /// ## Message Types: - /// - **Steward**: Sends system message about proposal addition - /// - **Regular User**: Sends ban request message to group - /// - /// ## Errors: - /// - `UserError::GroupNotFoundError` if group doesn't exist - /// - Various message building errors - pub async fn process_ban_request( - &mut self, - ban_request: BanRequest, - group_name: &str, - ) -> Result { - let user_to_ban = ban_request.user_to_ban.clone(); - info!( - "[user::process_ban_request]: Processing ban request for user {user_to_ban} in group {group_name}" - ); - - let group = { - let groups = self.groups.read().await; - groups - .get(group_name) - .cloned() - .ok_or_else(|| UserError::GroupNotFoundError)? - }; - - let is_steward = { - let group = group.read().await; - group.is_steward().await - }; - if is_steward { - // Steward: add the remove proposal to the queue - info!( - "[user::process_ban_request]: Steward adding remove proposal for user {user_to_ban}" - ); - self.add_remove_proposal(group_name, user_to_ban.to_string()) - .await?; - - let msg_to_send = self - .build_system_message( - format!("Remove proposal for user {user_to_ban} added to steward queue") - .into_bytes(), - group_name, - ) - .await?; - - Ok(msg_to_send) - } else { - // Regular user: send the ban request to the group - let updated_ban_request = BanRequest { - user_to_ban: ban_request.user_to_ban, - requester: self.identity_string(), - group_name: ban_request.group_name, - }; - self.build_changer_message(updated_ban_request.into(), group_name) - .await - } - } - - /// Store batch proposals for later processing when state becomes correct. - /// - /// ## Parameters: - /// - `group_name`: The name of the group to store proposals for - /// - `batch_msg`: The batch proposals message to store - /// - /// ## Effects: - /// - Stores batch proposals in pending queue for later processing - /// - Used when proposals arrive before group is in correct state - /// - /// ## Usage: - /// Called when batch proposals cannot be processed immediately - /// due to incorrect group state - async fn store_pending_batch_proposals( - &self, - group_name: &str, - batch_msg: BatchProposalsMessage, - ) { - let mut pending = self.pending_batch_proposals.write().await; - pending.insert(group_name.to_string(), batch_msg); - info!( - "[user::store_pending_batch_proposals]: Stored batch proposals for group {} to be processed later", - group_name - ); - } - - /// Check if there are pending batch proposals for a group. - /// - /// ## Parameters: - /// - `group_name`: The name of the group to check - /// - /// ## Returns: - /// - `true` if pending proposals exist, `false` otherwise - async fn has_pending_batch_proposals(&self, group_name: &str) -> bool { - let pending = self.pending_batch_proposals.read().await; - pending.contains_key(group_name) - } - - /// Retrieve and remove pending batch proposals for a group. - /// - /// ## Parameters: - /// - `group_name`: The name of the group to retrieve proposals for - /// - /// ## Returns: - /// - `Some(BatchProposalsMessage)` if proposals exist, `None` otherwise - /// - /// ## Effects: - /// - Removes proposals from pending queue after retrieval - async fn retrieve_pending_batch_proposals( - &self, - group_name: &str, - ) -> Option { - let mut pending = self.pending_batch_proposals.write().await; - pending.remove(group_name) - } - - /// Process any pending batch proposals that can now be processed. - /// - /// ## Parameters: - /// - `group_name`: The name of the group to process proposals for - /// - /// ## Returns: - /// - `Some(UserAction)` if proposals were processed, `None` otherwise - /// - /// ## Effects: - /// - Processes any stored batch proposals that were waiting for correct state - /// - Removes processed proposals from pending queue - /// - /// ## Usage: - /// Called when group state changes to allow processing of previously stored proposals - async fn process_pending_batch_proposals( - &mut self, - group_name: &str, - ) -> Result, UserError> { - if self.has_pending_batch_proposals(group_name).await { - if let Some(batch_msg) = self.retrieve_pending_batch_proposals(group_name).await { - info!( - "[user::process_pending_batch_proposals]: Processing pending batch proposals for group {}", - group_name - ); - let action = self - .process_batch_proposals_message(batch_msg, group_name) - .await?; - Ok(Some(action)) - } else { - Ok(None) - } - } else { - Ok(None) - } - } -} - -impl LocalSigner for PrivateKeySigner { - async fn local_sign_message(&self, message: &[u8]) -> Result, anyhow::Error> { - let signature = self.sign_message(message).await?; - let signature_bytes = signature.as_bytes().to_vec(); - Ok(signature_bytes) - } - - fn address(&self) -> Address { - self.address() - } - - fn address_string(&self) -> String { - self.address().to_string() - } - - fn address_bytes(&self) -> Vec { - self.address().as_slice().to_vec() - } -} diff --git a/src/user/README.md b/src/user/README.md new file mode 100644 index 0000000..9b60ec8 --- /dev/null +++ b/src/user/README.md @@ -0,0 +1,133 @@ +# User Module + +The `user` module encapsulates everything the desktop gateway needs to manage a single MLS participant. +It owns the MLS identity, tracks the local view of every group the user joined, +drives steward epochs, and bridges consensus messages between the core runtime, Waku network, and UI. + +## Directory Layout + +``` bash +src/user/ +├── consensus.rs # Voting lifecycle, consensus event fan-in/out +├── groups.rs # Group creation/join/leave utilities and state queries +├── messaging.rs # Application-level messaging helpers (ban requests, signing) +├── mod.rs # `User` actor definition and shared types +├── proposals.rs # Steward batch proposal processing and pending queues +├── README.md # This file +├── steward.rs # Steward-only helpers: epochs, proposals, apply flow +└── waku.rs # Waku ingestion + routing to per-topic handlers +``` + +Each file extends `impl User` with domain-specific responsibilities, +keeping the top-level actor (`mod.rs`) lean. + +## Core Concepts + +- **`User` actor** – Holds one `Identity`, an `MlsProvider`, + a map of per-group `Arc>`, the consensus facade, and the Ethereum signer. + +- **`UserAction` enum (`mod.rs`)** – Return type for most handlers. + Signals what the caller (gateway) should do next: + - `SendToWaku(WakuMessageToSend)` + - `SendToApp(AppMessage)` + - `LeaveGroup(String)` (triggers UI + cleanup) + - `DoNothing` (no side effects) + +- **Per-group concurrency** – Keeps a `HashMap>>`. + Each group has its own `RwLock`, allowing independent state transitions without blocking unrelated groups. + +- **Pending batches (`proposals.rs`)** – Non-steward clients may receive `BatchProposalsMessage` while + still catching up to state `Waiting`. + `PendingBatches` stores the payload until the group is ready to apply it. + +## Lifecycle Overview + +1. **Login** – The gateway calls `User::new` with a private key. + The actor derives an MLS identity (`mls_crypto::Identity`) + and keeps an `alloy::PrivateKeySigner` for consensus signatures. + +2. **Group discovery** – `groups.rs` exposes helpers: + - `create_group` – Either launches a steward-owned MLS group or prepares a placeholder for later joining. + - `join_group` – Applies a received `Welcome` message to hydrate the MLS state. + - `get_group_members` / `get_group_state` / `leave_group` – Used by the UI for metadata and teardown. + +3. **Message routing** – Incoming Waku traffic is delivered to `waku.rs::process_waku_message`, which: + - Filters out self-originated packets (matching `msg.meta` to the group `app_id`). + - Routes `WELCOME_SUBTOPIC` payloads to `process_welcome_subtopic`. + - Routes `APP_MSG_SUBTOPIC` payloads to `process_app_subtopic`. + - Converts MLS protocol messages into `GroupAction`s, + mapping them back into `UserAction`s so the gateway can forward data to the UI or back onto Waku. + +4. **Consensus bridge** – `consensus.rs` is the glue to `ConsensusService`: + - `process_consensus_proposal` and `process_consensus_vote` persist incoming messages and + transition group state. + - `process_user_vote` produces either consensus votes (steward) or user votes (regular members) + and wraps them for Waku transmission. + - `handle_consensus_event` reacts to events emitted by the service, + ensuring the MLS state machine aligns with voting outcomes. + It reuses `handle_consensus_result` to collapse steward/non-steward flows + and trigger follow-up actions (apply proposals, queue batch data, or leave the group). + +5. **Steward duties** – `steward.rs` layers steward-only APIs: + - Introspection helpers (`is_user_steward_for_group`, `get_current_epoch_proposals`, etc.). + - Epoch management (`start_steward_epoch`, `get_proposals_for_steward_voting`), + which kicks the MLS state machine into `Waiting` / `Voting`. + - Proposal actions (`add_remove_proposal`, `apply_proposals`) that serialize MLS proposals, + commits, and optional welcomes into `WakuMessageToSend` objects for broadcast. + +6. **Application-facing messaging** – `messaging.rs` contains: + - `build_group_message` – Wraps an `AppMessage` in MLS encryption for the target group. + - `process_ban_request` – Normalizes addresses, routes steward vs. member behavior + (queueing steward removal proposals or forwarding the request back to the group). + - An implementation of `LocalSigner` for `PrivateKeySigner`, + allowing consensus code to request signatures uniformly. + +7. **Proposal batches** – `proposals.rs` handles the post-consensus MLS churn: + - `process_batch_proposals_message` – Applies proposals, deserializes MLS commits, + and emits the resulting `UserAction`. + - `process_stored_batch_proposals` – Replays a deferred batch once the group transitions into `Waiting`. + +## Waku Topics & Message Types + +| Subtopic | Handler | Purpose | +|---------------------|-------------------------------|---------| +| `WELCOME_SUBTOPIC` | `process_welcome_subtopic` | Steward announcements, encrypted key packages, welcome messages | +| `APP_MSG_SUBTOPIC` | `process_app_subtopic` | Batch proposals, encrypted MLS traffic, consensus proposals/votes | + +`process_welcome_subtopic` contains the joining handshake logic: + +- **GroupAnnouncement** → Non-stewards encrypt and send their key package back. +- **UserKeyPackage** → Stewards decrypt, store invite proposals, and notify the UI. +- **InvitationToJoin** → Non-stewards validate, call `join_group`, and broadcast a system chat message. + +## State Machine Touch Points + +Although the primary MLS state machine lives in `crate::state_machine`, the user module coordinates transitions by calling: + +- `start_steward_epoch_with_validation` +- `start_voting`, `complete_voting`, `handle_yes_vote`, `handle_no_vote` +- `start_waiting`, `start_consensus_reached`, `start_working` + +This ensures both steward and non-steward clients converge on the same `GroupState` after each consensus result or batch commit. + +## Extending the Module + +When adding new functionality: + +1. Decide whether the behavior is steward-specific, consensus-related, +or generic per-group logic, then extend the appropriate file. +Keeping concerns separated avoids monolithic impl blocks. +2. Return a `UserAction` so the caller (gateway) can decide where to forward the outcome. +3. Prefer reusing `group_ref(group_name)` to fetch the per-group lock; release the lock (`drop`) before performing long-running work to avoid deadlocks. +4. If new Waku payloads are introduced, update `process_waku_message` with a deterministic routing branch and ensure the UI/gateway understands the resulting `AppMessage`. + +## Related Tests + +Integration scenarios that exercise this module live under: + +- `tests/user_test.rs` +- `tests/consensus_realtime_test.rs` +- `tests/state_machine_test.rs` +- `tests/consensus_multi_group_test.rs` + +These are good references when updating state transitions or consensus flows. diff --git a/src/user/consensus.rs b/src/user/consensus.rs new file mode 100644 index 0000000..9632e54 --- /dev/null +++ b/src/user/consensus.rs @@ -0,0 +1,355 @@ +use tracing::{error, info}; + +use ds::waku_actor::WakuMessageToSend; + +use crate::{ + consensus::ConsensusEvent, + error::UserError, + protos::{ + consensus::v1::{Proposal, Vote, VotePayload}, + de_mls::messages::v1::AppMessage, + }, + state_machine::GroupState, + user::{User, UserAction}, +}; + +impl User { + pub async fn set_up_consensus_threshold_for_group( + &mut self, + group_name: &str, + proposal_id: u32, + consensus_threshold: f64, + ) -> Result<(), UserError> { + self.consensus_service + .set_consensus_threshold_for_group_session(group_name, proposal_id, consensus_threshold) + .await?; + Ok(()) + } + /// Handle consensus result after it's determined. + /// + /// ## Parameters: + /// - `group_name`: The name of the group the consensus is for + /// - `vote_result`: Whether the consensus passed (true) or failed (false) + /// + /// ## Returns: + /// - Vector of Waku messages to send (if any) + /// + /// ## State Transitions: + /// **Steward:** + /// - **Vote YES**: Voting → ConsensusReached → Waiting → Working (creates and sends batch proposals, then applies them) + /// - **Vote NO**: Voting → Working (discards proposals) + /// + /// **Non-Steward:** + /// - **Vote YES**: Voting → ConsensusReached → Waiting → Working (waits for consensus + batch proposals, then applies them) + /// - **Vote NO**: Voting → Working (no proposals to apply) + /// + /// ## Effects: + /// - Completes voting in the group + /// - Handles proposal application or cleanup based on result + /// - Manages state transitions for both steward and non-steward users + /// - Processes pending batch proposals if available + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if group doesn't exist + /// - Various state machine and proposal processing errors + async fn handle_consensus_result( + &mut self, + group_name: &str, + vote_result: bool, + ) -> Result, UserError> { + let group = self.group_ref(group_name).await?; + group.write().await.complete_voting(vote_result).await?; + + // Handle vote result based on steward status + if group.read().await.is_steward().await { + if vote_result { + // Vote YES: Apply proposals and send commit messages + info!("[handle_consensus_result]: Vote YES, sending commit message"); + + // Apply proposals and complete (state must be ConsensusReached for this) + let messages = self.apply_proposals(group_name).await?; + group.write().await.handle_yes_vote().await?; + Ok(messages) + } else { + // Vote NO: Empty proposal queue without applying, no commit messages + info!( + "[handle_consensus_result]: Vote NO, emptying proposal queue without applying" + ); + + // Empty proposals without state requirement (direct steward call) + group.write().await.handle_no_vote().await?; + + Ok(vec![]) + } + } else if vote_result { + // Vote YES: Group already moved to ConsensusReached during complete_voting; transition to Waiting + { + let mut group_guard = group.write().await; + group_guard.start_waiting_after_consensus().await?; + } + info!("[handle_consensus_result]: Non-steward user transitioning to Waiting state to await batch proposals"); + + // Check if there are pending batch proposals that can now be processed + if self.pending_batch_proposals.contains(group_name).await { + info!("[handle_consensus_result]: Non-steward user has pending batch proposals, processing them now"); + let action = self.process_stored_batch_proposals(group_name).await?; + info!("[handle_consensus_result]: Successfully processed pending batch proposals"); + if let Some(action) = action { + match action { + UserAction::SendToWaku(waku_message) => { + info!("[handle_consensus_result]: Sending waku message to backend"); + Ok(vec![waku_message]) + } + UserAction::LeaveGroup(group_name) => { + self.leave_group(group_name.as_str()).await?; + info!("[handle_consensus_result]: Non-steward user left group {group_name}"); + Ok(vec![]) + } + UserAction::DoNothing => { + info!("[handle_consensus_result]: No action to process"); + Ok(vec![]) + } + _ => { + error!("[handle_consensus_result]: Invalid action to process"); + Err(UserError::InvalidUserAction(action.to_string())) + } + } + } else { + info!("[handle_consensus_result]: No action to process"); + Ok(vec![]) + } + } else { + info!("[handle_consensus_result]: No pending batch proposals to process"); + Ok(vec![]) + } + } else { + // Vote NO: Transition to Working state + group.write().await.start_working().await; + info!("[handle_consensus_result]: Non-steward user transitioning to Working state after failed vote"); + Ok(vec![]) + } + } + + /// Handle incoming consensus events and return commit messages if needed. + /// + /// ## Parameters: + /// - `group_name`: The name of the group the consensus event is for + /// - `event`: The consensus event to handle + /// + /// ## Returns: + /// - Vector of Waku messages to send (if any) + /// + /// ## Event Types Handled: + /// - **ConsensusReached**: Handles successful consensus with result + /// - **ConsensusFailed**: Handles consensus failure with liveness criteria + /// + /// ## Effects: + /// - Routes consensus events to appropriate handlers + /// - Manages state transitions based on consensus results + /// - Applies liveness criteria for failed consensus + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if group doesn't exist + /// - `UserError::InvalidGroupState` if group is in invalid state + /// - Various consensus handling errors + pub async fn handle_consensus_event( + &mut self, + group_name: &str, + event: ConsensusEvent, + ) -> Result, UserError> { + match event { + ConsensusEvent::ConsensusReached { + proposal_id, + result, + } => { + info!( + "[handle_consensus_event]: Consensus reached for proposal {proposal_id} in group {group_name}: {result}" + ); + + let group = self.group_ref(group_name).await?; + + let current_state = group.read().await.get_state().await; + info!( + "[handle_consensus_event]: Current state: {:?} for proposal {proposal_id}", + current_state + ); + + // Handle the consensus result and return commit messages + let messages = self.handle_consensus_result(group_name, result).await?; + Ok(messages) + } + ConsensusEvent::ConsensusFailed { + proposal_id, + reason, + } => { + info!( + "[handle_consensus_event]: Consensus failed for proposal {proposal_id} in group {group_name}: {reason}" + ); + + let group = self.group_ref(group_name).await?; + + let current_state = group.read().await.get_state().await; + + info!("[handle_consensus_event]: Handling consensus failure in {:?} state for proposal {proposal_id}", current_state); + + // Handle consensus failure based on current state + match current_state { + GroupState::Voting => { + // If we're in Voting state, complete voting with liveness criteria + // Get liveness criteria from the actual proposal + let liveness_result = self + .consensus_service + .get_proposal_liveness_criteria(group_name, proposal_id) + .await + .unwrap_or(false); // Default to false if proposal not found + + info!("[handle_consensus_result]:Applying liveness criteria for failed proposal {proposal_id}: {liveness_result}"); + let messages = self + .handle_consensus_result(group_name, liveness_result) + .await?; + Ok(messages) + } + _ => Err(UserError::InvalidGroupState(current_state.to_string())), + } + } + } + } + + /// Process incoming consensus proposal. + /// + /// ## Parameters: + /// - `proposal`: The consensus proposal to process + /// - `group_name`: The name of the group the proposal is for + /// + /// ## Returns: + /// - `UserAction` indicating what action should be taken + /// + /// ## Effects: + /// - Stores proposal in consensus service + /// - Starts voting phase in the group + /// - Creates voting proposal for frontend + /// + /// ## State Transitions: + /// - Any state → Voting (starts voting phase) + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if group doesn't exist + /// - Various consensus service errors + pub async fn process_consensus_proposal( + &mut self, + proposal: Proposal, + group_name: &str, + ) -> Result { + self.consensus_service + .process_incoming_proposal(group_name, proposal.clone()) + .await?; + + let group = self.group_ref(group_name).await?; + group.write().await.start_voting().await?; + info!( + "[process_consensus_proposal]: Starting voting for proposal {}", + proposal.proposal_id + ); + + // Send voting proposal to frontend + let voting_proposal: AppMessage = VotePayload { + group_id: group_name.to_string(), + proposal_id: proposal.proposal_id, + group_requests: proposal.group_requests.clone(), + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_secs(), + } + .into(); + + Ok(UserAction::SendToApp(voting_proposal)) + } + + /// Process user vote from frontend. + /// + /// ## Parameters: + /// - `proposal_id`: The ID of the proposal to vote on + /// - `user_vote`: The user's vote (true for yes, false for no) + /// - `group_name`: The name of the group the vote is for + /// + /// ## Returns: + /// - `UserAction` indicating what action should be taken + /// + /// ## Effects: + /// - For stewards: Creates consensus vote and sends to group + /// - For regular users: Processes user vote in consensus service + /// - Builds and sends appropriate message to group + /// + /// ## Message Types: + /// - **Steward**: Sends consensus vote message + /// - **Regular User**: Sends user vote message + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if group doesn't exist + /// - Various consensus service and message building errors + pub async fn process_user_vote( + &mut self, + proposal_id: u32, + user_vote: bool, + group_name: &str, + ) -> Result { + let group = self.group_ref(group_name).await?; + let app_message = if group.read().await.is_steward().await { + info!( + "[process_user_vote]: Steward voting for proposal {proposal_id} in group {group_name}" + ); + let proposal = self + .consensus_service + .vote_on_proposal(group_name, proposal_id, user_vote, self.eth_signer.clone()) + .await?; + proposal.into() + } else { + info!( + "[process_user_vote]: User voting for proposal {proposal_id} in group {group_name}" + ); + let vote = self + .consensus_service + .process_user_vote(group_name, proposal_id, user_vote, self.eth_signer.clone()) + .await?; + vote.into() + }; + + let waku_msg = self.build_group_message(app_message, group_name).await?; + + Ok(UserAction::SendToWaku(waku_msg)) + } + + /// Process incoming consensus vote and handle immediate state transitions. + /// + /// ## Parameters: + /// - `vote`: The consensus vote to process + /// - `group_name`: The name of the group the vote is for + /// + /// ## Returns: + /// - `UserAction` indicating what action should be taken + /// + /// ## Effects: + /// - Stores vote in consensus service + /// - Handles immediate state transitions if consensus is reached + /// + /// ## State Transitions: + /// When consensus is reached immediately after processing a vote: + /// - **Vote YES**: Non-steward transitions to Waiting state to await batch proposals + /// - **Vote NO**: Non-steward transitions to Working state immediately + /// - **Steward**: Relies on event-driven system for full proposal management + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if group doesn't exist + /// - Various consensus service errors + pub(crate) async fn process_consensus_vote( + &mut self, + vote: Vote, + group_name: &str, + ) -> Result { + self.consensus_service + .process_incoming_vote(group_name, vote.clone()) + .await?; + + Ok(UserAction::DoNothing) + } +} diff --git a/src/user/groups.rs b/src/user/groups.rs new file mode 100644 index 0000000..e0d98f6 --- /dev/null +++ b/src/user/groups.rs @@ -0,0 +1,184 @@ +use openmls::{ + group::MlsGroupJoinConfig, + prelude::{StagedWelcome, Welcome}, +}; +use std::sync::Arc; +use tokio::sync::RwLock; +use tracing::info; + +use crate::{error::UserError, group::Group, state_machine::GroupState, user::User}; +use mls_crypto::identity::normalize_wallet_address; + +impl User { + /// Fetch the shared reference to a group by name. + pub(crate) async fn group_ref(&self, name: &str) -> Result>, UserError> { + self.groups + .read() + .await + .get(name) + .cloned() + .ok_or(UserError::GroupNotFoundError) + } + + /// Create a new group for this user. + /// + /// ## Parameters: + /// - `group_name`: The name of the group to create + /// - `is_creation`: Whether this is a group creation (true) or joining (false) + /// + /// ## Effects: + /// - If `is_creation` is true: Creates MLS group with steward capabilities + /// - If `is_creation` is false: Creates empty group for later joining + /// - Adds group to user's groups map + /// + /// ## Errors: + /// - `UserError::GroupAlreadyExistsError` if group already exists + /// - Various MLS group creation errors + pub async fn create_group( + &mut self, + group_name: &str, + is_creation: bool, + ) -> Result<(), UserError> { + let mut groups = self.groups.write().await; + if groups.contains_key(group_name) { + return Err(UserError::GroupAlreadyExistsError); + } + let group = if is_creation { + Group::new( + group_name, + true, + Some(&self.provider), + Some(self.identity.signer()), + Some(&self.identity.credential_with_key()), + )? + } else { + Group::new(group_name, false, None, None, None)? + }; + + groups.insert(group_name.to_string(), Arc::new(RwLock::new(group))); + Ok(()) + } + + /// Join a group after receiving a welcome message. + /// + /// ## Parameters: + /// - `welcome`: The MLS welcome message containing group information + /// + /// ## Effects: + /// - Creates new MLS group from welcome message + /// - Sets the MLS group in the user's group instance + /// - Updates group state to reflect successful joining + /// + /// ## Preconditions: + /// - Group must already exist in user's groups map + /// - Welcome message must be valid and contain proper group data + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if group doesn't exist + /// - Various MLS group creation errors + pub(crate) async fn join_group(&mut self, welcome: Welcome) -> Result<(), UserError> { + let group_config = MlsGroupJoinConfig::builder().build(); + let mls_group = + StagedWelcome::new_from_welcome(&self.provider, &group_config, welcome, None)? + .into_group(&self.provider)?; + + let group_id = mls_group.group_id().to_vec(); + let group_name = String::from_utf8(group_id)?; + + let group = self.group_ref(&group_name).await?; + group.write().await.set_mls_group(mls_group)?; + + info!("[join_group]: User joined group {group_name}"); + Ok(()) + } + + /// Get the state of a group. + /// + /// ## Parameters: + /// - `group_name`: The name of the group to get the state of + /// + /// ## Returns: + /// - `GroupState` of the group + pub async fn get_group_state(&self, group_name: &str) -> Result { + let group = self.group_ref(group_name).await?; + let state = group.read().await.get_state().await; + + Ok(state) + } + + /// Get the number of members in a group. + /// + /// ## Parameters: + /// - `group_name`: The name of the group to get the number of members of + /// + /// ## Returns: + /// - The number of members in the group + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if group doesn't exist + pub async fn get_group_number_of_members(&self, group_name: &str) -> Result { + let group = self.group_ref(group_name).await?; + let members = group.read().await.members_identity().await?; + Ok(members.len()) + } + + /// Retrieve the list of member identities for a group. + /// + /// ## Parameters: + /// - `group_name`: Target group + /// + /// ## Returns: + /// - Vector of normalized wallet addresses (e.g., `0xabc...`) + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if group is missing + pub async fn get_group_members(&self, group_name: &str) -> Result, UserError> { + let group = self.group_ref(group_name).await?; + if !group.read().await.is_mls_group_initialized() { + return Ok(Vec::new()); + } + let members = group.read().await.members_identity().await?; + Ok(members + .into_iter() + .map(|raw| normalize_wallet_address(raw.as_slice()).to_string()) + .collect()) + } + + /// Get the MLS epoch of a group. + /// + /// ## Parameters: + /// - `group_name`: The name of the group to get the MLS epoch of + /// + /// ## Returns: + /// - The MLS epoch of the group + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if group doesn't exist + pub async fn get_group_mls_epoch(&self, group_name: &str) -> Result { + let group = self.group_ref(group_name).await?; + let epoch = group.read().await.epoch().await?; + Ok(epoch.as_u64()) + } + + /// Leave a group and clean up associated resources. + /// + /// ## Parameters: + /// - `group_name`: The name of the group to leave + /// + /// ## Effects: + /// - Removes group from user's groups map + /// - Cleans up all group-related resources + /// + /// ## Preconditions: + /// - Group must exist in user's groups map + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if group doesn't exist + pub async fn leave_group(&mut self, group_name: &str) -> Result<(), UserError> { + info!("[leave_group]: Leaving group {group_name}"); + let group = self.group_ref(group_name).await?; + self.groups.write().await.remove(group_name); + drop(group); + Ok(()) + } +} diff --git a/src/user/messaging.rs b/src/user/messaging.rs new file mode 100644 index 0000000..52e9104 --- /dev/null +++ b/src/user/messaging.rs @@ -0,0 +1,139 @@ +use alloy::{ + primitives::Address, + signers::{local::PrivateKeySigner, Signer}, +}; +use tracing::info; + +use crate::{ + error::UserError, + protos::de_mls::messages::v1::{AppMessage, BanRequest, ProposalAdded}, + user::{User, UserAction}, + LocalSigner, +}; +use ds::waku_actor::WakuMessageToSend; +use mls_crypto::normalize_wallet_address_str; + +impl User { + /// Prepare data to build a waku message for a group. + /// + /// ## Parameters: + /// - `app_message`: The application message to send to the group + /// - `group_name`: The name of the group to send the message to + /// + /// ## Returns: + /// - Waku message ready for transmission + /// + /// ## Preconditions: + /// - Group must be initialized with MLS group + /// + /// ## Effects: + /// - Preserves original AppMessage structure without wrapping + /// - Builds MLS message through the group + /// + /// ## Usage: + /// Used for consensus-related messages like proposals and votes + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if group doesn't exist + /// - `UserError::MlsGroupNotInitialized` if MLS group not initialized + /// - Various MLS message building errors + pub async fn build_group_message( + &mut self, + app_message: AppMessage, + group_name: &str, + ) -> Result { + let group = self.group_ref(group_name).await?; + if !group.read().await.is_mls_group_initialized() { + return Err(UserError::MlsGroupNotInitialized); + } + + let msg_to_send = group + .write() + .await + .build_message(&self.provider, self.identity.signer(), &app_message) + .await?; + + Ok(msg_to_send) + } + + /// Process incoming ban request. + /// + /// ## Parameters: + /// - `ban_request`: The ban request to process + /// - `group_name`: The name of the group the ban request is for + /// + /// ## Returns: + /// - Waku message to send to the group + /// + /// ## Effects: + /// - **For stewards**: Adds remove proposal to steward queue and sends system message + /// - **For regular users**: Forwards ban request to the group + /// + /// ## Message Types: + /// - **Steward**: Sends system message about proposal addition + /// - **Regular User**: Sends ban request message to group + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if group doesn't exist + /// - Various message building errors + pub async fn process_ban_request( + &mut self, + ban_request: BanRequest, + group_name: &str, + ) -> Result { + let normalized_user_to_ban = normalize_wallet_address_str(&ban_request.user_to_ban)?; + info!("[process_ban_request]: Processing ban request for user {normalized_user_to_ban} in group {group_name}"); + + let group = self.group_ref(group_name).await?; + let is_steward = group.read().await.is_steward().await; + if is_steward { + // Steward: add the remove proposal to the queue + info!( + "[process_ban_request]: Steward adding remove proposal for user {normalized_user_to_ban}" + ); + let request = self + .add_remove_proposal(group_name, normalized_user_to_ban.clone()) + .await?; + + // Send notification to UI about the new proposal + let proposal_added_msg: AppMessage = ProposalAdded { + group_id: group_name.to_string(), + request: request.into(), + } + .into(); + + Ok(UserAction::SendToApp(proposal_added_msg)) + } else { + // Regular user: send the ban request to the group + let updated_ban_request = BanRequest { + user_to_ban: normalized_user_to_ban, + requester: self.identity_string(), + group_name: ban_request.group_name, + }; + let msg = self + .build_group_message(updated_ban_request.into(), group_name) + .await?; + Ok(UserAction::SendToWaku(msg)) + } + } +} + +impl LocalSigner for PrivateKeySigner { + async fn local_sign_message(&self, message: &[u8]) -> Result, anyhow::Error> { + let signature = self.sign_message(message).await?; + let signature_bytes = signature.as_bytes().to_vec(); + Ok(signature_bytes) + } + + fn address(&self) -> Address { + self.address() + } + + fn address_string(&self) -> String { + self.address().to_string() + } + + fn address_bytes(&self) -> Vec { + self.address().as_slice().to_vec() + } +} diff --git a/src/user/mod.rs b/src/user/mod.rs new file mode 100644 index 0000000..2593ad5 --- /dev/null +++ b/src/user/mod.rs @@ -0,0 +1,122 @@ +pub mod consensus; +pub mod groups; +pub mod messaging; +pub mod proposals; +pub mod steward; +pub mod waku; + +use alloy::signers::local::PrivateKeySigner; +use kameo::Actor; +use std::{collections::HashMap, fmt::Display, str::FromStr, sync::Arc}; +use tokio::sync::{broadcast, RwLock}; + +use ds::waku_actor::WakuMessageToSend; +use mls_crypto::{ + identity::Identity, + openmls_provider::{MlsProvider, CIPHERSUITE}, +}; + +use crate::{ + consensus::{ConsensusEvent, ConsensusService}, + error::UserError, + group::Group, + protos::de_mls::messages::v1::AppMessage, + user::proposals::PendingBatches, +}; + +/// Represents the action to take after processing a user message or event. +/// +/// This enum defines the possible outcomes when processing user-related operations, +/// allowing the caller to determine the appropriate next steps for message handling, +/// group management, and network communication. +#[derive(Debug, Clone, PartialEq)] +pub enum UserAction { + SendToWaku(WakuMessageToSend), + SendToApp(AppMessage), + LeaveGroup(String), + DoNothing, +} + +impl Display for UserAction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UserAction::SendToWaku(_) => write!(f, "SendToWaku"), + UserAction::SendToApp(_) => write!(f, "SendToApp"), + UserAction::LeaveGroup(group_name) => write!(f, "LeaveGroup({group_name})"), + UserAction::DoNothing => write!(f, "DoNothing"), + } + } +} + +/// Represents a user in the MLS-based messaging system. +/// +/// The User struct manages the lifecycle of multiple groups, handles consensus operations, +/// and coordinates communication between the application layer and the Waku network. +/// It integrates with the consensus service for proposal management and voting. +/// +/// ## Key Features: +/// - Multi-group management and coordination +/// - Consensus service integration for proposal handling +/// - Waku message processing and routing +/// - Steward epoch coordination +/// - Member management through proposals +#[derive(Actor)] +pub struct User { + identity: Identity, + // Each group has its own lock for better concurrency + groups: Arc>>>>, + provider: MlsProvider, + consensus_service: ConsensusService, + eth_signer: PrivateKeySigner, + // Queue for batch proposals that arrive before consensus is reached + pending_batch_proposals: PendingBatches, +} + +impl User { + /// Create a new user instance with the specified Ethereum private key. + /// + /// ## Parameters: + /// - `user_eth_priv_key`: The user's Ethereum private key as a hex string + /// + /// ## Returns: + /// - New User instance with initialized identity and services + /// + /// ## Errors: + /// - `UserError` if private key parsing or identity creation fails + pub fn new( + user_eth_priv_key: &str, + consensus_service: &ConsensusService, + ) -> Result { + let signer = PrivateKeySigner::from_str(user_eth_priv_key)?; + let user_address = signer.address(); + + let crypto = MlsProvider::default(); + let id = Identity::new(CIPHERSUITE, &crypto, user_address.as_slice())?; + + let user = User { + groups: Arc::new(RwLock::new(HashMap::new())), + identity: id, + eth_signer: signer, + provider: crypto, + consensus_service: consensus_service.clone(), + pending_batch_proposals: PendingBatches::default(), + }; + Ok(user) + } + + /// Get a subscription to consensus events + pub fn subscribe_to_consensus_events(&self) -> broadcast::Receiver<(String, ConsensusEvent)> { + self.consensus_service.subscribe_to_events() + } + + /// Get the identity string for debugging and identification purposes. + /// + /// ## Returns: + /// - String representation of the user's identity + /// + /// ## Usage: + /// Primarily used for debugging, logging, and user identification + pub fn identity_string(&self) -> String { + self.identity.identity_string() + } +} diff --git a/src/user/proposals.rs b/src/user/proposals.rs new file mode 100644 index 0000000..3813ebb --- /dev/null +++ b/src/user/proposals.rs @@ -0,0 +1,146 @@ +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; + +use openmls::prelude::{DeserializeBytes, MlsMessageIn}; +use tracing::info; + +use crate::{ + error::UserError, + group::GroupAction, + protos::de_mls::messages::v1::BatchProposalsMessage, + state_machine::GroupState, + user::{User, UserAction}, +}; + +#[derive(Clone, Default)] +pub(crate) struct PendingBatches { + inner: Arc>>, +} + +impl PendingBatches { + pub(crate) async fn store(&self, group: &str, batch: BatchProposalsMessage) { + self.inner.write().await.insert(group.to_string(), batch); + } + + pub(crate) async fn take(&self, group: &str) -> Option { + self.inner.write().await.remove(group) + } + + pub(crate) async fn contains(&self, group: &str) -> bool { + self.inner.read().await.contains_key(group) + } +} + +impl User { + /// Process batch proposals message from the steward. + /// + /// ## Parameters: + /// - `batch_msg`: The batch proposals message to process + /// - `group_name`: The name of the group these proposals are for + /// + /// ## Returns: + /// - `UserAction` indicating what action should be taken + /// + /// ## Effects: + /// - Processes all MLS proposals in the batch + /// - Applies the commit message to complete the group update + /// - Transitions group to Working state after successful processing + /// + /// ## State Requirements: + /// - Group must be in Waiting state to process batch proposals + /// - If not in correct state, stores proposals for later processing + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if group doesn't exist + /// - Various MLS processing errors + pub(crate) async fn process_batch_proposals_message( + &mut self, + batch_msg: BatchProposalsMessage, + group_name: &str, + ) -> Result { + // Get the group lock + let group = self.group_ref(group_name).await?; + let initial_state = group.read().await.get_state().await; + if initial_state != GroupState::Waiting { + info!( + "[process_batch_proposals_message]: Cannot process batch proposals in {initial_state} state, storing for later processing" + ); + // Store the batch proposals for later processing + self.pending_batch_proposals + .store(group_name, batch_msg) + .await; + return Ok(UserAction::DoNothing); + } + + // Process all proposals before the commit + for proposal_bytes in batch_msg.mls_proposals { + let (mls_message_in, _) = MlsMessageIn::tls_deserialize_bytes(&proposal_bytes)?; + let protocol_message = mls_message_in.try_into_protocol_message()?; + + let _res = group + .write() + .await + .process_protocol_msg(protocol_message, &self.provider) + .await?; + } + + // Then process the commit message + let (mls_message_in, _) = MlsMessageIn::tls_deserialize_bytes(&batch_msg.commit_message)?; + let protocol_message = mls_message_in.try_into_protocol_message()?; + + let res = group + .write() + .await + .process_protocol_msg(protocol_message, &self.provider) + .await?; + + group.write().await.start_working().await; + + match res { + GroupAction::GroupAppMsg(msg) => Ok(UserAction::SendToApp(msg)), + GroupAction::LeaveGroup => Ok(UserAction::LeaveGroup(group_name.to_string())), + GroupAction::DoNothing => Ok(UserAction::DoNothing), + GroupAction::GroupProposal(proposal) => { + self.process_consensus_proposal(proposal, group_name).await + } + GroupAction::GroupVote(vote) => self.process_consensus_vote(vote, group_name).await, + } + } + + /// Try to process a batch proposals message that was deferred earlier. + /// + /// ## Parameters: + /// - `group_name`: The name of the group whose stored batch should be retried + /// + /// ## Returns: + /// - `Some(UserAction)` if a stored batch was processed, `None` otherwise + /// + /// ## Effects: + /// - Checks for a cached `BatchProposalsMessage` and processes it immediately if present + /// - Removes the stored batch once processing succeeds + /// + /// ## Usage: + /// Call after transitioning into `Waiting` so any deferred steward batch can be replayed. + pub(crate) async fn process_stored_batch_proposals( + &mut self, + group_name: &str, + ) -> Result, UserError> { + if self.pending_batch_proposals.contains(group_name).await { + if let Some(batch_msg) = self.pending_batch_proposals.take(group_name).await { + info!( + "[process_stored_batch_proposals]: Processing stored batch proposals for group {}", + group_name + ); + let action = self + .process_batch_proposals_message(batch_msg, group_name) + .await?; + Ok(Some(action)) + } else { + Ok(None) + } + } else { + Ok(None) + } + } +} diff --git a/src/user/steward.rs b/src/user/steward.rs new file mode 100644 index 0000000..3a959ce --- /dev/null +++ b/src/user/steward.rs @@ -0,0 +1,279 @@ +use tracing::{error, info}; + +use crate::{ + error::UserError, + protos::{ + consensus::v1::{UpdateRequest, VotePayload}, + de_mls::messages::v1::AppMessage, + }, + user::{User, UserAction}, +}; +use ds::waku_actor::WakuMessageToSend; + +impl User { + /// Check if the user is a steward for the given group. + /// ## Returns: + /// - Serialized `UiUpdateRequest` representing the removal request for UI consumption + /// + /// ## Parameters: + /// - `group_name`: The name of the group to check + /// + /// ## Returns: + /// - `true` if the user is a steward for the group, `false` otherwise + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if the group does not exist + pub async fn is_user_steward_for_group(&self, group_name: &str) -> Result { + let group = { + let groups = self.groups.read().await; + groups + .get(group_name) + .cloned() + .ok_or_else(|| UserError::GroupNotFoundError)? + }; + let is_steward = group.read().await.is_steward().await; + Ok(is_steward) + } + + /// Check if the MLS group is initialized for the given group. + /// + /// ## Parameters: + /// - `group_name`: The name of the group to check + /// + /// ## Returns: + /// - `true` if the MLS group is initialized for the group, `false` otherwise + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if the group does not exist + pub async fn is_user_mls_group_initialized_for_group( + &self, + group_name: &str, + ) -> Result { + let group = self.group_ref(group_name).await?; + let is_initialized = group.read().await.is_mls_group_initialized(); + Ok(is_initialized) + } + + /// Get the current epoch proposals for the given group. + /// + /// ## Parameters: + /// - `group_name`: The name of the group to get the proposals for + /// + /// ## Returns: + /// - A vector of `GroupUpdateRequest` representing the current epoch proposals + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if the group does not exist + pub async fn get_current_epoch_proposals( + &self, + group_name: &str, + ) -> Result, UserError> { + let group = self.group_ref(group_name).await?; + let proposals = group.read().await.get_current_epoch_proposals().await; + Ok(proposals) + } + + /// Prepare a steward announcement message for a group. + /// + /// ## Parameters: + /// - `group_name`: The name of the group to prepare the message for + /// + /// ## Returns: + /// - Waku message containing the steward announcement + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if group doesn't exist + /// - `GroupError::StewardNotSet` if no steward is configured + pub async fn prepare_steward_msg( + &mut self, + group_name: &str, + ) -> Result { + let group = self.group_ref(group_name).await?; + let msg_to_send = group.write().await.generate_steward_message().await?; + Ok(msg_to_send) + } + + /// Start a new steward epoch for the given group. It includes validation of the current state + /// and the number of proposals. If there are no proposals, it will stay in the Working state and return 0. + /// If there are proposals, it will change the state to Waiting and return the number of proposals. + /// + /// ## Parameters: + /// - `group_name`: The name of the group to start steward epoch for + /// + /// ## Returns: + /// - Number of proposals that will be voted on (0 if no proposals) + /// + /// ## State Transitions: + /// - **With proposals**: Working → Waiting (returns proposal count) + /// - **No proposals**: Working → Working (stays in Working, returns 0) + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if group doesn't exist + /// - `GroupError::InvalidStateTransition` if the group is not in the Working state + /// - `GroupError::StewardNotSet` if no steward is configured + pub async fn start_steward_epoch(&mut self, group_name: &str) -> Result { + let group = self.group_ref(group_name).await?; + let proposal_count = group + .write() + .await + .start_steward_epoch_with_validation() + .await?; + + if proposal_count == 0 { + info!("[start_steward_epoch]: No proposals to vote on, skipping steward epoch"); + } else { + info!("[start_steward_epoch]: Started steward epoch with {proposal_count} proposals"); + } + + Ok(proposal_count) + } + /// Start voting for the given group, returning the proposal ID. + /// + /// ## Parameters: + /// - `group_name`: The name of the group to start voting for + /// + /// ## Returns: + /// - Tuple of (proposal_id, UserAction) for steward actions + /// + /// ## Effects: + /// - Starts voting phase in the group + /// - Creates consensus proposal for voting + /// - Sends voting proposal to frontend + /// + /// ## State Transitions: + /// - **Waiting → Voting**: If proposals found and steward starts voting + /// - **Waiting → Working**: If no proposals found (edge case fix) + /// + /// ## Edge Case Handling: + /// If no proposals are found during voting phase (rare edge case where proposals + /// disappear between epoch start and voting), transitions back to Working state + /// to prevent getting stuck in Waiting state. + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if group doesn't exist + /// - `UserError::NoProposalsFound` if no proposals exist + /// - `ConsensusError::SystemTimeError` if the system time creation fails + pub async fn get_proposals_for_steward_voting( + &mut self, + group_name: &str, + ) -> Result<(u32, UserAction), UserError> { + info!( + "[get_proposals_for_steward_voting]: Getting proposals for steward voting in group {group_name}" + ); + + let group = self.group_ref(group_name).await?; + + // If this is the steward, create proposal with vote and send to group + if group.read().await.is_steward().await { + let proposals = group + .read() + .await + .get_proposals_for_voting_epoch_as_ui_update_requests() + .await; + if !proposals.is_empty() { + group.write().await.start_voting().await?; + + // Get group members for expected voters count + let members = group.read().await.members_identity().await?; + let participant_ids: Vec> = members.into_iter().collect(); + let expected_voters_count = participant_ids.len() as u32; + + // Create consensus proposal + let proposal = self + .consensus_service + .create_proposal( + group_name, + uuid::Uuid::new_v4().to_string(), + proposals.clone(), + self.identity.identity_string().into(), + expected_voters_count, + 3600, // 1 hour expiration + true, // liveness criteria + ) + .await?; + + info!( + "[get_proposals_for_steward_voting]: Created consensus proposal with ID {} and {} expected voters", + proposal.proposal_id, expected_voters_count + ); + + // Send voting proposal to frontend + let voting_proposal: AppMessage = VotePayload { + group_id: group_name.to_string(), + proposal_id: proposal.proposal_id, + group_requests: proposal.group_requests.clone(), + timestamp: proposal.timestamp, + } + .into(); + + Ok((proposal.proposal_id, UserAction::SendToApp(voting_proposal))) + } else { + error!("[get_proposals_for_steward_voting]: No proposals found"); + Err(UserError::NoProposalsFound) + } + } else { + // Not steward, do nothing + info!("[get_proposals_for_steward_voting]: Not steward, doing nothing"); + Ok((0, UserAction::DoNothing)) + } + } + + /// Add a remove proposal into the `steward::current_epoch_proposals` vector for the given group. + /// + /// ## Parameters: + /// - `group_name`: The name of the group to add the proposal to + /// - `identity`: The identity string of the member to remove + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if group doesn't exist + /// - `GroupError::InvalidIdentity` if the identity is invalid + pub async fn add_remove_proposal( + &mut self, + group_name: &str, + identity: String, + ) -> Result { + let group = self.group_ref(group_name).await?; + let request = group.write().await.store_remove_proposal(identity).await?; + Ok(request) + } + + /// Apply proposals for the given group, returning the batch message(s). + /// - Creates MLS proposals for all pending group updates + /// - Commits all proposals to the MLS group + /// - Generates batch proposals message and welcome message if needed + /// + /// ## Parameters: + /// - `group_name`: The name of the group to apply proposals for + /// + /// ## Returns: + /// - Vector of Waku messages containing batch proposals and welcome messages + /// + /// ## Preconditions: + /// - Group must be initialized with MLS group + /// - User must be steward for the group + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if group doesn't exist + /// - `UserError::MlsGroupNotInitialized` if MLS group not initialized + /// - `GroupError::StewardNotSet` if no steward is configured + /// - `GroupError::EmptyProposals` if no proposals exist + /// - `GroupError::InvalidStateTransition` if the group is not in the Waiting state + pub async fn apply_proposals( + &mut self, + group_name: &str, + ) -> Result, UserError> { + let group = self.group_ref(group_name).await?; + + if !group.read().await.is_mls_group_initialized() { + return Err(UserError::MlsGroupNotInitialized); + } + + let messages = group + .write() + .await + .create_batch_proposals_message(&self.provider, self.identity.signer()) + .await?; + info!("[apply_proposals]: Applied proposals for group {group_name}"); + Ok(messages) + } +} diff --git a/src/user/waku.rs b/src/user/waku.rs new file mode 100644 index 0000000..88dde95 --- /dev/null +++ b/src/user/waku.rs @@ -0,0 +1,304 @@ +use openmls::prelude::{DeserializeBytes, MlsMessageBodyIn, MlsMessageIn}; +use prost::Message; +use tracing::{debug, error, info}; +use waku_bindings::WakuMessage; + +use ds::waku_actor::WakuMessageToSend; +use ds::{APP_MSG_SUBTOPIC, WELCOME_SUBTOPIC}; + +use crate::error::UserError; +use crate::group::GroupAction; +use crate::message::MessageType; +use crate::protos::de_mls::messages::v1::{ + app_message, welcome_message, AppMessage, ConversationMessage, ProposalAdded, UserKeyPackage, + WelcomeMessage, +}; +use crate::user::{User, UserAction}; + +impl User { + /// Process messages from the welcome subtopic. + /// + /// ## Parameters: + /// - `msg`: The Waku message to process + /// - `group_name`: The name of the group this message is for + /// + /// ## Returns: + /// - `UserAction` indicating what action should be taken + /// + /// ## Message Types Handled: + /// - **GroupAnnouncement**: Steward announcements for group joining + /// - **UserKeyPackage**: Encrypted key packages from new members + /// - **InvitationToJoin**: MLS welcome messages for group joining + /// + /// ## Effects: + /// - For group announcements: Generates and sends key package + /// - For user key packages: Decrypts and stores invite proposals (steward only) + /// - For invitations: Processes MLS welcome and joins group + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if group doesn't exist + /// - `UserError::MessageVerificationFailed` if announcement verification fails + /// - Various MLS and encryption errors + pub async fn process_welcome_subtopic( + &mut self, + msg: WakuMessage, + group_name: &str, + ) -> Result { + // Get the group lock first + let group = self.group_ref(group_name).await?; + + let is_steward = { + let group = group.read().await; + group.is_steward().await + }; + let is_kp_shared = { + let group = group.read().await; + group.is_kp_shared() + }; + let is_mls_group_initialized = { + let group = group.read().await; + group.is_mls_group_initialized() + }; + + let received_msg = WelcomeMessage::decode(msg.payload())?; + if let Some(payload) = &received_msg.payload { + match payload { + welcome_message::Payload::GroupAnnouncement(group_announcement) => { + if is_steward || is_kp_shared { + Ok(UserAction::DoNothing) + } else { + info!( + "[process_welcome_subtopic]: User received group announcement msg for group {group_name}" + ); + if !group_announcement.verify()? { + return Err(UserError::MessageVerificationFailed); + } + + let new_kp = self.identity.generate_key_package(&self.provider)?; + let encrypted_key_package = group_announcement.encrypt(new_kp)?; + group.write().await.set_kp_shared(true); + + let welcome_msg: WelcomeMessage = UserKeyPackage { + encrypt_kp: encrypted_key_package, + } + .into(); + Ok(UserAction::SendToWaku(WakuMessageToSend::new( + welcome_msg.encode_to_vec(), + WELCOME_SUBTOPIC, + group_name, + group.read().await.app_id(), + ))) + } + } + welcome_message::Payload::UserKeyPackage(user_key_package) => { + if is_steward { + info!( + "[process_welcome_subtopic]: Steward received key package for the group {group_name}" + ); + let key_package = group + .write() + .await + .decrypt_steward_msg(user_key_package.encrypt_kp.clone()) + .await?; + + let request = group + .write() + .await + .store_invite_proposal(Box::new(key_package)) + .await?; + + // Send notification to UI about the new proposal + let proposal_added_msg: AppMessage = ProposalAdded { + group_id: group_name.to_string(), + request: Some(request), + } + .into(); + + Ok(UserAction::SendToApp(proposal_added_msg)) + } else { + Ok(UserAction::DoNothing) + } + } + welcome_message::Payload::InvitationToJoin(invitation_to_join) => { + if is_steward || is_mls_group_initialized { + Ok(UserAction::DoNothing) + } else { + // Release the lock before calling join_group + drop(group); + + // Parse the MLS message to get the welcome + let (mls_in, _) = MlsMessageIn::tls_deserialize_bytes( + &invitation_to_join.mls_message_out_bytes, + )?; + + let welcome = match mls_in.extract() { + MlsMessageBodyIn::Welcome(welcome) => welcome, + _ => return Err(UserError::FailedToExtractWelcomeMessage), + }; + + if welcome.secrets().iter().any(|egs| { + let hash_ref = egs.new_member().as_slice().to_vec(); + self.identity.is_key_package_exists(&hash_ref) + }) { + self.join_group(welcome).await?; + let app_msg = ConversationMessage { + message: format!( + "User {} joined to the group", + self.identity.identity_string() + ) + .as_bytes() + .to_vec(), + sender: "SYSTEM".to_string(), + group_name: group_name.to_string(), + } + .into(); + let msg = self.build_group_message(app_msg, group_name).await?; + Ok(UserAction::SendToWaku(msg)) + } else { + Ok(UserAction::DoNothing) + } + } + } + } + } else { + Err(UserError::EmptyWelcomeMessageError) + } + } + + /// Process messages from the application message subtopic. + /// + /// ## Parameters: + /// - `msg`: The Waku message to process + /// - `group_name`: The name of the group this message is for + /// + /// ## Returns: + /// - `UserAction` indicating what action should be taken + /// + /// ## Message Types Handled: + /// - **BatchProposalsMessage**: Batch proposals from steward + /// - **MLS Protocol Messages**: Encrypted group messages + /// - **Application Messages**: Various app-level messages + /// + /// ## Effects: + /// - Processes batch proposals and applies them to the group + /// - Handles MLS protocol messages through the group + /// - Routes consensus proposals and votes to appropriate handlers + /// + /// ## Preconditions: + /// - Group must be initialized with MLS group + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if group doesn't exist + /// - Various MLS processing errors + pub async fn process_app_subtopic( + &mut self, + msg: WakuMessage, + group_name: &str, + ) -> Result { + let group = self.group_ref(group_name).await?; + + if !group.read().await.is_mls_group_initialized() { + return Ok(UserAction::DoNothing); + } + + // Try to parse as AppMessage first + // This one required for commit messages as they are sent as AppMessage + // without group encryption + if let Ok(app_message) = AppMessage::decode(msg.payload()) { + match app_message.payload { + Some(app_message::Payload::BatchProposalsMessage(batch_msg)) => { + info!( + "[process_app_subtopic]: Processing batch proposals message for group {group_name}" + ); + // Release the lock before calling self methods + return self + .process_batch_proposals_message(batch_msg, group_name) + .await; + } + _ => { + error!( + "[process_app_subtopic]: Cannot process another app message here: {:?}", + app_message.payload.unwrap().message_type() + ); + return Err(UserError::InvalidAppMessageType); + } + } + } + + // Fall back to MLS protocol message + let (mls_message_in, _) = MlsMessageIn::tls_deserialize_bytes(msg.payload())?; + let mls_message = mls_message_in.try_into_protocol_message()?; + + let group = self.group_ref(group_name).await?; + let res = group + .write() + .await + .process_protocol_msg(mls_message, &self.provider) + .await?; + + // Handle the result outside of any lock scope + match res { + GroupAction::GroupAppMsg(msg) => { + info!("[process_app_subtopic]: sending to app"); + Ok(UserAction::SendToApp(msg)) + } + GroupAction::LeaveGroup => { + info!("[process_app_subtopic]: leaving group"); + Ok(UserAction::LeaveGroup(group_name.to_string())) + } + GroupAction::DoNothing => { + info!("[process_app_subtopic]: doing nothing"); + Ok(UserAction::DoNothing) + } + GroupAction::GroupProposal(proposal) => { + info!("[process_app_subtopic]: processing consensus proposal"); + self.process_consensus_proposal(proposal, group_name).await + } + GroupAction::GroupVote(vote) => { + info!("[process_app_subtopic]: processing consensus vote"); + self.process_consensus_vote(vote, group_name).await + } + } + } + + /// Process incoming Waku messages and route them to appropriate handlers. + /// + /// ## Parameters: + /// - `msg`: The Waku message to process + /// + /// ## Returns: + /// - `UserAction` indicating what action should be taken + /// + /// ## Message Routing: + /// - **Welcome Subtopic**: Routes to `process_welcome_subtopic()` + /// - **App Message Subtopic**: Routes to `process_app_subtopic()` + /// - **Unknown Topics**: Returns error + /// + /// ## Effects: + /// - Processes messages based on content topic + /// - Skips messages from the same app instance + /// - Routes to appropriate subtopic handlers + /// + /// ## Errors: + /// - `UserError::GroupNotFoundError` if group doesn't exist + /// - `UserError::UnknownContentTopicType` for unsupported topics + /// - Various processing errors from subtopic handlers + pub async fn process_waku_message( + &mut self, + msg: WakuMessage, + ) -> Result { + let group_name = msg.content_topic.application_name.to_string(); + let group = self.group_ref(&group_name).await?; + if msg.meta == group.read().await.app_id() { + debug!("[process_waku_message]: Message is from the same app, skipping"); + return Ok(UserAction::DoNothing); + } + + let ct_name = msg.content_topic.content_topic_name.to_string(); + match ct_name.as_str() { + WELCOME_SUBTOPIC => self.process_welcome_subtopic(msg, &group_name).await, + APP_MSG_SUBTOPIC => self.process_app_subtopic(msg, &group_name).await, + _ => Err(UserError::UnknownContentTopicType(ct_name)), + } + } +} diff --git a/src/user_actor.rs b/src/user_actor.rs index debc10c..f511cf0 100644 --- a/src/user_actor.rs +++ b/src/user_actor.rs @@ -6,7 +6,7 @@ use ds::waku_actor::WakuMessageToSend; use crate::{ consensus::ConsensusEvent, error::UserError, - protos::messages::v1::BanRequest, + protos::de_mls::messages::v1::{BanRequest, ConversationMessage}, user::{User, UserAction}, }; @@ -86,7 +86,13 @@ impl Message for User { msg: SendGroupMessage, _ctx: Context<'_, Self, Self::Reply>, ) -> Self::Reply { - self.build_group_message(msg.message, &msg.group_name).await + let app_msg = ConversationMessage { + message: msg.message, + sender: self.identity_string(), + group_name: msg.group_name.clone(), + } + .into(); + self.build_group_message(app_msg, &msg.group_name).await } } @@ -96,7 +102,7 @@ pub struct BuildBanMessage { } impl Message for User { - type Reply = Result; + type Reply = Result; async fn handle( &mut self, @@ -125,6 +131,22 @@ impl Message for User { } } +pub struct GetGroupMembersRequest { + pub group_name: String, +} + +impl Message for User { + type Reply = Result, UserError>; + + async fn handle( + &mut self, + msg: GetGroupMembersRequest, + _ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { + self.get_group_members(&msg.group_name).await + } +} + pub struct GetProposalsForStewardVotingRequest { pub group_name: String, } @@ -189,3 +211,37 @@ impl Message for User { .await } } + +pub struct IsStewardStatusRequest { + pub group_name: String, +} + +impl Message for User { + type Reply = Result; + + async fn handle( + &mut self, + msg: IsStewardStatusRequest, + _ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { + let is_steward = self.is_user_steward_for_group(&msg.group_name).await?; + Ok(is_steward) + } +} + +pub struct GetCurrentEpochProposalsRequest { + pub group_name: String, +} + +impl Message for User { + type Reply = Result, UserError>; + + async fn handle( + &mut self, + msg: GetCurrentEpochProposalsRequest, + _ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { + let proposals = self.get_current_epoch_proposals(&msg.group_name).await?; + Ok(proposals) + } +} diff --git a/src/user_app_instance.rs b/src/user_app_instance.rs index 79a7433..3f7c50e 100644 --- a/src/user_app_instance.rs +++ b/src/user_app_instance.rs @@ -1,85 +1,68 @@ +// src/user_app_instance.rs use alloy::signers::local::PrivateKeySigner; -use ds::build_content_topics; use kameo::actor::ActorRef; -use log::{error, info}; -use std::{str::FromStr, sync::Arc, time::Duration}; +use std::{str::FromStr, sync::Arc}; +use tokio::sync::mpsc::Sender; +use tracing::{error, info}; +use waku_bindings::WakuMessage; -use crate::user::{User, UserAction}; -use crate::user_actor::{ - ConsensusEventMessage, CreateGroupRequest, GetProposalsForStewardVotingRequest, - StartStewardEpochRequest, StewardMessageRequest, -}; -use crate::ws_actor::WsActor; +use ds::topic_filter::TopicFilter; +use ds::waku_actor::WakuMessageToSend; + +use crate::consensus::ConsensusService; +use crate::error::UserError; +use crate::group_registry::GroupRegistry; +use crate::user::User; +use crate::user_actor::ConsensusEventMessage; use crate::LocalSigner; -use crate::{error::UserError, AppState, Connection}; pub const STEWARD_EPOCH: u64 = 15; +#[derive(Debug, Clone)] +pub struct AppState { + pub waku_node: Sender, + pub pubsub: tokio::sync::broadcast::Sender, +} + +#[derive(Debug)] +pub struct CoreCtx { + pub app_state: Arc, + pub groups: Arc, + pub topics: Arc, + pub consensus: Arc, +} + +impl CoreCtx { + pub fn new(app_state: Arc) -> Self { + Self { + app_state, + groups: Arc::new(GroupRegistry::new()), + topics: Arc::new(TopicFilter::new()), + consensus: Arc::new(ConsensusService::new()), + } + } +} + pub async fn create_user_instance( - connection: Connection, + eth_private_key: String, app_state: Arc, - ws_actor: ActorRef, -) -> Result, UserError> { - let signer = PrivateKeySigner::from_str(&connection.eth_private_key)?; + consensus_service: &ConsensusService, +) -> Result<(ActorRef, String), UserError> { + let signer = PrivateKeySigner::from_str(ð_private_key)?; let user_address = signer.address_string(); - let group_name: String = connection.group_id.clone(); // Create user - let user = User::new(&connection.eth_private_key)?; + let user = User::new(ð_private_key, consensus_service)?; // Set up consensus event forwarding before spawning the actor let consensus_events = user.subscribe_to_consensus_events(); let user_ref = kameo::spawn(user); - user_ref - .ask(CreateGroupRequest { - group_name: group_name.clone(), - is_creation: connection.should_create_group, - }) - .await - .map_err(|e| UserError::UnableToCreateGroup(e.to_string()))?; - - let mut content_topics = build_content_topics(&group_name); - info!("Building content topics: {content_topics:?}"); - app_state - .content_topics - .write() - .await - .append(&mut content_topics); - - if connection.should_create_group { - info!("User {user_address:?} start sending steward message for group {group_name:?}"); - let user_clone = user_ref.clone(); - let group_name_clone = group_name.clone(); - let app_state_steward = app_state.clone(); - tokio::spawn(async move { - let mut interval = tokio::time::interval(Duration::from_secs(STEWARD_EPOCH)); - loop { - interval.tick().await; - let _ = async { - handle_steward_flow_per_epoch( - user_clone.clone(), - group_name_clone.clone(), - app_state_steward.clone(), - ws_actor.clone(), - ) - .await - .map_err(|e| UserError::UnableToHandleStewardEpoch(e.to_string()))?; - Ok::<(), UserError>(()) - } - .await - .inspect_err(|e| error!("Error sending steward message to waku: {e}")); - } - }); - }; - - // Set up consensus event forwarding loop + let app_state_consensus = app_state.clone(); let user_ref_consensus = user_ref.clone(); let mut consensus_events_receiver = consensus_events; - let app_state_consensus = app_state.clone(); tokio::spawn(async move { - info!("Starting consensus event forwarding loop"); + info!("Starting consensus event forwarding loop (user-only)"); while let Ok((group_name, event)) = consensus_events_receiver.recv().await { - info!("Forwarding consensus event for group {group_name}: {event:?}"); let result = user_ref_consensus .ask(ConsensusEventMessage { group_name: group_name.clone(), @@ -89,7 +72,6 @@ pub async fn create_user_instance( match result { Ok(commit_messages) => { - // Send commit messages to Waku if any if !commit_messages.is_empty() { info!( "Sending {} commit messages to Waku for group {}", @@ -108,88 +90,8 @@ pub async fn create_user_instance( } } } - info!("Consensus event forwarding loop ended"); + info!("Consensus forwarding loop ended"); }); - Ok(user_ref) -} - -/// Enhanced steward epoch flow with state machine: -/// -/// ## Complete Flow Steps: -/// 1. **Start steward epoch**: Collect pending proposals -/// - If no proposals: Stay in Working state, complete epoch without voting -/// - If proposals exist: Transition Working → Waiting -/// 2. **Send steward key**: Broadcast new steward key to waku network -/// 3. **Voting phase** (only if proposals exist): -/// - Get proposals for voting: Transition Waiting → Voting -/// - If no proposals found (edge case): Transition Waiting → Working, complete epoch -/// - If proposals found: Send voting proposal to group members -/// 4. **Complete voting**: Handle consensus result -/// - Vote YES: Transition Voting → Waiting → Working (after applying proposals) -/// - Vote NO: Transition Voting → Working (proposals discarded) -/// 5. **Apply proposals** (only if vote passed): Execute group changes and return to Working -/// -/// ## State Guarantees: -/// - Steward always returns to Working state after epoch completion -/// - No proposals scenario never leaves Working state -/// - All edge cases properly handled with state transitions -pub async fn handle_steward_flow_per_epoch( - user: ActorRef, - group_name: String, - app_state: Arc, - ws_actor: ActorRef, -) -> Result<(), UserError> { - info!("Starting steward epoch for group: {group_name}"); - - // Step 1: Start steward epoch - check for proposals and start epoch if needed - let proposals_count = user - .ask(StartStewardEpochRequest { - group_name: group_name.clone(), - }) - .await - .map_err(|e| UserError::GetGroupUpdateRequestsError(e.to_string()))?; - - // Step 2: Send new steward key to the waku node for new epoch - let msg = user - .ask(StewardMessageRequest { - group_name: group_name.clone(), - }) - .await - .map_err(|e| UserError::ProcessStewardMessageError(e.to_string()))?; - app_state.waku_node.send(msg).await?; - - if proposals_count == 0 { - info!("No proposals to vote on for group: {group_name}, completing epoch without voting"); - } else { - info!("Found {proposals_count} proposals to vote on for group: {group_name}"); - - // Step 3: Start voting process - steward gets proposals for voting - let action = user - .ask(GetProposalsForStewardVotingRequest { - group_name: group_name.clone(), - }) - .await - .map_err(|e| UserError::UnableToStartVoting(e.to_string()))?; - - // Step 4: Send proposals to ws to steward to vote or do nothing if no proposals - // After voting, steward sends vote and proposal to waku node and start consensus process - match action { - UserAction::SendToApp(app_msg) => { - info!("Sending app message to ws"); - ws_actor.ask(app_msg).await.map_err(|e| { - UserError::UnableToSendMessageToWs(format!("Failed to send message to ws: {e}")) - })?; - } - UserAction::DoNothing => { - info!("No action to take for group: {group_name}"); - return Ok(()); - } - _ => { - return Err(UserError::InvalidUserAction(action.to_string())); - } - } - } - - Ok(()) + Ok((user_ref, user_address)) } diff --git a/src/ws_actor.rs b/src/ws_actor.rs deleted file mode 100644 index 5359a9e..0000000 --- a/src/ws_actor.rs +++ /dev/null @@ -1,174 +0,0 @@ -use axum::extract::ws::{Message as WsMessage, WebSocket}; -use futures::{stream::SplitSink, SinkExt}; -use kameo::{ - message::{Context, Message}, - Actor, -}; -use log::info; -use serde_json::Value; - -use crate::{ - message::{ConnectMessage, UserMessage}, - protos::messages::v1::{app_message, AppMessage}, -}; - -/// This actor is used to handle messages from web socket -#[derive(Debug, Actor)] -pub struct WsActor { - /// This is the sender of the open web socket connection - pub ws_sender: SplitSink, - /// This variable is used to check if the user has connected to the ws, - /// if not, we parse message as ConnectMessage - pub is_initialized: bool, -} - -impl WsActor { - pub fn new(ws_sender: SplitSink) -> Self { - Self { - ws_sender, - is_initialized: false, - } - } -} - -/// This enum is used to represent the actions that can be performed on the web socket -/// Connect - this action is used to return connection data to the user -/// UserMessage - this action is used to handle message from web socket and return it to the user -/// DoNothing - this action is used for test purposes (return empty action if message is not valid) -#[derive(Debug, PartialEq)] -pub enum WsAction { - Connect(ConnectMessage), - UserMessage(UserMessage), - RemoveUser(String, String), - UserVote { - proposal_id: u32, - vote: bool, - group_id: String, - }, - DoNothing, -} - -/// This struct is used to represent the raw message from the web socket. -/// It is used to handle the message from the web socket and return it to the user -/// We can parse it to the ConnectMessage or UserMessage -#[derive(Debug, PartialEq)] -pub struct RawWsMessage { - pub message: String, -} - -impl Message for WsActor { - type Reply = Result; - - async fn handle( - &mut self, - msg: RawWsMessage, - _ctx: Context<'_, Self, Self::Reply>, - ) -> Self::Reply { - if !self.is_initialized { - let connect_message = serde_json::from_str(&msg.message)?; - self.is_initialized = true; - return Ok(WsAction::Connect(connect_message)); - } - match serde_json::from_str::(&msg.message) { - Ok(json_data) => { - // Handle different JSON message types - if let Some(type_field) = json_data.get("type") { - if let Some("user_vote") = type_field.as_str() { - if let (Some(proposal_id), Some(vote), Some(group_id)) = ( - json_data.get("proposal_id").and_then(|v| v.as_u64()), - json_data.get("vote").and_then(|v| v.as_bool()), - json_data.get("group_id").and_then(|v| v.as_str()), - ) { - return Ok(WsAction::UserVote { - proposal_id: proposal_id as u32, - vote, - group_id: group_id.to_string(), - }); - } - } - } - - // Check if it's a UserMessage format - if let (Some(message), Some(group_id)) = ( - json_data.get("message").and_then(|v| v.as_str()), - json_data.get("group_id").and_then(|v| v.as_str()), - ) { - // Handle commands - if message.starts_with("/") { - let mut tokens = message.split_whitespace(); - match tokens.next() { - Some("/ban") => { - let user_to_ban = tokens.next(); - if let Some(user_to_ban) = user_to_ban { - let user_to_ban = user_to_ban.to_lowercase(); - return Ok(WsAction::RemoveUser( - user_to_ban.to_string(), - group_id.to_string(), - )); - } else { - return Err(WsError::InvalidMessage); - } - } - _ => return Err(WsError::InvalidMessage), - } - } - - return Ok(WsAction::UserMessage(UserMessage { - message: message.as_bytes().to_vec(), - group_id: group_id.to_string(), - })); - } - - Err(WsError::InvalidMessage) - } - Err(_) => { - // Try to parse as UserMessage as fallback - match serde_json::from_str::(&msg.message) { - Ok(user_msg) => Ok(WsAction::UserMessage(user_msg)), - Err(_) => Err(WsError::InvalidMessage), - } - } - } - } -} - -/// This impl is used to send messages to the websocket -impl Message for WsActor { - type Reply = Result<(), WsError>; - - async fn handle( - &mut self, - msg: AppMessage, - _ctx: Context<'_, Self, Self::Reply>, - ) -> Self::Reply { - // Check if this is a voting proposal and format it specially for the frontend - let message_text = - if let Some(app_message::Payload::VotingProposal(voting_proposal)) = &msg.payload { - // Format as JSON for the frontend to parse - info!("[ws_actor::handle]: Sending voting proposal to ws"); - serde_json::json!({ - "type": "voting_proposal", - "proposal": { - "proposal_id": voting_proposal.proposal_id, - "group_name": voting_proposal.group_name, - "payload": voting_proposal.payload - } - }) - .to_string() - } else { - msg.to_string() - }; - self.ws_sender.send(WsMessage::Text(message_text)).await?; - Ok(()) - } -} - -#[derive(Debug, thiserror::Error)] -pub enum WsError { - #[error("Invalid message")] - InvalidMessage, - #[error("Malformed json: {0}")] - MalformedJson(#[from] serde_json::Error), - #[error("Failed to send message to websocket: {0}")] - SendMessageError(#[from] axum::Error), -} diff --git a/tests/consensus_multi_group_test.rs b/tests/consensus_multi_group_test.rs index a90623f..d06ab26 100644 --- a/tests/consensus_multi_group_test.rs +++ b/tests/consensus_multi_group_test.rs @@ -1,6 +1,6 @@ use alloy::signers::local::PrivateKeySigner; use de_mls::consensus::{compute_vote_hash, ConsensusEvent, ConsensusService}; -use de_mls::protos::messages::v1::consensus::v1::Vote; +use de_mls::protos::consensus::v1::Vote; use de_mls::LocalSigner; use prost::Message; use std::time::Duration; @@ -23,7 +23,7 @@ async fn test_basic_consensus_service() { .create_proposal( group_name, "Test Proposal".to_string(), - "Test payload".to_string(), + vec![], proposal_owner, expected_voters_count, 300, @@ -117,7 +117,7 @@ async fn test_multi_group_consensus_service() { .create_proposal( group1_name, "Test Proposal".to_string(), - "Test payload".to_string(), + vec![], proposal_owner_1, group1_members_count, 300, @@ -135,7 +135,7 @@ async fn test_multi_group_consensus_service() { .create_proposal( group2_name, "Test Proposal".to_string(), - "Test payload".to_string(), + vec![], proposal_owner_2.clone(), group2_members_count, 300, @@ -154,7 +154,7 @@ async fn test_multi_group_consensus_service() { .create_proposal( group2_name, "Test Proposal".to_string(), - "Test payload".to_string(), + vec![], proposal_owner_2, group2_members_count, 300, @@ -211,7 +211,7 @@ async fn test_consensus_threshold_calculation() { .create_proposal( group_name, "Test Proposal".to_string(), - "Test payload".to_string(), + vec![], proposal_owner, expected_voters_count, 300, @@ -328,7 +328,7 @@ async fn test_remove_group_sessions() { .create_proposal( group_name, "Test Proposal".to_string(), - "Test payload".to_string(), + vec![], proposal_owner, expected_voters_count, 300, diff --git a/tests/consensus_realtime_test.rs b/tests/consensus_realtime_test.rs index 63b910c..db01e49 100644 --- a/tests/consensus_realtime_test.rs +++ b/tests/consensus_realtime_test.rs @@ -1,6 +1,6 @@ use alloy::signers::local::PrivateKeySigner; use de_mls::consensus::{compute_vote_hash, ConsensusEvent, ConsensusService}; -use de_mls::protos::messages::v1::consensus::v1::Vote; +use de_mls::protos::consensus::v1::Vote; use de_mls::LocalSigner; use prost::Message; use std::time::Duration; @@ -22,7 +22,7 @@ async fn test_realtime_consensus_waiting() { .create_proposal( group_name, "Test Proposal".to_string(), - "Test payload".to_string(), + vec![], proposal_owner, expected_voters_count, 300, @@ -163,7 +163,7 @@ async fn test_consensus_timeout() { .create_proposal( group_name, "Test Proposal".to_string(), - "Test payload".to_string(), + vec![], proposal_owner, expected_voters_count, 300, @@ -247,7 +247,7 @@ async fn test_consensus_with_mixed_votes() { .create_proposal( group_name, "Test Proposal".to_string(), - "Test payload".to_string(), + vec![], proposal_owner, expected_voters_count, 300, @@ -399,7 +399,7 @@ async fn test_rfc_vote_chain_validation() { .create_proposal( group_name, "Test Proposal".to_string(), - "Test payload".to_string(), + vec![], signer1.address_bytes(), expected_voters_count, 300, @@ -478,7 +478,7 @@ async fn test_event_driven_timeout() { .create_proposal( group_name, "Test Proposal".to_string(), - "Test payload".to_string(), + vec![], proposal_owner, expected_voters_count, 300, @@ -554,7 +554,7 @@ async fn test_liveness_criteria_functionality() { .create_proposal( group_name, "Test Proposal False".to_string(), - "Test payload".to_string(), + vec![], proposal_owner.clone(), expected_voters_count, 300, @@ -573,8 +573,8 @@ async fn test_liveness_criteria_functionality() { let proposal_true = consensus_service .create_proposal( group_name, - "Test Proposal True".to_string(), - "Test payload".to_string(), + "Test Proposal True".to_owned(), + vec![], proposal_owner, expected_voters_count, 300, diff --git a/tests/state_machine_test.rs b/tests/state_machine_test.rs index 7caabd9..e78a49b 100644 --- a/tests/state_machine_test.rs +++ b/tests/state_machine_test.rs @@ -31,7 +31,7 @@ async fn test_state_machine_transitions() { let kp_user = id_steward .generate_key_package(&crypto) .expect("Failed to generate key package"); - group + let _ = group .store_invite_proposal(Box::new(kp_user)) .await .expect("Failed to store proposal"); @@ -118,7 +118,7 @@ async fn test_invalid_state_transitions() { let kp_user = id_steward .generate_key_package(&crypto) .expect("Failed to generate key package"); - group + let _ = group .store_invite_proposal(Box::new(kp_user)) .await .expect("Failed to store proposal"); @@ -156,12 +156,12 @@ async fn test_proposal_counting() { .generate_key_package(&crypto) .expect("Failed to generate key package"); - group + let _ = group .store_invite_proposal(Box::new(kp_user.clone())) .await .expect("Failed to store proposal"); - group - .store_remove_proposal("test_user".to_string()) + let _ = group + .store_remove_proposal("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string()) .await .expect("Failed to put remove proposal"); diff --git a/tests/user_test.rs b/tests/user_test.rs index f5d5c15..c8f3202 100644 --- a/tests/user_test.rs +++ b/tests/user_test.rs @@ -1,5 +1,6 @@ use de_mls::{ - protos::messages::v1::app_message, + consensus::ConsensusService, + protos::de_mls::messages::v1::app_message, state_machine::GroupState, user::{User, UserAction}, }; @@ -24,13 +25,17 @@ const CAROL_PRIVATE_KEY: &str = const GROUP_NAME: &str = "new_group"; async fn create_two_test_user_with_group(group_name: &str) -> (User, User) { - let mut alice = User::new(ALICE_PRIVATE_KEY).expect("Failed to create user for Alice"); + let consensus_service = ConsensusService::new(); + let mut alice = + User::new(ALICE_PRIVATE_KEY, &consensus_service).expect("Failed to create user for Alice"); alice .create_group(group_name, true) .await .expect("Failed to create group for Alice"); - let mut bob = User::new(BOB_PRIVATE_KEY).expect("Failed to create user for Bob"); + let consensus_service = ConsensusService::new(); + let mut bob = + User::new(BOB_PRIVATE_KEY, &consensus_service).expect("Failed to create user for Bob"); bob.create_group(group_name, false) .await .expect("Failed to create group for Bob"); @@ -39,18 +44,23 @@ async fn create_two_test_user_with_group(group_name: &str) -> (User, User) { } async fn create_three_test_user_with_group(group_name: &str) -> (User, User, User) { - let mut alice = User::new(ALICE_PRIVATE_KEY).expect("Failed to create user"); + let consensus_service = ConsensusService::new(); + let mut alice = + User::new(ALICE_PRIVATE_KEY, &consensus_service).expect("Failed to create user"); alice .create_group(group_name, true) .await .expect("Failed to create group for Alice"); - let mut bob = User::new(BOB_PRIVATE_KEY).expect("Failed to create user"); + let consensus_service = ConsensusService::new(); + let mut bob = User::new(BOB_PRIVATE_KEY, &consensus_service).expect("Failed to create user"); bob.create_group(group_name, false) .await .expect("Failed to create group for Bob"); - let mut carol = User::new(CAROL_PRIVATE_KEY).expect("Failed to create user"); + let consensus_service = ConsensusService::new(); + let mut carol = + User::new(CAROL_PRIVATE_KEY, &consensus_service).expect("Failed to create user"); carol .create_group(group_name, false) .await @@ -290,8 +300,8 @@ async fn user_vote_on_proposal( assert_eq!(user_state, GroupState::Voting); let proposal_id = match msg.payload { - Some(app_message::Payload::VotingProposal(proposal)) => proposal.proposal_id, - _ => panic!("User got an unexpected message: {msg:?}"), + Some(app_message::Payload::VotePayload(vote_payload)) => vote_payload.proposal_id, + _ => panic!("User got an unexpected message: {msg:?}",), }; // after getting voting proposal, user actually should send it into app and get vote result