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 825b9e6..0000000 Binary files a/frontend/static/favicon.png and /dev/null differ diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js deleted file mode 100644 index e94eb7b..0000000 --- a/frontend/svelte.config.js +++ /dev/null @@ -1,16 +0,0 @@ -import adapter from '@sveltejs/adapter-netlify'; -import preprocess from 'svelte-preprocess'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - preprocess: preprocess({ - postcss: true - }), - - kit: { - adapter: adapter() - } -}; - -export default config; - diff --git a/frontend/tailwind.config.cjs b/frontend/tailwind.config.cjs deleted file mode 100644 index c604c59..0000000 --- a/frontend/tailwind.config.cjs +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: ['./src/**/*.{html,js,svelte,ts}'], - theme: { - extend: {} - }, - plugins: [require('@tailwindcss/typography'), require('daisyui')], -}; \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json deleted file mode 100644 index cbda583..0000000 --- a/frontend/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "paths": { - "$lib": ["./src/lib"], - "$lib/*": ["./src/lib/*"] - } - } - // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias - // - // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes - // from the referenced tsconfig.json - TypeScript does not merge them in -} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts deleted file mode 100644 index 15787f7..0000000 --- a/frontend/vite.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {sveltekit} from '@sveltejs/kit/vite'; -import {defineConfig} from 'vite'; - -export default defineConfig({ - plugins: [sveltekit()], -}); diff --git a/mls_crypto/Cargo.toml b/mls_crypto/Cargo.toml index f6fa43d..21cc1e2 100644 --- a/mls_crypto/Cargo.toml +++ b/mls_crypto/Cargo.toml @@ -14,7 +14,7 @@ openmls_traits = "0.3.0" anyhow = "1.0.81" thiserror = "1.0.39" -alloy = { version = "0.11.0", features = [ +alloy = { version = "1.0.37", features = [ "providers", "node-bindings", "network", diff --git a/mls_crypto/src/error.rs b/mls_crypto/src/error.rs index db27997..48eb095 100644 --- a/mls_crypto/src/error.rs +++ b/mls_crypto/src/error.rs @@ -17,4 +17,6 @@ pub enum IdentityError { UnableToSaveSignatureKey(#[from] MemoryStorageError), #[error("Unable to create credential: {0}")] UnableToCreateCredential(#[from] CredentialError), + #[error("Invalid wallet address: {0}")] + InvalidWalletAddress(String), } diff --git a/mls_crypto/src/identity.rs b/mls_crypto/src/identity.rs index 8434d99..a98a4b9 100644 --- a/mls_crypto/src/identity.rs +++ b/mls_crypto/src/identity.rs @@ -1,8 +1,8 @@ -use alloy::{primitives::Address, signers::local::PrivateKeySigner}; +use alloy::{hex, primitives::Address, signers::local::PrivateKeySigner}; use openmls::{credentials::CredentialWithKey, key_packages::KeyPackage, prelude::BasicCredential}; use openmls_basic_credential::SignatureKeyPair; use openmls_traits::{types::Ciphersuite, OpenMlsProvider}; -use std::{collections::HashMap, fmt::Display}; +use std::{collections::HashMap, fmt::Display, str::FromStr}; use crate::error::IdentityError; use crate::openmls_provider::{MlsProvider, CIPHERSUITE}; @@ -102,3 +102,105 @@ pub fn random_identity() -> 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