├── .cargo
└── config.toml
├── .github
└── workflows
│ ├── build.yml
│ ├── clippy.yml.disabled
│ └── matrix_pr_merge_notify.yml
├── .gitignore
├── .gitmodules
├── .idea
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── modules.xml
├── river.iml
├── vcs.xml
└── workspace.xml
├── .prettierrc
├── .vscode
├── settings.json
└── tasks.json
├── .zed
└── settings.json
├── CONVENTIONS.md
├── Cargo.toml
├── DEVNOTES.md
├── LICENSE
├── Makefile.toml
├── README.md
├── common
├── Cargo.toml
└── src
│ ├── chat_delegate.rs
│ ├── crypto_values.rs
│ ├── lib.rs
│ ├── room_state.rs
│ ├── room_state
│ ├── ban.rs
│ ├── configuration.rs
│ ├── member.rs
│ ├── member_info.rs
│ ├── message.rs
│ └── upgrade.rs
│ ├── util.rs
│ └── web_container.rs
├── contracts
├── room-contract
│ ├── .rustc_info.json
│ ├── Cargo.toml
│ ├── src
│ │ └── lib.rs
│ └── wasm32-unknown-unknown
│ │ └── CACHEDIR.TAG
└── web-container-contract
│ ├── Cargo.toml
│ ├── freenet.toml
│ ├── src
│ └── lib.rs
│ ├── tests
│ └── integration_tests.rs
│ └── web-container-tool
│ ├── Cargo.toml
│ └── src
│ └── main.rs
├── delegates
└── chat-delegate
│ ├── Cargo.toml
│ ├── README.md
│ └── src
│ ├── context.rs
│ ├── handlers.rs
│ ├── lib.rs
│ ├── logging.rs
│ ├── models.rs
│ └── utils.rs
├── published-contract
├── contract-id.txt
├── web_container_contract.wasm
└── webapp.parameters
├── screenshot-20241009.png
├── tests.rs
└── ui
├── .gitignore
├── Cargo.toml
├── Dioxus.toml
├── assets
├── bulma.min.css
├── bulma.min.css.br
├── favicon.ico
├── favicon.ico.br
├── fontawesome
│ ├── css
│ │ ├── all.min.css
│ │ └── all.min.css.br
│ └── webfonts
│ │ ├── fa-brands-400.ttf
│ │ ├── fa-brands-400.ttf.br
│ │ ├── fa-brands-400.woff2
│ │ ├── fa-brands-400.woff2.br
│ │ ├── fa-regular-400.ttf
│ │ ├── fa-regular-400.ttf.br
│ │ ├── fa-regular-400.woff2
│ │ ├── fa-regular-400.woff2.br
│ │ ├── fa-solid-900.ttf
│ │ ├── fa-solid-900.ttf.br
│ │ ├── fa-solid-900.woff2
│ │ ├── fa-solid-900.woff2.br
│ │ ├── fa-v4compatibility.ttf
│ │ ├── fa-v4compatibility.ttf.br
│ │ ├── fa-v4compatibility.woff2
│ │ └── fa-v4compatibility.woff2.br
├── freenet_logo.svg
├── freenet_logo.svg.br
├── main.css
├── main.css.br
└── river_logo.svg
├── build.rs
├── freenet.toml
├── mockup.html
└── src
├── components.rs
├── components
├── app.rs
├── app
│ ├── chat_delegate.rs
│ ├── freenet_api.rs
│ ├── freenet_api
│ │ ├── connection_manager.rs
│ │ ├── constants.rs
│ │ ├── error.rs
│ │ ├── freenet_synchronizer.rs
│ │ ├── response_handler.rs
│ │ ├── response_handler
│ │ │ ├── get_response.rs
│ │ │ ├── put_response.rs
│ │ │ ├── subscribe_response.rs
│ │ │ ├── update_notification.rs
│ │ │ └── update_response.rs
│ │ └── room_synchronizer.rs
│ └── sync_info.rs
├── conversation.rs
├── conversation
│ ├── message_input.rs
│ └── not_member_notification.rs
├── members.rs
├── members
│ ├── invite_member_modal.rs
│ ├── member_info_modal.rs
│ └── member_info_modal
│ │ ├── ban_button.rs
│ │ ├── invited_by_field.rs
│ │ └── nickname_field.rs
├── room_list.rs
└── room_list
│ ├── create_room_modal.rs
│ ├── edit_room_modal.rs
│ ├── receive_invitation_modal.rs
│ └── room_name_field.rs
├── constants.rs
├── example_data.rs
├── invites.rs
├── main.rs
├── pending_invites.rs
├── room_data.rs
├── util.rs
└── util
├── ecies.rs
└── name_gen.rs
/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [target.wasm32-unknown-unknown]
2 | rustflags = ["--cfg", "getrandom_backend=\"custom\""]
3 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - main
8 | - master
9 | - develop
10 | - 'releases/**'
11 |
12 | env:
13 | CARGO_TERM_COLOR: always
14 |
15 | jobs:
16 | build:
17 | runs-on: freenet-default-runner
18 |
19 | steps:
20 | - uses: actions/checkout@v3
21 | with:
22 | submodules: recursive
23 |
24 | - name: Install Rust
25 | uses: dtolnay/rust-toolchain@stable
26 | with:
27 | targets: wasm32-unknown-unknown
28 |
29 | - name: Cache cargo
30 | uses: actions/cache@v3
31 | with:
32 | path: |
33 | ~/.cargo/registry
34 | ~/.cargo/git
35 | target/
36 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
37 | restore-keys: |
38 | ${{ runner.os }}-cargo-
39 |
40 | - name: Cache cargo-make
41 | uses: actions/cache@v3
42 | with:
43 | path: ~/.cargo/bin/cargo-make
44 | key: ${{ runner.os }}-cargo-make
45 |
46 | - name: Install cargo-make
47 | uses: actions-rs/cargo@v1
48 | with:
49 | command: install
50 | args: --force cargo-make
51 |
52 | - name: Install cargo-binstall
53 | uses: taiki-e/install-action@cargo-binstall
54 |
55 | - name: Install Dioxus CLI
56 | run: cargo binstall -y dioxus-cli
57 |
58 | - name: Build Project
59 | run: cargo make build --offline
60 |
--------------------------------------------------------------------------------
/.github/workflows/clippy.yml.disabled:
--------------------------------------------------------------------------------
1 | name: Clippy
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - main
8 | - master
9 | - develop
10 | - 'releases/**'
11 |
12 | env:
13 | CARGO_TERM_COLOR: always
14 |
15 | jobs:
16 | clippy:
17 | runs-on: freenet-default-runner
18 |
19 | steps:
20 | - uses: actions/checkout@v3
21 | with:
22 | submodules: recursive
23 |
24 | - name: Install Rust
25 | uses: dtolnay/rust-toolchain@stable
26 | with:
27 | components: clippy
28 | targets: wasm32-unknown-unknown
29 |
30 | - name: Cache cargo
31 | uses: actions/cache@v3
32 | with:
33 | path: |
34 | ~/.cargo/registry
35 | ~/.cargo/git
36 | target/
37 | key: ${{ runner.os }}-cargo-clippy-${{ hashFiles('**/Cargo.lock') }}
38 | restore-keys: |
39 | ${{ runner.os }}-cargo-clippy-
40 |
41 | - name: Install cargo-make
42 | uses: davidB/rust-cargo-make@v1
43 |
44 | - name: Run Clippy
45 | run: cargo make clippy
46 |
--------------------------------------------------------------------------------
/.github/workflows/matrix_pr_merge_notify.yml:
--------------------------------------------------------------------------------
1 | name: Notify Matrix on PR Merge
2 | on:
3 | pull_request:
4 | types:
5 | - closed
6 |
7 | jobs:
8 | notify_matrix:
9 | runs-on: ubuntu-latest
10 | if: github.event.pull_request.merged == true
11 | steps:
12 | - name: Send message to Matrix
13 | uses: olabiniV2/matrix-message@v0.0.1
14 | with:
15 | room_id: ${{ secrets.MATRIX_ROOM_ID }}
16 | access_token: ${{ secrets.MATRIX_ACCESS_TOKEN }}
17 | subject: "Pull Request Merged"
18 | message: "🤖 [Pull Request #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}](${{ github.event.pull_request.html_url }}) (created by [${{ github.event.pull_request.user.login }}](${{ github.event.pull_request.user.html_url }})) has been merged by [${{ github.event.pull_request.merged_by.login }}](${{ github.event.pull_request.merged_by.html_url }}) into the [${{ github.repository }}](${{ github.event.repository.html_url }}) repository."
19 | server: "matrix.org"
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | aider_stdlib_map.md
2 |
3 | # Generated by Cargo
4 | # will have compiled files and executables
5 | debug/
6 | target/
7 |
8 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
9 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
10 | Cargo.lock
11 |
12 | # These are backup files generated by rustfmt
13 | **/*.rs.bk
14 |
15 | # MSVC Windows builds of rustc generate these, which store debugging information
16 | *.pdb
17 |
18 | # RustRover
19 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
20 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
21 | # and can be added to the global gitignore or merged into this file. For a more nuclear
22 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
23 | #.idea/
24 | .aider*
25 | .aider.chat.history.md
26 | .env
27 |
28 | # Keep published contract files
29 | !published-contract/
30 |
31 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/.gitmodules
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/river.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "proseWrap": "always"
4 | }
5 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "rust-analyzer.diagnostics.enable": false
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "Build with dx in ui directory",
6 | "type": "shell",
7 | "command": "dx build --platform web",
8 | "options": {
9 | "cwd": "${workspaceFolder}/ui"
10 | },
11 | "problemMatcher": {
12 | "owner": "rust",
13 | "fileLocation": ["relative", "${workspaceFolder}"],
14 | "pattern": [
15 | {
16 | "regexp": "^(.+):(\\d+):(\\d+):\\s+(error|warning):\\s+(.*)$",
17 | "file": 1,
18 | "line": 2,
19 | "column": 3,
20 | "severity": 4,
21 | "message": 5
22 | }
23 | ]
24 | },
25 | "group": {
26 | "kind": "build",
27 | "isDefault": true
28 | }
29 | }
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/.zed/settings.json:
--------------------------------------------------------------------------------
1 | // Folder-specific settings
2 | //
3 | // For a full list of overridable settings, and general information on folder-specific settings,
4 | // see the documentation: https://zed.dev/docs/configuring-zed#settings-files
5 | {
6 | "lsp": {
7 | "rust-analyzer": {
8 | "initialization_options": {
9 | "rust": {
10 | "analyzerTargetDir": "contracts/room-contract"
11 | }
12 | }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/CONVENTIONS.md:
--------------------------------------------------------------------------------
1 | # River Coding Conventions
2 |
3 | - Keep files relatively short, ideally less than 200 lines.
4 |
5 | - Organize files top-down, highest level functions / structs first.
6 | - DO NOT CREATE mod.rs FILES, instead use the flat style for modules (ie. use foo.rs instead of foo/mod.rs)
7 | - If you're having weird rsx issues read https://dioxuslabs.com/learn/0.6/guide/rsx/ and
8 | https://dioxuslabs.com/learn/0.6/essentials/rsx/#
9 | - When working with Dioxus signals, avoid nested borrows of the same signal (e.g., don't call
10 | `write()` on a signal while already holding a `read()` reference). Instead, extract needed values
11 | into local variables before performing write operations.
12 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = [
3 | "common",
4 | "ui",
5 | "contracts/room-contract",
6 | "contracts/web-container-contract",
7 | "contracts/web-container-contract/web-container-tool",
8 | "delegates/chat-delegate",
9 | ]
10 | resolver = "2"
11 |
12 | [workspace.dependencies]
13 | # Serialization
14 | ciborium = "0.2.2"
15 | serde = { version = "1.0.130", features = ["derive"] }
16 |
17 | # Cryptography
18 | ed25519-dalek = { version = "2.1.1", default-features = false }
19 | blake3 = { version = "1.5.3", features = ["serde"] }
20 | x25519-dalek = { version = "2.0.0", features = ["static_secrets"] }
21 | curve25519-dalek = { version = "4.1.3" }
22 | sha2 = "0.10.8"
23 | aes-gcm = { version = "0.10.3", features = ["std"] }
24 | bs58 = "0.5.1"
25 |
26 | # Utilities
27 | byteorder = "1.5.0"
28 | rand = { version = "0.8.5", features = ["getrandom"], default-features = true }
29 | base64 = "0.22.1"
30 | once_cell = "1.18.0"
31 | data-encoding = "2.3.3"
32 | tracing = "0.1.40"
33 | chrono = { version = "0.4", features = ["serde"] }
34 |
35 | # Web-related
36 | web-sys = { version = "0.3.64", features = ["HtmlInputElement", "WindowClient", "Navigator", "Window", "UrlSearchParams", "Location"] }
37 | wasm-bindgen = "0.2.73"
38 | wasm-bindgen-futures = "0.4.43"
39 | futures-timer = "3.0.2"
40 |
41 | # Internal dependencies
42 | river-common = { path = "common", package = "river-common", default-features = true }
43 |
44 | # Freenet dependencies
45 | freenet-scaffold = "0.2.1"
46 | freenet-scaffold-macro = "0.2.1"
47 | freenet-stdlib = { version = "0.1.5", features = ["contract"] }
48 |
49 | [workspace.package]
50 | version = "0.1.0"
51 | edition = "2021"
52 |
53 | [profile.release]
54 | opt-level = 'z' # Optimize for size
55 | lto = true # Enable Link Time Optimization
56 | codegen-units = 1 # Reduce number of codegen units to increase optimizations
57 | panic = 'abort' # Abort on panic
58 | strip = true # Strip symbols from binary
59 |
60 | # Optimize dependencies in release mode
61 | [profile.release.package."*"]
62 | opt-level = 'z' # Optimize all dependencies for size as well
63 |
64 | [profile.wasm-dev]
65 | inherits = "dev"
66 | opt-level = 1
67 |
68 | [profile.server-dev]
69 | inherits = "dev"
70 |
71 | [profile.android-dev]
72 | inherits = "dev"
73 |
74 | [workspace.metadata.dx]
75 | toolchain = "nightly"
76 |
77 | # We need to pin specific versions to resolve dependency conflicts
78 |
--------------------------------------------------------------------------------
/DEVNOTES.md:
--------------------------------------------------------------------------------
1 | # River Chat Application Overview
2 |
3 | River is a decentralized chat application built on Freenet with the following key characteristics:
4 |
5 | ## Architecture
6 |
7 | - **Frontend:** Dioxus-based WebAssembly UI running in the browser
8 | - **Backend:** Freenet network for decentralized storage and communication
9 | - **Deployment:** The application is packaged, signed, and published as a Freenet contract
10 | - **Communication:** Uses WebSocket API to interact with the local Freenet node
11 |
12 | ## Key Components
13 |
14 | 1. **FreenetApiSynchronizer** – Manages WebSocket communication with Freenet
15 | 2. **Room State Management** – Implements a commutative monoid pattern for order-agnostic state
16 | updates
17 | 3. **Invitation System** – Allows users to invite others to chat rooms
18 | 4. **Cryptographic Security** – Uses ed25519 for signatures and authentication
19 |
20 | ## Implementation Details
21 |
22 | - Uses a comprehensive logging system for debugging WebSocket interactions
23 | - State updates are designed to be commutative (order-independent)
24 | - Handles both full state updates and delta updates
25 | - Implements proper error handling and status reporting
26 |
27 | ## Deployment Process
28 |
29 | 1. **Build the UI:** `cargo make build-ui`
30 | 2. **Compress the webapp:** `cargo make compress-webapp`
31 | 3. **Sign the webapp:** `cargo make sign-webapp`
32 | 4. **Publish to Freenet:** `cargo make publish-river`
33 |
34 | ## Testing Challenges
35 |
36 | - Testing requires the full deployment pipeline
37 | - Cannot easily test components in isolation
38 | - Need to publish to Freenet and access via the Freenet web interface
39 |
40 | ## Current Status
41 |
42 | - Core functionality is implemented
43 | - WebSocket API integration is ready for testing
44 | - Invitation system is implemented but untested
45 | - Limited runway to prove the concept works
46 |
--------------------------------------------------------------------------------
/common/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "river-common"
3 | version.workspace = true
4 | edition.workspace = true
5 |
6 | [dependencies]
7 | # Serialization
8 | ciborium.workspace = true
9 | serde.workspace = true
10 |
11 | # Cryptography
12 | ed25519-dalek = { workspace = true, default-features = false, features = ["alloc", "serde"] }
13 | blake3.workspace = true
14 | bs58.workspace = true
15 |
16 | # Utilities
17 | rand = { workspace = true, optional = true } # Make `rand` optional
18 | getrandom = { version = "0.2.15", optional = true, default-features = false }
19 | base64.workspace = true
20 | data-encoding.workspace = true
21 |
22 | # Internal dependencies
23 | freenet-scaffold.workspace = true
24 | freenet-scaffold-macro.workspace = true
25 | freenet-stdlib.workspace = true
26 |
27 | [dev-dependencies]
28 | rand.workspace = true
29 | ed25519-dalek = { workspace = true, features = ["rand_core"] }
30 |
--------------------------------------------------------------------------------
/common/src/chat_delegate.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 |
3 | /// Messages sent from the App to the Chat Delegate
4 | #[derive(Debug, Clone, Serialize, Deserialize)]
5 | pub enum ChatDelegateRequestMsg {
6 | StoreRequest {
7 | key: ChatDelegateKey,
8 | value: Vec,
9 | },
10 | GetRequest {
11 | key: ChatDelegateKey,
12 | },
13 | DeleteRequest {
14 | key: ChatDelegateKey,
15 | },
16 | ListRequest,
17 | }
18 |
19 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
20 | pub struct ChatDelegateKey(pub Vec);
21 |
22 | impl ChatDelegateKey {
23 | pub fn new(key: Vec) -> Self {
24 | Self(key)
25 | }
26 |
27 | pub fn as_bytes(&self) -> &[u8] {
28 | &self.0
29 | }
30 | }
31 |
32 | /// Responses sent from the Chat Delegate to the App
33 | #[derive(Debug, Clone, Serialize, Deserialize)]
34 | pub enum ChatDelegateResponseMsg {
35 | GetResponse {
36 | key: ChatDelegateKey,
37 | value: Option>,
38 | },
39 | ListResponse {
40 | keys: Vec,
41 | },
42 | StoreResponse {
43 | key: ChatDelegateKey,
44 | value_size: usize,
45 | result: Result<(), String>,
46 | },
47 | DeleteResponse {
48 | key: ChatDelegateKey,
49 | result: Result<(), String>,
50 | },
51 | }
52 |
--------------------------------------------------------------------------------
/common/src/crypto_values.rs:
--------------------------------------------------------------------------------
1 | use bs58;
2 | use ed25519_dalek::{Signature, SigningKey, VerifyingKey};
3 | use std::str::FromStr;
4 |
5 | #[derive(Debug, Clone, PartialEq)]
6 | pub enum CryptoValue {
7 | VerifyingKey(VerifyingKey),
8 | SigningKey(SigningKey),
9 | Signature(Signature),
10 | }
11 |
12 | impl CryptoValue {
13 | const VERSION_PREFIX: &'static str = "river:v1";
14 |
15 | pub fn to_encoded_string(&self) -> String {
16 | let type_str = match self {
17 | CryptoValue::VerifyingKey(_) => "vk",
18 | CryptoValue::SigningKey(_) => "sk",
19 | CryptoValue::Signature(_) => "sig",
20 | };
21 |
22 | let key_bytes = match self {
23 | CryptoValue::VerifyingKey(vk) => vk.to_bytes().to_vec(),
24 | CryptoValue::SigningKey(sk) => sk.to_bytes().to_vec(),
25 | CryptoValue::Signature(sig) => sig.to_bytes().to_vec(),
26 | };
27 |
28 | format!(
29 | "{}:{}:{}",
30 | Self::VERSION_PREFIX,
31 | type_str,
32 | bs58::encode(key_bytes).into_string()
33 | )
34 | }
35 |
36 | pub fn from_encoded_string(s: &str) -> Result {
37 | let parts: Vec<&str> = s.split(':').collect();
38 | if parts.len() != 4 || format!("{}:{}", parts[0], parts[1]) != Self::VERSION_PREFIX {
39 | return Err("Invalid format".to_string());
40 | }
41 |
42 | let decoded = bs58::decode(parts[3])
43 | .into_vec()
44 | .map_err(|e| format!("Base58 decode error: {}", e))?;
45 |
46 | match parts[2] {
47 | "vk" => {
48 | let bytes: [u8; 32] = decoded
49 | .try_into()
50 | .map_err(|_| "Invalid verifying key length".to_string())?;
51 | VerifyingKey::from_bytes(&bytes)
52 | .map(CryptoValue::VerifyingKey)
53 | .map_err(|e| format!("Invalid verifying key: {}", e))
54 | }
55 | "sk" => {
56 | let bytes: [u8; 32] = decoded
57 | .try_into()
58 | .map_err(|_| "Invalid signing key length".to_string())?;
59 | Ok(CryptoValue::SigningKey(SigningKey::from_bytes(&bytes)))
60 | }
61 | "sig" => {
62 | let bytes: [u8; 64] = decoded
63 | .try_into()
64 | .map_err(|_| "Invalid signature length".to_string())?;
65 | Ok(CryptoValue::Signature(Signature::from_bytes(&bytes)))
66 | }
67 | _ => Err("Unknown key type".to_string()),
68 | }
69 | }
70 | }
71 |
72 | impl FromStr for CryptoValue {
73 | type Err = String;
74 |
75 | fn from_str(s: &str) -> Result {
76 | // If string already contains prefix, use it directly
77 | if s.starts_with(Self::VERSION_PREFIX) {
78 | Self::from_encoded_string(s)
79 | } else {
80 | // Otherwise treat as raw base58 data
81 | let decoded = bs58::decode(s)
82 | .into_vec()
83 | .map_err(|e| format!("Base58 decode error: {}", e))?;
84 |
85 | // Try to interpret as signing key first
86 | if decoded.len() == 32 {
87 | let bytes: [u8; 32] = decoded
88 | .try_into()
89 | .map_err(|_| "Invalid signing key length".to_string())?;
90 | Ok(CryptoValue::SigningKey(SigningKey::from_bytes(&bytes)))
91 | } else {
92 | Err("Invalid key length".to_string())
93 | }
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/common/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod chat_delegate;
2 | pub mod crypto_values;
3 | pub mod room_state;
4 | pub mod util;
5 | pub mod web_container;
6 |
7 | pub use room_state::ChatRoomStateV1;
8 | pub use web_container::WebContainerMetadata;
9 |
--------------------------------------------------------------------------------
/common/src/room_state.rs:
--------------------------------------------------------------------------------
1 | pub mod ban;
2 | pub mod configuration;
3 | pub mod member;
4 | pub mod member_info;
5 | pub mod message;
6 | pub mod upgrade;
7 |
8 | use crate::room_state::ban::BansV1;
9 | use crate::room_state::configuration::AuthorizedConfigurationV1;
10 | use crate::room_state::member::{MemberId, MembersV1};
11 | use crate::room_state::member_info::MemberInfoV1;
12 | use crate::room_state::message::MessagesV1;
13 | use crate::room_state::upgrade::OptionalUpgradeV1;
14 | use ed25519_dalek::VerifyingKey;
15 | use freenet_scaffold_macro::composable;
16 | use serde::{Deserialize, Serialize};
17 |
18 | #[composable]
19 | #[derive(Serialize, Deserialize, Clone, Default, PartialEq, Debug)]
20 | pub struct ChatRoomStateV1 {
21 | // WARNING: The order of these fields is important for the purposes of the #[composable] macro.
22 | // `configuration` must be first, followed by `bans`, `members`, `member_info_modal`, and then `recent_messages`.
23 | // This is due to interdependencies between the fields and the order in which they must be applied in
24 | // the `apply_delta` function. DO NOT reorder fields without fully understanding the implications.
25 | /// Configures things like maximum message length, can be updated by the owner.
26 | pub configuration: AuthorizedConfigurationV1,
27 |
28 | /// A list of recently banned members, a banned member can't be present in the
29 | /// members list and will be removed from it ifc necessary.
30 | pub bans: BansV1,
31 |
32 | /// The members in the chat room along with who invited them
33 | pub members: MembersV1,
34 |
35 | /// Metadata about members like their nickname, can be updated by members themselves.
36 | pub member_info: MemberInfoV1,
37 |
38 | /// The most recent messages in the chat room, the number is limited by the room configuration.
39 | pub recent_messages: MessagesV1,
40 |
41 | /// If this contract has been replaced by a new contract this will contain the new contract address.
42 | /// This can only be set by the owner.
43 | pub upgrade: OptionalUpgradeV1,
44 | }
45 |
46 | #[derive(Serialize, Deserialize, Clone, Default, PartialEq, Debug)]
47 | pub struct ChatRoomParametersV1 {
48 | pub owner: VerifyingKey,
49 | }
50 |
51 | impl ChatRoomParametersV1 {
52 | pub fn owner_id(&self) -> MemberId {
53 | self.owner.into()
54 | }
55 | }
56 |
57 | #[cfg(test)]
58 | mod tests {
59 | use super::*;
60 | use crate::room_state::configuration::Configuration;
61 | use ed25519_dalek::SigningKey;
62 | use std::fmt::Debug;
63 |
64 | #[test]
65 | fn test_state() {
66 | let (state, parameters, owner_signing_key) = create_empty_chat_room_state();
67 |
68 | assert!(
69 | state.verify(&state, ¶meters).is_ok(),
70 | "Empty state should verify"
71 | );
72 |
73 | // Test that the configuration can be updated
74 | let mut new_cfg = state.configuration.configuration.clone();
75 | new_cfg.configuration_version += 1;
76 | new_cfg.max_recent_messages = 10; // Change from default of 100 to 10
77 | let new_cfg = AuthorizedConfigurationV1::new(new_cfg, &owner_signing_key);
78 |
79 | let mut cfg_modified_state = state.clone();
80 | cfg_modified_state.configuration = new_cfg;
81 | test_apply_delta(state.clone(), cfg_modified_state, ¶meters);
82 | }
83 |
84 | fn test_apply_delta(orig_state: CS, modified_state: CS, parameters: &CS::Parameters)
85 | where
86 | CS: ComposableState + Clone + PartialEq + Debug,
87 | {
88 | let orig_verify_result = orig_state.verify(&orig_state, parameters);
89 | assert!(
90 | orig_verify_result.is_ok(),
91 | "Original state verification failed: {:?}",
92 | orig_verify_result.err()
93 | );
94 |
95 | let modified_verify_result = modified_state.verify(&modified_state, parameters);
96 | assert!(
97 | modified_verify_result.is_ok(),
98 | "Modified state verification failed: {:?}",
99 | modified_verify_result.err()
100 | );
101 |
102 | let delta = modified_state.delta(
103 | &orig_state,
104 | parameters,
105 | &orig_state.summarize(&orig_state, parameters),
106 | );
107 |
108 | println!("Delta: {:?}", delta);
109 |
110 | let mut new_state = orig_state.clone();
111 | let apply_delta_result = new_state.apply_delta(&orig_state, parameters, &delta);
112 | assert!(
113 | apply_delta_result.is_ok(),
114 | "Applying delta failed: {:?}",
115 | apply_delta_result.err()
116 | );
117 |
118 | assert_eq!(new_state, modified_state);
119 | }
120 | fn create_empty_chat_room_state() -> (ChatRoomStateV1, ChatRoomParametersV1, SigningKey) {
121 | // Create a test room_state with a single member and two messages, one written by
122 | // the owner and one by the member - the member must be invited by the owner
123 | let rng = &mut rand::thread_rng();
124 | let owner_signing_key = SigningKey::generate(rng);
125 | let owner_verifying_key = owner_signing_key.verifying_key();
126 |
127 | let config = AuthorizedConfigurationV1::new(Configuration::default(), &owner_signing_key);
128 |
129 | (
130 | ChatRoomStateV1 {
131 | configuration: config,
132 | bans: BansV1::default(),
133 | members: MembersV1::default(),
134 | member_info: MemberInfoV1::default(),
135 | recent_messages: MessagesV1::default(),
136 | upgrade: OptionalUpgradeV1(None),
137 | },
138 | ChatRoomParametersV1 {
139 | owner: owner_verifying_key,
140 | },
141 | owner_signing_key,
142 | )
143 | }
144 |
145 | #[test]
146 | fn test_state_with_none_deltas() {
147 | let (state, parameters, owner_signing_key) = create_empty_chat_room_state();
148 |
149 | // Create a modified room_state with no changes (all deltas should be None)
150 | let modified_state = state.clone();
151 |
152 | // Apply the delta
153 | let summary = state.summarize(&state, ¶meters);
154 | let delta = modified_state.delta(&state, ¶meters, &summary);
155 |
156 | assert!(
157 | delta.is_none(),
158 | "Delta should be None when no changes are made"
159 | );
160 |
161 | // Now, let's modify only one field and check if other deltas are None
162 | let mut partially_modified_state = state.clone();
163 | let new_config = Configuration {
164 | configuration_version: 2,
165 | ..partially_modified_state.configuration.configuration.clone()
166 | };
167 | partially_modified_state.configuration =
168 | AuthorizedConfigurationV1::new(new_config, &owner_signing_key);
169 |
170 | let summary = state.summarize(&state, ¶meters);
171 | let delta = partially_modified_state
172 | .delta(&state, ¶meters, &summary)
173 | .unwrap();
174 |
175 | // Check that only the configuration delta is Some, and others are None
176 | assert!(
177 | delta.configuration.is_some(),
178 | "Configuration delta should be Some"
179 | );
180 | assert!(delta.bans.is_none(), "Bans delta should be None");
181 | assert!(delta.members.is_none(), "Members delta should be None");
182 | assert!(
183 | delta.member_info.is_none(),
184 | "Member info delta should be None"
185 | );
186 | assert!(
187 | delta.recent_messages.is_none(),
188 | "Recent messages delta should be None"
189 | );
190 | assert!(delta.upgrade.is_none(), "Upgrade delta should be None");
191 |
192 | // Apply the partial delta
193 | let mut new_state = state.clone();
194 | new_state
195 | .apply_delta(&state, ¶meters, &Some(delta))
196 | .unwrap();
197 |
198 | assert_eq!(
199 | new_state, partially_modified_state,
200 | "State should be partially modified"
201 | );
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/common/src/room_state/upgrade.rs:
--------------------------------------------------------------------------------
1 | use crate::room_state::member::MemberId;
2 | use crate::room_state::ChatRoomParametersV1;
3 | use crate::util::{sign_struct, truncated_base64, verify_struct};
4 | use crate::ChatRoomStateV1;
5 | use blake3::Hash;
6 | use ed25519_dalek::{Signature, SigningKey, VerifyingKey};
7 | use freenet_scaffold::ComposableState;
8 | use serde::{Deserialize, Serialize};
9 | use std::fmt;
10 |
11 | #[derive(Serialize, Deserialize, Clone, PartialEq, Debug, Default)]
12 | pub struct OptionalUpgradeV1(pub Option);
13 |
14 | #[derive(Serialize, Deserialize, Clone, PartialEq)]
15 | pub struct AuthorizedUpgradeV1 {
16 | pub upgrade: UpgradeV1,
17 | pub signature: Signature,
18 | }
19 |
20 | impl ComposableState for OptionalUpgradeV1 {
21 | type ParentState = ChatRoomStateV1;
22 | type Summary = Option;
23 | type Delta = AuthorizedUpgradeV1;
24 | type Parameters = ChatRoomParametersV1;
25 |
26 | fn verify(
27 | &self,
28 | _parent_state: &Self::ParentState,
29 | parameters: &Self::Parameters,
30 | ) -> Result<(), String> {
31 | if let Some(upgrade) = &self.0 {
32 | upgrade
33 | .validate(¶meters.owner)
34 | .map_err(|e| format!("Invalid signature: {}", e))
35 | } else {
36 | Ok(())
37 | }
38 | }
39 |
40 | fn summarize(
41 | &self,
42 | _parent_state: &Self::ParentState,
43 | _parameters: &Self::Parameters,
44 | ) -> Self::Summary {
45 | self.0.as_ref().map(|u| u.upgrade.version)
46 | }
47 |
48 | fn delta(
49 | &self,
50 | _parent_state: &Self::ParentState,
51 | _parameters: &Self::Parameters,
52 | old_state_summary: &Self::Summary,
53 | ) -> Option {
54 | match &self.0 {
55 | Some(upgrade) => {
56 | // If the upgrade has a higher version than the old room_state summary or of the old summary is None
57 | // then return the upgrade as a delta
58 | if old_state_summary.is_none_or(|old_version| upgrade.upgrade.version > old_version)
59 | {
60 | Some(upgrade.clone())
61 | } else {
62 | None
63 | }
64 | }
65 | None => None,
66 | }
67 | }
68 |
69 | fn apply_delta(
70 | &mut self,
71 | _parent_state: &Self::ParentState,
72 | parameters: &Self::Parameters,
73 | delta: &Option,
74 | ) -> Result<(), String> {
75 | if let Some(delta) = delta {
76 | // Verify the delta before applying it
77 | delta
78 | .validate(¶meters.owner)
79 | .map_err(|e| format!("Invalid upgrade signature: {}", e))?;
80 |
81 | *self = OptionalUpgradeV1(Some(delta.clone()));
82 | }
83 | Ok(())
84 | }
85 | }
86 |
87 | impl AuthorizedUpgradeV1 {
88 | pub fn new(upgrade: UpgradeV1, signing_key: &SigningKey) -> Self {
89 | Self {
90 | upgrade: upgrade.clone(),
91 | signature: sign_struct(&upgrade, signing_key),
92 | }
93 | }
94 |
95 | pub fn validate(
96 | &self,
97 | verifying_key: &VerifyingKey,
98 | ) -> Result<(), ed25519_dalek::SignatureError> {
99 | verify_struct(&self.upgrade, &self.signature, verifying_key)
100 | }
101 | }
102 |
103 | impl fmt::Debug for AuthorizedUpgradeV1 {
104 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 | f.debug_struct("AuthorizedUpgrade")
106 | .field("upgrade", &self.upgrade)
107 | .field(
108 | "signature",
109 | &format_args!("{}", truncated_base64(self.signature.to_bytes())),
110 | )
111 | .finish()
112 | }
113 | }
114 |
115 | #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
116 | pub struct UpgradeV1 {
117 | pub owner_member_id: MemberId,
118 | pub version: u8,
119 | pub new_chatroom_address: Hash,
120 | }
121 |
122 | #[cfg(test)]
123 | mod tests {
124 | use super::*;
125 | use crate::room_state::member::MemberId;
126 | use ed25519_dalek::SigningKey;
127 | use freenet_scaffold::util::FastHash;
128 | use rand::rngs::OsRng;
129 |
130 | fn create_test_upgrade(owner_id: MemberId) -> UpgradeV1 {
131 | UpgradeV1 {
132 | owner_member_id: owner_id,
133 | version: 1,
134 | new_chatroom_address: Hash::from([0; 32]),
135 | }
136 | }
137 |
138 | #[test]
139 | fn test_authorized_upgrade_new_and_validate() {
140 | let signing_key = SigningKey::generate(&mut OsRng);
141 | let verifying_key = signing_key.verifying_key();
142 | let owner_id = MemberId(FastHash(0));
143 |
144 | let upgrade = create_test_upgrade(owner_id);
145 | let authorized_upgrade = AuthorizedUpgradeV1::new(upgrade.clone(), &signing_key);
146 |
147 | assert_eq!(authorized_upgrade.upgrade, upgrade);
148 | assert!(authorized_upgrade.validate(&verifying_key).is_ok());
149 |
150 | // Test with wrong key
151 | let wrong_key = SigningKey::generate(&mut OsRng).verifying_key();
152 | assert!(authorized_upgrade.validate(&wrong_key).is_err());
153 | }
154 |
155 | #[test]
156 | fn test_optional_upgrade_verify() {
157 | let owner_signing_key = SigningKey::generate(&mut OsRng);
158 | let owner_verifying_key = owner_signing_key.verifying_key();
159 | let owner_id = MemberId::from(&owner_verifying_key);
160 |
161 | let upgrade = create_test_upgrade(owner_id);
162 | let authorized_upgrade = AuthorizedUpgradeV1::new(upgrade, &owner_signing_key);
163 |
164 | let optional_upgrade = OptionalUpgradeV1(Some(authorized_upgrade));
165 |
166 | let parent_state = ChatRoomStateV1::default();
167 | let parameters = ChatRoomParametersV1 {
168 | owner: owner_verifying_key,
169 | };
170 |
171 | // Verify that a valid upgrade passes verification
172 | assert!(
173 | optional_upgrade.verify(&parent_state, ¶meters).is_ok(),
174 | "Valid upgrade should pass verification"
175 | );
176 |
177 | // Test with invalid signature
178 | let mut invalid_upgrade = optional_upgrade.clone();
179 | if let Some(ref mut au) = invalid_upgrade.0 {
180 | au.signature = Signature::from_bytes(&[0; 64]); // Replace with an invalid signature
181 | }
182 | assert!(
183 | invalid_upgrade.verify(&parent_state, ¶meters).is_err(),
184 | "Upgrade with invalid signature should fail verification"
185 | );
186 |
187 | // Test with None
188 | let none_upgrade = OptionalUpgradeV1(None);
189 | assert!(
190 | none_upgrade.verify(&parent_state, ¶meters).is_ok(),
191 | "None upgrade should pass verification"
192 | );
193 | }
194 |
195 | #[test]
196 | fn test_optional_upgrade_summarize() {
197 | let signing_key = SigningKey::generate(&mut OsRng);
198 | let owner_id = MemberId(FastHash(0));
199 |
200 | let upgrade = create_test_upgrade(owner_id);
201 | let authorized_upgrade = AuthorizedUpgradeV1::new(upgrade, &signing_key);
202 |
203 | let optional_upgrade = OptionalUpgradeV1(Some(authorized_upgrade));
204 |
205 | let parent_state = ChatRoomStateV1::default();
206 | let parameters = ChatRoomParametersV1 {
207 | owner: signing_key.verifying_key(),
208 | };
209 |
210 | let summary = optional_upgrade.summarize(&parent_state, ¶meters);
211 | assert_eq!(summary, Some(1));
212 |
213 | let none_upgrade = OptionalUpgradeV1(None);
214 | let none_summary = none_upgrade.summarize(&parent_state, ¶meters);
215 | assert_eq!(none_summary, None);
216 | }
217 |
218 | #[test]
219 | fn test_optional_upgrade_delta() {
220 | let signing_key = SigningKey::generate(&mut OsRng);
221 | let owner_id = MemberId(FastHash(0));
222 |
223 | let upgrade = create_test_upgrade(owner_id);
224 | let authorized_upgrade = AuthorizedUpgradeV1::new(upgrade, &signing_key);
225 |
226 | let optional_upgrade = OptionalUpgradeV1(Some(authorized_upgrade.clone()));
227 |
228 | let parent_state = ChatRoomStateV1::default();
229 | let parameters = ChatRoomParametersV1 {
230 | owner: signing_key.verifying_key(),
231 | };
232 |
233 | let old_summary = None;
234 | let delta = optional_upgrade.delta(&parent_state, ¶meters, &old_summary);
235 |
236 | assert_eq!(delta, Some(authorized_upgrade));
237 |
238 | let none_upgrade = OptionalUpgradeV1(None);
239 | let none_delta = none_upgrade.delta(&parent_state, ¶meters, &old_summary);
240 | assert_eq!(none_delta, None);
241 | }
242 |
243 | #[test]
244 | fn test_optional_upgrade_apply_delta() {
245 | let signing_key = SigningKey::generate(&mut OsRng);
246 | let owner_id = MemberId(FastHash(0));
247 |
248 | let upgrade = create_test_upgrade(owner_id);
249 | let authorized_upgrade = AuthorizedUpgradeV1::new(upgrade, &signing_key);
250 |
251 | let mut optional_upgrade = OptionalUpgradeV1(None);
252 |
253 | let parent_state = ChatRoomStateV1::default();
254 | let parameters = ChatRoomParametersV1 {
255 | owner: signing_key.verifying_key(),
256 | };
257 |
258 | let delta = authorized_upgrade.clone();
259 | assert!(optional_upgrade
260 | .apply_delta(&parent_state, ¶meters, &Some(delta.clone()))
261 | .is_ok());
262 | assert_eq!(optional_upgrade, OptionalUpgradeV1(Some(delta)));
263 | }
264 | }
265 |
--------------------------------------------------------------------------------
/common/src/util.rs:
--------------------------------------------------------------------------------
1 | use base64::{engine::general_purpose, Engine as _};
2 | use data_encoding::BASE32;
3 | use ed25519_dalek::{Signature, SignatureError, Signer, SigningKey, Verifier, VerifyingKey};
4 | use serde::Serialize;
5 |
6 | pub fn sign_struct(message: T, signing_key: &SigningKey) -> Signature {
7 | let mut data_to_sign = Vec::new();
8 | ciborium::ser::into_writer(&message, &mut data_to_sign).expect("Serialization should not fail");
9 | signing_key.sign(&data_to_sign)
10 | }
11 |
12 | pub fn verify_struct(
13 | message: &T,
14 | signature: &Signature,
15 | verifying_key: &VerifyingKey,
16 | ) -> Result<(), SignatureError> {
17 | let mut data_to_sign = Vec::new();
18 | ciborium::ser::into_writer(message, &mut data_to_sign).expect("Serialization should not fail");
19 | verifying_key.verify(&data_to_sign, signature)
20 | }
21 |
22 | pub fn truncated_base64>(data: T) -> String {
23 | let encoded = general_purpose::STANDARD_NO_PAD.encode(data);
24 | encoded.chars().take(10).collect()
25 | }
26 |
27 | pub fn truncated_base32(bytes: &[u8]) -> String {
28 | let encoded = BASE32.encode(bytes);
29 | encoded.chars().take(8).collect()
30 | }
31 |
32 | #[cfg(test)]
33 | mod tests {
34 | use super::*;
35 | use rand::rngs::OsRng;
36 |
37 | #[test]
38 | fn test_sign_verify_struct() {
39 | let mut csprng = OsRng;
40 | let signing_key = SigningKey::generate(&mut csprng);
41 | let verifying_key = signing_key.verifying_key();
42 |
43 | let message = "Hello, World!";
44 | let signature = sign_struct(message, &signing_key);
45 | assert!(verify_struct(&message, &signature, &verifying_key).is_ok());
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/common/src/web_container.rs:
--------------------------------------------------------------------------------
1 | use ed25519_dalek::Signature;
2 | use serde::{Deserialize, Serialize};
3 |
4 | #[derive(Serialize, Deserialize)]
5 | pub struct WebContainerMetadata {
6 | pub version: u32,
7 | pub signature: Signature, // Signature of web interface + version number
8 | }
9 |
--------------------------------------------------------------------------------
/contracts/room-contract/.rustc_info.json:
--------------------------------------------------------------------------------
1 | {"rustc_fingerprint":10761947043259727164,"outputs":{"12016735552878863467":{"success":true,"status":"","code":0,"stdout":"rustc 1.87.0-nightly (227690a25 2025-03-16)\nbinary: rustc\ncommit-hash: 227690a258492c84ae9927d18289208d0180e62f\ncommit-date: 2025-03-16\nhost: x86_64-unknown-linux-gnu\nrelease: 1.87.0-nightly\nLLVM version: 20.1.0\n","stderr":""},"13822592305234237280":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/ian/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\nfmt_debug=\"full\"\noverflow_checks\npanic=\"unwind\"\nproc_macro\nrelocation_model=\"pic\"\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"x87\"\ntarget_has_atomic\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_has_atomic_equal_alignment=\"16\"\ntarget_has_atomic_equal_alignment=\"32\"\ntarget_has_atomic_equal_alignment=\"64\"\ntarget_has_atomic_equal_alignment=\"8\"\ntarget_has_atomic_equal_alignment=\"ptr\"\ntarget_has_atomic_load_store\ntarget_has_atomic_load_store=\"16\"\ntarget_has_atomic_load_store=\"32\"\ntarget_has_atomic_load_store=\"64\"\ntarget_has_atomic_load_store=\"8\"\ntarget_has_atomic_load_store=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_thread_local\ntarget_vendor=\"unknown\"\nub_checks\nunix\n","stderr":""}},"successes":{}}
--------------------------------------------------------------------------------
/contracts/room-contract/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "room-contract"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [dependencies]
7 | ciborium.workspace = true
8 | serde.workspace = true
9 | ed25519-dalek.workspace = true
10 | rand.workspace = true
11 | freenet-stdlib.workspace = true
12 | freenet-scaffold.workspace = true
13 | river-common.workspace = true
14 | getrandom = { version = "0.2.15", features = ["js"], default-features = false }
15 |
16 | [lib]
17 | crate-type = ["cdylib", "rlib"]
18 |
19 | [features]
20 | default = ["freenet-main-contract"]
21 | contract = ["freenet-stdlib/contract"]
22 | freenet-main-contract = []
23 | trace = ["freenet-stdlib/trace"]
24 |
--------------------------------------------------------------------------------
/contracts/room-contract/src/lib.rs:
--------------------------------------------------------------------------------
1 | use ciborium::{de::from_reader, ser::into_writer};
2 | use freenet_stdlib::prelude::*;
3 |
4 | use freenet_scaffold::ComposableState;
5 | use freenet_stdlib::prelude::ContractError;
6 | use river_common::room_state::{
7 | ChatRoomParametersV1, ChatRoomStateV1Delta, ChatRoomStateV1Summary,
8 | };
9 | use river_common::ChatRoomStateV1;
10 |
11 | #[allow(dead_code)]
12 | struct Contract;
13 |
14 | #[contract]
15 | impl ContractInterface for Contract {
16 | fn validate_state(
17 | parameters: Parameters<'static>,
18 | state: State<'static>,
19 | _related: RelatedContracts<'static>,
20 | ) -> Result {
21 | let bytes = state.as_ref();
22 | // allow empty room_state
23 | if bytes.is_empty() {
24 | return Ok(ValidateResult::Valid);
25 | }
26 | let chat_state = from_reader::(bytes)
27 | .map_err(|e| ContractError::Deser(e.to_string()))?;
28 |
29 | let parameters = from_reader::(parameters.as_ref())
30 | .map_err(|e| ContractError::Deser(e.to_string()))?;
31 |
32 | chat_state
33 | .verify(&chat_state, ¶meters)
34 | .map(|_| ValidateResult::Valid)
35 | .map_err(|_| ContractError::InvalidState)
36 | }
37 |
38 | fn update_state(
39 | parameters: Parameters<'static>,
40 | state: State<'static>,
41 | data: Vec>,
42 | ) -> Result, freenet_stdlib::prelude::ContractError> {
43 | let parameters = from_reader::(parameters.as_ref())
44 | .map_err(|e| ContractError::Deser(e.to_string()))?;
45 | let mut chat_state = from_reader::(state.as_ref())
46 | .map_err(|e| ContractError::Deser(e.to_string()))?;
47 |
48 | for update in data {
49 | match update {
50 | UpdateData::State(new_state) => {
51 | let new_state = from_reader::(new_state.as_ref())
52 | .map_err(|e| ContractError::Deser(e.to_string()))?;
53 | chat_state
54 | .merge(&chat_state.clone(), ¶meters, &new_state)
55 | .map_err(|_| ContractError::InvalidUpdate)?;
56 | }
57 | UpdateData::Delta(d) => {
58 | let delta = from_reader::(d.as_ref())
59 | .map_err(|e| ContractError::Deser(e.to_string()))?;
60 | chat_state
61 | .apply_delta(&chat_state.clone(), ¶meters, &Some(delta))
62 | .map_err(|_| ContractError::InvalidUpdate)?;
63 | }
64 | UpdateData::RelatedState {
65 | related_to: _,
66 | state: _,
67 | } => {
68 | // TODO: related room_state handling not needed for river
69 | }
70 | _ => unreachable!(),
71 | }
72 | }
73 |
74 | let mut updated_state = vec![];
75 | into_writer(&chat_state, &mut updated_state)
76 | .map_err(|e| ContractError::Deser(e.to_string()))?;
77 |
78 | Ok(UpdateModification::valid(updated_state.into()))
79 | }
80 |
81 | fn summarize_state(
82 | parameters: Parameters<'static>,
83 | state: State<'static>,
84 | ) -> Result, freenet_stdlib::prelude::ContractError> {
85 | let state = state.as_ref();
86 | if state.is_empty() {
87 | return Ok(StateSummary::from(vec![]));
88 | }
89 | let parameters = from_reader::(parameters.as_ref())
90 | .map_err(|e| ContractError::Deser(e.to_string()))?;
91 | let state = from_reader::(state)
92 | .map_err(|e| ContractError::Deser(e.to_string()))?;
93 | let summary = state.summarize(&state, ¶meters);
94 | let mut summary_bytes = vec![];
95 | into_writer(&summary, &mut summary_bytes)
96 | .map_err(|e| ContractError::Deser(e.to_string()))?;
97 | Ok(StateSummary::from(summary_bytes))
98 | }
99 |
100 | fn get_state_delta(
101 | parameters: Parameters<'static>,
102 | state: State<'static>,
103 | summary: StateSummary<'static>,
104 | ) -> Result, freenet_stdlib::prelude::ContractError> {
105 | let chat_state = from_reader::(state.as_ref())
106 | .map_err(|e| ContractError::Deser(e.to_string()))?;
107 | let parameters = from_reader::(parameters.as_ref())
108 | .map_err(|e| ContractError::Deser(e.to_string()))?;
109 | let summary = from_reader::(summary.as_ref())
110 | .map_err(|e| ContractError::Deser(e.to_string()))?;
111 | let delta = chat_state.delta(&chat_state, ¶meters, &summary);
112 | let mut delta_bytes = vec![];
113 | into_writer(&delta, &mut delta_bytes).map_err(|e| ContractError::Deser(e.to_string()))?;
114 | Ok(StateDelta::from(delta_bytes))
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/contracts/room-contract/wasm32-unknown-unknown/CACHEDIR.TAG:
--------------------------------------------------------------------------------
1 | Signature: 8a477f597d28d172789f06886806bc55
2 | # This file is a cache directory tag created by cargo.
3 | # For information about cache directory tags see https://bford.info/cachedir/
4 |
--------------------------------------------------------------------------------
/contracts/web-container-contract/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "web-container-contract"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [dependencies]
7 | byteorder.workspace = true
8 | ciborium.workspace = true
9 | freenet-stdlib.workspace = true
10 | river-common = { workspace = true }
11 | bs58 = { workspace = true }
12 |
13 | # Fully remove `rand_core` from `ed25519-dalek` for wasm builds
14 | ed25519-dalek = { workspace = true, default-features = false, features = ["alloc", "serde"] }
15 |
16 | [dev-dependencies]
17 | # Enable rand_core feature for tests
18 | ed25519-dalek = { workspace = true, features = ["rand_core"] }
19 | rand = { workspace = true, features = ["std_rng"] }
20 | tar = "0.4"
21 |
22 | [lib]
23 | crate-type = ["cdylib", "rlib"]
24 |
25 |
26 | [features]
27 | default = ["freenet-main-contract"]
28 | contract = ["freenet-stdlib/contract"]
29 | freenet-main-contract = []
30 | trace = ["freenet-stdlib/trace"]
31 |
32 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
33 | rand = { workspace = true }
34 |
35 | [target.'cfg(target_arch = "wasm32")'.dependencies]
36 | getrandom = { version = "0.2.15", features = ["js"], default-features = false }
37 |
38 |
--------------------------------------------------------------------------------
/contracts/web-container-contract/freenet.toml:
--------------------------------------------------------------------------------
1 | [contract]
2 | type = "webapp"
3 | lang = "rust"
4 |
5 | [webapp]
6 | lang = "rust"
7 | state-sources = { source_dirs = [ "../../target/dx/river-ui/release/web/public" ] }
8 | # metadata
--------------------------------------------------------------------------------
/contracts/web-container-contract/tests/integration_tests.rs:
--------------------------------------------------------------------------------
1 | #![cfg(not(target_arch = "wasm32"))]
2 |
3 | use byteorder::{BigEndian, WriteBytesExt};
4 | use ed25519_dalek::{Signer, SigningKey};
5 | use freenet_stdlib::prelude::*;
6 | use rand::rngs::OsRng;
7 | use river_common::web_container::WebContainerMetadata;
8 | use tar::Builder;
9 | use web_container_contract::WebContainerContract;
10 |
11 | // Mock implementation of freenet logger for tests
12 | #[no_mangle]
13 | pub extern "C" fn __frnt__logger__info(_ptr: i32, _len: i32) {}
14 |
15 | fn create_test_webapp() -> Vec {
16 | let mut builder = Builder::new(Vec::new());
17 | let content = b"test content";
18 | let mut header = tar::Header::new_gnu();
19 | header.set_size(content.len() as u64);
20 | builder
21 | .append_data(
22 | &mut header,
23 | &std::path::Path::new("index.html"),
24 | content.as_ref(),
25 | )
26 | .unwrap();
27 | builder.into_inner().unwrap()
28 | }
29 |
30 | #[test]
31 | fn test_tool_and_contract_compatibility() {
32 | // Generate a keypair like the tool does
33 | let signing_key = SigningKey::generate(&mut OsRng);
34 | let verifying_key = signing_key.verifying_key();
35 |
36 | // Create a test webapp archive
37 | let webapp_bytes = create_test_webapp();
38 |
39 | // Create message to sign (version + webapp) exactly as tool does
40 | let version: u32 = 1;
41 | let mut message = version.to_be_bytes().to_vec();
42 | message.extend_from_slice(&webapp_bytes);
43 |
44 | // Sign the message
45 | let signature = signing_key.sign(&message);
46 |
47 | // Create metadata struct
48 | let metadata = WebContainerMetadata { version, signature };
49 |
50 | // Serialize metadata to CBOR
51 | let mut metadata_bytes = Vec::new();
52 | ciborium::ser::into_writer(&metadata, &mut metadata_bytes).unwrap();
53 |
54 | // Create final state in WebApp format:
55 | // [metadata_length: u64][metadata: bytes][web_length: u64][web: bytes]
56 | let mut state = Vec::with_capacity(
57 | metadata_bytes.len() + webapp_bytes.len() + (std::mem::size_of::() * 2),
58 | );
59 | state
60 | .write_u64::(metadata_bytes.len() as u64)
61 | .unwrap();
62 | state.extend_from_slice(&metadata_bytes);
63 | state
64 | .write_u64::(webapp_bytes.len() as u64)
65 | .unwrap();
66 | state.extend_from_slice(&webapp_bytes);
67 |
68 | // Verify using contract code
69 | let result = WebContainerContract::validate_state(
70 | Parameters::from(verifying_key.to_bytes().to_vec()),
71 | State::from(state),
72 | RelatedContracts::default(),
73 | );
74 |
75 | assert!(matches!(result, Ok(ValidateResult::Valid)));
76 | }
77 |
78 | #[test]
79 | fn test_modified_webapp_fails_verification() {
80 | // Generate a keypair
81 | let signing_key = SigningKey::generate(&mut OsRng);
82 | let verifying_key = signing_key.verifying_key();
83 |
84 | // Create and sign original webapp
85 | let webapp_bytes = create_test_webapp();
86 | let version: u32 = 1;
87 | let mut message = version.to_be_bytes().to_vec();
88 | message.extend_from_slice(&webapp_bytes);
89 | let signature = signing_key.sign(&message);
90 |
91 | let metadata = WebContainerMetadata { version, signature };
92 |
93 | let mut metadata_bytes = Vec::new();
94 | ciborium::ser::into_writer(&metadata, &mut metadata_bytes).unwrap();
95 |
96 | // Create state but with modified webapp content
97 | let mut modified_webapp = webapp_bytes.clone();
98 | modified_webapp[0] ^= 1; // Flip one bit
99 |
100 | let mut state = Vec::with_capacity(
101 | metadata_bytes.len() + modified_webapp.len() + (std::mem::size_of::() * 2),
102 | );
103 | state
104 | .write_u64::(metadata_bytes.len() as u64)
105 | .unwrap();
106 | state.extend_from_slice(&metadata_bytes);
107 | state
108 | .write_u64::(modified_webapp.len() as u64)
109 | .unwrap();
110 | state.extend_from_slice(&modified_webapp);
111 |
112 | // This should fail verification
113 | let result = WebContainerContract::validate_state(
114 | Parameters::from(verifying_key.to_bytes().to_vec()),
115 | State::from(state),
116 | RelatedContracts::default(),
117 | );
118 |
119 | assert!(matches!(result, Err(ContractError::Other(_))));
120 | }
121 |
--------------------------------------------------------------------------------
/contracts/web-container-contract/web-container-tool/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "web-container-tool"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [dependencies]
7 | river-common.workspace = true
8 | ed25519-dalek = { workspace = true, features = ["rand_core"] }
9 | toml = "0.8"
10 | dirs = "6.0.0"
11 | rand = "0.8"
12 | clap = { version = "4.4", features = ["derive"] }
13 | ciborium.workspace = true
14 | bs58.workspace = true
15 | tracing = "0.1.41"
--------------------------------------------------------------------------------
/contracts/web-container-contract/web-container-tool/src/main.rs:
--------------------------------------------------------------------------------
1 | use clap::{Parser, Subcommand};
2 | use ed25519_dalek::{Signer, SigningKey};
3 | use river_common::crypto_values::CryptoValue;
4 | use river_common::web_container::WebContainerMetadata;
5 | use std::fs;
6 | use std::io::Write;
7 | use std::path::PathBuf;
8 |
9 | #[derive(Parser)]
10 | #[command(name = "web-container-tool")]
11 | #[command(about = "Web container key management tool")]
12 | struct Cli {
13 | #[command(subcommand)]
14 | command: Commands,
15 | }
16 |
17 | #[derive(Subcommand)]
18 | enum Commands {
19 | /// Generate a new keypair and save to config
20 | Generate,
21 | /// Sign a compressed webapp file
22 | Sign {
23 | /// Input compressed webapp file (e.g. webapp.tar.xz)
24 | #[arg(long, short)]
25 | input: String,
26 | /// Output file for metadata
27 | #[arg(long, short)]
28 | output: String,
29 | /// Output file for contract parameters
30 | #[arg(long)]
31 | parameters: String,
32 | /// Version number for the webapp
33 | #[arg(long, short)]
34 | version: u32,
35 | },
36 | }
37 |
38 | fn generate_keys() -> Result<(), Box> {
39 | // Generate keys
40 | let signing_key = SigningKey::generate(&mut rand::rngs::OsRng);
41 | let verifying_key = signing_key.verifying_key();
42 | let signing_key = CryptoValue::SigningKey(signing_key).to_encoded_string();
43 | let verifying_key = CryptoValue::VerifyingKey(verifying_key).to_encoded_string();
44 |
45 | // Create config structure
46 | let config = toml::toml! {
47 | [keys]
48 | signing_key = signing_key
49 | verifying_key = verifying_key
50 | };
51 |
52 | // Get config directory
53 | let mut config_dir = dirs::config_dir().ok_or("Could not find config directory")?;
54 | config_dir.push("river");
55 |
56 | // Create directory if it doesn't exist
57 | fs::create_dir_all(&config_dir)?;
58 |
59 | // Create config file path
60 | let mut config_path = config_dir;
61 | config_path.push("web-container-keys.toml");
62 |
63 | // Write config file
64 | fs::write(&config_path, toml::to_string(&config)?)?;
65 | println!("Keys written to: {}", config_path.display());
66 |
67 | Ok(())
68 | }
69 |
70 | fn get_config_path() -> Result> {
71 | let mut config_dir = dirs::config_dir().ok_or("Could not find config directory")?;
72 | config_dir.push("river");
73 | config_dir.push("web-container-keys.toml");
74 | Ok(config_dir)
75 | }
76 |
77 | fn read_signing_key() -> Result> {
78 | let config_path = get_config_path()?;
79 | let config_str = fs::read_to_string(&config_path)?;
80 | tracing::info!("Read config from: {}", config_path.display());
81 |
82 | let config: toml::Table = toml::from_str(&config_str)?;
83 | tracing::info!("Parsed TOML config");
84 |
85 | let signing_key_str = config["keys"]["signing_key"]
86 | .as_str()
87 | .ok_or("Could not find signing key in config")?;
88 | tracing::info!("Found signing key string: {}", signing_key_str);
89 |
90 | // Remove the "river:v1:sk:" prefix
91 | let key_data = signing_key_str
92 | .strip_prefix("river:v1:sk:")
93 | .ok_or("Signing key must start with 'river:v1:sk:'")?;
94 | tracing::info!("Stripped prefix, parsing key data: {}", key_data);
95 |
96 | tracing::info!("Attempting to parse key data as CryptoValue: {}", key_data);
97 | let signing_key = key_data
98 | .parse::()
99 | .map_err(|e| format!("Failed to parse signing key data: {}", e))?;
100 | tracing::info!("Successfully parsed as CryptoValue: {:?}", signing_key);
101 |
102 | match signing_key {
103 | CryptoValue::SigningKey(sk) => {
104 | tracing::info!("Successfully extracted SigningKey");
105 | Ok(sk)
106 | }
107 | other => Err(format!("Expected SigningKey, got {:?}", other).into()),
108 | }
109 | }
110 |
111 | fn sign_webapp(
112 | input: String,
113 | output: String,
114 | parameters: String,
115 | version: u32,
116 | ) -> Result<(), Box> {
117 | // Read the signing key
118 | let signing_key = match read_signing_key() {
119 | Ok(key) => {
120 | tracing::info!("Read signing key successfully");
121 | key
122 | }
123 | Err(e) => return Err(format!("Failed to read signing key: {}", e).into()),
124 | };
125 |
126 | // Read the compressed webapp
127 | let webapp_bytes = match fs::read(&input) {
128 | Ok(bytes) => {
129 | tracing::info!("Read {} bytes from webapp file", bytes.len());
130 | bytes
131 | }
132 | Err(e) => return Err(format!("Failed to read webapp file '{}': {}", input, e).into()),
133 | };
134 |
135 | // Create message to sign (version + webapp)
136 | let mut message = Vec::new();
137 | message.extend_from_slice(&version.to_be_bytes());
138 | message.extend_from_slice(&webapp_bytes);
139 |
140 | tracing::info!(
141 | "Created message to sign: {} bytes total ({} bytes version + {} bytes webapp)",
142 | message.len(),
143 | std::mem::size_of::(),
144 | webapp_bytes.len()
145 | );
146 | tracing::debug!("Version bytes (hex): {:02x?}", &version.to_be_bytes());
147 | tracing::debug!(
148 | "First 100 webapp bytes (hex): {:02x?}",
149 | &webapp_bytes[..100.min(webapp_bytes.len())]
150 | );
151 | tracing::debug!(
152 | "First 100 message bytes (hex): {:02x?}",
153 | &message[..100.min(message.len())]
154 | );
155 |
156 | // Output debug info
157 | let verifying_key = signing_key.verifying_key();
158 | tracing::debug!(
159 | "Verifying key (base58): {}",
160 | bs58::encode(verifying_key.to_bytes()).into_string()
161 | );
162 | tracing::debug!("Verifying key (hex): {:02x?}", verifying_key.to_bytes());
163 | tracing::info!("Message length: {} bytes", message.len());
164 | if message.len() > 20 {
165 | tracing::debug!(
166 | "Message first 10 bytes (base58): {}",
167 | bs58::encode(&message[..10]).into_string()
168 | );
169 | tracing::debug!(
170 | "Message last 10 bytes (base58): {}",
171 | bs58::encode(&message[message.len() - 10..]).into_string()
172 | );
173 | } else {
174 | tracing::debug!("Message (base58): {}", bs58::encode(&message).into_string());
175 | }
176 |
177 | // Sign the message
178 | let signature = signing_key.sign(&message);
179 | tracing::info!(
180 | "Generated signature (base58): {}",
181 | bs58::encode(signature.to_bytes()).into_string()
182 | );
183 | tracing::info!("Signature length: {} bytes", signature.to_bytes().len());
184 |
185 | // Create metadata
186 | let metadata = WebContainerMetadata { version, signature };
187 | tracing::info!("Created metadata struct with version {}", version);
188 |
189 | // Serialize metadata to check exact bytes
190 | let mut metadata_bytes = Vec::new();
191 | ciborium::ser::into_writer(&metadata, &mut metadata_bytes)
192 | .map_err(|e| format!("Failed to serialize metadata: {}", e))?;
193 | tracing::debug!("Serialized metadata size: {} bytes", metadata_bytes.len());
194 | tracing::debug!(
195 | "First 32 metadata bytes (hex): {:02x?}",
196 | &metadata_bytes[..32.min(metadata_bytes.len())]
197 | );
198 |
199 | // Create output file
200 | let mut output_file = match fs::File::create(&output) {
201 | Ok(file) => {
202 | tracing::info!("Created output file: {}", output);
203 | file
204 | }
205 | Err(e) => return Err(format!("Failed to create output file '{}': {}", output, e).into()),
206 | };
207 |
208 | // Serialize and write metadata as CBOR
209 | let mut metadata_bytes = Vec::new();
210 | ciborium::ser::into_writer(&metadata, &mut metadata_bytes)
211 | .map_err(|e| format!("Failed to serialize metadata: {}", e))?;
212 |
213 | output_file
214 | .write_all(&metadata_bytes)
215 | .map_err(|e| format!("Failed to write metadata: {}", e))?;
216 | if metadata_bytes.len() > 64 {
217 | tracing::debug!(
218 | "First 32 metadata bytes (hex): {:02x?}",
219 | &metadata_bytes[..32]
220 | );
221 | tracing::debug!(
222 | "Last 32 metadata bytes (hex): {:02x?}",
223 | &metadata_bytes[metadata_bytes.len() - 32..]
224 | );
225 | } else {
226 | tracing::debug!("Metadata bytes (hex): {:02x?}", &metadata_bytes);
227 | }
228 |
229 | println!("Metadata written to: {}", output);
230 |
231 | // Write parameters file containing verifying key bytes
232 | let verifying_key = signing_key.verifying_key();
233 | fs::write(¶meters, verifying_key.to_bytes())
234 | .map_err(|e| format!("Failed to write parameters to '{}': {}", parameters, e))?;
235 | println!("Parameters written to: {}", parameters);
236 |
237 | Ok(())
238 | }
239 |
240 | fn main() -> Result<(), Box> {
241 | let cli = Cli::parse();
242 |
243 | match cli.command {
244 | Commands::Generate => generate_keys(),
245 | Commands::Sign {
246 | input,
247 | output,
248 | parameters,
249 | version,
250 | } => sign_webapp(input, output, parameters, version),
251 | }
252 | }
253 |
--------------------------------------------------------------------------------
/delegates/chat-delegate/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "chat-delegate"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [features]
7 | default = ["freenet-main-delegate"]
8 | freenet-main-delegate = []
9 |
10 | [dependencies]
11 | ciborium.workspace = true
12 | freenet-stdlib.workspace = true
13 | bs58 = { workspace = true }
14 | serde = { workspace = true, features = ["derive"] }
15 | river-common.workspace = true
16 |
17 | [lib]
18 | crate-type = ["cdylib", "rlib"]
19 |
20 |
--------------------------------------------------------------------------------
/delegates/chat-delegate/README.md:
--------------------------------------------------------------------------------
1 | # ChatDelegate Overview
2 |
3 | The ChatDelegate is a key-value storage system for chat applications in Freenet. It provides origin-based data partitioning similar to a web browser's localStorage API, ensuring data isolation between different applications. This document explains how the delegate handles different types of messages and the overall flow of operations.
4 |
5 | ## Overview
6 |
7 | The ChatDelegate provides four main operations:
8 | 1. **Store** - Save data with a specific key
9 | 2. **Get** - Retrieve data for a key
10 | 3. **Delete** - Remove data for a key
11 | 4. **List** - Get all available keys
12 |
13 | Each operation involves a multi-step process due to the asynchronous nature of the delegate system.
14 |
15 | ## Origin-Based Data Partitioning
16 |
17 | A key security feature of the ChatDelegate is that all data is partitioned by "origin contract" - the contract key from which the user interface was downloaded. This works similarly to a web browser's localStorage, where data is partitioned by the hostname/port of the website:
18 |
19 | 1. Each piece of data is stored with a composite key that includes the origin contract ID
20 | 2. Applications can only access data that was stored by the same origin
21 | 3. This prevents different applications from accessing each other's data
22 | 4. The partitioning happens automatically and is transparent to the application
23 |
24 | This design ensures that even if multiple chat applications use the same delegate, their data remains isolated and secure from each other.
25 |
26 | ## Message Flow Architecture
27 |
28 | The delegate uses a state machine pattern where:
29 | 1. Initial application messages trigger operations
30 | 2. State is stored in a context object between operations
31 | 3. Responses from the storage system trigger follow-up actions
32 |
33 | ## Processing Inbound Messages
34 |
35 | The entry point is the `process` method in `lib.rs`, which handles four types of messages:
36 |
37 | ```rust
38 | match message {
39 | InboundDelegateMsg::ApplicationMessage(app_msg) => {
40 | // Handle client requests (Store/Get/Delete/List)
41 | handle_application_message(app_msg, &origin)
42 | }
43 | InboundDelegateMsg::GetSecretResponse(response) => {
44 | // Handle responses from the storage system
45 | handle_get_secret_response(response)
46 | }
47 | InboundDelegateMsg::UserResponse(_) => {
48 | // Not used in this delegate
49 | Ok(vec![])
50 | }
51 | InboundDelegateMsg::GetSecretRequest(_) => {
52 | // Not handled directly
53 | Err(DelegateError::Other("unexpected message type: get secret request".into()))
54 | }
55 | }
56 | ```
57 |
58 | ## Key Concepts
59 |
60 | Before diving into specific flows, let's understand some key concepts:
61 |
62 | 1. **Origin**: The contract ID that identifies the application
63 | 2. **ChatDelegateKey**: A wrapper around a byte vector that represents a key
64 | 3. **KeyIndex**: A list of all keys for a specific origin
65 | 4. **PendingOperation**: State stored in the context to track ongoing operations
66 |
67 | ## 1. Store Operation Flow
68 |
69 | When a client wants to store data:
70 |
71 | 1. **Client sends a StoreRequest**:
72 | ```rust
73 | ChatDelegateRequestMsg::StoreRequest { key, value }
74 | ```
75 |
76 | 2. **Delegate processes the request** (`handle_store_request`):
77 | - Creates a unique storage key by combining the origin and client key
78 | - Stores the operation in context for later processing
79 | - Immediately sends back a success response to the client
80 | - Sends a request to store the actual value
81 | - Requests the current key index to update it
82 |
83 | 3. **Delegate receives the key index** (`handle_key_index_response`):
84 | - Adds the new key to the index if it doesn't exist
85 | - Updates the index in storage
86 | - The operation is complete
87 |
88 | ## 2. Get Operation Flow
89 |
90 | When a client wants to retrieve data:
91 |
92 | 1. **Client sends a GetRequest**:
93 | ```rust
94 | ChatDelegateRequestMsg::GetRequest { key }
95 | ```
96 |
97 | 2. **Delegate processes the request** (`handle_get_request`):
98 | - Creates the unique storage key
99 | - Stores the operation in context
100 | - Sends a request to get the value from storage
101 |
102 | 3. **Delegate receives the value** (`handle_regular_get_response`):
103 | - Retrieves the pending operation from context
104 | - Sends the value back to the client
105 | - The operation is complete
106 |
107 | ## 3. Delete Operation Flow
108 |
109 | When a client wants to delete data:
110 |
111 | 1. **Client sends a DeleteRequest**:
112 | ```rust
113 | ChatDelegateRequestMsg::DeleteRequest { key }
114 | ```
115 |
116 | 2. **Delegate processes the request** (`handle_delete_request`):
117 | - Creates the unique storage key
118 | - Stores the operation in context
119 | - Immediately sends back a success response to the client
120 | - Sends a request to delete the value (by setting it to None)
121 | - Requests the current key index to update it
122 |
123 | 3. **Delegate receives the key index** (`handle_key_index_response`):
124 | - Removes the key from the index
125 | - Updates the index in storage
126 | - The operation is complete
127 |
128 | ## 4. List Operation Flow
129 |
130 | When a client wants to list all keys:
131 |
132 | 1. **Client sends a ListRequest**:
133 | ```rust
134 | ChatDelegateRequestMsg::ListRequest
135 | ```
136 |
137 | 2. **Delegate processes the request** (`handle_list_request`):
138 | - Stores the operation in context
139 | - Requests the current key index
140 |
141 | 3. **Delegate receives the key index** (`handle_key_index_response`):
142 | - Sends the list of keys back to the client
143 | - The operation is complete
144 |
145 | ## Key Management
146 |
147 | A critical aspect of the delegate is how it manages keys:
148 |
149 | 1. **Origin-Based Key Namespacing**: Each key is prefixed with the origin contract ID to enforce data partitioning:
150 | ```rust
151 | pub(crate) fn create_origin_key(origin: &Origin, key: &ChatDelegateKey) -> SecretsId {
152 | SecretsId::new(
153 | format!("{}{}{}", origin.to_b58(), ORIGIN_KEY_SEPARATOR,
154 | String::from_utf8_lossy(key.as_bytes()).to_string()).into_bytes()
155 | )
156 | }
157 | ```
158 |
159 | This ensures that:
160 | - Data from different origins is completely isolated
161 | - Applications can only access their own data
162 | - Key collisions between different applications are impossible
163 |
164 | 2. **Origin-Specific Key Index**: Each origin has its own separate key index:
165 | ```rust
166 | pub(crate) fn create_index_key(origin: &Origin) -> SecretsId {
167 | SecretsId::new(format!(
168 | "{}{}{}",
169 | origin.to_b58(),
170 | ORIGIN_KEY_SEPARATOR,
171 | KEY_INDEX_SUFFIX
172 | ).into_bytes())
173 | }
174 | ```
175 |
176 | This means:
177 | - Each application has its own isolated list of keys
178 | - The ListRequest operation only returns keys for the calling application's origin
179 | - Applications cannot enumerate keys from other origins
180 |
181 | ## Context Management
182 |
183 | The delegate uses a context object to maintain state between operations:
184 |
185 | ```rust
186 | pub(super) struct ChatDelegateContext {
187 | pub(super) pending_ops: HashMap,
188 | }
189 | ```
190 |
191 | This context is serialized and passed along with requests, then deserialized when responses are received.
192 |
193 | ## Tying It All Together
194 |
195 | The overall flow for any operation follows this pattern:
196 |
197 | 1. **Client Request**: Application sends a request to the delegate
198 | 2. **Initial Processing**: Delegate creates necessary storage keys and stores state in context
199 | 3. **Storage Operations**: Delegate interacts with the storage system
200 | 4. **Response Handling**: Delegate processes storage responses and updates state
201 | 5. **Client Response**: Delegate sends final response back to the application
202 |
203 | This asynchronous, multi-step approach allows the delegate to maintain consistency while providing a simple interface to client applications.
204 |
205 | ## Example: Complete Store Flow
206 |
207 | Let's trace a complete store operation with origin partitioning:
208 |
209 | 1. Client sends `StoreRequest { key: "user123", value: [profile data] }`
210 | 2. Delegate:
211 | - Identifies the origin contract ID (e.g., `abc123`) from which the request came
212 | - Creates storage key: `abc123:user123` (prefixing the client key with origin)
213 | - Creates index key: `abc123::key_index` (origin-specific index)
214 | - Stores pending operation in context
215 | - Sends success response to client
216 | - Sends request to store value at `abc123:user123`
217 | - Sends request to get current index at `abc123::key_index`
218 | 3. Delegate receives index (or empty if first key)
219 | 4. Delegate:
220 | - Adds "user123" to index if not present
221 | - Updates index in storage
222 | - Operation complete
223 |
224 | If a different application with origin `xyz789` tries to access this data:
225 | - It would use key `xyz789:user123` which is different from `abc123:user123`
226 | - It would not find the data stored by the first application
227 | - It would have its own separate key index at `xyz789::key_index`
228 |
229 | This architecture ensures both data consistency and security through origin-based isolation, while providing a responsive experience for client applications.
230 |
--------------------------------------------------------------------------------
/delegates/chat-delegate/src/context.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 |
3 | /// Context for the chat delegate, storing pending operations.
4 | #[derive(Debug, Clone, Serialize, Deserialize, Default)]
5 | pub(super) struct ChatDelegateContext {
6 | /// Map of secret IDs to pending operations
7 | pub(super) pending_ops: HashMap,
8 | }
9 |
10 | /// Structure to store the index of keys for an app
11 | #[derive(Debug, Clone, Serialize, Deserialize, Default)]
12 | pub(super) struct KeyIndex {
13 | pub(super) keys: Vec,
14 | }
15 |
16 | impl TryFrom for ChatDelegateContext {
17 | type Error = DelegateError;
18 |
19 | fn try_from(value: DelegateContext) -> Result {
20 | if value == DelegateContext::default() {
21 | return Ok(Self::default());
22 | }
23 | ciborium::from_reader(value.as_ref())
24 | .map_err(|err| DelegateError::Deser(format!("Failed to deserialize context: {err}")))
25 | }
26 | }
27 |
28 | impl TryFrom<&ChatDelegateContext> for DelegateContext {
29 | type Error = DelegateError;
30 |
31 | fn try_from(value: &ChatDelegateContext) -> Result {
32 | let mut buffer = Vec::new();
33 | ciborium::ser::into_writer(value, &mut buffer)
34 | .map_err(|err| DelegateError::Deser(format!("Failed to serialize context: {err}")))?;
35 | Ok(DelegateContext::new(buffer))
36 | }
37 | }
38 |
39 | impl TryFrom<&mut ChatDelegateContext> for DelegateContext {
40 | type Error = DelegateError;
41 |
42 | fn try_from(value: &mut ChatDelegateContext) -> Result {
43 | // Delegate to the immutable reference implementation
44 | Self::try_from(&*value)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/delegates/chat-delegate/src/lib.rs:
--------------------------------------------------------------------------------
1 | mod context;
2 | mod handlers;
3 | mod models;
4 | mod utils;
5 |
6 | use context::*;
7 | use freenet_stdlib::prelude::{
8 | delegate, ApplicationMessage, DelegateContext, DelegateError, DelegateInterface,
9 | GetSecretRequest, InboundDelegateMsg, OutboundDelegateMsg, Parameters, SecretsId,
10 | SetSecretRequest,
11 | };
12 | use handlers::*;
13 | use models::*;
14 | use utils::*;
15 |
16 | // Custom logging module to handle different environments
17 | mod logging;
18 |
19 | use river_common::chat_delegate::*;
20 | use serde::ser::SerializeTuple;
21 | use serde::{Deserialize, Serialize};
22 | use std::collections::HashMap;
23 |
24 | /// Chat delegate for storing and retrieving data in the Freenet secret storage.
25 | ///
26 | /// This delegate provides a key-value store interface for chat applications,
27 | /// maintaining an index of keys for each application and handling storage,
28 | /// retrieval, deletion, and listing operations.
29 | pub struct ChatDelegate;
30 |
31 | #[delegate]
32 | impl DelegateInterface for ChatDelegate {
33 | fn process(
34 | _parameters: Parameters<'static>,
35 | attested: Option<&'static [u8]>,
36 | message: InboundDelegateMsg,
37 | ) -> Result, DelegateError> {
38 | let message_type = match message {
39 | InboundDelegateMsg::ApplicationMessage(_) => "application message",
40 | InboundDelegateMsg::GetSecretResponse(_) => "get secret response",
41 | InboundDelegateMsg::UserResponse(_) => "user response",
42 | InboundDelegateMsg::GetSecretRequest(_) => "get secret request",
43 | };
44 |
45 | logging::info(&format!("Delegate received message of type {message_type}"));
46 |
47 | // Verify that attested is provided - this is the authenticated origin
48 | let origin: Origin = match attested {
49 | Some(origin) => Origin(origin.to_vec()),
50 | None => {
51 | logging::info("Missing attested origin");
52 | return Err(DelegateError::Other(format!(
53 | "missing attested origin for message type: {:?}",
54 | message_type
55 | )));
56 | }
57 | };
58 |
59 | let result = match message {
60 | InboundDelegateMsg::ApplicationMessage(app_msg) => {
61 | if app_msg.processed {
62 | logging::info("Received already processed message");
63 | Err(DelegateError::Other(
64 | "cannot process an already processed message".into(),
65 | ))
66 | } else {
67 | handle_application_message(app_msg, &origin)
68 | }
69 | }
70 |
71 | InboundDelegateMsg::GetSecretResponse(response) => handle_get_secret_response(response),
72 |
73 | InboundDelegateMsg::UserResponse(_) => {
74 | logging::info("Received unexpected UserResponse");
75 | Err(DelegateError::Other(
76 | "unexpected message type: UserResponse".into(),
77 | ))
78 | }
79 |
80 | InboundDelegateMsg::GetSecretRequest(_) => {
81 | // We don't handle direct get secret requests
82 | logging::info("Received unexpected GetSecretRequest");
83 | Err(DelegateError::Other(
84 | "unexpected message type: GetSecretRequest".into(),
85 | ))
86 | }
87 | };
88 |
89 | match &result {
90 | Ok(msgs) => logging::info(&format!("Process returning {} messages", msgs.len())),
91 | Err(e) => logging::info(&format!("Process returning error: {e}")),
92 | }
93 |
94 | result
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/delegates/chat-delegate/src/logging.rs:
--------------------------------------------------------------------------------
1 | #[cfg(target_arch = "wasm32")]
2 | pub fn info(msg: &str) {
3 | freenet_stdlib::log::info(msg);
4 | }
5 |
6 | #[cfg(not(target_arch = "wasm32"))]
7 | pub fn info(msg: &str) {
8 | println!("[INFO] {}", msg);
9 | }
10 |
--------------------------------------------------------------------------------
/delegates/chat-delegate/src/models.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 | use freenet_stdlib::prelude::ContractInstanceId;
3 |
4 | // Constants
5 | pub(crate) const KEY_INDEX_SUFFIX: &str = "::key_index";
6 | pub(crate) const ORIGIN_KEY_SEPARATOR: &str = ":";
7 |
8 | /// Different types of pending operations
9 | #[derive(Debug, Clone)]
10 | pub(crate) enum PendingOperation {
11 | /// Regular get operation for a specific key
12 | Get {
13 | origin: Origin,
14 | client_key: ChatDelegateKey,
15 | app: ContractInstanceId,
16 | },
17 | /// Store operation that needs to update the index
18 | Store {
19 | origin: Origin,
20 | client_key: ChatDelegateKey,
21 | },
22 | /// Delete operation that needs to update the index
23 | Delete {
24 | origin: Origin,
25 | client_key: ChatDelegateKey,
26 | },
27 | /// List operation to retrieve all keys
28 | List {
29 | origin: Origin,
30 | app: ContractInstanceId,
31 | },
32 | }
33 |
34 | impl PendingOperation {
35 | pub(crate) fn is_delete_operation(&self) -> bool {
36 | matches!(self, Self::Delete { .. })
37 | }
38 | }
39 |
40 | /// Parameters for the chat delegate.
41 | /// Currently empty, but could be extended with configuration options.
42 | #[derive(Debug, Clone, Serialize, Deserialize)]
43 | pub(crate) struct ChatDelegateParameters;
44 |
45 | impl TryFrom> for ChatDelegateParameters {
46 | type Error = DelegateError;
47 |
48 | fn try_from(_params: Parameters<'_>) -> Result {
49 | // Currently no parameters are used, but this could be extended
50 | Ok(Self {})
51 | }
52 | }
53 |
54 | /// A wrapper around SecretsId that implements Hash and Eq
55 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
56 | pub(crate) struct SecretIdKey(pub(crate) String);
57 |
58 | impl From<&SecretsId> for SecretIdKey {
59 | fn from(id: &SecretsId) -> Self {
60 | // Convert the SecretsId to a string representation for hashing
61 | Self(String::from_utf8_lossy(id.key()).to_string())
62 | }
63 | }
64 |
65 | // Add Serialize/Deserialize for PendingOperation
66 | impl Serialize for PendingOperation {
67 | fn serialize(&self, serializer: S) -> Result
68 | where
69 | S: serde::Serializer,
70 | {
71 | match self {
72 | Self::Get {
73 | origin,
74 | client_key,
75 | app,
76 | } => {
77 | let mut seq = serializer.serialize_tuple(4)?; // Increased size
78 | seq.serialize_element(&0u8)?; // Type tag for Get
79 | seq.serialize_element(origin)?;
80 | seq.serialize_element(client_key)?;
81 | seq.serialize_element(app)?; // Serialize app
82 | seq.end()
83 | }
84 | Self::Store { origin, client_key } => {
85 | let mut seq = serializer.serialize_tuple(3)?;
86 | seq.serialize_element(&1u8)?; // Type tag for Store
87 | seq.serialize_element(origin)?;
88 | seq.serialize_element(client_key)?;
89 | seq.end()
90 | }
91 | Self::Delete { origin, client_key } => {
92 | let mut seq = serializer.serialize_tuple(3)?;
93 | seq.serialize_element(&2u8)?; // Type tag for Delete
94 | seq.serialize_element(origin)?;
95 | seq.serialize_element(client_key)?;
96 | seq.end()
97 | }
98 | Self::List { origin, app } => {
99 | let mut seq = serializer.serialize_tuple(3)?; // Increased size
100 | seq.serialize_element(&3u8)?; // Type tag for List
101 | seq.serialize_element(origin)?;
102 | seq.serialize_element(app)?; // Serialize app
103 | seq.end()
104 | }
105 | }
106 | }
107 | }
108 |
109 | impl<'de> Deserialize<'de> for PendingOperation {
110 | fn deserialize(deserializer: D) -> Result
111 | where
112 | D: serde::Deserializer<'de>,
113 | {
114 | use serde::de::{Error, SeqAccess, Visitor};
115 | use std::fmt;
116 |
117 | struct PendingOpVisitor;
118 |
119 | impl<'de> Visitor<'de> for PendingOpVisitor {
120 | type Value = PendingOperation;
121 |
122 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
123 | formatter.write_str("a tuple with a type tag and operation data")
124 | }
125 |
126 | fn visit_seq(self, mut seq: A) -> Result
127 | where
128 | A: SeqAccess<'de>,
129 | {
130 | let tag: u8 = seq
131 | .next_element()?
132 | .ok_or_else(|| Error::invalid_length(0, &self))?;
133 |
134 | match tag {
135 | 0 => {
136 | // Get
137 | let origin: Origin = seq
138 | .next_element()?
139 | .ok_or_else(|| Error::invalid_length(1, &self))?;
140 | let client_key: ChatDelegateKey = seq
141 | .next_element()?
142 | .ok_or_else(|| Error::invalid_length(2, &self))?;
143 | let app: ContractInstanceId = seq
144 | .next_element()?
145 | .ok_or_else(|| Error::invalid_length(3, &self))?; // Deserialize app
146 | Ok(PendingOperation::Get {
147 | origin,
148 | client_key,
149 | app,
150 | })
151 | }
152 | 1 => {
153 | // Store
154 | let origin: Origin = seq
155 | .next_element()?
156 | .ok_or_else(|| Error::invalid_length(1, &self))?;
157 | let client_key: ChatDelegateKey = seq
158 | .next_element()?
159 | .ok_or_else(|| Error::invalid_length(2, &self))?;
160 | Ok(PendingOperation::Store { origin, client_key })
161 | }
162 | 2 => {
163 | // Delete
164 | let origin: Origin = seq
165 | .next_element()?
166 | .ok_or_else(|| Error::invalid_length(1, &self))?;
167 | let client_key: ChatDelegateKey = seq
168 | .next_element()?
169 | .ok_or_else(|| Error::invalid_length(2, &self))?;
170 | Ok(PendingOperation::Delete { origin, client_key })
171 | }
172 | 3 => {
173 | // List
174 | let origin: Origin = seq
175 | .next_element()?
176 | .ok_or_else(|| Error::invalid_length(1, &self))?;
177 | let app: ContractInstanceId = seq
178 | .next_element()?
179 | .ok_or_else(|| Error::invalid_length(2, &self))?; // Deserialize app
180 | Ok(PendingOperation::List { origin, app })
181 | }
182 | _ => Err(Error::custom(format!(
183 | "Unknown operation type tag: {}",
184 | tag
185 | ))),
186 | }
187 | }
188 | }
189 | // Adjust expected length based on the variant (max length is now 4 for Get)
190 | // Note: Deserializing tuples with variable lengths like this can be tricky.
191 | // A struct-based approach might be more robust if complexity increases.
192 | // For now, deserialize_tuple with a max length should work if variants are handled correctly.
193 | deserializer.deserialize_tuple(4, PendingOpVisitor)
194 | }
195 | }
196 |
197 | /// Origin contract ID
198 | #[derive(Debug, Clone, Serialize, Deserialize)]
199 | pub(crate) struct Origin(pub(crate) Vec);
200 |
201 | impl Origin {
202 | pub(crate) fn to_b58(&self) -> String {
203 | bs58::encode(&self.0).into_string()
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/delegates/chat-delegate/src/utils.rs:
--------------------------------------------------------------------------------
1 | use super::*;
2 | use freenet_stdlib::prelude::ContractInstanceId;
3 |
4 | /// Helper function to create a unique app key
5 | pub(crate) fn create_origin_key(origin: &Origin, key: &ChatDelegateKey) -> SecretsId {
6 | SecretsId::new(
7 | format!(
8 | "{}{}{}",
9 | origin.to_b58(),
10 | ORIGIN_KEY_SEPARATOR,
11 | String::from_utf8_lossy(key.as_bytes()).to_string()
12 | )
13 | .into_bytes(),
14 | )
15 | }
16 |
17 | /// Helper function to create an index key
18 | pub(crate) fn create_index_key(origin: &Origin) -> SecretsId {
19 | SecretsId::new(
20 | format!(
21 | "{}{}{}",
22 | origin.to_b58(),
23 | ORIGIN_KEY_SEPARATOR,
24 | KEY_INDEX_SUFFIX
25 | )
26 | .into_bytes(),
27 | )
28 | }
29 |
30 | /// Helper function to create a get request
31 | pub(crate) fn create_get_request(
32 | secret_id: SecretsId,
33 | context: &DelegateContext,
34 | ) -> Result {
35 | let get_secret = OutboundDelegateMsg::GetSecretRequest(GetSecretRequest {
36 | key: secret_id,
37 | context: context.clone(),
38 | processed: false,
39 | });
40 |
41 | Ok(get_secret)
42 | }
43 |
44 | /// Helper function to create a get index request
45 | pub(crate) fn create_get_index_request(
46 | index_secret_id: SecretsId,
47 | context: &DelegateContext,
48 | ) -> Result {
49 | let get_index = OutboundDelegateMsg::GetSecretRequest(GetSecretRequest {
50 | key: index_secret_id,
51 | context: context.clone(),
52 | processed: false,
53 | });
54 |
55 | Ok(get_index)
56 | }
57 |
58 | /// Helper function to create an app response
59 | pub(crate) fn create_app_response(
60 | response: &T,
61 | context: &DelegateContext,
62 | app: ContractInstanceId,
63 | ) -> Result {
64 | // Serialize response
65 | let mut response_bytes = Vec::new();
66 | ciborium::ser::into_writer(response, &mut response_bytes)
67 | .map_err(|e| DelegateError::Deser(format!("Failed to serialize response: {e}")))?;
68 |
69 | logging::info(&format!(
70 | "Creating app response with {} bytes",
71 | response_bytes.len()
72 | ));
73 |
74 | // Create response message
75 | let app_msg = ApplicationMessage::new(app, response_bytes)
76 | .with_context(context.clone())
77 | .processed(false); //
78 |
79 | Ok(OutboundDelegateMsg::ApplicationMessage(app_msg))
80 | }
81 |
--------------------------------------------------------------------------------
/published-contract/contract-id.txt:
--------------------------------------------------------------------------------
1 | BcfxyjCH4snaknrBoCiqhYc9UFvmiJvhsp5d4L5DuvRa
2 |
--------------------------------------------------------------------------------
/published-contract/web_container_contract.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/published-contract/web_container_contract.wasm
--------------------------------------------------------------------------------
/published-contract/webapp.parameters:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/published-contract/webapp.parameters
--------------------------------------------------------------------------------
/screenshot-20241009.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/screenshot-20241009.png
--------------------------------------------------------------------------------
/tests.rs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/tests.rs
--------------------------------------------------------------------------------
/ui/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 | /dist/
5 | /static/
6 | /.dioxus/
7 |
8 | # this file will generate by tailwind:
9 | /assets/tailwind.css
10 |
11 | # These are backup files generated by rustfmt
12 | **/*.rs.bk
13 |
14 | public/
15 |
--------------------------------------------------------------------------------
/ui/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "river-ui"
3 | version.workspace = true
4 | edition.workspace = true
5 | authors = ["Ian Clarke "]
6 |
7 | [features]
8 | default = [] # Add a default feature set if needed, often empty
9 | delegate = [] # Define the delegate feature
10 | example-data = []
11 | no-sync = []
12 |
13 | [dependencies]
14 | bs58 = "0.5.0"
15 | serde.workspace = true
16 | # Cryptography
17 | curve25519-dalek.workspace = true
18 | x25519-dalek.workspace = true
19 | # We can use rand_core in the UI crate because the wasm runs in a browser
20 | ed25519-dalek = { workspace = true, features = ["rand_core"] }
21 | sha2.workspace = true
22 | aes-gcm.workspace = true
23 |
24 | # Randomness
25 | rand.workspace = true
26 | getrandom = { version = "0.2.15", features = ["js", "wasm-bindgen", "js-sys"], default-features = false }
27 |
28 | # UI Framework
29 | dioxus = { version = "0.6.3", features = ["web"] }
30 |
31 | #dioxus-free-icons = { version = "0.8.6", features = ["font-awesome-brands", "font-awesome-regular", "font-awesome-solid"] }
32 | # Apprently the above doesn't work with dioxus 0.6 yet, so we use the git version
33 | #dioxus-free-icons = { git = "https://github.com/dioxus-community/dioxus-free-icons.git", branch = "feat/dioxus-0.6", features = ["font-awesome-brands", "font-awesome-regular", "font-awesome-solid"] }
34 | dioxus-free-icons = { version = "0.9.0", features = ["font-awesome-brands", "font-awesome-regular", "font-awesome-solid"] }
35 |
36 | # Web-related
37 | web-sys = { workspace = true, features = [
38 | "Clipboard",
39 | "Navigator",
40 | "Window",
41 | "Crypto",
42 | "Headers",
43 | "Response",
44 | "UrlSearchParams"
45 | ] }
46 | wasm-bindgen.workspace = true
47 | wasm-bindgen-futures.workspace = true
48 | lipsum = "0.9.1"
49 |
50 | # Utilities
51 | chrono.workspace = true
52 | markdown = "1.0.0-alpha.21"
53 | ciborium = "0.2.2"
54 | js-sys = "0.3.64"
55 | thiserror = "2.0.12"
56 | log = "0.4" # Add log crate
57 |
58 | # Internal dependencies
59 | river-common.workspace = true
60 |
61 | # Freenet dependencies
62 | freenet-scaffold.workspace = true
63 | freenet-stdlib = { workspace = true, features = ["net"] }
64 |
65 | futures = "0.3.30"
66 | futures-timer = "3.0.3"
67 |
68 | # Add this section for the build script
69 | [build-dependencies]
70 | chrono = "0.4"
71 |
--------------------------------------------------------------------------------
/ui/Dioxus.toml:
--------------------------------------------------------------------------------
1 | [application]
2 |
3 | # App (Project) Name
4 | name = "chatui"
5 |
6 | # Dioxus App Default Platform
7 | # web, desktop, fullstack
8 | default_platform = "web"
9 |
10 | # `build` & `serve` dist path
11 | out_dir = "dist"
12 |
13 | # resource (assets) file folder
14 | asset_dir = "assets"
15 |
16 | [web.app]
17 |
18 | # base_path will be uncommented during building by the uncomment-base-path task in Makefile.toml, and then
19 | # re-commented afterwards - so except during building it should be commented out I've requested an
20 | # improvement to dioxus to avoid this, see https://github.com/DioxusLabs/dioxus/issues/3745
21 |
22 | base_path = "v1/contract/web/BcfxyjCH4snaknrBoCiqhYc9UFvmiJvhsp5d4L5DuvRa"
23 |
24 | # HTML title tag content
25 | title = "River"
26 |
27 | [web.watcher]
28 |
29 | # when watcher trigger, regenerate the `index.html`
30 | reload_html = true
31 |
32 | # which files or dirs will be watcher monitoring
33 | watch_path = ["src", "assets"]
34 |
35 | # include `assets` in web platform
36 | [web.resource]
37 |
38 | # CSS style file
39 | #style = ["bulma.min.css", "main.css", "fontawesome/css/all.min.css"]
40 |
41 | # Javascript code file
42 | script = []
43 |
44 | # Static files to be included in the project
45 | include = ["fontawesome/webfonts"]
46 |
47 | [web.resource.dev]
48 |
49 | # Javascript code file
50 | # serve: [dev-server] only
51 | script = []
52 |
53 |
54 |
--------------------------------------------------------------------------------
/ui/assets/bulma.min.css.br:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/bulma.min.css.br
--------------------------------------------------------------------------------
/ui/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/favicon.ico
--------------------------------------------------------------------------------
/ui/assets/favicon.ico.br:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/favicon.ico.br
--------------------------------------------------------------------------------
/ui/assets/fontawesome/css/all.min.css.br:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/fontawesome/css/all.min.css.br
--------------------------------------------------------------------------------
/ui/assets/fontawesome/webfonts/fa-brands-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/fontawesome/webfonts/fa-brands-400.ttf
--------------------------------------------------------------------------------
/ui/assets/fontawesome/webfonts/fa-brands-400.ttf.br:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/fontawesome/webfonts/fa-brands-400.ttf.br
--------------------------------------------------------------------------------
/ui/assets/fontawesome/webfonts/fa-brands-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/fontawesome/webfonts/fa-brands-400.woff2
--------------------------------------------------------------------------------
/ui/assets/fontawesome/webfonts/fa-brands-400.woff2.br:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/fontawesome/webfonts/fa-brands-400.woff2.br
--------------------------------------------------------------------------------
/ui/assets/fontawesome/webfonts/fa-regular-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/fontawesome/webfonts/fa-regular-400.ttf
--------------------------------------------------------------------------------
/ui/assets/fontawesome/webfonts/fa-regular-400.ttf.br:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/fontawesome/webfonts/fa-regular-400.ttf.br
--------------------------------------------------------------------------------
/ui/assets/fontawesome/webfonts/fa-regular-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/fontawesome/webfonts/fa-regular-400.woff2
--------------------------------------------------------------------------------
/ui/assets/fontawesome/webfonts/fa-regular-400.woff2.br:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/fontawesome/webfonts/fa-regular-400.woff2.br
--------------------------------------------------------------------------------
/ui/assets/fontawesome/webfonts/fa-solid-900.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/fontawesome/webfonts/fa-solid-900.ttf
--------------------------------------------------------------------------------
/ui/assets/fontawesome/webfonts/fa-solid-900.ttf.br:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/fontawesome/webfonts/fa-solid-900.ttf.br
--------------------------------------------------------------------------------
/ui/assets/fontawesome/webfonts/fa-solid-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/fontawesome/webfonts/fa-solid-900.woff2
--------------------------------------------------------------------------------
/ui/assets/fontawesome/webfonts/fa-solid-900.woff2.br:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/fontawesome/webfonts/fa-solid-900.woff2.br
--------------------------------------------------------------------------------
/ui/assets/fontawesome/webfonts/fa-v4compatibility.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/fontawesome/webfonts/fa-v4compatibility.ttf
--------------------------------------------------------------------------------
/ui/assets/fontawesome/webfonts/fa-v4compatibility.ttf.br:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/fontawesome/webfonts/fa-v4compatibility.ttf.br
--------------------------------------------------------------------------------
/ui/assets/fontawesome/webfonts/fa-v4compatibility.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/fontawesome/webfonts/fa-v4compatibility.woff2
--------------------------------------------------------------------------------
/ui/assets/fontawesome/webfonts/fa-v4compatibility.woff2.br:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/fontawesome/webfonts/fa-v4compatibility.woff2.br
--------------------------------------------------------------------------------
/ui/assets/freenet_logo.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/ui/assets/freenet_logo.svg.br:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/freenet_logo.svg.br
--------------------------------------------------------------------------------
/ui/assets/main.css.br:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/freenet/river/0c12b63047fcf6cd2a5f189237144393e853d38b/ui/assets/main.css.br
--------------------------------------------------------------------------------
/ui/build.rs:
--------------------------------------------------------------------------------
1 | use chrono::Utc;
2 |
3 | fn main() {
4 | // Get the current UTC date and time
5 | let now = Utc::now();
6 | // Use ISO 8601 format (UTC) e.g., "2023-10-27T10:30:00Z"
7 | // This is easily parseable by JavaScript's Date object.
8 | let build_timestamp_iso = now.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
9 |
10 | // Set the BUILD_TIMESTAMP_ISO environment variable for the main crate compilation
11 | println!("cargo:rustc-env=BUILD_TIMESTAMP_ISO={}", build_timestamp_iso);
12 |
13 | // Re-run this script only if build.rs changes
14 | println!("cargo:rerun-if-changed=build.rs");
15 | }
16 |
--------------------------------------------------------------------------------
/ui/freenet.toml:
--------------------------------------------------------------------------------
1 | [contract]
2 | type = "webapp"
3 | lang = "rust"
4 |
5 | [webapp]
6 |
7 | [webapp.state-sources]
8 | source_dirs = ["dist"]
9 |
--------------------------------------------------------------------------------
/ui/src/components.rs:
--------------------------------------------------------------------------------
1 | pub mod app;
2 | pub mod conversation;
3 | pub mod members;
4 | pub mod room_list;
5 |
--------------------------------------------------------------------------------
/ui/src/components/app.rs:
--------------------------------------------------------------------------------
1 | pub mod chat_delegate;
2 | pub mod freenet_api;
3 | pub mod sync_info;
4 |
5 | use super::{conversation::Conversation, members::MemberList, room_list::RoomList};
6 | use crate::components::app::chat_delegate::set_up_chat_delegate;
7 | use crate::components::app::freenet_api::freenet_synchronizer::SynchronizerMessage;
8 | use crate::components::app::freenet_api::freenet_synchronizer::SynchronizerStatus;
9 | use crate::components::app::freenet_api::FreenetSynchronizer;
10 | use crate::components::members::member_info_modal::MemberInfoModal;
11 | use crate::components::members::Invitation;
12 | use crate::components::room_list::create_room_modal::CreateRoomModal;
13 | use crate::components::room_list::edit_room_modal::EditRoomModal;
14 | use crate::components::room_list::receive_invitation_modal::ReceiveInvitationModal;
15 | use crate::invites::PendingInvites;
16 | use crate::room_data::{CurrentRoom, Rooms};
17 | use dioxus::logger::tracing::{debug, error, info};
18 | use dioxus::prelude::*;
19 | use document::Stylesheet;
20 | use ed25519_dalek::VerifyingKey;
21 | use freenet_stdlib::client_api::WebApi;
22 | use river_common::room_state::member::MemberId;
23 | use wasm_bindgen::JsCast;
24 | use wasm_bindgen_futures::spawn_local;
25 | use wasm_bindgen_futures::JsFuture;
26 | use web_sys::{window, Response};
27 |
28 | pub static ROOMS: GlobalSignal = Global::new(initial_rooms);
29 | pub static CURRENT_ROOM: GlobalSignal =
30 | Global::new(|| CurrentRoom { owner_key: None });
31 | pub static MEMBER_INFO_MODAL: GlobalSignal =
32 | Global::new(|| MemberInfoModalSignal { member: None });
33 | pub static EDIT_ROOM_MODAL: GlobalSignal =
34 | Global::new(|| EditRoomModalSignal { room: None });
35 | pub static CREATE_ROOM_MODAL: GlobalSignal =
36 | Global::new(|| CreateRoomModalSignal { show: false });
37 | pub static PENDING_INVITES: GlobalSignal = Global::new(|| PendingInvites::new());
38 | pub static SYNC_STATUS: GlobalSignal =
39 | Global::new(|| SynchronizerStatus::Connecting);
40 | pub static SYNCHRONIZER: GlobalSignal =
41 | Global::new(|| FreenetSynchronizer::new());
42 | pub static WEB_API: GlobalSignal