de-mls with Waku (#29)

* start building waku for group_chat

* replace test

* replace test

* fix building issue on m2

* continue waku integration

* add admin trait

* update cfg

* update code

* replace cli to ws

* add docker for each instance

* fully working process for joining to the group

* update readme

* Add Waku and WebSocket actors for message processing and group management

- Introduced `WakuActor` for handling message sending and group subscriptions using Waku protocol.
- Implemented `Group` actor for managing group creation, member addition/removal, and message processing.
- Added `WsActor` for WebSocket communication, enabling user connections and message handling.
- Defined message structures for processing and sending messages within the Waku and WebSocket contexts.
- Enhanced error handling and logging for message operations.

* Refactor Waku and WebSocket integration for improved message handling

- Updated `WakuActor` to return a vector of `WakuContentTopic` upon subscription, enhancing group topic management.
- Introduced `AppState` struct to centralize application state, including Waku actor reference and content topics.
- Refactored main loop to utilize `AppState`, improving message flow between Waku and WebSocket actors.
- Enhanced message handling in `WsActor` to support `MessageToPrint`, allowing for structured message sending.
- Improved error handling and logging throughout the message processing pipeline.

* Refactor Waku message handling and clean up unused code

* Refactor and remove unused components from the project

- Deleted the `sc_key_store` module and its associated files, streamlining the codebase.
- Removed unused Docker and Git configuration files, enhancing project clarity.
- Cleaned up `.gitignore` and `.dockerignore` to reflect current project structure.
- Updated `Cargo.toml` files to remove references to deleted modules and dependencies.
- Refactored Waku and WebSocket actors to improve message handling and group management.
- Enhanced error handling and logging throughout the message processing pipeline.
- Adjusted frontend input styling for better user experience.

* Update CI workflow to use 'main' branch and add support for manual triggers

* Enhance Waku integration and documentation

- Added instructions for running a test Waku node in the README.
- Refactored Waku message handling in `ds_waku.rs` to improve content topic management and error handling.
- Updated `Cargo.toml` dependencies for better compatibility and removed unused entries.
- Improved error handling in `DeliveryServiceError` for Waku node operations.
- Cleaned up CI workflow by commenting out unused test jobs.
- Enhanced logging in tests for better traceability of message flows.

* Update CI workflow to include Go setup for testing

- Added steps to the CI configuration to set up Go version 1.20.x for user tests.
- Ensured consistent environment setup across different jobs in the CI pipeline.

* Update package versions to 1.0.0 in Cargo.toml files for the main project and 'ds' module

* Update README to include note on frontend implementation based on Chatr
This commit is contained in:
Ekaterina Broslavskaya
2024-12-25 15:06:31 +07:00
committed by GitHub
parent 49ab2b4aaa
commit c99eadb302
91 changed files with 5420 additions and 28088 deletions

4
.cargo/config.toml Normal file
View File

@@ -0,0 +1,4 @@
[target.'cfg(target_os = "macos")']
# when using osx, we need to link against some golang libraries, it did just work with this missing flags
# from: https://github.com/golang/go/issues/42459
rustflags = ["-C", "link-args=-framework CoreFoundation -framework Security -framework CoreServices -lresolv"]

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
.env/
.idea/
target/
frontend/

View File

@@ -1,8 +1,3 @@
name: "CI"
env:
FOUNDRY_PROFILE: "ci"
on:
workflow_dispatch:
pull_request:
@@ -10,122 +5,61 @@ on:
branches:
- "main"
concurrency:
cancel-in-progress: true
group: ${{github.workflow}}-${{github.ref}}
name: "CI"
jobs:
# ds_test:
# runs-on: ubuntu-latest
# steps:
# - name: Checkout code
# uses: actions/checkout@v3
# - name: Install stable toolchain
# uses: actions-rs/toolchain@v1
# with:
# profile: minimal
# toolchain: stable
# override: true
# - uses: Swatinem/rust-cache@v2
# - name: cargo test
# run: |
# cargo test --release
# working-directory: ds
user_test:
runs-on: ubuntu-latest
steps:
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.20.x'
- name: Checkout code
uses: actions/checkout@v3
- name: cargo test
run: |
cargo test --release
working-directory: tests
lint:
runs-on: "ubuntu-latest"
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: "Check out the repo"
uses: "actions/checkout@v3"
- name: Setup Go
uses: actions/setup-go@v4
with:
submodules: "recursive"
- name: "Install Foundry"
uses: "foundry-rs/foundry-toolchain@v1"
- name: "Install Pnpm"
uses: "pnpm/action-setup@v4"
go-version: '1.20.x'
- name: Checkout sources
uses: actions/checkout@v3
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
version: "8"
- name: "Install Node.js"
uses: "actions/setup-node@v3"
with:
node-version: "lts/*"
- name: "Install the Node.js dependencies"
run: "pnpm install"
working-directory: "contracts"
- name: "Lint the contracts"
run: "pnpm lint"
working-directory: "contracts"
- name: "Add lint summary"
profile: minimal
toolchain: stable
override: true
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- name: cargo fmt
if: success() || failure()
run: cargo fmt -- --check
- name: cargo clippy
if: success() || failure()
run: |
echo "## Lint result" >> $GITHUB_STEP_SUMMARY
echo "✅ Passed" >> $GITHUB_STEP_SUMMARY
build:
runs-on: "ubuntu-latest"
steps:
- name: "Check out the repo"
uses: "actions/checkout@v3"
with:
submodules: "recursive"
- name: "Install Foundry"
uses: "foundry-rs/foundry-toolchain@v1"
- name: "Build the contracts and print their size"
run: "forge build --sizes"
working-directory: "contracts"
- name: "Add build summary"
run: |
echo "## Build result" >> $GITHUB_STEP_SUMMARY
echo "✅ Passed" >> $GITHUB_STEP_SUMMARY
test:
needs: ["lint", "build"]
runs-on: "ubuntu-latest"
steps:
- name: "Check out the repo"
uses: "actions/checkout@v3"
with:
submodules: "recursive"
- name: "Install Foundry"
uses: "foundry-rs/foundry-toolchain@v1"
- name: "Show the Foundry config"
run: "forge config"
working-directory: "contracts"
- name: "Generate a fuzz seed that changes weekly to avoid burning through RPC allowance"
run: >
echo "FOUNDRY_FUZZ_SEED=$(
echo $(($EPOCHSECONDS - $EPOCHSECONDS % 604800))
)" >> $GITHUB_ENV
- name: "Run the tests"
run: "forge test"
working-directory: "contracts"
- name: "Add test summary"
run: |
echo "## Tests result" >> $GITHUB_STEP_SUMMARY
echo "✅ Passed" >> $GITHUB_STEP_SUMMARY
coverage:
needs: ["lint", "build"]
runs-on: "ubuntu-latest"
steps:
- name: "Check out the repo"
uses: "actions/checkout@v3"
with:
submodules: "recursive"
- name: "Install Foundry"
uses: "foundry-rs/foundry-toolchain@v1"
- name: "Generate the coverage report using the unit and the integration tests"
run: 'forge coverage --match-path "test/**/*.sol" --report lcov'
working-directory: "contracts"
- name: "Upload coverage report to Codecov"
uses: "codecov/codecov-action@v3"
with:
files: "./contracts/lcov.info"
- name: "Add coverage summary"
run: |
echo "## Coverage result" >> $GITHUB_STEP_SUMMARY
echo "✅ Uploaded to Codecov" >> $GITHUB_STEP_SUMMARY
strategy:
fail-fast: false
max-parallel: 16
cargo clippy --release -- -D warnings

12
.gitignore vendored
View File

@@ -15,12 +15,7 @@ Cargo.lock
.DS_Store
src/.DS_Store
## contracts
# directories
contracts/cache/**
contracts/node_modules/**
contracts/out/**
.idea
# files
*.env
@@ -30,9 +25,4 @@ contracts/out/**
lcov.info
yarn.lock
# broadcasts
contracts/!broadcast
contracts/broadcast/*
contracts/broadcast/*/31337/
.certora_internal

7
.gitmodules vendored
View File

@@ -1,7 +0,0 @@
[submodule "contracts/lib/forge-std"]
branch = "v1"
path = contracts/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "contracts/lib/openzeppelin-contracts"]
path = contracts/lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts

View File

@@ -1,58 +1,70 @@
[workspace]
members = ["sc_key_store", "ds", "crates/bindings", "mls_crypto"]
[workspace.dependencies]
foundry-contracts = { path = "crates/bindings" }
members = ["ds", "mls_crypto"]
# [workspace.dependencies]
# foundry-contracts = { path = "crates/bindings" }
[package]
name = "de-mls"
version = "0.1.0"
version = "1.0.0"
edition = "2021"
[[bin]]
name = "de-mls"
path = "src/main.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
foundry-contracts.workspace = true
# foundry-contracts.workspace = true
openmls = { version = "=0.5.0", features = ["test-utils"] }
openmls_basic_credential = "=0.2.0"
openmls_rust_crypto = "=0.2.0"
openmls_traits = "=0.2.0"
# waku-bindings = "0.6.0"
axum = { version = "0.6.10", features = ["ws"] }
futures = "0.3.26"
tower-http = { version = "0.4.0", features = ["cors"] }
tokio = { version = "=1.38.0", features = [
"macros",
"rt-multi-thread",
"full",
] }
tokio-util = "=0.7.11"
tokio-tungstenite = "0.15"
tungstenite = "0.14"
tokio-util = "0.7.13"
alloy = { git = "https://github.com/alloy-rs/alloy", features = [
"providers",
"node-bindings",
"network",
"transports",
"k256",
"signer-local",
] }
fred = { version = "=9.0.3", features = ["subscriber-client"] }
console-subscriber = "0.1.5"
kameo = "0.13.0"
waku-bindings = { git = "https://github.com/waku-org/waku-rust-bindings.git", branch = "force-cluster-15", subdir = "waku-bindings" }
waku-sys = { git = "https://github.com/waku-org/waku-rust-bindings.git", branch = "force-cluster-15", subdir = "waku-sys" }
rand = "=0.8.5"
serde_json = "=1.0"
serde = "=1.0.204"
url = "=2.5.2"
serde = { version = "=1.0.204", features = ["derive"] }
tls_codec = "=0.3.0"
hex = "=0.4.3"
chrono = "=0.4.38"
shlex = "=1.3.0"
clap = { version = "=4.5.8", features = ["derive"] }
secp256k1 = { version = "0.30.0", features = [
"rand",
"std",
"hashes",
"global-context",
] }
ecies = "0.2.7"
libsecp256k1 = "0.7.1"
anyhow = "=1.0.81"
thiserror = "=1.0.61"
uuid = "1.11.0"
bounded-vec-deque = "0.1.1"
crossterm = "=0.27.0"
ratatui = "=0.27.0"
textwrap = "=0.16.1"
env_logger = "0.11.5"
log = "0.4.22"
ds = { path = "ds" }
sc_key_store = { path = "sc_key_store" }
mls_crypto = { path = "mls_crypto" }

25
Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
####################################################################################################
## Build image
####################################################################################################
FROM rust:latest as builder
WORKDIR /app
RUN apt-get update && apt-get install -y libssl-dev pkg-config gcc clang
ENV PATH="/usr/local/go/bin:${PATH}"
COPY --from=golang:1.20 /usr/local/go/ /usr/local/go/
# Cache build dependencies
RUN echo "fn main() {}" > dummy.rs
COPY ["Cargo.toml", "./Cargo.toml"]
COPY ["ds/", "./ds/"]
COPY ["mls_crypto/", "./mls_crypto/"]
RUN sed -i 's#src/main.rs#dummy.rs#' Cargo.toml
RUN cargo build --release
RUN sed -i 's#dummy.rs#src/main.rs#' Cargo.toml
# Build the actual app
COPY ["src/", "./src/"]
RUN cargo build --release
CMD ["/app/target/release/de-mls"]

View File

@@ -1,28 +0,0 @@
## ref: https://gist.github.com/enil/e4af160c745057809053329df4ba1dc2
GIT=git
GIT_SUBMODULES=$(shell sed -nE 's/path = +(.+)/\1\/.git/ p' .gitmodules | paste -s -)
.PHONY: deps
deps: $(GIT_SUBMODULES)
$(GIT_SUBMODULES): %/.git: .gitmodules
$(GIT) submodule init
$(GIT) submodule update $*
@touch $@
.EXPORT_ALL_VARIABLES:
REDIS_PORT=6379
ANVIL_PORT=8545
start:
docker compose up -d
until cast chain-id --rpc-url "http://localhost:${ANVIL_PORT}" 2> /dev/null; do sleep 1; done
cd contracts && forge script --broadcast --rpc-url "http://localhost:${ANVIL_PORT}" script/Deploy.s.sol:Deploy 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --sig 'run(address)' --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
stop:
docker compose down
example: stop start
cargo run --release

View File

@@ -1,17 +1,30 @@
# de-mls
Decentralized MLS PoC using a smart contract for group coordination
## Run Redis Server
> Note: The frontend implementation is based on [chatr](https://github.com/0xLaurens/chatr), a real-time chat application built with Rust and SvelteKit
`docker-compose up`
## Run Test Waku Node
## Install deps
```bash
docker run -p 8645:8645 -p 60000:60000 wakuorg/nwaku:v0.33.1 --cluster-id=15 --rest --relay --rln-relay=false --pubsub-topic=/waku/2/rs/15/0
```
1. `Foundry`
2. `make deps`
## Run User Instance
## Scaffold Environment
Create a `.env` file in the `.env` folder for each client containing the following variables:
1. `make start`: This command will start the docker compose instance, and deploy the smart contract to the local network.
```text
NAME=client1
BACKEND_PORT=3000
FRONTEND_PORT=4000
NODE_NAME=<waku-node-ip>
```
2. `make stop`: This command will stop the docker compose instance.
Run docker compose up for the user instance
```bash
docker-compose --env-file ./.env/client1.env up --build
```
For each client, run the following command to start the frontend on the local host with the port specified in the `.env` file

View File

@@ -1,19 +0,0 @@
# EditorConfig http://EditorConfig.org
# top-most EditorConfig file
root = true
# All files
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.sol]
indent_size = 4
[*.tree]
indent_size = 1

View File

@@ -1,4 +0,0 @@
export API_KEY_INFURA="YOUR_API_KEY_INFURA"
export API_KEY_ETHERSCAN="YOUR_API_KEY_ETHERSCAN"
export MNEMONIC="YOUR_MNEMONIC"
export FOUNDRY_PROFILE="default"

View File

View File

@@ -1,6 +0,0 @@
ScKeystoreTest:test__addUser__addsUser__whenUserInfoIsValid() (gas: 106200)
ScKeystoreTest:test__addUser__reverts__whenUserAlreadyExists() (gas: 110242)
ScKeystoreTest:test__addUser__reverts__whenUserInfoIsMalformed() (gas: 8989)
ScKeystoreTest:test__getAllKeyPackagesForUser__returnsKeyPackages__whenUserExists() (gas: 111031)
ScKeystoreTest:test__getUser__returnsUserInfo__whenUserExists() (gas: 108923)
ScKeystoreTest:test__userExists__returnsFalse__whenUserDoesNotExist() (gas: 7863)

View File

@@ -1,4 +0,0 @@
[submodule "lib/forge-std"]
branch = "v1"
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std

View File

@@ -1,18 +0,0 @@
# directories
broadcast
cache
lib
node_modules
out
# files
*.env
*.log
.DS_Store
.pnp.*
lcov.info
package-lock.json
pnpm-lock.yaml
yarn.lock
slither.config.json

View File

@@ -1,7 +0,0 @@
bracketSpacing: true
printWidth: 120
proseWrap: "always"
singleQuote: false
tabWidth: 2
trailingComma: "all"
useTabs: false

View File

@@ -1,13 +0,0 @@
{
"extends": "solhint:recommended",
"rules": {
"code-complexity": ["error", 8],
"compiler-version": ["error", ">=0.8.19"],
"func-name-mixedcase": "off",
"func-visibility": ["error", { "ignoreConstructors": true }],
"max-line-length": ["error", 120],
"named-parameters-mapping": "warn",
"no-console": "off",
"not-rely-on-time": "off"
}
}

View File

View File

@@ -1,122 +0,0 @@
# de-mls contracts
[gha]: https://github.com/vacp2p/de-mls/actions
[gha-badge]: https://github.com/vacp2p/de-mls/actions/workflows/ci.yml/badge.svg
[foundry]: https://getfoundry.sh/
[foundry-badge]: https://img.shields.io/badge/Built%20with-Foundry-FFDB1C.svg
[license]: https://opensource.org/licenses/MIT
[license-badge]: https://img.shields.io/badge/License-MIT-blue.svg
## What's Inside
- [Forge](https://github.com/foundry-rs/foundry/blob/master/forge): compile, test, fuzz, format, and deploy smart
contracts
- [Forge Std](https://github.com/foundry-rs/forge-std): collection of helpful contracts and cheatcodes for testing
- [Solhint Community](https://github.com/solhint-community/solhint-community): linter for Solidity code
## Features
This template builds upon the frameworks and libraries mentioned above, so for details about their specific features,
please consult their respective documentation.
For example, if you're interested in exploring Foundry in more detail, you should look at the
[Foundry Book](https://book.getfoundry.sh/). In particular, you may be interested in reading the
[Writing Tests](https://book.getfoundry.sh/forge/writing-tests.html) tutorial.
## Usage
This is a list of the most frequently needed commands.
### Build
Build the contracts:
```sh
$ forge build
```
### Clean
Delete the build artifacts and cache directories:
```sh
$ forge clean
```
### Compile
Compile the contracts:
```sh
$ forge build
```
### Coverage
Get a test coverage report:
```sh
$ forge coverage
```
### Deploy
Deploy to Anvil:
```sh
$ forge script script/Deploy.s.sol --broadcast --fork-url http://localhost:8545
```
For this script to work, you need to have a `MNEMONIC` environment variable set to a valid
[BIP39 mnemonic](https://iancoleman.io/bip39/).
For instructions on how to deploy to a testnet or mainnet, check out the
[Solidity Scripting](https://book.getfoundry.sh/tutorials/solidity-scripting.html) tutorial.
### Format
Format the contracts:
```sh
$ forge fmt
```
### Gas Usage
Get a gas report:
```sh
$ forge test --gas-report
```
### Lint
Lint the contracts:
```sh
$ pnpm lint
```
#### Fixing linting issues
For any errors in solidity files, run `forge fmt`. For errors in any other file type, run `pnpm prettier:write`.
### Test
Run the tests:
```sh
$ forge test
```
## Notes
1. Foundry uses [git submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules) to manage dependencies. For
detailed instructions on working with dependencies, please refer to the
[guide](https://book.getfoundry.sh/projects/dependencies.html) in the book
2. You don't have to create a `.env` file, but filling in the environment variables may be useful when debugging and
testing against a fork.
## License
This project is licensed under MIT.

View File

@@ -1,28 +0,0 @@
codecov:
require_ci_to_pass: false
comment: false
ignore:
- "script"
- "test"
coverage:
status:
project:
default:
# advanced settings
# Prevents PR from being blocked with a reduction in coverage.
# Note, if we want to re-enable this, a `threshold` value can be used
# allow coverage to drop by x% while still posting a success status.
# `informational`: https://docs.codecov.com/docs/commit-status#informational
# `threshold`: https://docs.codecov.com/docs/commit-status#threshold
informational: true
patch:
default:
# advanced settings
# Prevents PR from being blocked with a reduction in coverage.
# Note, if we want to re-enable this, a `threshold` value can be used
# allow coverage to drop by x% while still posting a success status.
# `informational`: https://docs.codecov.com/docs/commit-status#informational
# `threshold`: https://docs.codecov.com/docs/commit-status#threshold
informational: true

View File

@@ -1,38 +0,0 @@
# Full reference https://github.com/foundry-rs/foundry/tree/master/config
[profile.default]
auto_detect_solc = false
block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT
bytecode_hash = "none"
cbor_metadata = false
evm_version = "paris"
fuzz = { runs = 1_000 }
gas_reports = ["*"]
libs = ["lib"]
optimizer = true
optimizer_runs = 10_000
out = "out"
script = "script"
solc = "0.8.24"
src = "src"
test = "test"
[profile.ci]
fuzz = { runs = 10_000 }
verbosity = 4
[etherscan]
sepolia = { key = "${API_KEY_ETHERSCAN}" }
[fmt]
bracket_spacing = true
int_types = "long"
line_length = 120
multiline_func_header = "all"
number_underscore = "thousands"
quote_style = "double"
tab_width = 4
wrap_comments = true
[rpc_endpoints]
sepolia = "https://sepolia.infura.io/v3/${API_KEY_INFURA}"

Submodule contracts/lib/forge-std deleted from 07263d193d

View File

@@ -1,31 +0,0 @@
{
"name": "@vacp2p/de-mls-contracts",
"description": "Foundry-based contracts for de-mls",
"version": "1.0.0",
"devDependencies": {
"prettier": "^3.0.0",
"solhint-community": "^3.6.0",
"commit-and-tag-version": "^12.2.0"
},
"keywords": [
"blockchain",
"ethereum",
"forge",
"foundry",
"smart-contracts",
"solidity",
"template"
],
"private": true,
"scripts": {
"clean": "rm -rf cache out",
"lint": "pnpm lint:sol && pnpm prettier:check",
"verify": "certoraRun certora/certora.conf",
"lint:sol": "forge fmt --check && pnpm solhint {script,src,test,certora}/**/*.sol",
"prettier:check": "prettier --check **/*.{json,md,yml} --ignore-path=.prettierignore",
"prettier:write": "prettier --write **/*.{json,md,yml} --ignore-path=.prettierignore",
"gas-report": "forge test --gas-report 2>&1 | (tee /dev/tty | awk '/Test result:/ {found=1; buffer=\"\"; next} found && !/Ran/ {buffer=buffer $0 ORS} /Ran/ {found=0} END {printf \"%s\", buffer}' > .gas-report)",
"release": "commit-and-tag-version",
"adorno": "pnpm prettier:write && forge fmt && forge snapshot && pnpm gas-report"
}
}

1894
contracts/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +0,0 @@
forge-std/=lib/forge-std/src/
Openzeppelin/=lib/openzeppelin-contracts/contracts

View File

@@ -1,41 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.19 <=0.9.0;
import { Script } from "forge-std/Script.sol";
abstract contract BaseScript is Script {
/// @dev Included to enable compilation of the script without a $MNEMONIC environment variable.
string internal constant TEST_MNEMONIC = "test test test test test test test test test test test junk";
/// @dev Needed for the deterministic deployments.
bytes32 internal constant ZERO_SALT = bytes32(0);
/// @dev The address of the transaction broadcaster.
address internal broadcaster;
/// @dev Used to derive the broadcaster's address if $ETH_FROM is not defined.
string internal mnemonic;
/// @dev Initializes the transaction broadcaster like this:
///
/// - If $ETH_FROM is defined, use it.
/// - Otherwise, derive the broadcaster address from $MNEMONIC.
/// - If $MNEMONIC is not defined, default to a test mnemonic.
///
/// The use case for $ETH_FROM is to specify the broadcaster key and its address via the command line.
constructor() {
address from = vm.envOr({ name: "ETH_FROM", defaultValue: address(0) });
if (from != address(0)) {
broadcaster = from;
} else {
mnemonic = vm.envOr({ name: "MNEMONIC", defaultValue: TEST_MNEMONIC });
(broadcaster,) = deriveRememberKey({ mnemonic: mnemonic, index: 0 });
}
}
modifier broadcast() {
vm.startBroadcast(broadcaster);
_;
vm.stopBroadcast();
}
}

View File

@@ -1,19 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.19 <=0.9.0;
import { ScKeystore } from "../src/ScKeystore.sol";
import { BaseScript } from "./Base.s.sol";
import { DeploymentConfig } from "./DeploymentConfig.s.sol";
contract Deploy is BaseScript {
function run(
address initialOwner
)
public
broadcast
returns (ScKeystore scKeystore, DeploymentConfig deploymentConfig)
{
deploymentConfig = new DeploymentConfig(broadcaster);
scKeystore = new ScKeystore(initialOwner);
}
}

View File

@@ -1,39 +0,0 @@
//// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.19 <=0.9.0;
import { Script } from "forge-std/Script.sol";
contract DeploymentConfig is Script {
error DeploymentConfig_InvalidDeployerAddress();
error DeploymentConfig_NoConfigForChain(uint256);
struct NetworkConfig {
address deployer;
}
NetworkConfig public activeNetworkConfig;
address private deployer;
constructor(address _broadcaster) {
if (_broadcaster == address(0)) revert DeploymentConfig_InvalidDeployerAddress();
deployer = _broadcaster;
if (block.chainid == 31_337) {
activeNetworkConfig = getOrCreateAnvilEthConfig();
} else {
revert DeploymentConfig_NoConfigForChain(block.chainid);
}
}
function getOrCreateAnvilEthConfig() public view returns (NetworkConfig memory) {
return NetworkConfig({ deployer: deployer });
}
// This function is a hack to have it excluded by `forge coverage` until
// https://github.com/foundry-rs/foundry/issues/2988 is fixed.
// See: https://github.com/foundry-rs/foundry/issues/2988#issuecomment-1437784542
// for more info.
// solhint-disable-next-line
function test() public { }
}

View File

@@ -1,8 +0,0 @@
{
"detectors_to_exclude": "naming-convention,reentrancy-events,solc-version,timestamp",
"filter_paths": "(lib|test)",
"solc_remaps": [
"@openzeppelin/contracts=lib/openzeppelin-contracts/contracts/",
"forge-std/=lib/forge-std/src/"
]
}

View File

@@ -1,10 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
interface IScKeystore {
function userExists(address user) external view returns (bool);
function addUser(address user) external;
function removeUser(address user) external;
}

View File

@@ -1,35 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.24;
import { Ownable } from "Openzeppelin/access/Ownable.sol";
import { IScKeystore } from "./IScKeystore.sol";
error UserAlreadyExists();
error UserDoesNotExist();
contract ScKeystore is Ownable, IScKeystore {
event UserAdded(address user);
event UserRemoved(address user);
mapping(address user => bool exists) private users;
constructor(address initialOwner) Ownable(initialOwner) { }
function userExists(address user) public view returns (bool) {
return users[user];
}
function addUser(address user) external onlyOwner {
if (userExists(user)) revert UserAlreadyExists();
users[user] = true;
emit UserAdded(user);
}
function removeUser(address user) external onlyOwner {
if (!userExists(user)) revert UserDoesNotExist();
users[user] == false;
emit UserRemoved(user);
}
}

View File

@@ -1,59 +0,0 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.19 <0.9.0;
import { Test } from "forge-std/Test.sol";
import { Deploy } from "../script/Deploy.s.sol";
import { DeploymentConfig } from "../script/DeploymentConfig.s.sol";
import "forge-std/console.sol";
import "../src/ScKeystore.sol"; // solhint-disable-line
contract ScKeystoreTest is Test {
ScKeystore internal s;
DeploymentConfig internal deploymentConfig;
address internal deployer;
function setUp() public virtual {
Deploy deployment = new Deploy();
(s, deploymentConfig) = deployment.run(address(this));
}
function addUser() internal {
s.addUser(address(this));
}
function test__owner() public view {
assert(s.owner() == address(this));
}
function test__userExists__returnsFalse__whenUserDoesNotExist() public view {
assert(!s.userExists(address(this)));
}
function test__addUser__reverts__whenUserAlreadyExists() public {
addUser();
vm.expectRevert(UserAlreadyExists.selector);
addUser();
}
function test__addUser__addsUser__whenUserInfoIsValid() public {
addUser();
assert(s.userExists(address(this)));
}
function test__addUser__reverts__whenSenderIsNotOwner() public {
vm.prank(address(0));
vm.expectRevert();
addUser();
vm.stopPrank();
}
function test__removeUser__reverts__whenUserDoesNotExist() public {
vm.expectRevert();
s.removeUser(address(0));
}
function test__removeUser() public {
addUser();
s.removeUser(address(this));
}
}

View File

@@ -1,7 +0,0 @@
[package]
name = "foundry-contracts"
version = "0.1.0"
edition = "2021"
[dependencies]
alloy = { git = "https://github.com/alloy-rs/alloy", features = ["sol-types", "contract"] }

View File

@@ -1,219 +0,0 @@
/**
Generated by the following Solidity interface...
```solidity
interface Context {}
```
...which was generated by the following JSON ABI:
```json
[]
```*/
#[allow(non_camel_case_types, non_snake_case, clippy::style)]
pub mod Context {
use super::*;
use alloy::sol_types as alloy_sol_types;
/// The creation / init bytecode of the contract.
///
/// ```text
///0x
/// ```
#[rustfmt::skip]
#[allow(clippy::all)]
pub static BYTECODE: alloy_sol_types::private::Bytes = alloy_sol_types::private::Bytes::from_static(
b"",
);
/// The runtime bytecode of the contract, as deployed on the network.
///
/// ```text
///0x
/// ```
#[rustfmt::skip]
#[allow(clippy::all)]
pub static DEPLOYED_BYTECODE: alloy_sol_types::private::Bytes = alloy_sol_types::private::Bytes::from_static(
b"",
);
use alloy::contract as alloy_contract;
/**Creates a new wrapper around an on-chain [`Context`](self) contract instance.
See the [wrapper's documentation](`ContextInstance`) for more details.*/
#[inline]
pub const fn new<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
>(
address: alloy_sol_types::private::Address,
provider: P,
) -> ContextInstance<T, P, N> {
ContextInstance::<T, P, N>::new(address, provider)
}
/**Deploys this contract using the given `provider` and constructor arguments, if any.
Returns a new instance of the contract, if the deployment was successful.
For more fine-grained control over the deployment process, use [`deploy_builder`] instead.*/
#[inline]
pub fn deploy<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
>(
provider: P,
) -> impl ::core::future::Future<Output = alloy_contract::Result<ContextInstance<T, P, N>>>
{
ContextInstance::<T, P, N>::deploy(provider)
}
/**Creates a `RawCallBuilder` for deploying this contract using the given `provider`
and constructor arguments, if any.
This is a simple wrapper around creating a `RawCallBuilder` with the data set to
the bytecode concatenated with the constructor's ABI-encoded arguments.*/
#[inline]
pub fn deploy_builder<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
>(
provider: P,
) -> alloy_contract::RawCallBuilder<T, P, N> {
ContextInstance::<T, P, N>::deploy_builder(provider)
}
/**A [`Context`](self) instance.
Contains type-safe methods for interacting with an on-chain instance of the
[`Context`](self) contract located at a given `address`, using a given
provider `P`.
If the contract bytecode is available (see the [`sol!`](alloy_sol_types::sol!)
documentation on how to provide it), the `deploy` and `deploy_builder` methods can
be used to deploy a new instance of the contract.
See the [module-level documentation](self) for all the available methods.*/
#[derive(Clone)]
pub struct ContextInstance<T, P, N = alloy_contract::private::Ethereum> {
address: alloy_sol_types::private::Address,
provider: P,
_network_transport: ::core::marker::PhantomData<(N, T)>,
}
#[automatically_derived]
impl<T, P, N> ::core::fmt::Debug for ContextInstance<T, P, N> {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
f.debug_tuple("ContextInstance")
.field(&self.address)
.finish()
}
}
/// Instantiation and getters/setters.
#[automatically_derived]
impl<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
> ContextInstance<T, P, N>
{
/**Creates a new wrapper around an on-chain [`Context`](self) contract instance.
See the [wrapper's documentation](`ContextInstance`) for more details.*/
#[inline]
pub const fn new(address: alloy_sol_types::private::Address, provider: P) -> Self {
Self {
address,
provider,
_network_transport: ::core::marker::PhantomData,
}
}
/**Deploys this contract using the given `provider` and constructor arguments, if any.
Returns a new instance of the contract, if the deployment was successful.
For more fine-grained control over the deployment process, use [`deploy_builder`] instead.*/
#[inline]
pub async fn deploy(provider: P) -> alloy_contract::Result<ContextInstance<T, P, N>> {
let call_builder = Self::deploy_builder(provider);
let contract_address = call_builder.deploy().await?;
Ok(Self::new(contract_address, call_builder.provider))
}
/**Creates a `RawCallBuilder` for deploying this contract using the given `provider`
and constructor arguments, if any.
This is a simple wrapper around creating a `RawCallBuilder` with the data set to
the bytecode concatenated with the constructor's ABI-encoded arguments.*/
#[inline]
pub fn deploy_builder(provider: P) -> alloy_contract::RawCallBuilder<T, P, N> {
alloy_contract::RawCallBuilder::new_raw_deploy(
provider,
::core::clone::Clone::clone(&BYTECODE),
)
}
/// Returns a reference to the address.
#[inline]
pub const fn address(&self) -> &alloy_sol_types::private::Address {
&self.address
}
/// Sets the address.
#[inline]
pub fn set_address(&mut self, address: alloy_sol_types::private::Address) {
self.address = address;
}
/// Sets the address and returns `self`.
pub fn at(mut self, address: alloy_sol_types::private::Address) -> Self {
self.set_address(address);
self
}
/// Returns a reference to the provider.
#[inline]
pub const fn provider(&self) -> &P {
&self.provider
}
}
impl<T, P: ::core::clone::Clone, N> ContextInstance<T, &P, N> {
/// Clones the provider and returns a new instance with the cloned provider.
#[inline]
pub fn with_cloned_provider(self) -> ContextInstance<T, P, N> {
ContextInstance {
address: self.address,
provider: ::core::clone::Clone::clone(&self.provider),
_network_transport: ::core::marker::PhantomData,
}
}
}
/// Function calls.
#[automatically_derived]
impl<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
> ContextInstance<T, P, N>
{
/// Creates a new call builder using this contract instance's provider and address.
///
/// Note that the call can be any function call, not just those defined in this
/// contract. Prefer using the other methods for building type-safe contract calls.
pub fn call_builder<C: alloy_sol_types::SolCall>(
&self,
call: &C,
) -> alloy_contract::SolCallBuilder<T, &P, C, N> {
alloy_contract::SolCallBuilder::new_sol(&self.provider, &self.address, call)
}
}
/// Event filters.
#[automatically_derived]
impl<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
> ContextInstance<T, P, N>
{
/// Creates a new event filter using this contract instance's provider and address.
///
/// Note that the type can be any event, not just those defined in this contract.
/// Prefer using the other methods for building type-safe event filters.
pub fn event_filter<E: alloy_sol_types::SolEvent>(
&self,
) -> alloy_contract::Event<T, &P, E, N> {
alloy_contract::Event::new_sol(&self.provider, &self.address)
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,440 +0,0 @@
/**
Generated by the following Solidity interface...
```solidity
interface IERC165 {
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
```
...which was generated by the following JSON ABI:
```json
[
{
"type": "function",
"name": "supportsInterface",
"inputs": [
{
"name": "interfaceID",
"type": "bytes4",
"internalType": "bytes4"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
}
]
```*/
#[allow(non_camel_case_types, non_snake_case, clippy::style)]
pub mod IERC165 {
use super::*;
use alloy::sol_types as alloy_sol_types;
/// The creation / init bytecode of the contract.
///
/// ```text
///0x
/// ```
#[rustfmt::skip]
#[allow(clippy::all)]
pub static BYTECODE: alloy_sol_types::private::Bytes = alloy_sol_types::private::Bytes::from_static(
b"",
);
/// The runtime bytecode of the contract, as deployed on the network.
///
/// ```text
///0x
/// ```
#[rustfmt::skip]
#[allow(clippy::all)]
pub static DEPLOYED_BYTECODE: alloy_sol_types::private::Bytes = alloy_sol_types::private::Bytes::from_static(
b"",
);
/**Function with signature `supportsInterface(bytes4)` and selector `0x01ffc9a7`.
```solidity
function supportsInterface(bytes4 interfaceID) external view returns (bool);
```*/
#[allow(non_camel_case_types, non_snake_case)]
#[derive(Clone)]
pub struct supportsInterfaceCall {
pub interfaceID: alloy::sol_types::private::FixedBytes<4>,
}
///Container type for the return parameters of the [`supportsInterface(bytes4)`](supportsInterfaceCall) function.
#[allow(non_camel_case_types, non_snake_case)]
#[derive(Clone)]
pub struct supportsInterfaceReturn {
pub _0: bool,
}
#[allow(non_camel_case_types, non_snake_case, clippy::style)]
const _: () = {
use alloy::sol_types as alloy_sol_types;
{
#[doc(hidden)]
type UnderlyingSolTuple<'a> = (alloy::sol_types::sol_data::FixedBytes<4>,);
#[doc(hidden)]
type UnderlyingRustTuple<'a> = (alloy::sol_types::private::FixedBytes<4>,);
#[cfg(test)]
#[allow(dead_code, unreachable_patterns)]
fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq<UnderlyingRustTuple>) {
match _t {
alloy_sol_types::private::AssertTypeEq::<
<UnderlyingSolTuple as alloy_sol_types::SolType>::RustType,
>(_) => {}
}
}
#[automatically_derived]
#[doc(hidden)]
impl ::core::convert::From<supportsInterfaceCall> for UnderlyingRustTuple<'_> {
fn from(value: supportsInterfaceCall) -> Self {
(value.interfaceID,)
}
}
#[automatically_derived]
#[doc(hidden)]
impl ::core::convert::From<UnderlyingRustTuple<'_>> for supportsInterfaceCall {
fn from(tuple: UnderlyingRustTuple<'_>) -> Self {
Self {
interfaceID: tuple.0,
}
}
}
}
{
#[doc(hidden)]
type UnderlyingSolTuple<'a> = (alloy::sol_types::sol_data::Bool,);
#[doc(hidden)]
type UnderlyingRustTuple<'a> = (bool,);
#[cfg(test)]
#[allow(dead_code, unreachable_patterns)]
fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq<UnderlyingRustTuple>) {
match _t {
alloy_sol_types::private::AssertTypeEq::<
<UnderlyingSolTuple as alloy_sol_types::SolType>::RustType,
>(_) => {}
}
}
#[automatically_derived]
#[doc(hidden)]
impl ::core::convert::From<supportsInterfaceReturn> for UnderlyingRustTuple<'_> {
fn from(value: supportsInterfaceReturn) -> Self {
(value._0,)
}
}
#[automatically_derived]
#[doc(hidden)]
impl ::core::convert::From<UnderlyingRustTuple<'_>> for supportsInterfaceReturn {
fn from(tuple: UnderlyingRustTuple<'_>) -> Self {
Self { _0: tuple.0 }
}
}
}
#[automatically_derived]
impl alloy_sol_types::SolCall for supportsInterfaceCall {
type Parameters<'a> = (alloy::sol_types::sol_data::FixedBytes<4>,);
type Token<'a> = <Self::Parameters<'a> as alloy_sol_types::SolType>::Token<'a>;
type Return = supportsInterfaceReturn;
type ReturnTuple<'a> = (alloy::sol_types::sol_data::Bool,);
type ReturnToken<'a> = <Self::ReturnTuple<'a> as alloy_sol_types::SolType>::Token<'a>;
const SIGNATURE: &'static str = "supportsInterface(bytes4)";
const SELECTOR: [u8; 4] = [1u8, 255u8, 201u8, 167u8];
#[inline]
fn new<'a>(
tuple: <Self::Parameters<'a> as alloy_sol_types::SolType>::RustType,
) -> Self {
tuple.into()
}
#[inline]
fn tokenize(&self) -> Self::Token<'_> {
(
<alloy::sol_types::sol_data::FixedBytes<
4,
> as alloy_sol_types::SolType>::tokenize(&self.interfaceID),
)
}
#[inline]
fn abi_decode_returns(
data: &[u8],
validate: bool,
) -> alloy_sol_types::Result<Self::Return> {
<Self::ReturnTuple<'_> as alloy_sol_types::SolType>::abi_decode_sequence(
data, validate,
)
.map(Into::into)
}
}
};
///Container for all the [`IERC165`](self) function calls.
pub enum IERC165Calls {
supportsInterface(supportsInterfaceCall),
}
#[automatically_derived]
impl IERC165Calls {
/// All the selectors of this enum.
///
/// Note that the selectors might not be in the same order as the variants.
/// No guarantees are made about the order of the selectors.
///
/// Prefer using `SolInterface` methods instead.
pub const SELECTORS: &'static [[u8; 4usize]] = &[[1u8, 255u8, 201u8, 167u8]];
}
#[automatically_derived]
impl alloy_sol_types::SolInterface for IERC165Calls {
const NAME: &'static str = "IERC165Calls";
const MIN_DATA_LENGTH: usize = 32usize;
const COUNT: usize = 1usize;
#[inline]
fn selector(&self) -> [u8; 4] {
match self {
Self::supportsInterface(_) => {
<supportsInterfaceCall as alloy_sol_types::SolCall>::SELECTOR
}
}
}
#[inline]
fn selector_at(i: usize) -> ::core::option::Option<[u8; 4]> {
Self::SELECTORS.get(i).copied()
}
#[inline]
fn valid_selector(selector: [u8; 4]) -> bool {
Self::SELECTORS.binary_search(&selector).is_ok()
}
#[inline]
#[allow(unsafe_code, non_snake_case)]
fn abi_decode_raw(
selector: [u8; 4],
data: &[u8],
validate: bool,
) -> alloy_sol_types::Result<Self> {
static DECODE_SHIMS: &[fn(&[u8], bool) -> alloy_sol_types::Result<IERC165Calls>] = &[{
fn supportsInterface(
data: &[u8],
validate: bool,
) -> alloy_sol_types::Result<IERC165Calls> {
<supportsInterfaceCall as alloy_sol_types::SolCall>::abi_decode_raw(
data, validate,
)
.map(IERC165Calls::supportsInterface)
}
supportsInterface
}];
let Ok(idx) = Self::SELECTORS.binary_search(&selector) else {
return Err(alloy_sol_types::Error::unknown_selector(
<Self as alloy_sol_types::SolInterface>::NAME,
selector,
));
};
(unsafe { DECODE_SHIMS.get_unchecked(idx) })(data, validate)
}
#[inline]
fn abi_encoded_size(&self) -> usize {
match self {
Self::supportsInterface(inner) => {
<supportsInterfaceCall as alloy_sol_types::SolCall>::abi_encoded_size(inner)
}
}
}
#[inline]
fn abi_encode_raw(&self, out: &mut alloy_sol_types::private::Vec<u8>) {
match self {
Self::supportsInterface(inner) => {
<supportsInterfaceCall as alloy_sol_types::SolCall>::abi_encode_raw(inner, out)
}
}
}
}
use alloy::contract as alloy_contract;
/**Creates a new wrapper around an on-chain [`IERC165`](self) contract instance.
See the [wrapper's documentation](`IERC165Instance`) for more details.*/
#[inline]
pub const fn new<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
>(
address: alloy_sol_types::private::Address,
provider: P,
) -> IERC165Instance<T, P, N> {
IERC165Instance::<T, P, N>::new(address, provider)
}
/**Deploys this contract using the given `provider` and constructor arguments, if any.
Returns a new instance of the contract, if the deployment was successful.
For more fine-grained control over the deployment process, use [`deploy_builder`] instead.*/
#[inline]
pub fn deploy<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
>(
provider: P,
) -> impl ::core::future::Future<Output = alloy_contract::Result<IERC165Instance<T, P, N>>>
{
IERC165Instance::<T, P, N>::deploy(provider)
}
/**Creates a `RawCallBuilder` for deploying this contract using the given `provider`
and constructor arguments, if any.
This is a simple wrapper around creating a `RawCallBuilder` with the data set to
the bytecode concatenated with the constructor's ABI-encoded arguments.*/
#[inline]
pub fn deploy_builder<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
>(
provider: P,
) -> alloy_contract::RawCallBuilder<T, P, N> {
IERC165Instance::<T, P, N>::deploy_builder(provider)
}
/**A [`IERC165`](self) instance.
Contains type-safe methods for interacting with an on-chain instance of the
[`IERC165`](self) contract located at a given `address`, using a given
provider `P`.
If the contract bytecode is available (see the [`sol!`](alloy_sol_types::sol!)
documentation on how to provide it), the `deploy` and `deploy_builder` methods can
be used to deploy a new instance of the contract.
See the [module-level documentation](self) for all the available methods.*/
#[derive(Clone)]
pub struct IERC165Instance<T, P, N = alloy_contract::private::Ethereum> {
address: alloy_sol_types::private::Address,
provider: P,
_network_transport: ::core::marker::PhantomData<(N, T)>,
}
#[automatically_derived]
impl<T, P, N> ::core::fmt::Debug for IERC165Instance<T, P, N> {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
f.debug_tuple("IERC165Instance")
.field(&self.address)
.finish()
}
}
/// Instantiation and getters/setters.
#[automatically_derived]
impl<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
> IERC165Instance<T, P, N>
{
/**Creates a new wrapper around an on-chain [`IERC165`](self) contract instance.
See the [wrapper's documentation](`IERC165Instance`) for more details.*/
#[inline]
pub const fn new(address: alloy_sol_types::private::Address, provider: P) -> Self {
Self {
address,
provider,
_network_transport: ::core::marker::PhantomData,
}
}
/**Deploys this contract using the given `provider` and constructor arguments, if any.
Returns a new instance of the contract, if the deployment was successful.
For more fine-grained control over the deployment process, use [`deploy_builder`] instead.*/
#[inline]
pub async fn deploy(provider: P) -> alloy_contract::Result<IERC165Instance<T, P, N>> {
let call_builder = Self::deploy_builder(provider);
let contract_address = call_builder.deploy().await?;
Ok(Self::new(contract_address, call_builder.provider))
}
/**Creates a `RawCallBuilder` for deploying this contract using the given `provider`
and constructor arguments, if any.
This is a simple wrapper around creating a `RawCallBuilder` with the data set to
the bytecode concatenated with the constructor's ABI-encoded arguments.*/
#[inline]
pub fn deploy_builder(provider: P) -> alloy_contract::RawCallBuilder<T, P, N> {
alloy_contract::RawCallBuilder::new_raw_deploy(
provider,
::core::clone::Clone::clone(&BYTECODE),
)
}
/// Returns a reference to the address.
#[inline]
pub const fn address(&self) -> &alloy_sol_types::private::Address {
&self.address
}
/// Sets the address.
#[inline]
pub fn set_address(&mut self, address: alloy_sol_types::private::Address) {
self.address = address;
}
/// Sets the address and returns `self`.
pub fn at(mut self, address: alloy_sol_types::private::Address) -> Self {
self.set_address(address);
self
}
/// Returns a reference to the provider.
#[inline]
pub const fn provider(&self) -> &P {
&self.provider
}
}
impl<T, P: ::core::clone::Clone, N> IERC165Instance<T, &P, N> {
/// Clones the provider and returns a new instance with the cloned provider.
#[inline]
pub fn with_cloned_provider(self) -> IERC165Instance<T, P, N> {
IERC165Instance {
address: self.address,
provider: ::core::clone::Clone::clone(&self.provider),
_network_transport: ::core::marker::PhantomData,
}
}
}
/// Function calls.
#[automatically_derived]
impl<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
> IERC165Instance<T, P, N>
{
/// Creates a new call builder using this contract instance's provider and address.
///
/// Note that the call can be any function call, not just those defined in this
/// contract. Prefer using the other methods for building type-safe contract calls.
pub fn call_builder<C: alloy_sol_types::SolCall>(
&self,
call: &C,
) -> alloy_contract::SolCallBuilder<T, &P, C, N> {
alloy_contract::SolCallBuilder::new_sol(&self.provider, &self.address, call)
}
///Creates a new call builder for the [`supportsInterface`] function.
pub fn supportsInterface(
&self,
interfaceID: alloy::sol_types::private::FixedBytes<4>,
) -> alloy_contract::SolCallBuilder<T, &P, supportsInterfaceCall, N> {
self.call_builder(&supportsInterfaceCall { interfaceID })
}
}
/// Event filters.
#[automatically_derived]
impl<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
> IERC165Instance<T, P, N>
{
/// Creates a new event filter using this contract instance's provider and address.
///
/// Note that the type can be any event, not just those defined in this contract.
/// Prefer using the other methods for building type-safe event filters.
pub fn event_filter<E: alloy_sol_types::SolEvent>(
&self,
) -> alloy_contract::Event<T, &P, E, N> {
alloy_contract::Event::new_sol(&self.provider, &self.address)
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,500 +0,0 @@
/**
Generated by the following Solidity interface...
```solidity
interface IERC721TokenReceiver {
function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes memory _data) external returns (bytes4);
}
```
...which was generated by the following JSON ABI:
```json
[
{
"type": "function",
"name": "onERC721Received",
"inputs": [
{
"name": "_operator",
"type": "address",
"internalType": "address"
},
{
"name": "_from",
"type": "address",
"internalType": "address"
},
{
"name": "_tokenId",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "_data",
"type": "bytes",
"internalType": "bytes"
}
],
"outputs": [
{
"name": "",
"type": "bytes4",
"internalType": "bytes4"
}
],
"stateMutability": "nonpayable"
}
]
```*/
#[allow(non_camel_case_types, non_snake_case, clippy::style)]
pub mod IERC721TokenReceiver {
use super::*;
use alloy::sol_types as alloy_sol_types;
/// The creation / init bytecode of the contract.
///
/// ```text
///0x
/// ```
#[rustfmt::skip]
#[allow(clippy::all)]
pub static BYTECODE: alloy_sol_types::private::Bytes = alloy_sol_types::private::Bytes::from_static(
b"",
);
/// The runtime bytecode of the contract, as deployed on the network.
///
/// ```text
///0x
/// ```
#[rustfmt::skip]
#[allow(clippy::all)]
pub static DEPLOYED_BYTECODE: alloy_sol_types::private::Bytes = alloy_sol_types::private::Bytes::from_static(
b"",
);
/**Function with signature `onERC721Received(address,address,uint256,bytes)` and selector `0x150b7a02`.
```solidity
function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes memory _data) external returns (bytes4);
```*/
#[allow(non_camel_case_types, non_snake_case)]
#[derive(Clone)]
pub struct onERC721ReceivedCall {
pub _operator: alloy::sol_types::private::Address,
pub _from: alloy::sol_types::private::Address,
pub _tokenId: alloy::sol_types::private::U256,
pub _data: alloy::sol_types::private::Bytes,
}
///Container type for the return parameters of the [`onERC721Received(address,address,uint256,bytes)`](onERC721ReceivedCall) function.
#[allow(non_camel_case_types, non_snake_case)]
#[derive(Clone)]
pub struct onERC721ReceivedReturn {
pub _0: alloy::sol_types::private::FixedBytes<4>,
}
#[allow(non_camel_case_types, non_snake_case, clippy::style)]
const _: () = {
use alloy::sol_types as alloy_sol_types;
{
#[doc(hidden)]
type UnderlyingSolTuple<'a> = (
alloy::sol_types::sol_data::Address,
alloy::sol_types::sol_data::Address,
alloy::sol_types::sol_data::Uint<256>,
alloy::sol_types::sol_data::Bytes,
);
#[doc(hidden)]
type UnderlyingRustTuple<'a> = (
alloy::sol_types::private::Address,
alloy::sol_types::private::Address,
alloy::sol_types::private::U256,
alloy::sol_types::private::Bytes,
);
#[cfg(test)]
#[allow(dead_code, unreachable_patterns)]
fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq<UnderlyingRustTuple>) {
match _t {
alloy_sol_types::private::AssertTypeEq::<
<UnderlyingSolTuple as alloy_sol_types::SolType>::RustType,
>(_) => {}
}
}
#[automatically_derived]
#[doc(hidden)]
impl ::core::convert::From<onERC721ReceivedCall> for UnderlyingRustTuple<'_> {
fn from(value: onERC721ReceivedCall) -> Self {
(value._operator, value._from, value._tokenId, value._data)
}
}
#[automatically_derived]
#[doc(hidden)]
impl ::core::convert::From<UnderlyingRustTuple<'_>> for onERC721ReceivedCall {
fn from(tuple: UnderlyingRustTuple<'_>) -> Self {
Self {
_operator: tuple.0,
_from: tuple.1,
_tokenId: tuple.2,
_data: tuple.3,
}
}
}
}
{
#[doc(hidden)]
type UnderlyingSolTuple<'a> = (alloy::sol_types::sol_data::FixedBytes<4>,);
#[doc(hidden)]
type UnderlyingRustTuple<'a> = (alloy::sol_types::private::FixedBytes<4>,);
#[cfg(test)]
#[allow(dead_code, unreachable_patterns)]
fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq<UnderlyingRustTuple>) {
match _t {
alloy_sol_types::private::AssertTypeEq::<
<UnderlyingSolTuple as alloy_sol_types::SolType>::RustType,
>(_) => {}
}
}
#[automatically_derived]
#[doc(hidden)]
impl ::core::convert::From<onERC721ReceivedReturn> for UnderlyingRustTuple<'_> {
fn from(value: onERC721ReceivedReturn) -> Self {
(value._0,)
}
}
#[automatically_derived]
#[doc(hidden)]
impl ::core::convert::From<UnderlyingRustTuple<'_>> for onERC721ReceivedReturn {
fn from(tuple: UnderlyingRustTuple<'_>) -> Self {
Self { _0: tuple.0 }
}
}
}
#[automatically_derived]
impl alloy_sol_types::SolCall for onERC721ReceivedCall {
type Parameters<'a> = (
alloy::sol_types::sol_data::Address,
alloy::sol_types::sol_data::Address,
alloy::sol_types::sol_data::Uint<256>,
alloy::sol_types::sol_data::Bytes,
);
type Token<'a> = <Self::Parameters<'a> as alloy_sol_types::SolType>::Token<'a>;
type Return = onERC721ReceivedReturn;
type ReturnTuple<'a> = (alloy::sol_types::sol_data::FixedBytes<4>,);
type ReturnToken<'a> = <Self::ReturnTuple<'a> as alloy_sol_types::SolType>::Token<'a>;
const SIGNATURE: &'static str = "onERC721Received(address,address,uint256,bytes)";
const SELECTOR: [u8; 4] = [21u8, 11u8, 122u8, 2u8];
#[inline]
fn new<'a>(
tuple: <Self::Parameters<'a> as alloy_sol_types::SolType>::RustType,
) -> Self {
tuple.into()
}
#[inline]
fn tokenize(&self) -> Self::Token<'_> {
(
<alloy::sol_types::sol_data::Address as alloy_sol_types::SolType>::tokenize(
&self._operator,
),
<alloy::sol_types::sol_data::Address as alloy_sol_types::SolType>::tokenize(
&self._from,
),
<alloy::sol_types::sol_data::Uint<256> as alloy_sol_types::SolType>::tokenize(
&self._tokenId,
),
<alloy::sol_types::sol_data::Bytes as alloy_sol_types::SolType>::tokenize(
&self._data,
),
)
}
#[inline]
fn abi_decode_returns(
data: &[u8],
validate: bool,
) -> alloy_sol_types::Result<Self::Return> {
<Self::ReturnTuple<'_> as alloy_sol_types::SolType>::abi_decode_sequence(
data, validate,
)
.map(Into::into)
}
}
};
///Container for all the [`IERC721TokenReceiver`](self) function calls.
pub enum IERC721TokenReceiverCalls {
onERC721Received(onERC721ReceivedCall),
}
#[automatically_derived]
impl IERC721TokenReceiverCalls {
/// All the selectors of this enum.
///
/// Note that the selectors might not be in the same order as the variants.
/// No guarantees are made about the order of the selectors.
///
/// Prefer using `SolInterface` methods instead.
pub const SELECTORS: &'static [[u8; 4usize]] = &[[21u8, 11u8, 122u8, 2u8]];
}
#[automatically_derived]
impl alloy_sol_types::SolInterface for IERC721TokenReceiverCalls {
const NAME: &'static str = "IERC721TokenReceiverCalls";
const MIN_DATA_LENGTH: usize = 160usize;
const COUNT: usize = 1usize;
#[inline]
fn selector(&self) -> [u8; 4] {
match self {
Self::onERC721Received(_) => {
<onERC721ReceivedCall as alloy_sol_types::SolCall>::SELECTOR
}
}
}
#[inline]
fn selector_at(i: usize) -> ::core::option::Option<[u8; 4]> {
Self::SELECTORS.get(i).copied()
}
#[inline]
fn valid_selector(selector: [u8; 4]) -> bool {
Self::SELECTORS.binary_search(&selector).is_ok()
}
#[inline]
#[allow(unsafe_code, non_snake_case)]
fn abi_decode_raw(
selector: [u8; 4],
data: &[u8],
validate: bool,
) -> alloy_sol_types::Result<Self> {
static DECODE_SHIMS: &[fn(
&[u8],
bool,
)
-> alloy_sol_types::Result<IERC721TokenReceiverCalls>] = &[{
fn onERC721Received(
data: &[u8],
validate: bool,
) -> alloy_sol_types::Result<IERC721TokenReceiverCalls> {
<onERC721ReceivedCall as alloy_sol_types::SolCall>::abi_decode_raw(
data, validate,
)
.map(IERC721TokenReceiverCalls::onERC721Received)
}
onERC721Received
}];
let Ok(idx) = Self::SELECTORS.binary_search(&selector) else {
return Err(alloy_sol_types::Error::unknown_selector(
<Self as alloy_sol_types::SolInterface>::NAME,
selector,
));
};
(unsafe { DECODE_SHIMS.get_unchecked(idx) })(data, validate)
}
#[inline]
fn abi_encoded_size(&self) -> usize {
match self {
Self::onERC721Received(inner) => {
<onERC721ReceivedCall as alloy_sol_types::SolCall>::abi_encoded_size(inner)
}
}
}
#[inline]
fn abi_encode_raw(&self, out: &mut alloy_sol_types::private::Vec<u8>) {
match self {
Self::onERC721Received(inner) => {
<onERC721ReceivedCall as alloy_sol_types::SolCall>::abi_encode_raw(inner, out)
}
}
}
}
use alloy::contract as alloy_contract;
/**Creates a new wrapper around an on-chain [`IERC721TokenReceiver`](self) contract instance.
See the [wrapper's documentation](`IERC721TokenReceiverInstance`) for more details.*/
#[inline]
pub const fn new<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
>(
address: alloy_sol_types::private::Address,
provider: P,
) -> IERC721TokenReceiverInstance<T, P, N> {
IERC721TokenReceiverInstance::<T, P, N>::new(address, provider)
}
/**Deploys this contract using the given `provider` and constructor arguments, if any.
Returns a new instance of the contract, if the deployment was successful.
For more fine-grained control over the deployment process, use [`deploy_builder`] instead.*/
#[inline]
pub fn deploy<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
>(
provider: P,
) -> impl ::core::future::Future<
Output = alloy_contract::Result<IERC721TokenReceiverInstance<T, P, N>>,
> {
IERC721TokenReceiverInstance::<T, P, N>::deploy(provider)
}
/**Creates a `RawCallBuilder` for deploying this contract using the given `provider`
and constructor arguments, if any.
This is a simple wrapper around creating a `RawCallBuilder` with the data set to
the bytecode concatenated with the constructor's ABI-encoded arguments.*/
#[inline]
pub fn deploy_builder<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
>(
provider: P,
) -> alloy_contract::RawCallBuilder<T, P, N> {
IERC721TokenReceiverInstance::<T, P, N>::deploy_builder(provider)
}
/**A [`IERC721TokenReceiver`](self) instance.
Contains type-safe methods for interacting with an on-chain instance of the
[`IERC721TokenReceiver`](self) contract located at a given `address`, using a given
provider `P`.
If the contract bytecode is available (see the [`sol!`](alloy_sol_types::sol!)
documentation on how to provide it), the `deploy` and `deploy_builder` methods can
be used to deploy a new instance of the contract.
See the [module-level documentation](self) for all the available methods.*/
#[derive(Clone)]
pub struct IERC721TokenReceiverInstance<T, P, N = alloy_contract::private::Ethereum> {
address: alloy_sol_types::private::Address,
provider: P,
_network_transport: ::core::marker::PhantomData<(N, T)>,
}
#[automatically_derived]
impl<T, P, N> ::core::fmt::Debug for IERC721TokenReceiverInstance<T, P, N> {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
f.debug_tuple("IERC721TokenReceiverInstance")
.field(&self.address)
.finish()
}
}
/// Instantiation and getters/setters.
#[automatically_derived]
impl<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
> IERC721TokenReceiverInstance<T, P, N>
{
/**Creates a new wrapper around an on-chain [`IERC721TokenReceiver`](self) contract instance.
See the [wrapper's documentation](`IERC721TokenReceiverInstance`) for more details.*/
#[inline]
pub const fn new(address: alloy_sol_types::private::Address, provider: P) -> Self {
Self {
address,
provider,
_network_transport: ::core::marker::PhantomData,
}
}
/**Deploys this contract using the given `provider` and constructor arguments, if any.
Returns a new instance of the contract, if the deployment was successful.
For more fine-grained control over the deployment process, use [`deploy_builder`] instead.*/
#[inline]
pub async fn deploy(
provider: P,
) -> alloy_contract::Result<IERC721TokenReceiverInstance<T, P, N>> {
let call_builder = Self::deploy_builder(provider);
let contract_address = call_builder.deploy().await?;
Ok(Self::new(contract_address, call_builder.provider))
}
/**Creates a `RawCallBuilder` for deploying this contract using the given `provider`
and constructor arguments, if any.
This is a simple wrapper around creating a `RawCallBuilder` with the data set to
the bytecode concatenated with the constructor's ABI-encoded arguments.*/
#[inline]
pub fn deploy_builder(provider: P) -> alloy_contract::RawCallBuilder<T, P, N> {
alloy_contract::RawCallBuilder::new_raw_deploy(
provider,
::core::clone::Clone::clone(&BYTECODE),
)
}
/// Returns a reference to the address.
#[inline]
pub const fn address(&self) -> &alloy_sol_types::private::Address {
&self.address
}
/// Sets the address.
#[inline]
pub fn set_address(&mut self, address: alloy_sol_types::private::Address) {
self.address = address;
}
/// Sets the address and returns `self`.
pub fn at(mut self, address: alloy_sol_types::private::Address) -> Self {
self.set_address(address);
self
}
/// Returns a reference to the provider.
#[inline]
pub const fn provider(&self) -> &P {
&self.provider
}
}
impl<T, P: ::core::clone::Clone, N> IERC721TokenReceiverInstance<T, &P, N> {
/// Clones the provider and returns a new instance with the cloned provider.
#[inline]
pub fn with_cloned_provider(self) -> IERC721TokenReceiverInstance<T, P, N> {
IERC721TokenReceiverInstance {
address: self.address,
provider: ::core::clone::Clone::clone(&self.provider),
_network_transport: ::core::marker::PhantomData,
}
}
}
/// Function calls.
#[automatically_derived]
impl<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
> IERC721TokenReceiverInstance<T, P, N>
{
/// Creates a new call builder using this contract instance's provider and address.
///
/// Note that the call can be any function call, not just those defined in this
/// contract. Prefer using the other methods for building type-safe contract calls.
pub fn call_builder<C: alloy_sol_types::SolCall>(
&self,
call: &C,
) -> alloy_contract::SolCallBuilder<T, &P, C, N> {
alloy_contract::SolCallBuilder::new_sol(&self.provider, &self.address, call)
}
///Creates a new call builder for the [`onERC721Received`] function.
pub fn onERC721Received(
&self,
_operator: alloy::sol_types::private::Address,
_from: alloy::sol_types::private::Address,
_tokenId: alloy::sol_types::private::U256,
_data: alloy::sol_types::private::Bytes,
) -> alloy_contract::SolCallBuilder<T, &P, onERC721ReceivedCall, N> {
self.call_builder(&onERC721ReceivedCall {
_operator,
_from,
_tokenId,
_data,
})
}
}
/// Event filters.
#[automatically_derived]
impl<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
> IERC721TokenReceiverInstance<T, P, N>
{
/// Creates a new event filter using this contract instance's provider and address.
///
/// Note that the type can be any event, not just those defined in this contract.
/// Prefer using the other methods for building type-safe event filters.
pub fn event_filter<E: alloy_sol_types::SolEvent>(
&self,
) -> alloy_contract::Event<T, &P, E, N> {
alloy_contract::Event::new_sol(&self.provider, &self.address)
}
}
}

View File

@@ -1,743 +0,0 @@
/**
Generated by the following Solidity interface...
```solidity
interface IScKeystore {
function addUser(address user) external;
function removeUser(address user) external;
function userExists(address user) external view returns (bool);
}
```
...which was generated by the following JSON ABI:
```json
[
{
"type": "function",
"name": "addUser",
"inputs": [
{
"name": "user",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "removeUser",
"inputs": [
{
"name": "user",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "userExists",
"inputs": [
{
"name": "user",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
}
]
```*/
#[allow(non_camel_case_types, non_snake_case, clippy::style)]
pub mod IScKeystore {
use super::*;
use alloy::sol_types as alloy_sol_types;
/// The creation / init bytecode of the contract.
///
/// ```text
///0x
/// ```
#[rustfmt::skip]
#[allow(clippy::all)]
pub static BYTECODE: alloy_sol_types::private::Bytes = alloy_sol_types::private::Bytes::from_static(
b"",
);
/// The runtime bytecode of the contract, as deployed on the network.
///
/// ```text
///0x
/// ```
#[rustfmt::skip]
#[allow(clippy::all)]
pub static DEPLOYED_BYTECODE: alloy_sol_types::private::Bytes = alloy_sol_types::private::Bytes::from_static(
b"",
);
/**Function with signature `addUser(address)` and selector `0x421b2d8b`.
```solidity
function addUser(address user) external;
```*/
#[allow(non_camel_case_types, non_snake_case)]
#[derive(Clone)]
pub struct addUserCall {
pub user: alloy::sol_types::private::Address,
}
///Container type for the return parameters of the [`addUser(address)`](addUserCall) function.
#[allow(non_camel_case_types, non_snake_case)]
#[derive(Clone)]
pub struct addUserReturn {}
#[allow(non_camel_case_types, non_snake_case, clippy::style)]
const _: () = {
use alloy::sol_types as alloy_sol_types;
{
#[doc(hidden)]
type UnderlyingSolTuple<'a> = (alloy::sol_types::sol_data::Address,);
#[doc(hidden)]
type UnderlyingRustTuple<'a> = (alloy::sol_types::private::Address,);
#[cfg(test)]
#[allow(dead_code, unreachable_patterns)]
fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq<UnderlyingRustTuple>) {
match _t {
alloy_sol_types::private::AssertTypeEq::<
<UnderlyingSolTuple as alloy_sol_types::SolType>::RustType,
>(_) => {}
}
}
#[automatically_derived]
#[doc(hidden)]
impl ::core::convert::From<addUserCall> for UnderlyingRustTuple<'_> {
fn from(value: addUserCall) -> Self {
(value.user,)
}
}
#[automatically_derived]
#[doc(hidden)]
impl ::core::convert::From<UnderlyingRustTuple<'_>> for addUserCall {
fn from(tuple: UnderlyingRustTuple<'_>) -> Self {
Self { user: tuple.0 }
}
}
}
{
#[doc(hidden)]
type UnderlyingSolTuple<'a> = ();
#[doc(hidden)]
type UnderlyingRustTuple<'a> = ();
#[cfg(test)]
#[allow(dead_code, unreachable_patterns)]
fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq<UnderlyingRustTuple>) {
match _t {
alloy_sol_types::private::AssertTypeEq::<
<UnderlyingSolTuple as alloy_sol_types::SolType>::RustType,
>(_) => {}
}
}
#[automatically_derived]
#[doc(hidden)]
impl ::core::convert::From<addUserReturn> for UnderlyingRustTuple<'_> {
fn from(value: addUserReturn) -> Self {
()
}
}
#[automatically_derived]
#[doc(hidden)]
impl ::core::convert::From<UnderlyingRustTuple<'_>> for addUserReturn {
fn from(tuple: UnderlyingRustTuple<'_>) -> Self {
Self {}
}
}
}
#[automatically_derived]
impl alloy_sol_types::SolCall for addUserCall {
type Parameters<'a> = (alloy::sol_types::sol_data::Address,);
type Token<'a> = <Self::Parameters<'a> as alloy_sol_types::SolType>::Token<'a>;
type Return = addUserReturn;
type ReturnTuple<'a> = ();
type ReturnToken<'a> = <Self::ReturnTuple<'a> as alloy_sol_types::SolType>::Token<'a>;
const SIGNATURE: &'static str = "addUser(address)";
const SELECTOR: [u8; 4] = [66u8, 27u8, 45u8, 139u8];
#[inline]
fn new<'a>(
tuple: <Self::Parameters<'a> as alloy_sol_types::SolType>::RustType,
) -> Self {
tuple.into()
}
#[inline]
fn tokenize(&self) -> Self::Token<'_> {
(
<alloy::sol_types::sol_data::Address as alloy_sol_types::SolType>::tokenize(
&self.user,
),
)
}
#[inline]
fn abi_decode_returns(
data: &[u8],
validate: bool,
) -> alloy_sol_types::Result<Self::Return> {
<Self::ReturnTuple<'_> as alloy_sol_types::SolType>::abi_decode_sequence(
data, validate,
)
.map(Into::into)
}
}
};
/**Function with signature `removeUser(address)` and selector `0x98575188`.
```solidity
function removeUser(address user) external;
```*/
#[allow(non_camel_case_types, non_snake_case)]
#[derive(Clone)]
pub struct removeUserCall {
pub user: alloy::sol_types::private::Address,
}
///Container type for the return parameters of the [`removeUser(address)`](removeUserCall) function.
#[allow(non_camel_case_types, non_snake_case)]
#[derive(Clone)]
pub struct removeUserReturn {}
#[allow(non_camel_case_types, non_snake_case, clippy::style)]
const _: () = {
use alloy::sol_types as alloy_sol_types;
{
#[doc(hidden)]
type UnderlyingSolTuple<'a> = (alloy::sol_types::sol_data::Address,);
#[doc(hidden)]
type UnderlyingRustTuple<'a> = (alloy::sol_types::private::Address,);
#[cfg(test)]
#[allow(dead_code, unreachable_patterns)]
fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq<UnderlyingRustTuple>) {
match _t {
alloy_sol_types::private::AssertTypeEq::<
<UnderlyingSolTuple as alloy_sol_types::SolType>::RustType,
>(_) => {}
}
}
#[automatically_derived]
#[doc(hidden)]
impl ::core::convert::From<removeUserCall> for UnderlyingRustTuple<'_> {
fn from(value: removeUserCall) -> Self {
(value.user,)
}
}
#[automatically_derived]
#[doc(hidden)]
impl ::core::convert::From<UnderlyingRustTuple<'_>> for removeUserCall {
fn from(tuple: UnderlyingRustTuple<'_>) -> Self {
Self { user: tuple.0 }
}
}
}
{
#[doc(hidden)]
type UnderlyingSolTuple<'a> = ();
#[doc(hidden)]
type UnderlyingRustTuple<'a> = ();
#[cfg(test)]
#[allow(dead_code, unreachable_patterns)]
fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq<UnderlyingRustTuple>) {
match _t {
alloy_sol_types::private::AssertTypeEq::<
<UnderlyingSolTuple as alloy_sol_types::SolType>::RustType,
>(_) => {}
}
}
#[automatically_derived]
#[doc(hidden)]
impl ::core::convert::From<removeUserReturn> for UnderlyingRustTuple<'_> {
fn from(value: removeUserReturn) -> Self {
()
}
}
#[automatically_derived]
#[doc(hidden)]
impl ::core::convert::From<UnderlyingRustTuple<'_>> for removeUserReturn {
fn from(tuple: UnderlyingRustTuple<'_>) -> Self {
Self {}
}
}
}
#[automatically_derived]
impl alloy_sol_types::SolCall for removeUserCall {
type Parameters<'a> = (alloy::sol_types::sol_data::Address,);
type Token<'a> = <Self::Parameters<'a> as alloy_sol_types::SolType>::Token<'a>;
type Return = removeUserReturn;
type ReturnTuple<'a> = ();
type ReturnToken<'a> = <Self::ReturnTuple<'a> as alloy_sol_types::SolType>::Token<'a>;
const SIGNATURE: &'static str = "removeUser(address)";
const SELECTOR: [u8; 4] = [152u8, 87u8, 81u8, 136u8];
#[inline]
fn new<'a>(
tuple: <Self::Parameters<'a> as alloy_sol_types::SolType>::RustType,
) -> Self {
tuple.into()
}
#[inline]
fn tokenize(&self) -> Self::Token<'_> {
(
<alloy::sol_types::sol_data::Address as alloy_sol_types::SolType>::tokenize(
&self.user,
),
)
}
#[inline]
fn abi_decode_returns(
data: &[u8],
validate: bool,
) -> alloy_sol_types::Result<Self::Return> {
<Self::ReturnTuple<'_> as alloy_sol_types::SolType>::abi_decode_sequence(
data, validate,
)
.map(Into::into)
}
}
};
/**Function with signature `userExists(address)` and selector `0x0e666e49`.
```solidity
function userExists(address user) external view returns (bool);
```*/
#[allow(non_camel_case_types, non_snake_case)]
#[derive(Clone)]
pub struct userExistsCall {
pub user: alloy::sol_types::private::Address,
}
///Container type for the return parameters of the [`userExists(address)`](userExistsCall) function.
#[allow(non_camel_case_types, non_snake_case)]
#[derive(Clone)]
pub struct userExistsReturn {
pub _0: bool,
}
#[allow(non_camel_case_types, non_snake_case, clippy::style)]
const _: () = {
use alloy::sol_types as alloy_sol_types;
{
#[doc(hidden)]
type UnderlyingSolTuple<'a> = (alloy::sol_types::sol_data::Address,);
#[doc(hidden)]
type UnderlyingRustTuple<'a> = (alloy::sol_types::private::Address,);
#[cfg(test)]
#[allow(dead_code, unreachable_patterns)]
fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq<UnderlyingRustTuple>) {
match _t {
alloy_sol_types::private::AssertTypeEq::<
<UnderlyingSolTuple as alloy_sol_types::SolType>::RustType,
>(_) => {}
}
}
#[automatically_derived]
#[doc(hidden)]
impl ::core::convert::From<userExistsCall> for UnderlyingRustTuple<'_> {
fn from(value: userExistsCall) -> Self {
(value.user,)
}
}
#[automatically_derived]
#[doc(hidden)]
impl ::core::convert::From<UnderlyingRustTuple<'_>> for userExistsCall {
fn from(tuple: UnderlyingRustTuple<'_>) -> Self {
Self { user: tuple.0 }
}
}
}
{
#[doc(hidden)]
type UnderlyingSolTuple<'a> = (alloy::sol_types::sol_data::Bool,);
#[doc(hidden)]
type UnderlyingRustTuple<'a> = (bool,);
#[cfg(test)]
#[allow(dead_code, unreachable_patterns)]
fn _type_assertion(_t: alloy_sol_types::private::AssertTypeEq<UnderlyingRustTuple>) {
match _t {
alloy_sol_types::private::AssertTypeEq::<
<UnderlyingSolTuple as alloy_sol_types::SolType>::RustType,
>(_) => {}
}
}
#[automatically_derived]
#[doc(hidden)]
impl ::core::convert::From<userExistsReturn> for UnderlyingRustTuple<'_> {
fn from(value: userExistsReturn) -> Self {
(value._0,)
}
}
#[automatically_derived]
#[doc(hidden)]
impl ::core::convert::From<UnderlyingRustTuple<'_>> for userExistsReturn {
fn from(tuple: UnderlyingRustTuple<'_>) -> Self {
Self { _0: tuple.0 }
}
}
}
#[automatically_derived]
impl alloy_sol_types::SolCall for userExistsCall {
type Parameters<'a> = (alloy::sol_types::sol_data::Address,);
type Token<'a> = <Self::Parameters<'a> as alloy_sol_types::SolType>::Token<'a>;
type Return = userExistsReturn;
type ReturnTuple<'a> = (alloy::sol_types::sol_data::Bool,);
type ReturnToken<'a> = <Self::ReturnTuple<'a> as alloy_sol_types::SolType>::Token<'a>;
const SIGNATURE: &'static str = "userExists(address)";
const SELECTOR: [u8; 4] = [14u8, 102u8, 110u8, 73u8];
#[inline]
fn new<'a>(
tuple: <Self::Parameters<'a> as alloy_sol_types::SolType>::RustType,
) -> Self {
tuple.into()
}
#[inline]
fn tokenize(&self) -> Self::Token<'_> {
(
<alloy::sol_types::sol_data::Address as alloy_sol_types::SolType>::tokenize(
&self.user,
),
)
}
#[inline]
fn abi_decode_returns(
data: &[u8],
validate: bool,
) -> alloy_sol_types::Result<Self::Return> {
<Self::ReturnTuple<'_> as alloy_sol_types::SolType>::abi_decode_sequence(
data, validate,
)
.map(Into::into)
}
}
};
///Container for all the [`IScKeystore`](self) function calls.
pub enum IScKeystoreCalls {
addUser(addUserCall),
removeUser(removeUserCall),
userExists(userExistsCall),
}
#[automatically_derived]
impl IScKeystoreCalls {
/// All the selectors of this enum.
///
/// Note that the selectors might not be in the same order as the variants.
/// No guarantees are made about the order of the selectors.
///
/// Prefer using `SolInterface` methods instead.
pub const SELECTORS: &'static [[u8; 4usize]] = &[
[14u8, 102u8, 110u8, 73u8],
[66u8, 27u8, 45u8, 139u8],
[152u8, 87u8, 81u8, 136u8],
];
}
#[automatically_derived]
impl alloy_sol_types::SolInterface for IScKeystoreCalls {
const NAME: &'static str = "IScKeystoreCalls";
const MIN_DATA_LENGTH: usize = 32usize;
const COUNT: usize = 3usize;
#[inline]
fn selector(&self) -> [u8; 4] {
match self {
Self::addUser(_) => <addUserCall as alloy_sol_types::SolCall>::SELECTOR,
Self::removeUser(_) => <removeUserCall as alloy_sol_types::SolCall>::SELECTOR,
Self::userExists(_) => <userExistsCall as alloy_sol_types::SolCall>::SELECTOR,
}
}
#[inline]
fn selector_at(i: usize) -> ::core::option::Option<[u8; 4]> {
Self::SELECTORS.get(i).copied()
}
#[inline]
fn valid_selector(selector: [u8; 4]) -> bool {
Self::SELECTORS.binary_search(&selector).is_ok()
}
#[inline]
#[allow(unsafe_code, non_snake_case)]
fn abi_decode_raw(
selector: [u8; 4],
data: &[u8],
validate: bool,
) -> alloy_sol_types::Result<Self> {
static DECODE_SHIMS: &[fn(&[u8], bool) -> alloy_sol_types::Result<IScKeystoreCalls>] =
&[
{
fn userExists(
data: &[u8],
validate: bool,
) -> alloy_sol_types::Result<IScKeystoreCalls> {
<userExistsCall as alloy_sol_types::SolCall>::abi_decode_raw(
data, validate,
)
.map(IScKeystoreCalls::userExists)
}
userExists
},
{
fn addUser(
data: &[u8],
validate: bool,
) -> alloy_sol_types::Result<IScKeystoreCalls> {
<addUserCall as alloy_sol_types::SolCall>::abi_decode_raw(
data, validate,
)
.map(IScKeystoreCalls::addUser)
}
addUser
},
{
fn removeUser(
data: &[u8],
validate: bool,
) -> alloy_sol_types::Result<IScKeystoreCalls> {
<removeUserCall as alloy_sol_types::SolCall>::abi_decode_raw(
data, validate,
)
.map(IScKeystoreCalls::removeUser)
}
removeUser
},
];
let Ok(idx) = Self::SELECTORS.binary_search(&selector) else {
return Err(alloy_sol_types::Error::unknown_selector(
<Self as alloy_sol_types::SolInterface>::NAME,
selector,
));
};
(unsafe { DECODE_SHIMS.get_unchecked(idx) })(data, validate)
}
#[inline]
fn abi_encoded_size(&self) -> usize {
match self {
Self::addUser(inner) => {
<addUserCall as alloy_sol_types::SolCall>::abi_encoded_size(inner)
}
Self::removeUser(inner) => {
<removeUserCall as alloy_sol_types::SolCall>::abi_encoded_size(inner)
}
Self::userExists(inner) => {
<userExistsCall as alloy_sol_types::SolCall>::abi_encoded_size(inner)
}
}
}
#[inline]
fn abi_encode_raw(&self, out: &mut alloy_sol_types::private::Vec<u8>) {
match self {
Self::addUser(inner) => {
<addUserCall as alloy_sol_types::SolCall>::abi_encode_raw(inner, out)
}
Self::removeUser(inner) => {
<removeUserCall as alloy_sol_types::SolCall>::abi_encode_raw(inner, out)
}
Self::userExists(inner) => {
<userExistsCall as alloy_sol_types::SolCall>::abi_encode_raw(inner, out)
}
}
}
}
use alloy::contract as alloy_contract;
/**Creates a new wrapper around an on-chain [`IScKeystore`](self) contract instance.
See the [wrapper's documentation](`IScKeystoreInstance`) for more details.*/
#[inline]
pub const fn new<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
>(
address: alloy_sol_types::private::Address,
provider: P,
) -> IScKeystoreInstance<T, P, N> {
IScKeystoreInstance::<T, P, N>::new(address, provider)
}
/**Deploys this contract using the given `provider` and constructor arguments, if any.
Returns a new instance of the contract, if the deployment was successful.
For more fine-grained control over the deployment process, use [`deploy_builder`] instead.*/
#[inline]
pub fn deploy<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
>(
provider: P,
) -> impl ::core::future::Future<Output = alloy_contract::Result<IScKeystoreInstance<T, P, N>>>
{
IScKeystoreInstance::<T, P, N>::deploy(provider)
}
/**Creates a `RawCallBuilder` for deploying this contract using the given `provider`
and constructor arguments, if any.
This is a simple wrapper around creating a `RawCallBuilder` with the data set to
the bytecode concatenated with the constructor's ABI-encoded arguments.*/
#[inline]
pub fn deploy_builder<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
>(
provider: P,
) -> alloy_contract::RawCallBuilder<T, P, N> {
IScKeystoreInstance::<T, P, N>::deploy_builder(provider)
}
/**A [`IScKeystore`](self) instance.
Contains type-safe methods for interacting with an on-chain instance of the
[`IScKeystore`](self) contract located at a given `address`, using a given
provider `P`.
If the contract bytecode is available (see the [`sol!`](alloy_sol_types::sol!)
documentation on how to provide it), the `deploy` and `deploy_builder` methods can
be used to deploy a new instance of the contract.
See the [module-level documentation](self) for all the available methods.*/
#[derive(Clone)]
pub struct IScKeystoreInstance<T, P, N = alloy_contract::private::Ethereum> {
address: alloy_sol_types::private::Address,
provider: P,
_network_transport: ::core::marker::PhantomData<(N, T)>,
}
#[automatically_derived]
impl<T, P, N> ::core::fmt::Debug for IScKeystoreInstance<T, P, N> {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
f.debug_tuple("IScKeystoreInstance")
.field(&self.address)
.finish()
}
}
/// Instantiation and getters/setters.
#[automatically_derived]
impl<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
> IScKeystoreInstance<T, P, N>
{
/**Creates a new wrapper around an on-chain [`IScKeystore`](self) contract instance.
See the [wrapper's documentation](`IScKeystoreInstance`) for more details.*/
#[inline]
pub const fn new(address: alloy_sol_types::private::Address, provider: P) -> Self {
Self {
address,
provider,
_network_transport: ::core::marker::PhantomData,
}
}
/**Deploys this contract using the given `provider` and constructor arguments, if any.
Returns a new instance of the contract, if the deployment was successful.
For more fine-grained control over the deployment process, use [`deploy_builder`] instead.*/
#[inline]
pub async fn deploy(provider: P) -> alloy_contract::Result<IScKeystoreInstance<T, P, N>> {
let call_builder = Self::deploy_builder(provider);
let contract_address = call_builder.deploy().await?;
Ok(Self::new(contract_address, call_builder.provider))
}
/**Creates a `RawCallBuilder` for deploying this contract using the given `provider`
and constructor arguments, if any.
This is a simple wrapper around creating a `RawCallBuilder` with the data set to
the bytecode concatenated with the constructor's ABI-encoded arguments.*/
#[inline]
pub fn deploy_builder(provider: P) -> alloy_contract::RawCallBuilder<T, P, N> {
alloy_contract::RawCallBuilder::new_raw_deploy(
provider,
::core::clone::Clone::clone(&BYTECODE),
)
}
/// Returns a reference to the address.
#[inline]
pub const fn address(&self) -> &alloy_sol_types::private::Address {
&self.address
}
/// Sets the address.
#[inline]
pub fn set_address(&mut self, address: alloy_sol_types::private::Address) {
self.address = address;
}
/// Sets the address and returns `self`.
pub fn at(mut self, address: alloy_sol_types::private::Address) -> Self {
self.set_address(address);
self
}
/// Returns a reference to the provider.
#[inline]
pub const fn provider(&self) -> &P {
&self.provider
}
}
impl<T, P: ::core::clone::Clone, N> IScKeystoreInstance<T, &P, N> {
/// Clones the provider and returns a new instance with the cloned provider.
#[inline]
pub fn with_cloned_provider(self) -> IScKeystoreInstance<T, P, N> {
IScKeystoreInstance {
address: self.address,
provider: ::core::clone::Clone::clone(&self.provider),
_network_transport: ::core::marker::PhantomData,
}
}
}
/// Function calls.
#[automatically_derived]
impl<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
> IScKeystoreInstance<T, P, N>
{
/// Creates a new call builder using this contract instance's provider and address.
///
/// Note that the call can be any function call, not just those defined in this
/// contract. Prefer using the other methods for building type-safe contract calls.
pub fn call_builder<C: alloy_sol_types::SolCall>(
&self,
call: &C,
) -> alloy_contract::SolCallBuilder<T, &P, C, N> {
alloy_contract::SolCallBuilder::new_sol(&self.provider, &self.address, call)
}
///Creates a new call builder for the [`addUser`] function.
pub fn addUser(
&self,
user: alloy::sol_types::private::Address,
) -> alloy_contract::SolCallBuilder<T, &P, addUserCall, N> {
self.call_builder(&addUserCall { user })
}
///Creates a new call builder for the [`removeUser`] function.
pub fn removeUser(
&self,
user: alloy::sol_types::private::Address,
) -> alloy_contract::SolCallBuilder<T, &P, removeUserCall, N> {
self.call_builder(&removeUserCall { user })
}
///Creates a new call builder for the [`userExists`] function.
pub fn userExists(
&self,
user: alloy::sol_types::private::Address,
) -> alloy_contract::SolCallBuilder<T, &P, userExistsCall, N> {
self.call_builder(&userExistsCall { user })
}
}
/// Event filters.
#[automatically_derived]
impl<
T: alloy_contract::private::Transport + ::core::clone::Clone,
P: alloy_contract::private::Provider<T, N>,
N: alloy_contract::private::Network,
> IScKeystoreInstance<T, P, N>
{
/// Creates a new event filter using this contract instance's provider and address.
///
/// Note that the type can be any event, not just those defined in this contract.
/// Prefer using the other methods for building type-safe event filters.
pub fn event_filter<E: alloy_sol_types::SolEvent>(
&self,
) -> alloy_contract::Event<T, &P, E, N> {
alloy_contract::Event::new_sol(&self.provider, &self.address)
}
}
}

View File

@@ -1,19 +0,0 @@
#![allow(unused_imports, clippy::all, rustdoc::all)]
//! This module contains the sol! generated bindings for solidity contracts.
//! This is autogenerated code.
//! Do not manually edit these files.
//! These files may be overwritten by the codegen system at any time.
pub mod context;
pub mod deploy;
pub mod deploymentconfig;
pub mod ierc165;
pub mod ierc20;
pub mod ierc721;
pub mod ierc721enumerable;
pub mod ierc721metadata;
pub mod ierc721tokenreceiver;
pub mod isckeystore;
pub mod mockerc20;
pub mod mockerc721;
pub mod ownable;
pub mod sckeystore;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +1,23 @@
version: "3.8"
name: ${NAME}
services:
redis:
image: redis@sha256:0c6f34a2d41992ee1e02d52d712c12ac46c4d5a63efdab74915141a52c529586
entrypoint: ["redis-server", "--port", "${REDIS_PORT}"]
frontend:
build:
context: frontend
dockerfile: Dockerfile
ports:
- "${REDIS_PORT}:${REDIS_PORT}"
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 1s
timeout: 3s
retries: 5
- ${FRONTEND_PORT}:5173
environment:
- PUBLIC_API_URL=http://127.0.0.1:${BACKEND_PORT}
- PUBLIC_WEBSOCKET_URL=ws://127.0.0.1:${BACKEND_PORT}
depends_on:
- backend
anvil:
image: ghcr.io/foundry-rs/foundry:latest
healthcheck:
test:
[
"CMD",
"cast",
"chain-id",
"--rpc-url",
"http://localhost:${ANVIL_PORT}",
]
interval: 5s
timeout: 3s
retries: 5
backend:
build:
context: .
dockerfile: Dockerfile
ports:
- "${ANVIL_PORT}:${ANVIL_PORT}"
entrypoint: ["anvil", "--host", "0.0.0.0"]
platform: linux/amd64
- ${BACKEND_PORT}:3000
environment:
- RUST_LOG=info
- NODE=${NODE}

View File

@@ -1,39 +1,29 @@
[package]
name = "ds"
version = "0.1.0"
version = "1.0.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# chrono = "=0.4.38"
# waku-bindings = "=0.6.0"
bus = "=2.4.1"
fred = { version = "=9.0.3", features = ["subscriber-client"] }
tokio = { version = "=1.38.0", features = ["full"] }
tokio-tungstenite = "0.15"
tungstenite = "0.14"
futures-util = "0.3"
tokio-stream = "0.1"
alloy = { git = "https://github.com/alloy-rs/alloy", features = [
"providers",
"node-bindings",
"network",
"transports",
"k256",
"rlp",
] }
tokio-util = "=0.7.11"
waku-bindings = { git = "https://github.com/waku-org/waku-rust-bindings.git", branch = "force-cluster-15", subdir = "waku-bindings" }
waku-sys = { git = "https://github.com/waku-org/waku-rust-bindings.git", branch = "force-cluster-15", subdir = "waku-sys" }
openmls = { version = "=0.5.0", features = ["test-utils"] }
rand = { version = "^0.8" }
tokio = { version = "=1.38.0", features = ["full"] }
kameo = "=0.13.0"
chrono = "=0.4.38"
uuid = { version = "=1.11.0", features = [
"v4",
"fast-rng",
"macro-diagnostics",
] }
anyhow = "=1.0.81"
thiserror = "=1.0.61"
tls_codec = "=0.3.0"
serde_json = "=1.0"
serde = "=1.0.204"
sc_key_store = { path = "../sc_key_store" }
url = "2.5.2"
env_logger = "=0.11.5"
log = "=0.4.22"

View File

@@ -1,206 +0,0 @@
use alloy::primitives::Address;
use alloy::signers::Signature;
use futures_util::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::{mpsc, Mutex};
use tokio_tungstenite::tungstenite::protocol::Message;
use crate::chat_server::ServerMessage;
use crate::DeliveryServiceError;
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub enum ChatMessages {
Request(RequestMLSPayload),
Response(ResponseMLSPayload),
Welcome(String),
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub enum ReqMessageType {
InviteToGroup,
RemoveFromGroup,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct RequestMLSPayload {
sc_address: String,
group_name: String,
pub msg_type: ReqMessageType,
}
impl RequestMLSPayload {
pub fn new(sc_address: String, group_name: String, msg_type: ReqMessageType) -> Self {
RequestMLSPayload {
sc_address,
group_name,
msg_type,
}
}
pub fn msg_to_sign(&self) -> String {
self.sc_address.to_owned() + &self.group_name
}
pub fn group_name(&self) -> String {
self.group_name.clone()
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct ResponseMLSPayload {
signature: String,
user_address: String,
pub group_name: String,
key_package: Vec<u8>,
}
impl ResponseMLSPayload {
pub fn new(
signature: String,
user_address: String,
group_name: String,
key_package: Vec<u8>,
) -> Self {
Self {
signature,
user_address,
group_name,
key_package,
}
}
pub fn validate(
&self,
sc_address: String,
group_name: String,
) -> Result<(String, Vec<u8>), DeliveryServiceError> {
let recover_sig: Signature = serde_json::from_str(&self.signature)?;
let addr = Address::from_str(&self.user_address)?;
// Recover the signer from the message.
let recovered =
recover_sig.recover_address_from_msg(sc_address.to_owned() + &group_name)?;
if recovered.ne(&addr) {
return Err(DeliveryServiceError::ValidationError(recovered.to_string()));
}
Ok((self.user_address.clone(), self.key_package.clone()))
}
}
pub struct ChatClient {
sender: mpsc::UnboundedSender<Message>,
}
impl ChatClient {
pub async fn connect(
server_addr: &str,
username: String,
) -> Result<(Self, mpsc::UnboundedReceiver<Message>), DeliveryServiceError> {
let (ws_stream, _) = tokio_tungstenite::connect_async(server_addr).await?;
let (mut write, read) = ws_stream.split();
let (sender, receiver) = mpsc::unbounded_channel();
let (msg_sender, msg_receiver) = mpsc::unbounded_channel();
let receiver = Arc::new(Mutex::new(receiver));
// Spawn a task to handle outgoing messages
tokio::spawn(async move {
while let Some(message) = receiver.lock().await.recv().await {
if let Err(err) = write.send(message).await {
return Err(DeliveryServiceError::SenderError(err.to_string()));
}
}
Ok(())
});
// Spawn a task to handle incoming messages
tokio::spawn(async move {
let mut read = read;
while let Some(Ok(message)) = read.next().await {
if let Err(err) = msg_sender.send(message) {
return Err(DeliveryServiceError::SenderError(err.to_string()));
}
}
Ok(())
});
// Send a SystemJoin message when registering
let join_msg = ServerMessage::SystemJoin {
username: username.to_string(),
};
let join_json = serde_json::to_string(&join_msg).unwrap();
sender
.send(Message::Text(join_json))
.map_err(|err| DeliveryServiceError::SenderError(err.to_string()))?;
Ok((ChatClient { sender }, msg_receiver))
}
pub fn send_message(&self, msg: ServerMessage) -> Result<(), DeliveryServiceError> {
let msg_json = serde_json::to_string(&msg).unwrap();
self.sender
.send(Message::Text(msg_json))
.map_err(|err| DeliveryServiceError::SenderError(err.to_string()))?;
Ok(())
}
}
#[test]
fn test_sign() {
use alloy::signers::SignerSync;
let signer = alloy::signers::local::PrivateKeySigner::from_str(
"0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
)
.unwrap();
// Sign a message.
let message = "You are joining the group with smart contract: ".to_owned()
+ "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512";
let signature = signer.sign_message_sync(message.as_bytes()).unwrap();
let json = serde_json::to_string(&signature).unwrap();
let recover_sig: Signature = serde_json::from_str(&json).unwrap();
// Recover the signer from the message.
let recovered = recover_sig.recover_address_from_msg(message).unwrap();
assert_eq!(recovered, signer.address());
}
#[test]
fn json_test() {
let inner_msg = ChatMessages::Request(RequestMLSPayload::new(
"sc_address".to_string(),
"group_name".to_string(),
ReqMessageType::InviteToGroup,
));
let res = serde_json::to_string(&inner_msg);
assert!(res.is_ok());
let json_inner_msg = res.unwrap();
let server_msg = ServerMessage::InMessage {
from: "alice".to_string(),
to: vec!["bob".to_string()],
msg: json_inner_msg,
};
let res = serde_json::to_string(&server_msg);
assert!(res.is_ok());
let json_server_msg = res.unwrap();
////
if let Ok(chat_message) = serde_json::from_str::<ServerMessage>(&json_server_msg) {
assert_eq!(chat_message, server_msg);
match chat_message {
ServerMessage::InMessage { from, to, msg } => {
if let Ok(chat_msg) = serde_json::from_str::<ChatMessages>(&msg) {
assert_eq!(chat_msg, inner_msg);
}
}
ServerMessage::SystemJoin { username } => {}
}
}
}

View File

@@ -1,114 +0,0 @@
use futures_util::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::{
net::TcpListener,
sync::{mpsc, Mutex},
};
use tokio_tungstenite::{accept_async, tungstenite::protocol::Message};
use crate::DeliveryServiceError;
type Tx = mpsc::UnboundedSender<Message>;
type PeerMap = Arc<Mutex<HashMap<String, Tx>>>;
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(tag = "type")]
pub enum ServerMessage {
InMessage {
from: String,
to: Vec<String>,
msg: String,
},
SystemJoin {
username: String,
},
}
pub async fn start_server(addr: &str) -> Result<(), DeliveryServiceError> {
let listener = TcpListener::bind(addr).await?;
let peers = PeerMap::new(Mutex::new(HashMap::new()));
while let Ok((stream, _)) = listener.accept().await {
let peers = peers.clone();
tokio::spawn(async move {
if let Err(e) = handle_connection(peers, stream).await {
eprintln!("Error in connection handling: {:?}", e);
}
});
}
Ok(())
}
async fn handle_connection(
peers: PeerMap,
stream: tokio::net::TcpStream,
) -> Result<(), DeliveryServiceError> {
let ws_stream = accept_async(stream).await?;
let (mut write, mut read) = ws_stream.split();
let (sender, receiver) = mpsc::unbounded_channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut username = String::new();
// Spawn a task to handle outgoing messages
tokio::spawn(async move {
while let Some(message) = receiver.lock().await.recv().await {
if let Err(err) = write.send(message).await {
return Err(DeliveryServiceError::SenderError(err.to_string()));
}
}
Ok(())
});
// Handle incoming messages
while let Some(Ok(Message::Text(text))) = read.next().await {
if let Ok(chat_message) = serde_json::from_str::<ServerMessage>(&text) {
match chat_message {
ServerMessage::SystemJoin {
username: join_username,
} => {
username = join_username.clone();
peers
.lock()
.await
.insert(join_username.clone(), sender.clone());
println!("{} joined the chat", join_username);
}
ServerMessage::InMessage { from, to, msg } => {
println!("Received message from {} to {:?}: {}", from, to, msg);
for recipient in to {
if let Some(recipient_sender) = peers.lock().await.get(&recipient) {
let message = ServerMessage::InMessage {
from: from.clone(),
to: vec![recipient.clone()],
msg: msg.clone(),
};
let message_json = serde_json::to_string(&message).unwrap();
recipient_sender
.send(Message::Text(message_json))
.map_err(|err| {
DeliveryServiceError::SenderError(err.to_string())
})?;
}
}
}
}
}
}
// Remove the user from the map when they disconnect
if !username.is_empty() {
peers.lock().await.remove(&username);
println!("{} left the chat", username);
}
Ok(())
}
#[tokio::test]
async fn start_test() {
start_server("127.0.0.1:8080").await.unwrap()
}

View File

@@ -1,60 +0,0 @@
use fred::{
clients::{RedisClient, SubscriberClient},
prelude::*,
types::Message,
};
use serde::{Deserialize, Serialize};
use tokio::sync::broadcast::Receiver;
use crate::DeliveryServiceError;
// use waku_bindings::*;
pub struct RClient {
client: RedisClient,
sub_client: SubscriberClient,
// broadcaster: Receiver<Message>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SenderStruct {
pub sender: String,
pub msg: Vec<u8>,
}
impl RClient {
pub async fn new_with_group(
group_id: String,
) -> Result<(Self, Receiver<Message>), DeliveryServiceError> {
let redis_client = RedisClient::default();
let subscriber: SubscriberClient =
Builder::default_centralized().build_subscriber_client()?;
redis_client.init().await?;
subscriber.init().await?;
subscriber.subscribe(group_id.clone()).await?;
Ok((
RClient {
client: redis_client,
sub_client: subscriber.clone(),
},
subscriber.message_rx(),
))
}
pub async fn remove_group(&mut self, group_id: String) -> Result<(), DeliveryServiceError> {
self.sub_client.unsubscribe(group_id).await?;
Ok(())
}
pub async fn msg_send(
&mut self,
msg: Vec<u8>,
sender: String,
group_id: String,
) -> Result<(), DeliveryServiceError> {
let json_value = SenderStruct { sender, msg };
let bytes = serde_json::to_vec(&json_value)?;
self.client.publish(group_id, bytes.as_slice()).await?;
Ok(())
}
}

123
ds/src/ds_waku.rs Normal file
View File

@@ -0,0 +1,123 @@
use core::result::Result;
use std::{
borrow::Cow,
str::FromStr,
sync::{Arc, Mutex as SyncMutex},
thread,
time::Duration,
};
use waku_bindings::*;
use crate::DeliveryServiceError;
pub const GROUP_VERSION: &str = "1";
pub const APP_MSG_SUBTOPIC: &str = "app_msg";
pub const COMMIT_MSG_SUBTOPIC: &str = "commit_msg";
pub const WELCOME_SUBTOPIC: &str = "welcome";
pub const SUBTOPICS: [&str; 3] = [APP_MSG_SUBTOPIC, COMMIT_MSG_SUBTOPIC, WELCOME_SUBTOPIC];
/// The pubsub topic for the Waku Node
/// Fixed for now because nodes on the network would need to be subscribed to existing pubsub topics
pub fn pubsub_topic() -> WakuPubSubTopic {
"/waku/2/rs/15/0".to_string()
}
/// Build the content topics for a group. Subtopics are fixed for de-mls group communication.
///
/// Input:
/// - group_name: The name of the group
/// - group_version: The version of the group
///
/// Returns:
/// - content_topics: The content topics of the group
pub fn build_content_topics(group_name: &str, group_version: &str) -> Vec<WakuContentTopic> {
SUBTOPICS
.iter()
.map(|subtopic| build_content_topic(group_name, group_version, subtopic))
.collect::<Vec<WakuContentTopic>>()
}
/// Build the content topic for the given group and subtopic
/// Input:
/// - group_name: The name of the group
/// - group_version: The version of the group
/// - subtopic: The subtopic of the group
///
/// Returns:
/// - content_topic: The content topic of the subtopic
pub fn build_content_topic(
group_name: &str,
group_version: &str,
subtopic: &str,
) -> WakuContentTopic {
WakuContentTopic {
application_name: Cow::from(group_name.to_string()),
version: Cow::from(group_version.to_string()),
content_topic_name: Cow::from(subtopic.to_string()),
encoding: Encoding::Proto,
}
}
/// Build the content filter for the given pubsub topic and content topics
/// Input:
/// - pubsub_topic: The pubsub topic of the Waku Node
/// - content_topics: The content topics of the group
///
/// Returns:
/// - content_filter: The content filter of the group
pub fn content_filter(
pubsub_topic: &WakuPubSubTopic,
content_topics: &[WakuContentTopic],
) -> ContentFilter {
ContentFilter::new(Some(pubsub_topic.to_string()), content_topics.to_vec())
}
/// Setup the Waku Node Handle
/// Input:
/// - nodes_addresses: The addresses of the nodes to connect to
///
/// Returns:
/// - node_handle: The Waku Node Handle
#[allow(clippy::field_reassign_with_default)]
pub fn setup_node_handle(
nodes_addresses: Vec<String>,
) -> Result<WakuNodeHandle<Running>, DeliveryServiceError> {
let mut config = WakuNodeConfig::default();
// Set the port to 0 to let the system choose a random port
config.port = Some(0);
config.log_level = Some(WakuLogLevel::Panic);
let node_handle = waku_new(Some(config))
.map_err(|e| DeliveryServiceError::WakuNodeAlreadyInitialized(e.to_string()))?;
let node_handle = node_handle
.start()
.map_err(|e| DeliveryServiceError::WakuNodeAlreadyInitialized(e.to_string()))?;
let content_filter = ContentFilter::new(Some(pubsub_topic()), vec![]);
node_handle
.relay_subscribe(&content_filter)
.map_err(|e| DeliveryServiceError::WakuSubscribeToContentFilterError(e.to_string()))?;
for address in nodes_addresses
.iter()
.map(|a| Multiaddr::from_str(a.as_str()))
{
let address =
address.map_err(|e| DeliveryServiceError::FailedToParseMultiaddr(e.to_string()))?;
let peerid = node_handle
.add_peer(&address, ProtocolId::Relay)
.map_err(|e| DeliveryServiceError::WakuAddPeerError(e.to_string()))?;
node_handle
.connect_peer_with_id(&peerid, None)
.map_err(|e| DeliveryServiceError::WakuConnectPeerError(e.to_string()))?;
thread::sleep(Duration::from_secs(2));
}
Ok(node_handle)
}
/// Check if a content topic exists in a list of topics or if the list is empty
pub fn match_content_topic(
content_topics: &Arc<SyncMutex<Vec<WakuContentTopic>>>,
topic: &WakuContentTopic,
) -> bool {
let locked_topics = content_topics.lock().unwrap();
locked_topics.is_empty() || locked_topics.iter().any(|t| t == topic)
}

View File

@@ -1,33 +1,27 @@
use alloy::{hex::FromHexError, primitives::SignatureError};
use fred::error::RedisError;
pub mod chat_client;
pub mod chat_server;
pub mod ds;
pub mod ds_waku;
pub mod waku_actor;
#[derive(Debug, thiserror::Error)]
pub enum DeliveryServiceError {
#[error("Validation failed: {0}")]
ValidationError(String),
#[error("Waku publish message error: {0}")]
WakuPublishMessageError(String),
#[error("Waku relay topics error: {0}")]
WakuRelayTopicsError(String),
#[error("Waku subscribe to group error: {0}")]
WakuSubscribeToGroupError(String),
#[error("Waku receive message error: {0}")]
WakuReceiveMessageError(String),
#[error("Waku node already initialized: {0}")]
WakuNodeAlreadyInitialized(String),
#[error("Waku subscribe to content filter error: {0}")]
WakuSubscribeToContentFilterError(String),
#[error("Waku add peer error: {0}")]
WakuAddPeerError(String),
#[error("Waku connect peer error: {0}")]
WakuConnectPeerError(String),
#[error("Redis operation failed: {0}")]
RedisError(#[from] RedisError),
#[error("Failed to send message to channel: {0}")]
SenderError(String),
#[error("WebSocket handshake failed.")]
HandshakeError(#[from] tokio_tungstenite::tungstenite::Error),
#[error("Serialization error: {0}")]
TlsError(#[from] tls_codec::Error),
#[error("JSON processing error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Failed to bind to the address.")]
BindError(#[from] std::io::Error),
#[error("Failed to parse address: {0}")]
AlloyFromHexError(#[from] FromHexError),
#[error("Failed to recover signature: {0}")]
AlloySignatureError(#[from] SignatureError),
#[error("Failed to parse multiaddr: {0}")]
FailedToParseMultiaddr(String),
#[error("An unknown error occurred: {0}")]
Other(anyhow::Error),

151
ds/src/waku_actor.rs Normal file
View File

@@ -0,0 +1,151 @@
use chrono::Utc;
use core::result::Result;
use kameo::{
message::{Context, Message},
Actor,
};
use log::debug;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use waku_bindings::{Running, WakuContentTopic, WakuMessage, WakuNodeHandle};
use crate::ds_waku::{pubsub_topic, GROUP_VERSION};
use crate::{
ds_waku::{build_content_topic, build_content_topics, content_filter},
DeliveryServiceError,
};
/// WakuActor is the actor that handles the Waku Node
#[derive(Actor)]
pub struct WakuActor {
node: Arc<WakuNodeHandle<Running>>,
}
impl WakuActor {
/// Create a new WakuActor
/// Input:
/// - node: The Waku Node to handle. Waku Node is already running
pub fn new(node: Arc<WakuNodeHandle<Running>>) -> Self {
Self { node }
}
}
/// Message to send to the Waku Node
/// This message is used to send a message to the Waku Node
/// Input:
/// - msg: The message to send
/// - subtopic: The subtopic to send the message to
/// - group_id: The group to send the message to
/// - app_id: The app is unique identifier for the application that is sending the message for filtering own messages
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProcessMessageToSend {
pub msg: Vec<u8>,
pub subtopic: String,
pub group_id: String,
pub app_id: Vec<u8>,
}
impl ProcessMessageToSend {
/// Build a WakuMessage from the message to send
/// Input:
/// - msg: The message to send
///
/// Returns:
/// - WakuMessage: The WakuMessage to send
pub fn build_waku_message(&self) -> Result<WakuMessage, DeliveryServiceError> {
let content_topic = build_content_topic(&self.group_id, GROUP_VERSION, &self.subtopic);
Ok(WakuMessage::new(
self.msg.clone(),
content_topic,
2,
Utc::now().timestamp() as usize,
self.app_id.clone(),
true,
))
}
}
/// Handle the message to send to the Waku Node
/// Input:
/// - msg: The message to send
///
/// Returns:
/// - msg_id: The message id of the message sent to the Waku Node
impl Message<ProcessMessageToSend> for WakuActor {
type Reply = Result<String, DeliveryServiceError>;
async fn handle(
&mut self,
msg: ProcessMessageToSend,
_ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
let waku_message = msg.build_waku_message()?;
let msg_id = self
.node
.relay_publish_message(&waku_message, Some(pubsub_topic()), None)
.map_err(|e| {
debug!("Failed to relay publish the message: {:?}", e);
DeliveryServiceError::WakuPublishMessageError(e)
})?;
Ok(msg_id)
}
}
/// Message for actor to subscribe to a group
/// It contains the group name to subscribe to
pub struct ProcessSubscribeToGroup {
pub group_name: String,
}
/// Handle the message for actor to subscribe to a group
/// Input:
/// - group_name: The group to subscribe to
///
/// Returns:
/// - content_topics: The content topics of the group
impl Message<ProcessSubscribeToGroup> for WakuActor {
type Reply = Result<Vec<WakuContentTopic>, DeliveryServiceError>;
async fn handle(
&mut self,
msg: ProcessSubscribeToGroup,
_ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
let content_topics = build_content_topics(&msg.group_name, GROUP_VERSION);
let content_filter = content_filter(&pubsub_topic(), &content_topics);
self.node.relay_subscribe(&content_filter).map_err(|e| {
debug!("Failed to relay subscribe to the group: {:?}", e);
DeliveryServiceError::WakuSubscribeToGroupError(e)
})?;
Ok(content_topics)
}
}
/// Message for actor to unsubscribe from a group
/// It contains the group name to unsubscribe from
pub struct ProcessUnsubscribeFromGroup {
pub group_name: String,
}
/// Handle the message for actor to unsubscribe from a group
/// Input:
/// - group_name: The group to unsubscribe from
///
/// Returns:
/// - ()
impl Message<ProcessUnsubscribeFromGroup> for WakuActor {
type Reply = Result<(), DeliveryServiceError>;
async fn handle(
&mut self,
msg: ProcessUnsubscribeFromGroup,
_ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
let content_topics = build_content_topics(&msg.group_name, GROUP_VERSION);
let content_filter = content_filter(&pubsub_topic(), &content_topics);
self.node
.relay_unsubscribe(&content_filter)
.map_err(|e| DeliveryServiceError::WakuRelayTopicsError(e.to_string()))?;
Ok(())
}
}

132
ds/tests/ds_waku_test.rs Normal file
View File

@@ -0,0 +1,132 @@
use kameo::{
actor::pubsub::PubSub,
message::{Context, Message},
Actor,
};
use log::{error, info};
use std::{
sync::{Arc, Mutex},
time::Duration,
};
use tokio::sync::mpsc::channel;
use waku_bindings::WakuMessage;
use ds::{
ds_waku::{
build_content_topics, match_content_topic, setup_node_handle, APP_MSG_SUBTOPIC,
GROUP_VERSION,
},
waku_actor::{ProcessMessageToSend, ProcessSubscribeToGroup, WakuActor},
DeliveryServiceError,
};
use waku_bindings::waku_set_event_callback;
#[derive(Debug, Clone, Actor)]
pub struct ActorA {
pub app_id: String,
}
impl ActorA {
pub fn new() -> Self {
let app_id = uuid::Uuid::new_v4().to_string();
Self { app_id }
}
}
impl Message<WakuMessage> for ActorA {
type Reply = Result<WakuMessage, DeliveryServiceError>;
async fn handle(
&mut self,
msg: WakuMessage,
_ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
println!("ActorA received message: {:?}", msg.timestamp());
Ok(msg)
}
}
#[tokio::test]
async fn test_waku_client() {
let group_name = "new_group".to_string();
let mut pubsub = PubSub::<WakuMessage>::new();
let (sender_alice, mut receiver_alice) = channel(100);
// TODO: get node from env
let res = setup_node_handle(vec![]);
assert!(res.is_ok());
let node = res.unwrap();
let uuid = uuid::Uuid::new_v4().as_bytes().to_vec();
let waku_actor = WakuActor::new(Arc::new(node));
let actor_ref = kameo::spawn(waku_actor);
let actor_a = ActorA::new();
let actor_a_ref = kameo::spawn(actor_a);
pubsub.subscribe(actor_a_ref);
let content_topics = Arc::new(Mutex::new(build_content_topics(&group_name, GROUP_VERSION)));
assert!(actor_ref
.ask(ProcessSubscribeToGroup {
group_name: group_name.clone(),
})
.await
.is_ok());
waku_set_event_callback(move |signal| {
match signal.event() {
waku_bindings::Event::WakuMessage(event) => {
let content_topic = event.waku_message().content_topic();
// Check if message belongs to a relevant topic
assert!(match_content_topic(&content_topics, content_topic));
let msg = event.waku_message().clone();
info!("msg: {:?}", msg.timestamp());
assert!(sender_alice.blocking_send(msg).is_ok());
}
waku_bindings::Event::Unrecognized(data) => {
error!("Unrecognized event!\n {data:?}");
}
_ => {
error!(
"Unrecognized signal!\n {:?}",
serde_json::to_string(&signal)
);
}
}
});
let sender = tokio::spawn(async move {
for _ in 0..10 {
assert!(actor_ref
.ask(ProcessMessageToSend {
msg: format!("test_message").as_bytes().to_vec(),
subtopic: APP_MSG_SUBTOPIC.to_string(),
group_id: group_name.clone(),
app_id: uuid.clone(),
})
.await
.is_ok());
tokio::time::sleep(Duration::from_secs(1)).await;
}
info!("sender handle is finished");
});
let receiver = tokio::spawn(async move {
while let Some(msg) = receiver_alice.recv().await {
info!("msg received: {:?}", msg.timestamp());
pubsub.publish(msg).await;
}
info!("receiver handle is finished");
});
tokio::select! {
x = sender => {
info!("get from sender: {:?}", x);
}
w = receiver => {
info!("get from receiver: {:?}", w);
}
}
}

10
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
frontend/.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

11
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM node:18-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
EXPOSE 5173
CMD [ "npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"]

2656
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

33
frontend/package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "client",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-netlify": "^1.0.0-next.88",
"@sveltejs/kit": "^1.15.2",
"@tailwindcss/typography": "^0.5.9",
"autoprefixer": "^10.4.14",
"daisyui": "^2.51.3",
"postcss": "^8.4.21",
"svelte": "^3.54.0",
"svelte-check": "^3.0.1",
"tailwindcss": "^3.2.7",
"tslib": "^2.4.1",
"typescript": "^4.9.3",
"vite": "^4.5.2"
},
"type": "module",
"dependencies": {
"@sveltejs/adapter-static": "^2.0.1",
"svelte-french-toast": "^1.0.4-beta.0",
"svelte-preprocess": "^5.0.1"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

3
frontend/src/app.css Normal file
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

12
frontend/src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}
export {};

12
frontend/src/app.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -0,0 +1,7 @@
import { writable } from "svelte/store";
export const user = writable("");
export const channel = writable("");
export const eth_private_key = writable("");
export const group = writable("");
export const createNewRoom = writable(false);

View File

@@ -0,0 +1,11 @@
<script>
import "../app.css"
import {Toaster} from 'svelte-french-toast';
</script>
<Toaster/>
<div class="flex flex-col h-screen sm:justify-center sm:items-center">
<div class="mx-auto px-3 md:px-0 sm:w-full md:w-4/5 lg:w-3/5 pt-24 sm:pt-0">
<slot/>
</div>
</div>

View File

@@ -0,0 +1,99 @@
<script lang="ts">
import {user, channel, eth_private_key, group, createNewRoom} from "$lib/stores/user"
import {goto, invalidate} from '$app/navigation';
import { env } from '$env/dynamic/public'
import toast from 'svelte-french-toast';
let status, rooms;
export let data;
$:({status, rooms} = data);
let eth_pk = "";
let room = "";
let create_new_room = false;
const join_room = () => {
eth_private_key.set(eth_pk);
group.set(room);
createNewRoom.set(create_new_room);
goto("/chat");
}
const select_room = (selected_room: string) => {
room = selected_room;
};
const filled_in = () => {
return !(eth_pk.length > 0 && room.length > 0);
};
const reload = () => {
toast.success("Reloaded rooms")
let url = `${env.PUBLIC_API_URL}`;
if (url.endsWith("/")) {
url = url.slice(0, -1);
}
invalidate(`${url}/rooms`);
}
</script>
<div class="flex flex-col justify-center">
<div class="title">
<h1 class="text-3xl font-bold text-center">Chatr: a Websocket chatroom</h1>
</div>
<div class="join self-center">
</div>
<div class="rooms self-center my-5">
<div class="flex justify-between py-2">
<h2 class="text-xl font-bold ">
List of active chatroom's
</h2>
<button class="btn btn-square btn-sm btn-accent" on:click={reload}>↻</button>
</div>
{#if status && rooms.length < 1}
<div class="card bg-base-300 w-96 shadow-xl text-center">
<div class="card-body">
<h3 class="card-title ">{status}</h3>
</div>
</div>
{/if}
{#if rooms}
{#each rooms as room}
<div class="card bg-base-300 w-96 shadow-xl my-3" on:click={select_room(room)}>
<div class="card-body">
<div class="flex justify-between">
<h2 class="card-title">{room}</h2>
<button class="btn btn-primary btn-md">Select Room</button>
</div>
</div>
</div>
{/each}
{/if}
</div>
<div class="create self-center my-5 w-[40rem]">
<div>
<label class="label" for="eth-private-key">
<span class="label-text">Eth Private Key</span>
</label>
<input id="eth-private-key" placeholder="Eth Private Key" bind:value={eth_pk}
class="input input-bordered input-primary w-full bg-base-200 mb-4 mr-3">
</div>
<div>
<label class="label" for="room-name">
<span class="label-text">Room name</span>
</label>
<input id="room-name" placeholder="Room Name" bind:value={room}
class="input input-bordered input-primary w-full bg-base-200 mb-4 mr-3">
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Create Room</span>
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={create_new_room} />
</label>
</div>
<button class="btn btn-primary" disabled="{filled_in(eth_pk, room, create_new_room)}" on:click={join_room}>Join Room.</button>
</div>
<div class="github self-center">
<p>
Check out <a class="link link-accent" href="https://github.com/0xLaurens/chatr" target="_blank"
rel="noreferrer">Chatr</a>, to view the source code!
</p>
</div>
</div>

View File

@@ -0,0 +1,18 @@
import type {PageLoad} from './$types';
import {env} from "$env/dynamic/public";
export const load: PageLoad = async ({fetch}) => {
try {
let url = `${env.PUBLIC_API_URL}`;
if (url.endsWith("/")) {
url = url.slice(0, -1);
}
const res = await fetch(`${url}/rooms`);
return await res.json();
} catch (e) {
return {
status: "API offline (try again in a min)",
rooms: []
}
}
}

View File

@@ -0,0 +1,117 @@
<script lang="ts">
import {onMount, onDestroy} from "svelte";
import {user, channel, eth_private_key, group, createNewRoom} from "../../lib/stores/user";
import {goto} from '$app/navigation';
import {env} from '$env/dynamic/public'
import toast from "svelte-french-toast";
import { json } from "@sveltejs/kit";
let status = "🔴";
let statusTip = "Disconnected";
let message = "";
let messages: any[] = [];
let socket: WebSocket;
let interval: number;
let delay = 2000;
let timeout = false;
$: {
if (interval || (!timeout && interval)) {
clearInterval(interval);
}
if (timeout == true) {
interval = setInterval(() => {
if (delay < 30_000) delay = delay * 2;
console.log("reconnecting in:", delay)
connect();
}, delay)
}
}
function connect() {
socket = new WebSocket(`${env.PUBLIC_WEBSOCKET_URL}/ws`)
socket.addEventListener("open", () => {
status = "🟢"
statusTip = "Connected";
timeout = false;
socket.send(JSON.stringify({
eth_private_key: $eth_private_key,
group_id: $group,
should_create: $createNewRoom,
}));
})
socket.addEventListener("close", () => {
status = "🔴";
statusTip = "Disconnected";
if (timeout == false) {
delay = 2000;
timeout = true;
}
})
socket.addEventListener('message', function (event) {
if (event.data == "Username already taken.") {
toast.error(event.data)
goto("/");
} else {
messages = [...messages, event.data]
}
})
}
onMount(() => {
if ($eth_private_key.length < 1 || $group.length < 1 ) {
toast.error("Something went wrong!")
goto("/");
} else {
connect()
}
}
)
onDestroy(() => {
if (socket) {
socket.close()
}
if (interval) {
clearInterval(interval)
}
timeout = false
})
const sendMessage = () => {
socket.send(JSON.stringify({
message: message,
group_id: $group,
}));
message = "";
};
const clear_messages = () => {
messages = [];
};
</script>
<div class="title flex justify-between">
<h1 class="text-3xl font-bold cursor-default">Chat Room <span class="tooltip" data-tip="{statusTip}">{status}</span>
</h1>
<button class="btn btn-accent" on:click={clear_messages}>clear</button>
</div>
<div class="card h-96 flex-grow bg-base-300 shadow-xl my-10">
<div class="card-body">
<div class="flex flex-col overflow-y-auto max-h-80 scroll-smooth">
{#each messages as msg}
<div class="my-2">{msg}</div>
{/each}
</div>
</div>
</div>
<div class="message-box flex justify-end">
<form on:submit|preventDefault={sendMessage}>
<input placeholder="Message" class="input input-bordered input-primary w-[51rem] bg-base-200 mb-2"
bind:value={message}>
<button class="btn btn-primary w-full sm:w-auto btn-wide">Send</button>
</form>
</div>

BIN
frontend/static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

16
frontend/svelte.config.js Normal file
View File

@@ -0,0 +1,16 @@
import adapter from '@sveltejs/adapter-netlify';
import preprocess from 'svelte-preprocess';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: preprocess({
postcss: true
}),
kit: {
adapter: adapter()
}
};
export default config;

View File

@@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {}
},
plugins: [require('@tailwindcss/typography'), require('daisyui')],
};

21
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"paths": {
"$lib": ["src/lib"],
"$lib/*": ["src/lib/*"]
}
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

6
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,6 @@
import {sveltekit} from '@sveltejs/kit/vite';
import {defineConfig} from 'vite';
export default defineConfig({
plugins: [sveltekit()],
});

View File

@@ -1,32 +0,0 @@
[package]
name = "sc_key_store"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
foundry-contracts.workspace = true
openmls = { version = "=0.5.0", features = ["test-utils"] }
openmls_basic_credential = "=0.2.0"
thiserror = "=1.0.61"
anyhow = "=1.0.81"
tls_codec = "=0.3.0"
hex = "0.4.3"
url = "2.5.2"
eyre = "=0.6"
tokio = { version = "=1.38.0", features = ["macros", "rt-multi-thread"] }
alloy = { git = "https://github.com/alloy-rs/alloy", features = [
"providers",
"node-bindings",
"network",
"signer-local",
"transports",
"transport-http",
"k256",
] }
mls_crypto = { path = "../mls_crypto" }

View File

@@ -1,36 +0,0 @@
pub mod sc_ks;
use alloy::hex::FromHexError;
pub trait SCKeyStoreService {
fn does_user_exist(
&self,
address: &str,
) -> impl std::future::Future<Output = Result<bool, KeyStoreError>>;
fn add_user(
&mut self,
address: &str,
) -> impl std::future::Future<Output = Result<(), KeyStoreError>>;
fn remove_user(
&self,
address: &str,
) -> impl std::future::Future<Output = Result<(), KeyStoreError>>;
}
#[derive(Debug, thiserror::Error)]
pub enum KeyStoreError {
#[error("User already exists.")]
UserAlreadyExistsError,
#[error("User not found.")]
UserNotFoundError,
#[error("Alloy contract operation failed: {0}")]
AlloyContractError(#[from] alloy::contract::Error),
#[error("Failed to parse address: {0}")]
AddressParseError(#[from] FromHexError),
#[error("An unexpected error occurred: {0}")]
UnexpectedError(anyhow::Error),
}

View File

@@ -1,67 +0,0 @@
use alloy::{network::Network, primitives::Address, providers::Provider, transports::Transport};
use foundry_contracts::sckeystore::ScKeystore::{self, ScKeystoreInstance};
use std::str::FromStr;
use crate::{KeyStoreError, SCKeyStoreService};
pub struct ScKeyStorage<T, P, N> {
instance: ScKeystoreInstance<T, P, N>,
address: String,
}
impl<T, P, N> ScKeyStorage<T, P, N>
where
T: Transport + Clone,
P: Provider<T, N>,
N: Network,
{
pub fn new(provider: P, address: Address) -> Self {
Self {
instance: ScKeystore::new(address, provider),
address: address.to_string(),
}
}
pub fn sc_adsress(&self) -> String {
self.address.clone()
}
}
impl<T: Transport + Clone, P: Provider<T, N>, N: Network> SCKeyStoreService
for ScKeyStorage<T, P, N>
{
async fn does_user_exist(&self, address: &str) -> Result<bool, KeyStoreError> {
let address = Address::from_str(address)?;
let res = self.instance.userExists(address).call().await;
match res {
Ok(is_exist) => Ok(is_exist._0),
Err(err) => Err(KeyStoreError::AlloyContractError(err)),
}
}
async fn add_user(&mut self, address: &str) -> Result<(), KeyStoreError> {
if self.does_user_exist(address).await? {
return Err(KeyStoreError::UserAlreadyExistsError);
}
let add_to_acl_binding = self.instance.addUser(Address::from_str(address)?);
let res = add_to_acl_binding.send().await;
match res {
Ok(_) => Ok(()),
Err(err) => Err(KeyStoreError::AlloyContractError(err)),
}
}
async fn remove_user(&self, address: &str) -> Result<(), KeyStoreError> {
if !self.does_user_exist(address).await? {
return Err(KeyStoreError::UserNotFoundError);
}
let remove_user_binding = self.instance.removeUser(Address::from_str(address)?);
let res = remove_user_binding.send().await;
match res {
Ok(_) => Ok(()),
Err(err) => Err(KeyStoreError::AlloyContractError(err)),
}
}
}

View File

@@ -1,242 +0,0 @@
use clap::{arg, command, Parser, Subcommand};
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::Text,
widgets::{Block, Borders, List, ListItem, Paragraph, Wrap},
Frame, Terminal,
};
use std::{
io::{stdout, Read, Write},
sync::Arc,
};
use tokio::{
sync::mpsc::{Receiver, Sender},
sync::Mutex,
};
use tokio_util::sync::CancellationToken;
use url::Url;
use crate::CliError;
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct Args {
/// User private key that correspond to Ethereum wallet
#[arg(short = 'K', long)]
pub user_priv_key: String,
// /// Rpc url
// #[arg(short = 'U', long,
// default_value_t = Url::from_str("http://localhost:8545").unwrap())]
// pub storage_url: Url,
// /// Storage contract address
// #[arg(short = 'S', long)]
// pub storage_addr: String,
}
pub enum Msg {
Input(Message),
Refresh(String),
Exit,
}
#[derive(Clone)]
pub enum Message {
Incoming(String, String, String),
Mine(String, String, String),
System(String),
Error(String),
}
#[derive(Debug, Parser)]
#[command(multicall = true)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Debug, Subcommand, Clone)]
pub enum Commands {
CreateGroup {
group_name: String,
storage_address: String,
storage_url: Url,
},
Invite {
group_name: String,
users_wallet_addrs: Vec<String>,
},
SendMessage {
group_name: String,
msg: Vec<String>,
},
// RemoveUser { user_wallet: String },
Exit,
}
pub fn readline() -> Result<String, CliError> {
write!(std::io::stdout(), "$ ")?;
std::io::stdout().flush()?;
let mut buffer = String::new();
std::io::stdin().read_to_string(&mut buffer)?;
Ok(buffer)
}
pub async fn event_handler(
messages_tx: Sender<Msg>,
cli_tx: Sender<Commands>,
token: CancellationToken,
) -> Result<(), CliError> {
let mut input = String::new();
loop {
if let Event::Key(key) = tokio::task::spawn_blocking(event::read).await?? {
match key.code {
KeyCode::Char(c) => {
input.push(c);
}
KeyCode::Backspace => {
input.pop();
}
KeyCode::Enter => {
let line: String = std::mem::take(&mut input);
let args = shlex::split(&line).ok_or(CliError::SplitLineError)?;
let cli = Cli::try_parse_from(args);
if cli.is_err() {
messages_tx
.send(Msg::Input(Message::System("Unknown command".to_string())))
.await
.map_err(|err| CliError::SenderError(err.to_string()))?;
continue;
}
cli_tx
.send(cli.unwrap().command)
.await
.map_err(|err| CliError::SenderError(err.to_string()))?;
}
KeyCode::Esc => {
messages_tx
.send(Msg::Exit)
.await
.map_err(|err| CliError::SenderError(err.to_string()))?;
token.cancel();
break;
}
_ => {}
}
messages_tx
.send(Msg::Refresh(input.clone()))
.await
.map_err(|err| CliError::SenderError(err.to_string()))?;
}
}
Ok::<_, CliError>(())
}
pub fn ui(f: &mut Frame, messages: &[Message], input: &str) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Length(3)].as_ref())
.split(f.size());
let message_items: Vec<ListItem> = messages
.iter()
.map(|message| {
let (content, style) = match message {
Message::Incoming(group, from, msg) => (
format!("[0x{}]@{}: {}", from, group, msg),
Style::default().fg(Color::LightGreen),
),
Message::Mine(group, from, msg) => (
format!("[0x{}]@{}: {}", from, group, msg),
Style::default()
.fg(Color::LightGreen)
.add_modifier(Modifier::BOLD),
),
Message::System(msg) => (format!("[System]: {}", msg), Style::default()),
Message::Error(msg) => (msg.clone(), Style::default().fg(Color::LightRed)),
};
ListItem::new(content).style(style)
})
.collect();
let messages_history = List::new(message_items).block(
Block::default()
.borders(Borders::ALL)
.title("messages history"),
);
let input_line = Paragraph::new(Text::raw(input))
.style(
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)
.block(Block::default().borders(Borders::ALL).title("input line"))
.wrap(Wrap { trim: false });
f.render_widget(messages_history, chunks[0]);
f.render_widget(input_line, chunks[1]);
}
pub async fn terminal_handler(
mut messages_rx: Receiver<Msg>,
token: CancellationToken,
) -> Result<(), CliError> {
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let terminal = Arc::new(Mutex::new(Terminal::new(backend)?));
let messages = Arc::new(Mutex::new(vec![]));
let input = Arc::new(Mutex::new(String::new()));
let messages_clone = Arc::clone(&messages);
let input_clone = Arc::clone(&input);
while let Some(msg) = messages_rx.recv().await {
match msg {
Msg::Input(m) => {
let mut messages = messages_clone.lock().await;
messages.push(m);
if messages.len() == 100 {
messages.remove(0);
}
}
Msg::Refresh(i) => {
let mut input = input_clone.lock().await;
*input = i;
}
Msg::Exit => {
token.cancel();
break;
}
};
let messages = Arc::clone(&messages_clone);
let input = Arc::clone(&input_clone);
let terminal = Arc::clone(&terminal);
tokio::task::spawn_blocking(move || {
let messages = messages.blocking_lock();
let input = input.blocking_lock();
terminal
.blocking_lock()
.draw(|f| ui(f, &messages, &input))
.unwrap();
})
.await?;
}
// Restore terminal
disable_raw_mode()?;
let mut terminal_lock = terminal.lock().await;
execute!(terminal_lock.backend_mut(), LeaveAlternateScreen)?;
terminal_lock.show_cursor()?;
Ok(())
}

View File

@@ -1,213 +0,0 @@
use alloy::hex::ToHexExt;
use ds::{
chat_client::{
ChatClient, ChatMessages, ReqMessageType, RequestMLSPayload, ResponseMLSPayload,
},
chat_server::ServerMessage,
};
// use waku_bindings::*;
use openmls::prelude::MlsMessageOut;
use std::{collections::HashMap, sync::Arc};
use tls_codec::Serialize;
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
use crate::ContactError;
pub const CHAT_SERVER_ADDR: &str = "ws://127.0.0.1:8080";
pub struct ContactsList {
contacts: Arc<Mutex<HashMap<String, Contact>>>,
group_id2sc: HashMap<String, String>,
pub future_req: HashMap<String, CancellationToken>,
pub chat_client: ChatClient,
}
pub struct Contact {
// map group_name to key_package bytes
group_id2user_kp: HashMap<String, Vec<u8>>,
// user_p2p_addr: WakuPeers,
}
impl Contact {
pub fn get_relevant_kp(&mut self, group_name: String) -> Result<Vec<u8>, ContactError> {
match self.group_id2user_kp.remove(&group_name) {
Some(kp) => Ok(kp.clone()),
None => Err(ContactError::MissingKeyPackageForGroup),
}
}
pub fn add_key_package(
&mut self,
key_package: Vec<u8>,
group_name: String,
) -> Result<(), ContactError> {
match self.group_id2user_kp.insert(group_name, key_package) {
Some(_) => Err(ContactError::DuplicateUserError),
None => Ok(()),
}
}
}
impl ContactsList {
pub async fn new(chat_client: ChatClient) -> Result<Self, ContactError> {
Ok(ContactsList {
contacts: Arc::new(Mutex::new(HashMap::new())),
group_id2sc: HashMap::new(),
future_req: HashMap::new(),
chat_client,
})
}
pub fn send_welcome_msg_to_users(
&mut self,
self_address: String,
users_address: Vec<String>,
welcome: MlsMessageOut,
) -> Result<(), ContactError> {
let bytes = welcome.tls_serialize_detached()?;
let welcome_str: String = bytes.encode_hex();
let msg = ChatMessages::Welcome(welcome_str);
self.chat_client.send_message(ServerMessage::InMessage {
from: self_address,
to: users_address,
msg: serde_json::to_string(&msg)?,
})?;
Ok(())
}
pub fn send_msg_req(
&mut self,
self_address: String,
user_address: String,
group_name: String,
msg_type: ReqMessageType,
) -> Result<(), ContactError> {
self.future_req
.insert(user_address.clone(), CancellationToken::new());
let sc_address = match self.group_id2sc.get(&group_name).cloned() {
Some(sc) => sc,
None => return Err(ContactError::MissingSmartContractForGroup),
};
let req = ChatMessages::Request(RequestMLSPayload::new(sc_address, group_name, msg_type));
self.chat_client.send_message(ServerMessage::InMessage {
from: self_address,
to: vec![user_address],
msg: serde_json::to_string(&req)?,
})?;
Ok(())
}
pub fn send_resp_msg_to_user(
&mut self,
self_address: String,
user_address: &str,
resp: ResponseMLSPayload,
) -> Result<(), ContactError> {
let resp_j = ChatMessages::Response(resp);
self.chat_client.send_message(ServerMessage::InMessage {
from: self_address,
to: vec![user_address.to_string()],
msg: serde_json::to_string(&resp_j)?,
})?;
Ok(())
}
pub async fn add_new_contact(&mut self, user_address: &str) -> Result<(), ContactError> {
let mut contacts = self.contacts.lock().await;
if contacts.contains_key(user_address) {
return Err(ContactError::DuplicateUserError);
}
match contacts.insert(
user_address.to_string(),
Contact {
group_id2user_kp: HashMap::new(),
},
) {
Some(_) => Err(ContactError::DuplicateUserError),
None => Ok(()),
}
}
pub async fn add_key_package_to_contact(
&mut self,
user_wallet: &str,
key_package: Vec<u8>,
group_name: String,
) -> Result<(), ContactError> {
let mut contacts = self.contacts.lock().await;
match contacts.get_mut(user_wallet) {
Some(user) => user.add_key_package(key_package, group_name)?,
None => return Err(ContactError::UserNotFoundError),
}
Ok(())
}
pub async fn does_user_in_contacts(&self, user_wallet: &str) -> bool {
let contacts = self.contacts.lock().await;
contacts.get(user_wallet).is_some()
}
pub async fn prepare_joiners(
&mut self,
user_wallets: Vec<String>,
group_name: String,
) -> Result<HashMap<String, Vec<u8>>, ContactError> {
let mut joiners_kp = HashMap::with_capacity(user_wallets.len());
for user_wallet in user_wallets {
if joiners_kp.contains_key(&user_wallet) {
return Err(ContactError::DuplicateUserError);
}
let mut contacts = self.contacts.lock().await;
match contacts.get_mut(&user_wallet) {
Some(contact) => match contact.get_relevant_kp(group_name.clone()) {
Ok(kp) => match joiners_kp.insert(user_wallet, kp) {
Some(_) => return Err(ContactError::DuplicateUserError),
None => continue,
},
Err(err) => return Err(err),
},
None => return Err(ContactError::UserNotFoundError),
}
}
Ok(joiners_kp)
}
pub fn handle_response(&mut self, user_address: &str) -> Result<(), ContactError> {
match self.future_req.get(user_address) {
Some(token) => {
token.cancel();
Ok(())
}
None => Err(ContactError::UserNotFoundError),
}
}
pub fn insert_group2sc(
&mut self,
group_name: String,
sc_address: String,
) -> Result<(), ContactError> {
match self.group_id2sc.insert(group_name, sc_address) {
Some(_) => Err(ContactError::GroupAlreadyExistsError),
None => Ok(()),
}
}
pub fn group2sc(&self, group_name: String) -> Result<String, ContactError> {
match self.group_id2sc.get(&group_name).cloned() {
Some(addr) => Ok(addr),
None => Err(ContactError::GroupNotFoundError(group_name)),
}
}
}

View File

@@ -1,52 +0,0 @@
use std::fmt::Display;
#[derive(Default, Debug)]
pub struct Conversation {
messages: Vec<ConversationMessage>,
}
#[derive(Clone, Debug)]
pub struct ConversationMessage {
pub group: String,
pub author: String,
pub message: String,
}
impl Display for ConversationMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(
f,
"Group: {:#?}\nAuthor: {:#?}\nMessage: {:#?}",
self.group, self.author, self.message
)
}
}
impl Conversation {
/// Add a message string to the conversation list.
pub fn add(&mut self, conversation_message: ConversationMessage) {
self.messages.push(conversation_message)
}
/// Get a list of messages in the conversation.
/// The function returns the `last_n` messages.
pub fn get(&self, last_n: usize) -> Option<&[ConversationMessage]> {
let num_messages = self.messages.len();
let start = if last_n > num_messages {
0
} else {
num_messages - last_n
};
self.messages.get(start..num_messages)
}
}
impl ConversationMessage {
pub fn new(group: String, author: String, message: String) -> Self {
Self {
group,
author,
message,
}
}
}

359
src/group_actor.rs Normal file
View File

@@ -0,0 +1,359 @@
use alloy::hex;
use chrono::Utc;
use ds::{
ds_waku::{APP_MSG_SUBTOPIC, COMMIT_MSG_SUBTOPIC, WELCOME_SUBTOPIC},
waku_actor::ProcessMessageToSend,
};
use kameo::Actor;
use libsecp256k1::{PublicKey, SecretKey};
use openmls::{group::*, prelude::*};
use openmls_basic_credential::SignatureKeyPair;
use std::{fmt::Display, sync::Arc};
use tokio::sync::Mutex;
use crate::*;
use mls_crypto::openmls_provider::*;
#[derive(Clone, Debug)]
pub enum GroupAction {
MessageToPrint(MessageToPrint),
RemoveGroup,
DoNothing,
}
#[derive(Clone, Debug, Actor)]
pub struct Group {
group_name: String,
mls_group: Option<Arc<Mutex<MlsGroup>>>,
admin: Option<Admin>,
is_kp_shared: bool,
app_id: Vec<u8>,
}
impl Group {
pub fn new(
group_name: String,
is_creation: bool,
provider: Option<&MlsCryptoProvider>,
signer: Option<&SignatureKeyPair>,
credential_with_key: Option<&CredentialWithKey>,
) -> Result<Self, GroupError> {
let uuid = uuid::Uuid::new_v4().as_bytes().to_vec();
if is_creation {
let group_id = group_name.as_bytes();
// Create a new MLS group instance
let group_config = MlsGroupConfig::builder()
.use_ratchet_tree_extension(true)
.build();
let mls_group = MlsGroup::new_with_group_id(
provider.unwrap(),
signer.unwrap(),
&group_config,
GroupId::from_slice(group_id),
credential_with_key.unwrap().clone(),
)?;
Ok(Group {
group_name,
mls_group: Some(Arc::new(Mutex::new(mls_group))),
admin: Some(Admin::new()),
is_kp_shared: true,
app_id: uuid.clone(),
})
} else {
Ok(Group {
group_name,
mls_group: None,
admin: None,
is_kp_shared: false,
app_id: uuid.clone(),
})
}
}
pub async fn members_identity(&self) -> Vec<String> {
let mls_group = self.mls_group.as_ref().unwrap().lock().await;
mls_group
.members()
.map(|m| hex::encode(m.credential.identity()))
.collect()
}
pub fn set_mls_group(&mut self, mls_group: MlsGroup) -> Result<(), GroupError> {
self.is_kp_shared = true;
self.mls_group = Some(Arc::new(Mutex::new(mls_group)));
Ok(())
}
pub fn is_mls_group_initialized(&self) -> bool {
self.mls_group.is_some()
}
pub fn is_kp_shared(&self) -> bool {
self.is_kp_shared
}
pub fn set_kp_shared(&mut self, is_kp_shared: bool) {
self.is_kp_shared = is_kp_shared;
}
pub fn is_admin(&self) -> bool {
self.admin.is_some()
}
pub fn app_id(&self) -> Vec<u8> {
self.app_id.clone()
}
pub fn decrypt_admin_msg(&self, message: Vec<u8>) -> Result<KeyPackage, GroupError> {
if !self.is_admin() {
return Err(GroupError::AdminNotSetError);
}
let msg: KeyPackage = self.admin.as_ref().unwrap().decrypt_msg(message)?;
Ok(msg)
}
pub async fn add_members(
&mut self,
users_kp: Vec<KeyPackage>,
provider: &MlsCryptoProvider,
signer: &SignatureKeyPair,
) -> Result<Vec<ProcessMessageToSend>, GroupError> {
if !self.is_mls_group_initialized() {
return Err(GroupError::MlsGroupNotInitializedError);
}
let mut mls_group = self.mls_group.as_mut().unwrap().lock().await;
let (out_messages, welcome, _group_info) =
mls_group.add_members(provider, signer, &users_kp)?;
mls_group.merge_pending_commit(provider)?;
let msg_to_send_commit = ProcessMessageToSend {
msg: out_messages.tls_serialize_detached()?,
subtopic: COMMIT_MSG_SUBTOPIC.to_string(),
group_id: self.group_name.clone(),
app_id: self.app_id.clone(),
};
let welcome_serialized = welcome.tls_serialize_detached()?;
let welcome_msg: Vec<u8> = serde_json::to_vec(&WelcomeMessage {
message_type: WelcomeMessageType::WelcomeShare,
message_payload: welcome_serialized,
})?;
let msg_to_send_welcome = ProcessMessageToSend {
msg: welcome_msg,
subtopic: WELCOME_SUBTOPIC.to_string(),
group_id: self.group_name.clone(),
app_id: self.app_id.clone(),
};
Ok(vec![msg_to_send_commit, msg_to_send_welcome])
}
pub async fn remove_members(
&mut self,
users: Vec<String>,
provider: &MlsCryptoProvider,
signer: &SignatureKeyPair,
) -> Result<ProcessMessageToSend, GroupError> {
if !self.is_mls_group_initialized() {
return Err(GroupError::MlsGroupNotInitializedError);
}
let mut mls_group = self.mls_group.as_mut().unwrap().lock().await;
let mut leaf_indexs = Vec::new();
let members = mls_group.members().collect::<Vec<_>>();
for user in users {
for m in members.iter() {
if hex::encode(m.credential.identity()) == user {
leaf_indexs.push(m.index);
}
}
}
// Remove operation on the mls group
let (remove_message, _welcome, _group_info) =
mls_group.remove_members(provider, signer, &leaf_indexs)?;
// Second, process the removal on our end.
mls_group.merge_pending_commit(provider)?;
let msg_to_send_commit = ProcessMessageToSend {
msg: remove_message.tls_serialize_detached()?,
subtopic: COMMIT_MSG_SUBTOPIC.to_string(),
group_id: self.group_name.clone(),
app_id: self.app_id.clone(),
};
Ok(msg_to_send_commit)
}
pub async fn process_protocol_msg(
&mut self,
message: ProtocolMessage,
provider: &MlsCryptoProvider,
signature_key: Vec<u8>,
) -> Result<GroupAction, GroupError> {
let group_id = message.group_id().as_slice().to_vec();
if group_id != self.group_name.as_bytes().to_vec() {
return Ok(GroupAction::DoNothing);
}
if !self.is_mls_group_initialized() {
return Err(GroupError::MlsGroupNotInitializedError);
}
let mut mls_group = self.mls_group.as_mut().unwrap().lock().await;
// If the message is from a previous epoch, we don't need to process it and it's a commit for welcome message
if message.epoch() < mls_group.epoch() && message.epoch() == 0.into() {
return Ok(GroupAction::DoNothing);
}
let processed_message = mls_group.process_message(provider, message)?;
let processed_message_credential: Credential = processed_message.credential().clone();
match processed_message.into_content() {
ProcessedMessageContent::ApplicationMessage(application_message) => {
let sender_name = {
let user_id = mls_group.members().find_map(|m| {
if m.credential.identity() == processed_message_credential.identity()
&& (signature_key != m.signature_key.as_slice())
{
Some(hex::encode(m.credential.identity()))
} else {
None
}
});
if user_id.is_none() {
return Ok(GroupAction::DoNothing);
}
user_id.unwrap()
};
let conversation_message = MessageToPrint::new(
sender_name,
String::from_utf8(application_message.into_bytes())?,
self.group_name.clone(),
);
return Ok(GroupAction::MessageToPrint(conversation_message));
}
ProcessedMessageContent::ProposalMessage(_proposal_ptr) => (),
ProcessedMessageContent::ExternalJoinProposalMessage(_external_proposal_ptr) => (),
ProcessedMessageContent::StagedCommitMessage(commit_ptr) => {
let mut remove_proposal: bool = false;
if commit_ptr.self_removed() {
remove_proposal = true;
}
mls_group.merge_staged_commit(provider, *commit_ptr)?;
if remove_proposal {
// here we need to remove group instance locally and
// also remove correspond key package from local storage ans sc storage
if mls_group.is_active() {
return Err(GroupError::GroupStillActiveError);
}
return Ok(GroupAction::RemoveGroup);
}
}
};
Ok(GroupAction::DoNothing)
}
pub fn generate_admin_message(&mut self) -> Result<ProcessMessageToSend, GroupError> {
let admin = match self.admin.as_mut() {
Some(a) => a,
None => return Err(GroupError::AdminNotSetError),
};
admin.generate_new_key_pair();
let admin_msg = admin.generate_admin_message();
let wm = WelcomeMessage {
message_type: WelcomeMessageType::GroupAnnouncement,
message_payload: serde_json::to_vec(&admin_msg)?,
};
let msg_to_send = ProcessMessageToSend {
msg: serde_json::to_vec(&wm)?,
subtopic: WELCOME_SUBTOPIC.to_string(),
group_id: self.group_name.clone(),
app_id: self.app_id.clone(),
};
Ok(msg_to_send)
}
pub async fn create_message(
&mut self,
provider: &MlsCryptoProvider,
signer: &SignatureKeyPair,
msg: &str,
identity: Vec<u8>,
) -> Result<ProcessMessageToSend, GroupError> {
let message_out = self
.mls_group
.as_mut()
.unwrap()
.lock()
.await
.create_message(provider, signer, msg.as_bytes())?
.tls_serialize_detached()?;
let app_msg = serde_json::to_vec(&AppMessage {
sender: identity,
message: message_out,
})?;
Ok(ProcessMessageToSend {
msg: app_msg,
subtopic: APP_MSG_SUBTOPIC.to_string(),
group_id: self.group_name.clone(),
app_id: self.app_id.clone(),
})
}
}
impl Display for Group {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Group: {:#?}", self.group_name)
}
}
#[derive(Clone, Debug)]
pub struct Admin {
current_key_pair: PublicKey,
current_key_pair_private: SecretKey,
key_pair_timestamp: u64,
}
pub trait AdminTrait {
fn new() -> Self;
fn generate_new_key_pair(&mut self);
fn generate_admin_message(&self) -> GroupAnnouncement;
fn decrypt_msg(&self, message: Vec<u8>) -> Result<KeyPackage, MessageError>;
}
impl AdminTrait for Admin {
fn new() -> Self {
let (public_key, secret_key) = generate_keypair();
Admin {
current_key_pair: public_key,
current_key_pair_private: secret_key,
key_pair_timestamp: Utc::now().timestamp() as u64,
}
}
fn generate_new_key_pair(&mut self) {
let (public_key, secret_key) = generate_keypair();
self.current_key_pair = public_key;
self.current_key_pair_private = secret_key;
self.key_pair_timestamp = Utc::now().timestamp() as u64;
}
fn generate_admin_message(&self) -> GroupAnnouncement {
let signature = sign_message(
&self.current_key_pair.serialize_compressed(),
&self.current_key_pair_private,
);
GroupAnnouncement::new(
self.current_key_pair.serialize_compressed().to_vec(),
signature,
)
}
fn decrypt_msg(&self, message: Vec<u8>) -> Result<KeyPackage, MessageError> {
let msg: Vec<u8> = decrypt_message(&message, self.current_key_pair_private)?;
let key_package: KeyPackage = serde_json::from_slice(&msg)?;
Ok(key_package)
}
}

View File

@@ -1,5 +1,5 @@
use alloy::primitives::Address;
use std::collections::HashMap;
use std::{collections::HashMap, fmt::Display};
use openmls::{credentials::CredentialWithKey, key_packages::*, prelude::*};
use openmls_basic_credential::SignatureKeyPair;
@@ -72,13 +72,25 @@ impl Identity {
self.credential_with_key.credential.identity().to_vec()
}
pub fn identity_string(&self) -> String {
address_string(self.credential_with_key.credential.identity())
}
pub fn signature_pub_key(&self) -> Vec<u8> {
self.signer.public().to_vec()
}
}
impl ToString for Identity {
fn to_string(&self) -> String {
Address::from_slice(self.credential_with_key.credential.identity()).to_string()
impl Display for Identity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
Address::from_slice(self.credential_with_key.credential.identity())
)
}
}
pub fn address_string(identity: &[u8]) -> String {
Address::from_slice(identity).to_string()
}

View File

@@ -1,66 +1,145 @@
use alloy::{hex::FromHexError, primitives::SignatureError, signers::local::LocalSignerError};
use ds::DeliveryServiceError;
use fred::error::RedisError;
use alloy::signers::local::LocalSignerError;
use ecies::{decrypt, encrypt};
use kameo::{actor::ActorRef, error::SendError};
use libsecp256k1::{sign, verify, Message, PublicKey, SecretKey, Signature as libSignature};
use openmls::{error::LibraryError, prelude::*};
use openmls_rust_crypto::MemoryKeyStoreError;
use sc_key_store::KeyStoreError;
use std::{str::Utf8Error, string::FromUtf8Error};
use tokio::task::JoinError;
use rand::thread_rng;
use secp256k1::hashes::{sha256, Hash};
use serde::{Deserialize, Serialize};
use std::{
collections::HashSet,
fmt::Display,
str::Utf8Error,
string::FromUtf8Error,
sync::{Arc, Mutex},
};
use waku_bindings::{WakuContentTopic, WakuMessage};
pub mod cli;
pub mod contact;
pub mod conversation;
use ds::{
waku_actor::{ProcessMessageToSend, ProcessSubscribeToGroup, WakuActor},
DeliveryServiceError,
};
pub mod group_actor;
pub mod identity;
pub mod main_loop;
pub mod user;
pub mod ws_actor;
#[derive(Debug, thiserror::Error)]
pub enum CliError {
#[error("Can't split the line")]
SplitLineError,
#[error("Failed to cancel token")]
TokenCancellingError,
#[error("Problem from std::io library: {0}")]
IoError(#[from] std::io::Error),
#[error("Failed to send message to channel: {0}")]
SenderError(String),
#[error("Redis error: {0}")]
RedisError(#[from] RedisError),
#[error("Failed from tokio join: {0}")]
TokioJoinError(#[from] JoinError),
#[error("Unknown error: {0}")]
AnyHowError(anyhow::Error),
pub struct AppState {
pub waku_actor: ActorRef<WakuActor>,
pub rooms: Mutex<HashSet<String>>,
pub content_topics: Arc<Mutex<Vec<WakuContentTopic>>>,
pub pubsub: tokio::sync::broadcast::Sender<WakuMessage>,
}
#[derive(Debug, thiserror::Error)]
pub enum ContactError {
#[error("Key package for the specified group does not exist.")]
MissingKeyPackageForGroup,
#[error("SmartContract address for the specified group does not exist.")]
MissingSmartContractForGroup,
#[error("User not found.")]
UserNotFoundError,
#[error("Group not found: {0}")]
GroupNotFoundError(String),
#[error("Duplicate user found in joiners list.")]
DuplicateUserError,
#[error("Group already exists")]
GroupAlreadyExistsError,
#[error("Invalid user address in signature.")]
InvalidUserSignatureError,
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum WelcomeMessageType {
GroupAnnouncement,
KeyPackageShare,
WelcomeShare,
}
#[error(transparent)]
DeliveryServiceError(#[from] DeliveryServiceError),
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WelcomeMessage {
pub message_type: WelcomeMessageType,
pub message_payload: Vec<u8>,
}
#[error("Failed to parse signature: {0}")]
AlloySignatureParsingError(#[from] SignatureError),
#[error("JSON processing error: {0}")]
JsonProcessingError(#[from] serde_json::Error),
#[error("Serialization error: {0}")]
SerializationError(#[from] tls_codec::Error),
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GroupAnnouncement {
pub_key: Vec<u8>,
signature: Vec<u8>,
}
impl GroupAnnouncement {
pub fn new(pub_key: Vec<u8>, signature: Vec<u8>) -> Self {
GroupAnnouncement { pub_key, signature }
}
pub fn verify(&self) -> Result<bool, MessageError> {
let verified = verify_message(&self.pub_key, &self.signature, &self.pub_key)?;
Ok(verified)
}
pub fn encrypt(&self, data: Vec<u8>) -> Result<Vec<u8>, MessageError> {
let encrypted = encrypt_message(&data, &self.pub_key)?;
Ok(encrypted)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppMessage {
pub sender: Vec<u8>,
pub message: Vec<u8>,
}
impl AppMessage {
pub fn new(sender: Vec<u8>, message: Vec<u8>) -> Self {
AppMessage { sender, message }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MessageToPrint {
pub sender: String,
pub message: String,
pub group_name: String,
}
impl MessageToPrint {
pub fn new(sender: String, message: String, group_name: String) -> Self {
MessageToPrint {
sender,
message,
group_name,
}
}
}
impl Display for MessageToPrint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.sender, self.message)
}
}
pub fn generate_keypair() -> (PublicKey, SecretKey) {
let secret_key = SecretKey::random(&mut thread_rng());
let public_key = PublicKey::from_secret_key(&secret_key);
(public_key, secret_key)
}
pub fn sign_message(message: &[u8], secret_key: &SecretKey) -> Vec<u8> {
let digest = sha256::Hash::hash(message);
let msg = Message::parse(&digest.to_byte_array());
let signature = sign(&msg, secret_key);
signature.0.serialize_der().as_ref().to_vec()
}
pub fn verify_message(
message: &[u8],
signature: &[u8],
public_key: &[u8],
) -> Result<bool, MessageError> {
let digest = sha256::Hash::hash(message);
let msg = Message::parse(&digest.to_byte_array());
let signature = libSignature::parse_der(signature)?;
let mut pub_key_bytes: [u8; 33] = [0; 33];
pub_key_bytes[..].copy_from_slice(public_key);
let public_key = PublicKey::parse_compressed(&pub_key_bytes)?;
Ok(verify(&msg, &signature, &public_key))
}
pub fn encrypt_message(message: &[u8], public_key: &[u8]) -> Result<Vec<u8>, MessageError> {
let encrypted = encrypt(public_key, message)?;
Ok(encrypted)
}
pub fn decrypt_message(message: &[u8], secret_key: SecretKey) -> Result<Vec<u8>, MessageError> {
let secret_key_serialized = secret_key.serialize();
let decrypted = decrypt(&secret_key_serialized, message)?;
Ok(decrypted)
}
#[derive(Debug, thiserror::Error)]
@@ -80,34 +159,13 @@ pub enum IdentityError {
}
#[derive(Debug, thiserror::Error)]
pub enum UserError {
#[error("User lacks connection to the smart contract.")]
MissingSmartContractConnection,
#[error("Group not found: {0}")]
GroupNotFoundError(String),
#[error("Group already exists: {0}")]
GroupAlreadyExistsError(String),
#[error("Unsupported message type.")]
UnsupportedMessageType,
#[error("User already exists: {0}")]
UserAlreadyExistsError(String),
#[error("Welcome message cannot be empty.")]
EmptyWelcomeMessageError,
#[error("Message from user is invalid")]
InvalidChatMessageError,
#[error("Message from server is invalid")]
InvalidServerMessageError,
#[error("User not found.")]
UserNotFoundError,
pub enum GroupError {
#[error("Admin not set")]
AdminNotSetError,
#[error(transparent)]
DeliveryServiceError(#[from] DeliveryServiceError),
#[error(transparent)]
KeyStoreError(#[from] KeyStoreError),
#[error(transparent)]
IdentityError(#[from] IdentityError),
#[error(transparent)]
ContactError(#[from] ContactError),
MessageError(#[from] MessageError),
#[error("MLS group not initialized")]
MlsGroupNotInitializedError,
#[error("Error while creating MLS group: {0}")]
MlsGroupCreationError(#[from] NewGroupError<MemoryKeyStoreError>),
@@ -121,33 +179,100 @@ pub enum UserError {
MlsProcessMessageError(#[from] ProcessMessageError),
#[error("Error while creating message: {0}")]
MlsCreateMessageError(#[from] CreateMessageError),
#[error("Failed to create staged join: {0}")]
MlsWelcomeError(#[from] WelcomeError<MemoryKeyStoreError>),
#[error("Failed to remove member from MLS group: {0}")]
MlsRemoveMemberError(#[from] RemoveMembersError<MemoryKeyStoreError>),
#[error("Failed to validate user key package: {0}")]
MlsKeyPackageVerificationError(#[from] KeyPackageVerifyError),
#[error("Failed to remove members: {0}")]
MlsRemoveMembersError(#[from] RemoveMembersError<MemoryKeyStoreError>),
#[error("Group still active")]
GroupStillActiveError,
#[error("UTF-8 parsing error: {0}")]
Utf8ParsingError(#[from] FromUtf8Error),
#[error("UTF-8 string parsing error: {0}")]
Utf8StringParsingError(#[from] Utf8Error),
#[error("JSON processing error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Serialization error: {0}")]
SerializationError(#[from] tls_codec::Error),
#[error("Failed to parse address: {0}")]
AddressParsingError(#[from] FromHexError),
#[error("An unknown error occurred: {0}")]
Other(anyhow::Error),
}
#[derive(Debug, thiserror::Error)]
pub enum MessageError {
#[error("Failed to verify signature: {0}")]
SignatureVerificationError(#[from] libsecp256k1::Error),
#[error("JSON processing error: {0}")]
JsonError(#[from] serde_json::Error),
}
#[derive(Debug, thiserror::Error)]
pub enum UserError {
#[error(transparent)]
DeliveryServiceError(#[from] DeliveryServiceError),
#[error(transparent)]
IdentityError(#[from] IdentityError),
#[error(transparent)]
GroupError(#[from] GroupError),
#[error(transparent)]
MessageError(#[from] MessageError),
#[error("Group already exists: {0}")]
GroupAlreadyExistsError(String),
#[error("Group not found: {0}")]
GroupNotFoundError(String),
#[error("Unsupported message type.")]
UnsupportedMessageType,
#[error("Welcome message cannot be empty.")]
EmptyWelcomeMessageError,
#[error("Message verification failed")]
MessageVerificationFailed,
#[error("Unknown content topic type: {0}")]
UnknownContentTopicType(String),
#[error("Failed to create staged join: {0}")]
MlsWelcomeError(#[from] WelcomeError<MemoryKeyStoreError>),
#[error("UTF-8 parsing error: {0}")]
Utf8ParsingError(#[from] FromUtf8Error),
#[error("UTF-8 string parsing error: {0}")]
Utf8StringParsingError(#[from] Utf8Error),
#[error("JSON processing error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Serialization error: {0}")]
SerializationError(#[from] tls_codec::Error),
#[error("Failed to parse signer: {0}")]
SignerParsingError(#[from] LocalSignerError),
#[error("Signing error: {0}")]
SigningError(#[from] alloy::signers::Error),
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("An unknown error occurred: {0}")]
UnknownError(anyhow::Error),
#[error("Failed to subscribe to group: {0}")]
KameoSubscribeToGroupError(#[from] SendError<ProcessSubscribeToGroup, DeliveryServiceError>),
#[error("Failed to publish message: {0}")]
KameoPublishMessageError(#[from] SendError<ProcessMessageToSend, DeliveryServiceError>),
#[error("Failed to create group: {0}")]
KameoCreateGroupError(String),
#[error("Failed to send message to user: {0}")]
KameoSendMessageError(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verify_message() {
let message = b"Hello, world!";
let (public_key, secret_key) = generate_keypair();
let signature = sign_message(message, &secret_key);
let verified = verify_message(message, &signature, &public_key.serialize_compressed());
assert!(verified.is_ok());
assert!(verified.unwrap());
}
#[test]
fn test_encrypt_decrypt_message() {
let message = b"Hello, world!";
let (public_key, secret_key) = generate_keypair();
let encrypted = encrypt_message(message, &public_key.serialize_compressed());
let decrypted = decrypt_message(&encrypted.unwrap(), secret_key);
assert_eq!(message, decrypted.unwrap().as_slice());
}
}

View File

@@ -1,264 +1,337 @@
use alloy::{providers::ProviderBuilder, signers::local::PrivateKeySigner};
use clap::Parser;
use std::{error::Error, str::FromStr, sync::Arc};
use tokio::sync::{mpsc, Mutex};
use tokio_tungstenite::tungstenite::protocol::Message as TokioMessage;
use axum::{
extract::ws::{Message, WebSocket, WebSocketUpgrade},
extract::State,
http::Method,
response::IntoResponse,
routing::get,
Router,
};
use bounded_vec_deque::BoundedVecDeque;
use futures::StreamExt;
use kameo::actor::ActorRef;
use log::{error, info};
use serde_json::json;
use std::{
collections::HashSet,
net::SocketAddr,
sync::{Arc, Mutex},
};
use tokio::sync::mpsc::{channel, Sender};
use tokio_util::sync::CancellationToken;
use tower_http::cors::{Any, CorsLayer};
use waku_bindings::{waku_set_event_callback, WakuMessage};
use de_mls::{cli::*, user::User, CliError, UserError};
use de_mls::{
main_loop::{main_loop, Connection},
user::{ProcessLeaveGroup, ProcessRemoveUser, ProcessSendMessage, User, UserAction},
ws_actor::{RawWsMessage, WsAction, WsActor},
AppState, MessageToPrint,
};
use ds::{
chat_client::{ChatClient, ChatMessages},
chat_server::ServerMessage,
ds_waku::{match_content_topic, setup_node_handle},
waku_actor::{ProcessUnsubscribeFromGroup, WakuActor},
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let token = CancellationToken::new();
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
let port = std::env::var("PORT")
.map(|val| val.parse::<u16>())
.unwrap_or(Ok(3000))?;
let addr = SocketAddr::from(([0, 0, 0, 0], port));
let (cli_tx, mut cli_gr_rx) = mpsc::channel::<Commands>(100);
let args = Args::parse();
let signer = PrivateKeySigner::from_str(&args.user_priv_key)?;
let user_address = signer.address().to_string();
let (client, mut client_recv) =
ChatClient::connect("ws://127.0.0.1:8080", user_address.clone()).await?;
//// Create user
let user_n = User::new(&args.user_priv_key, client).await?;
let user_arc = Arc::new(Mutex::new(user_n));
let (messages_tx, messages_rx) = mpsc::channel::<Msg>(100);
messages_tx
.send(Msg::Input(Message::System(format!(
"Hello, {:}",
user_address.clone()
))))
.await?;
let messages_tx2 = messages_tx.clone();
let event_token = token.clone();
let h1 = tokio::spawn(async move { event_handler(messages_tx2, cli_tx, event_token).await });
let res_msg_tx = messages_tx.clone();
let main_token = token.clone();
let user = user_arc.clone();
let h2 = tokio::spawn(async move {
let (redis_tx, mut redis_rx) = mpsc::channel::<Vec<u8>>(100);
loop {
tokio::select! {
Some(msg) = client_recv.recv() => {
if let TokioMessage::Text(text) = msg {
if let Ok(chat_message) = serde_json::from_str::<ServerMessage>(&text) {
if let ServerMessage::InMessage { from, to, msg } = chat_message {
if let Ok(chat_msg) = serde_json::from_str::<ChatMessages>(&msg) {
match chat_msg {
ChatMessages::Request(req) => {
let res = user.as_ref().lock().await.send_responce_on_request(req, &from);
if let Err(err) = res {
res_msg_tx
.send(Msg::Input(Message::Error(err.to_string())))
.await.map_err(|err| CliError::SenderError(err.to_string()))?;
}
},
ChatMessages::Response(resp) => {
let res = user.as_ref().lock().await.parce_responce(resp).await;
if let Err(err) = res {
res_msg_tx
.send(Msg::Input(Message::Error(err.to_string())))
.await.map_err(|err| CliError::SenderError(err.to_string()))?;
}
},
ChatMessages::Welcome(welcome) => {
let res = user.as_ref().lock().await.join_group(welcome).await;
match res {
Ok(mut buf) => {
let msg = format!("Succesfully join to the group: {:#?}", buf.1);
res_msg_tx.send(Msg::Input(Message::System(msg))).await.map_err(|err| CliError::SenderError(err.to_string()))?;
let redis_tx = redis_tx.clone();
tokio::spawn(async move {
while let Ok(msg) = buf.0.recv().await {
let bytes: Vec<u8> = msg.value.convert()?;
redis_tx.send(bytes).await.map_err(|err| CliError::SenderError(err.to_string()))?;
}
Ok::<_, CliError>(())
});
},
Err(err) => {
res_msg_tx
.send(Msg::Input(Message::Error(err.to_string())))
.await.map_err(|err| CliError::SenderError(err.to_string()))?;
},
};
},
}
} else {
res_msg_tx
.send(Msg::Input(Message::Error(UserError::InvalidChatMessageError.to_string())))
.await.map_err(|err| CliError::SenderError(err.to_string()))?;
}
};
} else {
res_msg_tx
.send(Msg::Input(Message::Error(UserError::InvalidServerMessageError.to_string())))
.await.map_err(|err| CliError::SenderError(err.to_string()))?;
}
}
}
Some(val) = redis_rx.recv() =>{
let res = user.as_ref().lock().await.receive_msg(val).await;
match res {
Ok(msg) => {
match msg {
Some(m) => res_msg_tx.send(Msg::Input(Message::Incoming(m.group, m.author, m.message))).await.map_err(|err| CliError::SenderError(err.to_string()))?,
None => continue
}
},
Err(err) => {
res_msg_tx
.send(Msg::Input(Message::Error(err.to_string())))
.await.map_err(|err| CliError::SenderError(err.to_string()))?;
},
};
}
Some(command) = cli_gr_rx.recv() => {
// res_msg_tx.send(Msg::Input(Message::System(format!("Get command: {:?}", command)))).await?;
match command {
Commands::CreateGroup { group_name, storage_address, storage_url } => {
let client_provider = ProviderBuilder::new()
.with_recommended_fillers()
.wallet(user.as_ref().lock().await.wallet())
.on_http(storage_url);
let res = user.as_ref().lock().await.connect_to_smart_contract(&storage_address, client_provider).await;
match res {
Ok(_) => {
let msg = format!("Successfully connect to Smart Contract on address {:}\n", storage_address);
res_msg_tx.send(Msg::Input(Message::System(msg))).await.map_err(|err| CliError::SenderError(err.to_string()))?;
},
Err(err) => {
res_msg_tx
.send(Msg::Input(Message::Error(err.to_string())))
.await.map_err(|err| CliError::SenderError(err.to_string()))?;
},
};
let res = user.as_ref().lock().await.create_group(group_name.clone()).await;
match res {
Ok(mut br) => {
let msg = format!("Successfully create group: {:?}", group_name.clone());
res_msg_tx.send(Msg::Input(Message::System(msg))).await.map_err(|err| CliError::SenderError(err.to_string()))?;
let redis_tx = redis_tx.clone();
tokio::spawn(async move {
while let Ok(msg) = br.recv().await {
let bytes: Vec<u8> = msg.value.convert()?;
redis_tx.send(bytes).await.map_err(|err| CliError::SenderError(err.to_string()))?;
}
Ok::<_, CliError>(())
});
},
Err(err) => {
res_msg_tx
.send(Msg::Input(Message::Error(err.to_string())))
.await.map_err(|err| CliError::SenderError(err.to_string()))?;
},
};
},
Commands::Invite { group_name, users_wallet_addrs } => {
let user_clone = user.clone();
let res_msg_tx_c = messages_tx.clone();
tokio::spawn(async move {
for user_wallet in users_wallet_addrs.iter() {
let user_clone_ref = user_clone.as_ref();
let opt_token =
{
let mut user_clone_ref_lock = user_clone_ref.lock().await;
let res = user_clone_ref_lock.handle_send_req(user_wallet, group_name.clone()).await;
match res {
Ok(token) => {
token
},
Err(err) => {
res_msg_tx_c
.send(Msg::Input(Message::Error(err.to_string())))
.await.map_err(|err| CliError::SenderError(err.to_string()))?;
None
},
}
};
match opt_token {
Some(token) => token.cancelled().await,
None => return Err(CliError::TokenCancellingError),
};
{
let mut user_clone_ref_lock = user_clone.as_ref().lock().await;
user_clone_ref_lock.contacts.future_req.remove(user_wallet);
let res = user_clone_ref_lock.add_user_to_acl(user_wallet).await;
if let Err(err) = res {
res_msg_tx_c
.send(Msg::Input(Message::Error(err.to_string())))
.await.map_err(|err| CliError::SenderError(err.to_string()))?;
};
}
}
let res = user_clone.as_ref().lock().await.invite(users_wallet_addrs.clone(), group_name.clone()).await;
match res {
Ok(_) => {
let msg = format!("Invite {:?} to the group {:}\n",
users_wallet_addrs, group_name
);
res_msg_tx_c.send(Msg::Input(Message::System(msg))).await.map_err(|err| CliError::SenderError(err.to_string()))?;
},
Err(err) => {
res_msg_tx_c
.send(Msg::Input(Message::Error(err.to_string())))
.await.map_err(|err| CliError::SenderError(err.to_string()))?;
},
};
Ok::<_, CliError>(())
});
},
Commands::SendMessage { group_name, msg } => {
let message = msg.join(" ");
let res = user.as_ref().lock().await.send_msg(&message, group_name.clone(), user_address.clone()).await;
match res {
Ok(_) => {
res_msg_tx.send(Msg::Input(Message::Mine(group_name, user_address.clone(), message ))).await.map_err(|err| CliError::SenderError(err.to_string()))?;
},
Err(err) => {
res_msg_tx
.send(Msg::Input(Message::Error(err.to_string())))
.await.map_err(|err| CliError::SenderError(err.to_string()))?;
},
};
},
Commands::Exit => {
res_msg_tx.send(Msg::Input(Message::System("Bye!".to_string()))).await.map_err(|err| CliError::SenderError(err.to_string()))?;
break
},
}
}
_ = main_token.cancelled() => {
break;
}
else => {
res_msg_tx.send(Msg::Input(Message::System("Something went wrong".to_string()))).await.map_err(|err| CliError::SenderError(err.to_string()))?;
break
}
};
}
Ok::<_, CliError>(())
let node_name = std::env::var("NODE")?;
let node = setup_node_handle(vec![node_name])?;
let waku_actor = kameo::actor::spawn(WakuActor::new(Arc::new(node)));
let (tx, _) = tokio::sync::broadcast::channel(100);
let app_state = Arc::new(AppState {
waku_actor,
rooms: Mutex::new(HashSet::new()),
content_topics: Arc::new(Mutex::new(Vec::new())),
pubsub: tx.clone(),
});
let h3 = tokio::spawn(async move { terminal_handler(messages_rx, token).await });
let (waku_sender, mut waku_receiver) = channel::<WakuMessage>(100);
handle_waku(waku_sender, app_state.clone()).await;
let handler_res = tokio::join!(h1, h2, h3);
handler_res.0??;
handler_res.1??;
handler_res.2??;
let recv_messages = tokio::spawn(async move {
info!("Running recv messages from waku");
while let Some(msg) = waku_receiver.recv().await {
let _ = tx.send(msg);
}
});
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods(vec![Method::GET]);
let app = Router::new()
.route("/", get(|| async { "Hello World!" }))
.route("/ws", get(handler))
.route("/rooms", get(get_rooms))
.with_state(app_state)
.layer(cors);
println!("Hosted on {:?}", addr);
let res = axum::Server::bind(&addr).serve(app.into_make_service());
tokio::select! {
Err(x) = res => {
error!("Error hosting server: {}", x);
}
Err(w) = recv_messages => {
error!("Error receiving messages from waku: {}", w);
}
}
Ok(())
}
async fn handler(ws: WebSocketUpgrade, State(state): State<Arc<AppState>>) -> impl IntoResponse {
ws.on_upgrade(|socket| handle_socket(socket, state))
}
async fn handle_waku(waku_sender: Sender<WakuMessage>, state: Arc<AppState>) {
info!("Setting up waku event callback");
let mut seen_messages = BoundedVecDeque::<String>::new(40);
waku_set_event_callback(move |signal| {
match signal.event() {
waku_bindings::Event::WakuMessage(event) => {
let msg_id = event.message_id();
if seen_messages.contains(msg_id) {
return;
}
seen_messages.push_back(msg_id.clone());
let content_topic = event.waku_message().content_topic();
// Check if message belongs to a relevant topic
if !match_content_topic(&state.content_topics, content_topic) {
error!("Content topic not match: {:?}", content_topic);
return;
};
let msg = event.waku_message().clone();
info!("Received message from waku: {:?}", event.message_id());
waku_sender
.blocking_send(msg)
.expect("Failed to send message to waku");
}
waku_bindings::Event::Unrecognized(data) => {
error!("Unrecognized event!\n {data:?}");
}
_ => {
error!(
"Unrecognized signal!\n {:?}",
serde_json::to_string(&signal)
);
}
}
});
}
async fn handle_socket(socket: WebSocket, state: Arc<AppState>) {
let (ws_sender, mut ws_receiver) = socket.split();
let ws_actor = kameo::spawn(WsActor::new(ws_sender));
let mut main_loop_connection = None::<Connection>;
let cancel_token = CancellationToken::new();
while let Some(Ok(Message::Text(data))) = ws_receiver.next().await {
let res = ws_actor.ask(RawWsMessage { message: data }).await;
match res {
Ok(WsAction::Connect(connect)) => {
info!("Got connect: {:?}", &connect);
main_loop_connection = Some(Connection {
eth_private_key: connect.eth_private_key.clone(),
group_id: connect.group_id.clone(),
should_create_group: connect.should_create,
});
let mut rooms = state.rooms.lock().unwrap();
if !rooms.contains(&connect.group_id.clone()) {
rooms.insert(connect.group_id.clone());
}
info!("Prepare info for main loop: {:?}", main_loop_connection);
break;
}
Ok(_) => {
info!("Got chat message for non-existent user");
}
Err(e) => error!("Error handling message: {}", e),
}
}
let user_actor = main_loop(main_loop_connection.unwrap().clone(), state.clone())
.await
.expect("Failed to start main loop");
let user_actor_clone = user_actor.clone();
let state_clone = state.clone();
let ws_actor_clone = ws_actor.clone();
let mut waku_receiver = state.pubsub.subscribe();
let cancel_token_clone = cancel_token.clone();
let mut recv_messages = tokio::spawn(async move {
info!("Running recv messages from waku");
while let Ok(msg) = waku_receiver.recv().await {
let res = handle_user_actions(
msg,
state_clone.waku_actor.clone(),
ws_actor_clone.clone(),
user_actor_clone.clone(),
cancel_token_clone.clone(),
)
.await;
if let Err(e) = res {
error!("Error handling waku message: {}", e);
}
}
});
let user_ref_clone = user_actor.clone();
let mut send_messages = {
tokio::spawn(async move {
info!("Running recieve messages from websocket");
while let Some(Ok(Message::Text(text))) = ws_receiver.next().await {
let res = handle_ws_message(
RawWsMessage { message: text },
ws_actor.clone(),
user_ref_clone.clone(),
state.waku_actor.clone(),
)
.await;
if let Err(e) = res {
error!("Error handling websocket message: {}", e);
}
}
})
};
info!("Waiting for main loop to finish");
tokio::select! {
_ = (&mut recv_messages) => {
info!("recv_messages finished");
send_messages.abort();
}
_ = (&mut send_messages) => {
info!("send_messages finished");
send_messages.abort();
}
_ = cancel_token.cancelled() => {
info!("Cancel token cancelled");
send_messages.abort();
recv_messages.abort();
}
};
info!("Main loop finished");
}
async fn handle_user_actions(
msg: WakuMessage,
waku_actor: ActorRef<WakuActor>,
ws_actor: ActorRef<WsActor>,
user_actor: ActorRef<User>,
cancel_token: CancellationToken,
) -> Result<(), Box<dyn std::error::Error>> {
let actions = user_actor.ask(msg).await?;
for action in actions {
match action {
UserAction::SendToWaku(msg) => {
let id = waku_actor.ask(msg).await?;
info!("Successfully publish message with id: {:?}", id);
}
UserAction::SendToGroup(msg) => {
info!("Send to group: {:?}", msg);
ws_actor.ask(msg).await?;
}
UserAction::RemoveGroup(group_name) => {
waku_actor
.ask(ProcessUnsubscribeFromGroup {
group_name: group_name.clone(),
})
.await?;
user_actor
.ask(ProcessLeaveGroup {
group_name: group_name.clone(),
})
.await?;
info!("Leave group: {:?}", &group_name);
ws_actor
.ask(MessageToPrint {
sender: "system".to_string(),
message: format!("Group {} removed you", group_name),
group_name: group_name.clone(),
})
.await?;
cancel_token.cancel();
}
UserAction::DoNothing => {}
}
}
Ok(())
}
async fn handle_ws_message(
msg: RawWsMessage,
ws_actor: ActorRef<WsActor>,
user_actor: ActorRef<User>,
waku_actor: ActorRef<WakuActor>,
) -> Result<(), Box<dyn std::error::Error>> {
let action = ws_actor.ask(msg).await?;
match action {
WsAction::Connect(connect) => {
info!("Got unexpected connect: {:?}", &connect);
}
WsAction::UserMessage(msg) => {
info!("Got user message: {:?}", &msg);
let mtp = MessageToPrint {
message: msg.message.clone(),
group_name: msg.group_id.clone(),
sender: "me".to_string(),
};
ws_actor.ask(mtp).await?;
let pmt = user_actor
.ask(ProcessSendMessage {
msg: msg.message,
group_name: msg.group_id,
})
.await?;
let id = waku_actor.ask(pmt).await?;
info!("Successfully publish message with id: {:?}", id);
}
WsAction::RemoveUser(user_to_ban, group_name) => {
info!("Got remove user: {:?}", &user_to_ban);
let pmt = user_actor
.ask(ProcessRemoveUser {
user_to_ban: user_to_ban.clone(),
group_name: group_name.clone(),
})
.await?;
let id = waku_actor.ask(pmt).await?;
info!("Successfully publish message with id: {:?}", id);
ws_actor
.ask(MessageToPrint {
sender: "system".to_string(),
message: format!("User {} was removed from group", user_to_ban),
group_name: group_name.clone(),
})
.await?;
}
WsAction::DoNothing => {}
}
Ok(())
}
async fn get_rooms(State(state): State<Arc<AppState>>) -> String {
let rooms = state.rooms.lock().unwrap();
let vec = rooms.iter().collect::<Vec<&String>>();
match vec.len() {
0 => json!({
"status": "No rooms found yet!",
"rooms": []
})
.to_string(),
_ => json!({
"status": "Success!",
"rooms": vec
})
.to_string(),
}
}

79
src/main_loop.rs Normal file
View File

@@ -0,0 +1,79 @@
use alloy::signers::local::PrivateKeySigner;
use kameo::actor::ActorRef;
use log::{error, info};
use std::{str::FromStr, sync::Arc, time::Duration};
use crate::user::{ProcessAdminMessage, ProcessCreateGroup, User};
use crate::{AppState, UserError};
use ds::waku_actor::ProcessSubscribeToGroup;
#[derive(Debug, Clone)]
pub struct Connection {
pub eth_private_key: String,
pub group_id: String,
pub should_create_group: bool,
}
pub async fn main_loop(
connection: Connection,
app_state: Arc<AppState>,
) -> Result<ActorRef<User>, UserError> {
let signer = PrivateKeySigner::from_str(&connection.eth_private_key)?;
let user_address = signer.address().to_string();
let group_name: String = connection.group_id.clone();
// Create user
let user = User::new(&connection.eth_private_key)?;
let user_ref = kameo::spawn(user);
user_ref
.ask(ProcessCreateGroup {
group_name: group_name.clone(),
is_creation: connection.should_create_group,
})
.await
.map_err(|e| UserError::KameoCreateGroupError(e.to_string()))?;
let mut content_topics = app_state
.waku_actor
.ask(ProcessSubscribeToGroup {
group_name: group_name.clone(),
})
.await?;
app_state
.content_topics
.lock()
.unwrap()
.append(&mut content_topics);
if connection.should_create_group {
info!(
"User {:?} start sending admin message for group {:?}",
user_address, group_name
);
let user_clone = user_ref.clone();
let group_name_clone = group_name.clone();
let node_clone = app_state.waku_actor.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(30));
loop {
interval.tick().await;
let res = async {
let msg = user_clone
.ask(ProcessAdminMessage {
group_name: group_name_clone.clone(),
})
.await
.map_err(|e| UserError::KameoSendMessageError(e.to_string()))?;
let id = node_clone.ask(msg).await?;
info!("Successfully publish admin message with id: {:?}", id);
Ok::<(), UserError>(())
}
.await;
if let Err(e) = res {
error!("Error sending admin message to waku: {}", e);
}
}
});
};
Ok(user_ref)
}

View File

@@ -1,512 +1,511 @@
use alloy::{
hex::{self},
network::{EthereumWallet, Network},
primitives::Address,
providers::Provider,
signers::{local::PrivateKeySigner, SignerSync},
transports::Transport,
use alloy::{network::EthereumWallet, signers::local::PrivateKeySigner};
use kameo::{
message::{Context, Message},
Actor,
};
use fred::types::Message;
use log::info;
use openmls::{group::*, prelude::*};
use serde::{Deserialize, Serialize};
use std::{
cell::RefCell,
collections::HashMap,
fmt::Display,
str::{from_utf8, FromStr},
};
use tokio::sync::broadcast::Receiver;
use tokio_util::sync::CancellationToken;
use waku_bindings::WakuMessage;
use ds::{
chat_client::{ChatClient, ReqMessageType, RequestMLSPayload, ResponseMLSPayload},
ds::*,
ds_waku::{APP_MSG_SUBTOPIC, COMMIT_MSG_SUBTOPIC, WELCOME_SUBTOPIC},
waku_actor::ProcessMessageToSend,
};
use mls_crypto::openmls_provider::*;
use sc_key_store::{sc_ks::ScKeyStorage, *};
use crate::{contact::ContactsList, conversation::*};
use crate::{
group_actor::{Group, GroupAction},
AppMessage, GroupAnnouncement, MessageToPrint, WelcomeMessage, WelcomeMessageType,
};
use crate::{identity::Identity, UserError};
pub struct Group {
group_name: String,
conversation: Conversation,
mls_group: RefCell<MlsGroup>,
rc_client: RClient,
// pubsub_topic: WakuPubSubTopic,
// content_topics: Vec<WakuContentTopic>,
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum UserAction {
SendToWaku(ProcessMessageToSend),
SendToGroup(MessageToPrint),
RemoveGroup(String),
DoNothing,
}
impl Display for Group {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Group: {:#?}", self.group_name)
#[derive(Actor)]
pub struct User {
identity: Identity,
groups: HashMap<String, Group>,
provider: MlsCryptoProvider,
eth_signer: PrivateKeySigner,
}
impl Message<WakuMessage> for User {
type Reply = Result<Vec<UserAction>, UserError>;
async fn handle(
&mut self,
msg: WakuMessage,
_ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
let actions = self.process_waku_msg(msg).await?;
Ok(actions)
}
}
pub struct User<T, P, N> {
pub identity: Identity,
pub groups: HashMap<String, Group>,
provider: MlsCryptoProvider,
eth_signer: PrivateKeySigner,
// we don't need on-chain connection if we don't create a group
sc_ks: Option<ScKeyStorage<T, P, N>>,
pub contacts: ContactsList,
pub struct ProcessCreateGroup {
pub group_name: String,
pub is_creation: bool,
}
impl<T, P, N> User<T, P, N>
where
T: Transport + Clone,
P: Provider<T, N>,
N: Network,
{
impl Message<ProcessCreateGroup> for User {
type Reply = Result<(), UserError>;
async fn handle(
&mut self,
msg: ProcessCreateGroup,
_ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
self.create_group(msg.group_name.clone(), msg.is_creation)
.await?;
Ok(())
}
}
pub struct ProcessAdminMessage {
pub group_name: String,
}
impl Message<ProcessAdminMessage> for User {
type Reply = Result<ProcessMessageToSend, UserError>;
async fn handle(
&mut self,
msg: ProcessAdminMessage,
_ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
self.prepare_admin_msg(msg.group_name.clone()).await
}
}
pub struct ProcessLeaveGroup {
pub group_name: String,
}
impl Message<ProcessLeaveGroup> for User {
type Reply = Result<(), UserError>;
async fn handle(
&mut self,
msg: ProcessLeaveGroup,
_ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
self.leave_group(msg.group_name.clone()).await?;
Ok(())
}
}
pub struct ProcessSendMessage {
pub msg: String,
pub group_name: String,
}
impl Message<ProcessSendMessage> for User {
type Reply = Result<ProcessMessageToSend, UserError>;
async fn handle(
&mut self,
msg: ProcessSendMessage,
_ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
self.prepare_msg_to_send(&msg.msg, msg.group_name.clone())
.await
}
}
pub struct ProcessRemoveUser {
pub user_to_ban: String,
pub group_name: String,
}
impl Message<ProcessRemoveUser> for User {
type Reply = Result<ProcessMessageToSend, UserError>;
async fn handle(
&mut self,
msg: ProcessRemoveUser,
_ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
self.remove_users_from_group(vec![msg.user_to_ban], msg.group_name.clone())
.await
}
}
impl User {
/// Create a new user with the given name and a fresh set of credentials.
pub async fn new(user_eth_priv_key: &str, chat_client: ChatClient) -> Result<Self, UserError> {
pub fn new(user_eth_priv_key: &str) -> Result<Self, UserError> {
let signer = PrivateKeySigner::from_str(user_eth_priv_key)?;
let user_address = signer.address();
let crypto = MlsCryptoProvider::default();
let id = Identity::new(CIPHERSUITE, &crypto, user_address.as_slice())?;
let user = User {
groups: HashMap::new(),
identity: id,
eth_signer: signer,
provider: crypto,
sc_ks: None,
contacts: ContactsList::new(chat_client).await?,
};
Ok(user)
}
pub async fn connect_to_smart_contract(
&mut self,
sc_storage_address: &str,
provider: P,
) -> Result<(), UserError> {
let storage_address = Address::from_str(sc_storage_address)?;
self.sc_ks = Some(ScKeyStorage::new(provider, storage_address));
self.sc_ks
.as_mut()
.unwrap()
.add_user(&self.identity.to_string())
.await?;
Ok(())
}
pub async fn create_group(
&mut self,
group_name: String,
) -> Result<Receiver<Message>, UserError> {
let group_id = group_name.as_bytes();
if self.groups.contains_key(&group_name) {
is_creation: bool,
) -> Result<(), UserError> {
if self.if_group_exists(group_name.clone()) {
return Err(UserError::GroupAlreadyExistsError(group_name));
}
let group_config = MlsGroupConfig::builder()
.use_ratchet_tree_extension(true)
.build();
let mls_group = MlsGroup::new_with_group_id(
&self.provider,
&self.identity.signer,
&group_config,
GroupId::from_slice(group_id),
self.identity.credential_with_key.clone(),
)?;
let (rc, broadcaster) = RClient::new_with_group(group_name.clone()).await?;
let group = Group {
group_name: group_name.clone(),
conversation: Conversation::default(),
mls_group: RefCell::new(mls_group),
rc_client: rc,
// pubsub_topic: WakuPubSubTopic::new(),
// content_topics: Vec::new(),
let group = if is_creation {
Group::new(
group_name.clone(),
true,
Some(&self.provider),
Some(&self.identity.signer),
Some(&self.identity.credential_with_key),
)?
} else {
Group::new(group_name.clone(), false, None, None, None)?
};
self.groups.insert(group_name.clone(), group);
self.contacts
.insert_group2sc(group_name, self.sc_address()?)?;
Ok(broadcaster)
}
pub async fn add_user_to_acl(&mut self, user_address: &str) -> Result<(), UserError> {
if self.sc_ks.is_none() {
return Err(UserError::MissingSmartContractConnection);
}
self.sc_ks.as_mut().unwrap().add_user(user_address).await?;
Ok(())
}
pub async fn restore_key_package(
&mut self,
mut signed_kp: &[u8],
) -> Result<KeyPackage, UserError> {
if self.sc_ks.is_none() {
return Err(UserError::MissingSmartContractConnection);
pub fn get_group(&self, group_name: String) -> Result<Group, UserError> {
match self.groups.get(&group_name) {
Some(g) => Ok(g.clone()),
None => Err(UserError::GroupNotFoundError(group_name)),
}
let key_package_in = KeyPackageIn::tls_deserialize(&mut signed_kp)?;
let key_package =
key_package_in.validate(self.provider.crypto(), ProtocolVersion::Mls10)?;
Ok(key_package)
}
pub async fn invite(
pub fn if_group_exists(&self, group_name: String) -> bool {
self.groups.contains_key(&group_name)
}
pub async fn handle_welcome_subtopic(
&mut self,
users: Vec<String>,
msg: WakuMessage,
group_name: String,
) -> Result<(), UserError> {
if self.sc_ks.is_none() {
return Err(UserError::MissingSmartContractConnection);
) -> Result<Vec<UserAction>, UserError> {
let group = match self.groups.get_mut(&group_name) {
Some(g) => g,
None => return Err(UserError::GroupNotFoundError(group_name)),
};
let welcome_msg: WelcomeMessage = serde_json::from_slice(msg.payload())?;
match welcome_msg.message_type {
WelcomeMessageType::GroupAnnouncement => {
let app_id = group.app_id();
if group.is_admin() || group.is_kp_shared() {
Ok(vec![UserAction::DoNothing])
} else {
info!(
"User {:?} received group announcement message for group {:?}",
self.identity.identity_string(),
group_name
);
let group_announcement: GroupAnnouncement =
serde_json::from_slice(&welcome_msg.message_payload)?;
if !group_announcement.verify()? {
return Err(UserError::MessageVerificationFailed);
}
let key_package = serde_json::to_vec(
&self
.identity
.generate_key_package(CIPHERSUITE, &self.provider)?,
)?;
let encrypted_key_package = group_announcement.encrypt(key_package)?;
let msg: Vec<u8> = serde_json::to_vec(&WelcomeMessage {
message_type: WelcomeMessageType::KeyPackageShare,
message_payload: encrypted_key_package,
})?;
group.set_kp_shared(true);
Ok(vec![UserAction::SendToWaku(ProcessMessageToSend {
msg,
subtopic: WELCOME_SUBTOPIC.to_string(),
group_id: group_name.clone(),
app_id: app_id.clone(),
})])
}
}
WelcomeMessageType::KeyPackageShare => {
// We already shared the key package with the group admin and we don't need to do it again
if !group.is_admin() {
Ok(vec![UserAction::DoNothing])
} else {
info!(
"User {:?} received key package share message for group {:?}",
self.identity.identity_string(),
group_name
);
let key_package = group.decrypt_admin_msg(welcome_msg.message_payload)?;
let msgs = self.invite_users(vec![key_package], group_name).await?;
Ok(msgs
.iter()
.map(|msg| UserAction::SendToWaku(msg.clone()))
.collect())
}
}
WelcomeMessageType::WelcomeShare => {
if group.is_admin() {
Ok(vec![UserAction::DoNothing])
} else {
info!(
"User {:?} received welcome share message for group {:?}",
self.identity.identity_string(),
group_name
);
let welc = MlsMessageIn::tls_deserialize_bytes(welcome_msg.message_payload)?;
let welcome = match welc.into_welcome() {
Some(w) => w,
None => return Err(UserError::EmptyWelcomeMessageError),
};
// find the key package in the welcome message
if welcome.secrets().iter().any(|egs| {
let hash_ref = egs.new_member().as_slice().to_vec();
self.provider
.key_store()
.read(&hash_ref)
.map(|kp: KeyPackage| (kp, hash_ref))
.is_some()
}) {
self.join_group(welcome)?;
let msg = self
.prepare_msg_to_send("User joined to the group", group_name)
.await?;
Ok(vec![UserAction::SendToWaku(msg)])
} else {
Ok(vec![UserAction::DoNothing])
}
}
}
}
}
let users_for_invite = self
.contacts
.prepare_joiners(users.clone(), group_name.clone())
.await?;
let mut joiners_key_package: Vec<KeyPackage> = Vec::with_capacity(users_for_invite.len());
let mut user_addrs = Vec::with_capacity(users_for_invite.len());
for (user_addr, user_kp) in users_for_invite {
joiners_key_package.push(self.restore_key_package(&user_kp).await?);
user_addrs.push(user_addr);
pub async fn process_waku_msg(
&mut self,
msg: WakuMessage,
) -> Result<Vec<UserAction>, UserError> {
let ct = msg.content_topic();
let group_name = ct.application_name.to_string();
let group = match self.groups.get(&group_name) {
Some(g) => g,
None => return Err(UserError::GroupNotFoundError(group_name)),
};
let app_id = group.app_id();
if msg.meta() == app_id {
return Ok(vec![UserAction::DoNothing]);
}
let ct = ct.content_topic_name.to_string();
match ct.as_str() {
WELCOME_SUBTOPIC => self.handle_welcome_subtopic(msg, group_name).await,
COMMIT_MSG_SUBTOPIC => {
if group.is_mls_group_initialized() {
info!(
"User {:?} received commit message for group {:?}",
self.identity.identity_string(),
group_name
);
let res = MlsMessageIn::tls_deserialize_bytes(msg.payload())?;
let action = match res.extract() {
MlsMessageInBody::PrivateMessage(message) => {
self.process_protocol_msg(message.into()).await?
}
MlsMessageInBody::PublicMessage(message) => {
self.process_protocol_msg(message.into()).await?
}
_ => return Err(UserError::UnsupportedMessageType),
};
Ok(vec![action])
} else {
Ok(vec![UserAction::DoNothing])
}
}
APP_MSG_SUBTOPIC => {
info!(
"User {:?} received app message for group {:?}",
self.identity.identity_string(),
group_name
);
let buf: AppMessage = serde_json::from_slice(msg.payload())?;
if buf.sender == self.identity.identity() {
return Ok(vec![UserAction::DoNothing]);
}
let res = MlsMessageIn::tls_deserialize_bytes(&buf.message)?;
let action = match res.extract() {
MlsMessageInBody::PrivateMessage(message) => {
self.process_protocol_msg(message.into()).await?
}
MlsMessageInBody::PublicMessage(message) => {
self.process_protocol_msg(message.into()).await?
}
_ => return Err(UserError::UnsupportedMessageType),
};
Ok(vec![action])
}
_ => Err(UserError::UnknownContentTopicType(ct)),
}
}
async fn invite_users(
&mut self,
users_kp: Vec<KeyPackage>,
group_name: String,
) -> Result<Vec<ProcessMessageToSend>, UserError> {
// Build a proposal with this key package and do the MLS bits.
let group = match self.groups.get_mut(&group_name) {
Some(g) => g,
None => return Err(UserError::GroupNotFoundError(group_name)),
};
let (out_messages, welcome, _group_info) = group.mls_group.borrow_mut().add_members(
&self.provider,
&self.identity.signer,
&joiners_key_package,
)?;
group
.rc_client
.msg_send(
out_messages.tls_serialize_detached()?,
self.identity.to_string(),
group_name,
)
let out_messages = group
.add_members(users_kp, &self.provider, &self.identity.signer)
.await?;
// Second, process the invitation on our end.
group
.mls_group
.borrow_mut()
.merge_pending_commit(&self.provider)?;
// Send welcome by p2p
self.contacts
.send_welcome_msg_to_users(self.identity.to_string(), user_addrs, welcome)?;
info!(
"User {:?} invited users to group {:?}",
self.identity.identity_string(),
group_name
);
Ok(out_messages)
}
fn join_group(&mut self, welcome: Welcome) -> Result<(), UserError> {
let group_config = MlsGroupConfig::builder()
.use_ratchet_tree_extension(true)
.build();
let mls_group = MlsGroup::new_from_welcome(&self.provider, &group_config, welcome, None)?;
let group_id = mls_group.group_id().to_vec();
let group_name = String::from_utf8(group_id)?;
if !self.if_group_exists(group_name.clone()) {
return Err(UserError::GroupNotFoundError(group_name));
}
self.groups
.get_mut(&group_name)
.unwrap()
.set_mls_group(mls_group)?;
info!(
"User {:?} joined group {:?}",
self.identity.identity_string(),
group_name
);
Ok(())
}
pub async fn receive_msg(
&mut self,
msg_bytes: Vec<u8>,
) -> Result<Option<ConversationMessage>, UserError> {
let buf: SenderStruct = serde_json::from_slice(&msg_bytes)?;
if buf.sender == self.identity.to_string() {
return Ok(None);
}
let res = MlsMessageIn::tls_deserialize_bytes(&buf.msg)?;
let msg = match res.extract() {
MlsMessageInBody::PrivateMessage(message) => {
self.process_protocol_msg(message.into())?
}
MlsMessageInBody::PublicMessage(message) => {
self.process_protocol_msg(message.into())?
}
_ => return Err(UserError::UnsupportedMessageType),
};
Ok(msg)
}
pub fn process_protocol_msg(
pub async fn process_protocol_msg(
&mut self,
message: ProtocolMessage,
) -> Result<Option<ConversationMessage>, UserError> {
) -> Result<UserAction, UserError> {
let group_name = from_utf8(message.group_id().as_slice())?.to_string();
let group = match self.groups.get_mut(&group_name) {
Some(g) => g,
None => return Err(UserError::GroupNotFoundError(group_name)),
};
let mut mls_group = group.mls_group.borrow_mut();
if !group.is_mls_group_initialized() {
return Ok(UserAction::DoNothing);
}
let res = group
.process_protocol_msg(
message,
&self.provider,
self.identity
.credential_with_key
.signature_key
.as_slice()
.to_vec(),
)
.await?;
let processed_message = mls_group.process_message(&self.provider, message)?;
let processed_message_credential: Credential = processed_message.credential().clone();
match processed_message.into_content() {
ProcessedMessageContent::ApplicationMessage(application_message) => {
let sender_name = {
let user_id = mls_group.members().find_map(|m| {
if m.credential.identity() == processed_message_credential.identity()
&& (self.identity.credential_with_key.signature_key.as_slice()
!= m.signature_key.as_slice())
{
Some(hex::encode(m.credential.identity()))
} else {
None
}
});
user_id.unwrap_or("".to_owned())
};
let conversation_message = ConversationMessage::new(
group_name,
sender_name,
String::from_utf8(application_message.into_bytes())?,
);
group.conversation.add(conversation_message.clone());
return Ok(Some(conversation_message));
}
ProcessedMessageContent::ProposalMessage(_proposal_ptr) => (),
ProcessedMessageContent::ExternalJoinProposalMessage(_external_proposal_ptr) => (),
ProcessedMessageContent::StagedCommitMessage(commit_ptr) => {
let mut remove_proposal: bool = false;
if commit_ptr.self_removed() {
remove_proposal = true;
}
mls_group.merge_staged_commit(&self.provider, *commit_ptr)?;
if remove_proposal {
// here we need to remove group instance locally and
// also remove correspond key package from local storage ans sc storage
return Ok(None);
}
}
};
Ok(None)
match res {
GroupAction::MessageToPrint(msg) => Ok(UserAction::SendToGroup(msg)),
GroupAction::RemoveGroup => Ok(UserAction::RemoveGroup(group_name)),
GroupAction::DoNothing => Ok(UserAction::DoNothing),
}
}
pub async fn send_msg(
pub async fn prepare_admin_msg(
&mut self,
group_name: String,
) -> Result<ProcessMessageToSend, UserError> {
if !self.if_group_exists(group_name.clone()) {
return Err(UserError::GroupNotFoundError(group_name));
}
let msg_to_send = self
.groups
.get_mut(&group_name)
.unwrap()
.generate_admin_message()?;
Ok(msg_to_send)
}
pub async fn prepare_msg_to_send(
&mut self,
msg: &str,
group_name: String,
sender: String,
) -> Result<(), UserError> {
) -> Result<ProcessMessageToSend, UserError> {
let group = match self.groups.get_mut(&group_name) {
Some(g) => g,
None => return Err(UserError::GroupNotFoundError(group_name)),
};
let message_out = group.mls_group.borrow_mut().create_message(
&self.provider,
&self.identity.signer,
msg.as_bytes(),
)?;
group
.rc_client
.msg_send(message_out.tls_serialize_detached()?, sender, group_name)
.await?;
Ok(())
if !group.is_mls_group_initialized() {
Err(UserError::GroupNotFoundError(group_name))
} else {
let msg_to_send = group
.create_message(
&self.provider,
&self.identity.signer,
msg,
self.identity.identity().to_vec(),
)
.await?;
Ok(msg_to_send)
}
}
pub async fn join_group(
pub async fn remove_users_from_group(
&mut self,
welcome: String,
) -> Result<(Receiver<Message>, String), UserError> {
let wbytes = hex::decode(welcome).unwrap();
let welc = MlsMessageIn::tls_deserialize_bytes(wbytes).unwrap();
let welcome = welc.into_welcome();
if welcome.is_none() {
return Err(UserError::EmptyWelcomeMessageError);
}
let group_config = MlsGroupConfig::builder()
.use_ratchet_tree_extension(true)
.build();
// TODO: After we move from openmls, we will have to delete the used key package here ourselves.
let mls_group =
MlsGroup::new_from_welcome(&self.provider, &group_config, welcome.unwrap(), None)?;
let group_id = mls_group.group_id().to_vec();
let group_name = String::from_utf8(group_id)?;
let (rc, br) = RClient::new_with_group(group_name.clone()).await?;
let group = Group {
group_name: group_name.clone(),
conversation: Conversation::default(),
mls_group: RefCell::new(mls_group),
rc_client: rc,
};
match self.groups.insert(group_name.clone(), group) {
Some(old) => Err(UserError::GroupAlreadyExistsError(old.group_name)),
None => Ok((br, group_name)),
}
}
// pub async fn remove(&mut self, name: String, group_name: String) -> Result<(), UserError> {
// // Get the group ID
// let group = match self.groups.get_mut(&group_name) {
// Some(g) => g,
// None => return Err(UserError::UnknownGroupError(group_name)),
// };
// // Get the user leaf index
// let leaf_index = group.find_member_index(name)?;
// // Remove operation on the mls group
// let (remove_message, _welcome, _group_info) = group.mls_group.borrow_mut().remove_members(
// &self.provider,
// &self.identity.signer,
// &[leaf_index],
// )?;
// group.rc_client.msg_send(remove_message).await?;
// // Second, process the removal on our end.
// group
// .mls_group
// .borrow_mut()
// .merge_pending_commit(&self.provider)?;
// Ok(())
// }
/// Return the last 100 messages sent to the group.
pub fn read_msgs(
&self,
users: Vec<String>,
group_name: String,
) -> Result<Option<Vec<ConversationMessage>>, UserError> {
self.groups.get(&group_name).map_or_else(
|| Err(UserError::GroupNotFoundError(group_name)),
|g| {
Ok(g.conversation
.get(100)
.map(|messages: &[crate::conversation::ConversationMessage]| messages.to_vec()))
},
)
}
pub fn group_members(&self, group_name: String) -> Result<Vec<String>, UserError> {
let group = match self.groups.get(&group_name) {
Some(g) => g,
None => return Err(UserError::GroupNotFoundError(group_name)),
};
Ok(group.group_members(self.identity.signature_pub_key().as_slice()))
}
pub fn user_groups(&self) -> Result<Vec<String>, UserError> {
if self.groups.is_empty() {
return Ok(Vec::default());
) -> Result<ProcessMessageToSend, UserError> {
if !self.if_group_exists(group_name.clone()) {
return Err(UserError::GroupNotFoundError(group_name));
}
Ok(self.groups.keys().map(|k| k.to_owned()).collect())
let group = self.groups.get_mut(&group_name).unwrap();
let msg = group
.remove_members(users, &self.provider, &self.identity.signer)
.await?;
Ok(msg)
}
pub async fn leave_group(&mut self, group_name: String) -> Result<(), UserError> {
if !self.if_group_exists(group_name.clone()) {
return Err(UserError::GroupNotFoundError(group_name));
}
self.groups.remove(&group_name);
Ok(())
}
pub fn wallet(&self) -> EthereumWallet {
EthereumWallet::from(self.eth_signer.clone())
}
fn sign(&self, msg: String) -> Result<String, UserError> {
let signature = self.eth_signer.sign_message_sync(msg.as_bytes())?;
let res = serde_json::to_string(&signature)?;
Ok(res)
}
pub fn send_responce_on_request(
&mut self,
req: RequestMLSPayload,
user_address: &str,
) -> Result<(), UserError> {
let self_address = self.identity.to_string();
match req.msg_type {
ReqMessageType::InviteToGroup => {
let signature = self.sign(req.msg_to_sign())?;
let key_package = self
.identity
.generate_key_package(CIPHERSUITE, &self.provider)?;
let resp = ResponseMLSPayload::new(
signature,
self_address.clone(),
req.group_name(),
key_package.tls_serialize_detached()?,
);
self.contacts
.send_resp_msg_to_user(self_address, user_address, resp)?;
Ok(())
}
ReqMessageType::RemoveFromGroup => Ok(()),
}
}
pub async fn parce_responce(&mut self, resp: ResponseMLSPayload) -> Result<(), UserError> {
if self.sc_ks.is_none() {
return Err(UserError::MissingSmartContractConnection);
}
let group_name = resp.group_name.clone();
let sc_address = self.contacts.group2sc(group_name.clone())?;
let (user_wallet, kp) = resp.validate(sc_address, group_name.clone())?;
self.contacts
.add_key_package_to_contact(&user_wallet, kp, group_name.clone())
.await?;
self.contacts.handle_response(&user_wallet)?;
Ok(())
}
pub fn sc_address(&self) -> Result<String, UserError> {
if self.sc_ks.is_none() {
return Err(UserError::MissingSmartContractConnection);
}
Ok(self.sc_ks.as_ref().unwrap().sc_adsress())
}
pub async fn handle_send_req(
&mut self,
user_wallet: &str,
group_name: String,
) -> Result<Option<CancellationToken>, UserError> {
if !self.contacts.does_user_in_contacts(user_wallet).await {
self.contacts.add_new_contact(user_wallet).await?;
}
self.contacts
.send_msg_req(
self.identity.to_string(),
user_wallet.to_owned(),
group_name,
ReqMessageType::InviteToGroup,
)
.unwrap();
Ok(self.contacts.future_req.get(user_wallet).cloned())
}
}
impl Group {
/// Get a member
fn find_member_index(&self, user_id: String) -> Result<LeafNodeIndex, GroupError> {
let member = self
.mls_group
.borrow()
.members()
.find(|m| m.credential.identity().eq(user_id.as_bytes()));
match member {
Some(m) => Ok(m.index),
None => Err(GroupError::UnknownGroupMemberError(user_id)),
}
}
pub fn group_members(&self, user_signature: &[u8]) -> Vec<String> {
self.mls_group
.borrow()
.members()
.filter(|m| m.signature_key == user_signature)
.map(|m| hex::encode(m.credential.identity()))
.collect::<Vec<String>>()
}
}
#[derive(Debug, thiserror::Error)]
pub enum GroupError {
#[error("Unknown group member : {0}")]
UnknownGroupMemberError(String),
}

133
src/ws_actor.rs Normal file
View File

@@ -0,0 +1,133 @@
use axum::extract::ws::{Message as WsMessage, WebSocket};
use futures::{stream::SplitSink, SinkExt};
use kameo::{
message::{Context, Message},
Actor,
};
use serde::{Deserialize, Serialize};
use crate::MessageToPrint;
/// This actor is used to handle messages from web socket
#[derive(Debug, Actor)]
pub struct WsActor {
/// This is the sender of the open web socket connection
pub ws_sender: SplitSink<WebSocket, WsMessage>,
/// This variable is used to check if the user has connected to the ws, if not, we parce message as ConnectMessage
pub is_initialized: bool,
}
impl WsActor {
pub fn new(ws_sender: SplitSink<WebSocket, WsMessage>) -> Self {
Self {
ws_sender,
is_initialized: false,
}
}
}
/// This enum is used to represent the actions that can be performed on the web socket
/// Connect - this action is used to return connection data to the user
/// UserMessage - this action is used to handle message from web socket and return it to the user
/// RemoveUser - this action is used to remove a user from the group
/// DoNothing - this action is used for test purposes (return empty action if message is not valid)
#[derive(Debug, PartialEq)]
pub enum WsAction {
Connect(ConnectMessage),
UserMessage(UserMessage),
RemoveUser(String, String),
DoNothing,
}
/// This struct is used to represent the message from the user that we got from web socket
#[derive(Deserialize, Debug, PartialEq, Serialize)]
pub struct UserMessage {
pub message: String,
pub group_id: String,
}
/// This struct is used to represent the connection data that web socket sends to the user
#[derive(Deserialize, Debug, PartialEq)]
pub struct ConnectMessage {
/// This is the private key of the user that we will use to authenticate the user
pub eth_private_key: String,
/// This is the id of the group that the user is joining
pub group_id: String,
/// This is the flag that indicates if the user should create a new group or subscribe to an existing one
pub should_create: bool,
}
/// This struct is used to represent the raw message from the web socket.
/// It is used to handle the message from the web socket and return it to the user
/// We can parse it to the ConnectMessage or UserMessage
/// if it starts with "/ban" it will be parsed to RemoveUser, otherwise it will be parsed to UserMessage
#[derive(Deserialize, Debug, PartialEq)]
pub struct RawWsMessage {
pub message: String,
}
impl Message<RawWsMessage> for WsActor {
type Reply = Result<WsAction, WsError>;
async fn handle(
&mut self,
msg: RawWsMessage,
_ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
if !self.is_initialized {
let connect_message = serde_json::from_str(&msg.message)?;
self.is_initialized = true;
return Ok(WsAction::Connect(connect_message));
}
match serde_json::from_str(&msg.message) {
Ok(UserMessage { message, group_id }) => {
if message.starts_with("/") {
let mut tokens = message.split_whitespace();
match tokens.next() {
Some("/ban") => {
let user_to_ban = tokens.next();
if user_to_ban.is_none() {
return Err(WsError::InvalidMessage);
} else {
let user_to_ban = user_to_ban.unwrap().to_lowercase();
return Ok(WsAction::RemoveUser(
user_to_ban.to_string(),
group_id.clone(),
));
}
}
_ => return Err(WsError::InvalidMessage),
}
}
Ok(WsAction::UserMessage(UserMessage { message, group_id }))
}
Err(_) => Err(WsError::InvalidMessage),
}
}
}
/// This impl is used to send messages to the websocket
impl Message<MessageToPrint> for WsActor {
type Reply = Result<(), WsError>;
async fn handle(
&mut self,
msg: MessageToPrint,
_ctx: Context<'_, Self, Self::Reply>,
) -> Self::Reply {
self.ws_sender
.send(WsMessage::Text(msg.to_string()))
.await?;
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum WsError {
#[error("Invalid message")]
InvalidMessage,
#[error("Malformed json")]
MalformedJson(#[from] serde_json::Error),
#[error("Failed to send message")]
SendMessageError(#[from] axum::Error),
}

224
tests/user_test.rs Normal file
View File

@@ -0,0 +1,224 @@
use de_mls::{
user::{User, UserAction},
ws_actor::{RawWsMessage, UserMessage, WsAction},
};
#[tokio::test]
async fn test_admin_message_flow() {
let group_name = "new_group".to_string();
let alice_priv_key = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d";
let res = User::new(alice_priv_key);
assert!(res.is_ok(), "Failed to create user");
let mut alice = res.unwrap();
assert!(
alice.create_group(group_name.clone(), true).await.is_ok(),
"Failed to create group"
);
let res = alice.get_group(group_name.clone());
assert!(res.is_ok(), "Failed to get group");
let alice_group = res.unwrap();
assert_eq!(
alice_group.is_mls_group_initialized(),
true,
"MLS group is notinitialized"
);
let bob_priv_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
let res = User::new(bob_priv_key);
assert!(res.is_ok(), "Failed to create user");
let mut bob = res.unwrap();
assert!(
bob.create_group(group_name.clone(), false).await.is_ok(),
"Failed to create group"
);
let res = bob.get_group(group_name.clone());
assert!(res.is_ok(), "Failed to get group");
let bob_group = res.unwrap();
assert_eq!(
bob_group.is_mls_group_initialized(),
false,
"MLS group is initialized"
);
let _ = join_group_flow(&mut alice, &mut bob, group_name.clone()).await;
}
async fn join_group_flow(alice: &mut User, bob: &mut User, group_name: String) -> UserAction {
// Alice send Group Announcement msg to Bob
let res = alice.prepare_admin_msg(group_name.clone()).await;
assert!(res.is_ok(), "Failed to prepare admin message");
let alice_ga_msg = res.unwrap();
let res = alice_ga_msg.build_waku_message();
assert!(res.is_ok(), "Failed to build waku message");
let waku_ga_message = res.unwrap();
// Bob receives the Group Announcement msg and send Key Package Share msg to Alice
let res = bob.process_waku_msg(waku_ga_message).await;
assert!(res.is_ok(), "Failed to process waku message");
let user_action = res.unwrap();
assert!(user_action.len() == 1, "User action is not a single action");
let bob_kp_message = match user_action[0].clone() {
UserAction::SendToWaku(msg) => msg,
_ => panic!("User action is not SendToWaku"),
};
let res = bob_kp_message.build_waku_message();
assert!(res.is_ok(), "Failed to build waku message");
let waku_kp_message = res.unwrap();
// Alice receives the Key Package Share msg and send Welcome msg to Bob
let res = alice.process_waku_msg(waku_kp_message).await;
assert!(res.is_ok(), "Failed to process waku message");
let user_action_invite = res.unwrap();
assert!(
user_action_invite.len() == 2,
"User action is not a two actions"
);
let alice_welcome_message = match user_action_invite[1].clone() {
UserAction::SendToWaku(msg) => msg,
_ => panic!("User action is not SendToWaku"),
};
let res = alice_welcome_message.build_waku_message();
assert!(res.is_ok(), "Failed to build waku message");
let waku_welcome_message = res.unwrap();
// Bob receives the Welcome msg and join the group
let res = bob.process_waku_msg(waku_welcome_message).await;
assert!(res.is_ok(), "Failed to process waku message");
let user_action = res.unwrap();
assert!(user_action.len() == 1, "User action is not a single action");
let bob_group = bob.get_group(group_name.clone()).unwrap();
assert!(
bob_group.is_mls_group_initialized(),
"MLS group is not initialized"
);
user_action_invite[0].clone()
}
#[tokio::test]
async fn test_remove_user_flow() {
let group_name = "new_group".to_string();
let alice_priv_key = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d";
let mut alice = User::new(alice_priv_key).unwrap();
alice.create_group(group_name.clone(), true).await.unwrap();
let bob_priv_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
let mut bob = User::new(bob_priv_key).unwrap();
bob.create_group(group_name.clone(), false).await.unwrap();
let carol_priv_key = "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a";
let mut carol = User::new(carol_priv_key).unwrap();
carol.create_group(group_name.clone(), false).await.unwrap();
let _ = join_group_flow(&mut alice, &mut bob, group_name.clone()).await;
let res = bob.get_group(group_name.clone());
assert!(res.is_ok(), "Failed to get group");
let bob_group = res.unwrap();
assert!(
bob_group.is_mls_group_initialized(),
"MLS group is not initialized"
);
let commit_action = join_group_flow(&mut alice, &mut carol, group_name.clone()).await;
let res = carol.get_group(group_name.clone());
assert!(res.is_ok(), "Failed to get group");
let carol_group = res.unwrap();
assert!(
carol_group.is_mls_group_initialized(),
"MLS group is not initialized"
);
let pmt = match commit_action {
UserAction::SendToWaku(msg) => msg,
_ => panic!("User action is not SendToWaku"),
};
let commit_message = pmt.build_waku_message();
assert!(commit_message.is_ok(), "Failed to build waku message");
let waku_commit_message = commit_message.unwrap();
let res = bob.process_waku_msg(waku_commit_message.clone()).await;
assert!(res.is_ok(), "Failed to process waku message");
let raw_msg = RawWsMessage {
message: serde_json::to_string(&UserMessage {
message: "/ban f39fd6e51aad88f6f4ce6ab8827279cfffb92266".to_string(),
group_id: group_name.clone(),
})
.unwrap(),
};
let ws_action = match serde_json::from_str(&raw_msg.message) {
Ok(UserMessage { message, group_id }) => {
let ws_action = if message.starts_with("/") {
let mut tokens = message.split_whitespace();
let ws = match tokens.next() {
Some("/ban") => {
let user_to_ban = tokens.next().unwrap();
WsAction::RemoveUser(user_to_ban.to_string(), group_id.clone())
}
_ => {
assert!(false, "Invalid user message");
WsAction::DoNothing
}
};
ws
} else {
WsAction::UserMessage(UserMessage { message, group_id })
};
ws_action
}
Err(_) => {
assert!(false, "Failed to parse user message");
WsAction::DoNothing
}
};
assert_eq!(
ws_action,
WsAction::RemoveUser(
"f39fd6e51aad88f6f4ce6ab8827279cfffb92266".to_string(),
group_name.clone()
)
);
let pmt = match ws_action {
WsAction::RemoveUser(user_to_ban, group_name) => {
let res = alice
.remove_users_from_group(vec![user_to_ban], group_name.clone())
.await;
assert!(res.is_ok(), "Failed to remove user from group");
res.unwrap()
}
_ => panic!("User action is not RemoveUser"),
};
let commit_message = pmt.build_waku_message();
assert!(commit_message.is_ok(), "Failed to build waku message");
let waku_commit_message = commit_message.unwrap();
let res = carol.process_waku_msg(waku_commit_message.clone()).await;
assert!(res.is_ok(), "Failed to process waku message");
let carol_group = carol.get_group(group_name.clone()).unwrap();
assert!(
carol_group.members_identity().await.len() == 2,
"Bob is not removed from the group"
);
let res = bob.process_waku_msg(waku_commit_message.clone()).await;
assert!(res.is_ok(), "Failed to process waku message");
let user_action = res.unwrap();
assert!(user_action.len() == 1, "User action is not a single action");
assert_eq!(
user_action[0].clone(),
UserAction::RemoveGroup(group_name.clone()),
"User action is not RemoveGroup"
);
let res = bob.leave_group(group_name.clone()).await;
assert!(res.is_ok(), "Failed to leave group");
assert_eq!(bob.if_group_exists(group_name.clone()), false);
}