From 72fa9966857153546cc64bf4a6a0a9e3ea74df00 Mon Sep 17 00:00:00 2001
From: Andrei Shuvalov <6286552+dronbas@users.noreply.github.com>
Date: Fri, 19 Jun 2026 15:04:12 +0200
Subject: [PATCH 1/3] Add bee connect & bee wallet
---
.cargo/config.toml | 19 +
.github/workflows/ci.yml | 50 +-
.github/workflows/release-rc.yml | 29 +-
.github/workflows/release.yml | 29 +-
.gitleaks.toml | 71 +
Cargo.toml | 37 +-
LICENSE.md | 661 +++
NOTICE.md | 31 +
README.md | 113 +-
bee_connect/Cargo.toml | 37 +
bee_connect/PROTOCOL.md | 483 ++
bee_connect/README.md | 398 ++
bee_connect/src/client.rs | 2821 +++++++++++
bee_connect/src/dex_protocol.rs | 308 ++
bee_connect/src/dh.rs | 927 ++++
bee_connect/src/errors.rs | 2 +
bee_connect/src/lib.rs | 45 +
bee_connect/src/message.rs | 197 +
bee_connect/src/wasm.rs | 984 ++++
bee_connect/tests/live_network.rs | 803 +++
bee_crypto/Cargo.toml | 43 +
bee_crypto/README.md | 62 +
bee_crypto/src/adapters/mod.rs | 3 +
bee_crypto/src/adapters/native/dto/crypto.rs | 81 +
bee_crypto/src/adapters/native/dto/mod.rs | 1 +
bee_crypto/src/adapters/native/mod.rs | 181 +
bee_crypto/src/adapters/wasm/dto/crypto.rs | 103 +
bee_crypto/src/adapters/wasm/dto/mod.rs | 22 +
bee_crypto/src/adapters/wasm/mod.rs | 182 +
bee_crypto/src/client.rs | 156 +
bee_crypto/src/errors.rs | 2 +
bee_crypto/src/lib.rs | 14 +
bee_crypto/src/services/boc_hash.rs | 32 +
bee_crypto/src/services/chacha.rs | 493 ++
bee_crypto/src/services/encoding.rs | 11 +
bee_crypto/src/services/hash.rs | 139 +
bee_crypto/src/services/key.rs | 102 +
bee_crypto/src/services/mnemonic.rs | 100 +
bee_crypto/src/services/mod.rs | 24 +
bee_crypto/src/services/signature.rs | 147 +
bee_crypto/src/services/storage.rs | 235 +
bee_crypto/tests/integration.rs | 246 +
bee_errors/Cargo.toml | 24 +
bee_errors/src/lib.rs | 322 ++
bee_infra/Cargo.toml | 14 +-
bee_infra/src/lib.rs | 76 +-
bee_infra/src/rate_limiter.rs | 115 +
bee_infra/src/retry.rs | 318 ++
bee_miner/Cargo.toml | 15 +-
bee_miner/src/core/context.rs | 50 +-
bee_miner/src/core/keys.rs | 91 +-
bee_miner/src/core/merkle.rs | 19 +-
bee_miner/src/graphql/block.rs | 21 +
bee_miner/src/graphql/mod.rs | 21 +
bee_miner/src/lib.rs | 1 +
bee_miner/src/wasm/miner.rs | 46 +-
bee_miner/src/wasm/mod.rs | 25 +-
bee_miner/src/wasm/worker.rs | 6 +-
bee_sdk/Cargo.toml | 8 +-
bee_sdk/src/lib.rs | 3 +
bee_shared/Cargo.toml | 5 +-
bee_shared/src/miner/leaf.rs | 55 +
bee_shared/src/verifier.rs | 32 +-
bee_verifier/Cargo.toml | 5 +-
bee_verifier/src/bindings.rs | 4 +-
bee_verifier/src/lib.rs | 10 +-
bee_wallet/ARCHITECTURE.md | 44 +
bee_wallet/Cargo.toml | 108 +
bee_wallet/README.md | 75 +
bee_wallet/src/adapters/mod.rs | 4 +
.../src/adapters/native/dto/balances.rs | 37 +
bee_wallet/src/adapters/native/dto/deploy.rs | 36 +
bee_wallet/src/adapters/native/dto/keys.rs | 15 +
bee_wallet/src/adapters/native/dto/miner.rs | 23 +
bee_wallet/src/adapters/native/dto/mod.rs | 7 +
.../src/adapters/native/dto/multifactor.rs | 4 +
bee_wallet/src/adapters/native/dto/tx.rs | 54 +
bee_wallet/src/adapters/native/dto/zkp.rs | 30 +
bee_wallet/src/adapters/native/mod.rs | 452 ++
bee_wallet/src/adapters/wasm/dto/balances.rs | 80 +
bee_wallet/src/adapters/wasm/dto/connect.rs | 219 +
bee_wallet/src/adapters/wasm/dto/deploy.rs | 241 +
bee_wallet/src/adapters/wasm/dto/keys.rs | 28 +
bee_wallet/src/adapters/wasm/dto/miner.rs | 56 +
bee_wallet/src/adapters/wasm/dto/mod.rs | 396 ++
.../src/adapters/wasm/dto/multifactor.rs | 475 ++
bee_wallet/src/adapters/wasm/dto/names.rs | 52 +
bee_wallet/src/adapters/wasm/dto/tx.rs | 105 +
bee_wallet/src/adapters/wasm/dto/write.rs | 112 +
bee_wallet/src/adapters/wasm/dto/zkp.rs | 210 +
bee_wallet/src/adapters/wasm/mod.rs | 764 +++
bee_wallet/src/bin/faucet.rs | 227 +
bee_wallet/src/bin/wallet_info.rs | 1148 +++++
bee_wallet/src/client.rs | 145 +
bee_wallet/src/dapp.rs | 68 +
bee_wallet/src/errors.rs | 11 +
bee_wallet/src/infra/mod.rs | 215 +
bee_wallet/src/lib.rs | 98 +
bee_wallet/src/modules/balance.rs | 75 +
bee_wallet/src/modules/connect.rs | 426 ++
bee_wallet/src/modules/deploy.rs | 370 ++
bee_wallet/src/modules/dex.rs | 69 +
bee_wallet/src/modules/history.rs | 27 +
bee_wallet/src/modules/miner.rs | 155 +
bee_wallet/src/modules/mod.rs | 23 +
bee_wallet/src/modules/multifactor.rs | 431 ++
bee_wallet/src/modules/names.rs | 64 +
bee_wallet/src/modules/tokens.rs | 419 ++
bee_wallet/src/modules/zkp.rs | 253 +
bee_wallet/src/progress.rs | 161 +
bee_wallet/src/services/balance/mod.rs | 212 +
bee_wallet/src/services/boost.rs | 40 +
bee_wallet/src/services/connect.rs | 903 ++++
bee_wallet/src/services/deploy/mod.rs | 44 +
.../services/deploy/prepare_deploy_params.rs | 100 +
bee_wallet/src/services/dex.rs | 105 +
bee_wallet/src/services/miner/cmd.rs | 288 ++
bee_wallet/src/services/miner/mod.rs | 26 +
bee_wallet/src/services/miner/query.rs | 35 +
bee_wallet/src/services/mod.rs | 25 +
bee_wallet/src/services/multifactor/cmd.rs | 507 ++
bee_wallet/src/services/multifactor/jwk.rs | 129 +
.../services/multifactor/key_derivation.rs | 488 ++
bee_wallet/src/services/multifactor/mod.rs | 7 +
bee_wallet/src/services/multifactor/query.rs | 155 +
.../src/services/multifactor/whitelist.rs | 131 +
bee_wallet/src/services/resolvers/mod.rs | 61 +
bee_wallet/src/services/tokens/mod.rs | 572 +++
bee_wallet/src/services/tokens/sell_orders.rs | 557 +++
.../src/services/transaction/history.rs | 1497 ++++++
bee_wallet/src/services/transaction/mod.rs | 1 +
bee_wallet/src/services/validate_name.rs | 50 +
bee_wallet/src/services/zkp/auth.rs | 36 +
.../src/services/zkp/client_login_complete.rs | 726 +++
.../src/services/zkp/client_login_prepare.rs | 331 ++
bee_wallet/src/services/zkp/mod.rs | 38 +
.../zkp/poseidon_lite_constants_1.json | 1 +
.../zkp/poseidon_lite_constants_2.json | 1 +
.../zkp/poseidon_lite_constants_4.json | 1 +
.../zkp/poseidon_lite_constants_5.json | 1 +
bee_wallet/src/services/zkp/utils/error.rs | 53 +
bee_wallet/src/services/zkp/utils/mod.rs | 3 +
bee_wallet/src/services/zkp/utils/provider.rs | 131 +
bee_wallet/src/services/zkp/utils/zk_login.rs | 125 +
bee_wallet/src/types.rs | 342 ++
bee_wallet/tests/dex_flows/common/context.rs | 34 +
bee_wallet/tests/dex_flows/common/keys.rs | 28 +
bee_wallet/tests/dex_flows/common/misc.rs | 36 +
bee_wallet/tests/dex_flows/common/mod.rs | 20 +
bee_wallet/tests/dex_flows/common/pn.rs | 211 +
bee_wallet/tests/dex_flows/common/voucher.rs | 231 +
bee_wallet/tests/dex_flows/common/wallet.rs | 72 +
bee_wallet/tests/dex_flows/flows.rs | 449 ++
bee_wallet/tests/dex_flows/main.rs | 24 +
bee_wallet/tests/hello.abi.json | 91 +
bee_wallet/tests/hello.sol | 131 +
bee_wallet/tests/hello.tvc | Bin 0 -> 994 bytes
bee_wallet/tests/integration.rs | 4299 +++++++++++++++++
examples/javascript/miner-react/README.md | 128 +-
examples/javascript/miner-react/bun.lock | 10 +-
examples/javascript/miner-react/package.json | 9 +-
.../javascript/miner-react/public/nackl.svg | 34 +
examples/javascript/miner-react/src/App.css | 154 +-
examples/javascript/miner-react/src/App.tsx | 1323 ++++-
examples/javascript/miner-react/src/index.css | 30 +-
.../javascript/miner-react/vite.config.ts | 16 +-
scripts/build_wasm.sh | 67 +
167 files changed, 34464 insertions(+), 377 deletions(-)
create mode 100644 .gitleaks.toml
create mode 100644 LICENSE.md
create mode 100644 NOTICE.md
create mode 100644 bee_connect/Cargo.toml
create mode 100644 bee_connect/PROTOCOL.md
create mode 100644 bee_connect/README.md
create mode 100644 bee_connect/src/client.rs
create mode 100644 bee_connect/src/dex_protocol.rs
create mode 100644 bee_connect/src/dh.rs
create mode 100644 bee_connect/src/errors.rs
create mode 100644 bee_connect/src/lib.rs
create mode 100644 bee_connect/src/message.rs
create mode 100644 bee_connect/src/wasm.rs
create mode 100644 bee_connect/tests/live_network.rs
create mode 100644 bee_crypto/Cargo.toml
create mode 100644 bee_crypto/README.md
create mode 100644 bee_crypto/src/adapters/mod.rs
create mode 100644 bee_crypto/src/adapters/native/dto/crypto.rs
create mode 100644 bee_crypto/src/adapters/native/dto/mod.rs
create mode 100644 bee_crypto/src/adapters/native/mod.rs
create mode 100644 bee_crypto/src/adapters/wasm/dto/crypto.rs
create mode 100644 bee_crypto/src/adapters/wasm/dto/mod.rs
create mode 100644 bee_crypto/src/adapters/wasm/mod.rs
create mode 100644 bee_crypto/src/client.rs
create mode 100644 bee_crypto/src/errors.rs
create mode 100644 bee_crypto/src/lib.rs
create mode 100644 bee_crypto/src/services/boc_hash.rs
create mode 100644 bee_crypto/src/services/chacha.rs
create mode 100644 bee_crypto/src/services/encoding.rs
create mode 100644 bee_crypto/src/services/hash.rs
create mode 100644 bee_crypto/src/services/key.rs
create mode 100644 bee_crypto/src/services/mnemonic.rs
create mode 100644 bee_crypto/src/services/mod.rs
create mode 100644 bee_crypto/src/services/signature.rs
create mode 100644 bee_crypto/src/services/storage.rs
create mode 100644 bee_crypto/tests/integration.rs
create mode 100644 bee_errors/Cargo.toml
create mode 100644 bee_errors/src/lib.rs
create mode 100644 bee_infra/src/rate_limiter.rs
create mode 100644 bee_infra/src/retry.rs
create mode 100644 bee_miner/src/graphql/block.rs
create mode 100644 bee_miner/src/graphql/mod.rs
create mode 100644 bee_wallet/ARCHITECTURE.md
create mode 100644 bee_wallet/Cargo.toml
create mode 100644 bee_wallet/README.md
create mode 100644 bee_wallet/src/adapters/mod.rs
create mode 100644 bee_wallet/src/adapters/native/dto/balances.rs
create mode 100644 bee_wallet/src/adapters/native/dto/deploy.rs
create mode 100644 bee_wallet/src/adapters/native/dto/keys.rs
create mode 100644 bee_wallet/src/adapters/native/dto/miner.rs
create mode 100644 bee_wallet/src/adapters/native/dto/mod.rs
create mode 100644 bee_wallet/src/adapters/native/dto/multifactor.rs
create mode 100644 bee_wallet/src/adapters/native/dto/tx.rs
create mode 100644 bee_wallet/src/adapters/native/dto/zkp.rs
create mode 100644 bee_wallet/src/adapters/native/mod.rs
create mode 100644 bee_wallet/src/adapters/wasm/dto/balances.rs
create mode 100644 bee_wallet/src/adapters/wasm/dto/connect.rs
create mode 100644 bee_wallet/src/adapters/wasm/dto/deploy.rs
create mode 100644 bee_wallet/src/adapters/wasm/dto/keys.rs
create mode 100644 bee_wallet/src/adapters/wasm/dto/miner.rs
create mode 100644 bee_wallet/src/adapters/wasm/dto/mod.rs
create mode 100644 bee_wallet/src/adapters/wasm/dto/multifactor.rs
create mode 100644 bee_wallet/src/adapters/wasm/dto/names.rs
create mode 100644 bee_wallet/src/adapters/wasm/dto/tx.rs
create mode 100644 bee_wallet/src/adapters/wasm/dto/write.rs
create mode 100644 bee_wallet/src/adapters/wasm/dto/zkp.rs
create mode 100644 bee_wallet/src/adapters/wasm/mod.rs
create mode 100644 bee_wallet/src/bin/faucet.rs
create mode 100644 bee_wallet/src/bin/wallet_info.rs
create mode 100644 bee_wallet/src/client.rs
create mode 100644 bee_wallet/src/dapp.rs
create mode 100644 bee_wallet/src/errors.rs
create mode 100644 bee_wallet/src/infra/mod.rs
create mode 100644 bee_wallet/src/lib.rs
create mode 100644 bee_wallet/src/modules/balance.rs
create mode 100644 bee_wallet/src/modules/connect.rs
create mode 100644 bee_wallet/src/modules/deploy.rs
create mode 100644 bee_wallet/src/modules/dex.rs
create mode 100644 bee_wallet/src/modules/history.rs
create mode 100644 bee_wallet/src/modules/miner.rs
create mode 100644 bee_wallet/src/modules/mod.rs
create mode 100644 bee_wallet/src/modules/multifactor.rs
create mode 100644 bee_wallet/src/modules/names.rs
create mode 100644 bee_wallet/src/modules/tokens.rs
create mode 100644 bee_wallet/src/modules/zkp.rs
create mode 100644 bee_wallet/src/progress.rs
create mode 100644 bee_wallet/src/services/balance/mod.rs
create mode 100644 bee_wallet/src/services/boost.rs
create mode 100644 bee_wallet/src/services/connect.rs
create mode 100644 bee_wallet/src/services/deploy/mod.rs
create mode 100644 bee_wallet/src/services/deploy/prepare_deploy_params.rs
create mode 100644 bee_wallet/src/services/dex.rs
create mode 100644 bee_wallet/src/services/miner/cmd.rs
create mode 100644 bee_wallet/src/services/miner/mod.rs
create mode 100644 bee_wallet/src/services/miner/query.rs
create mode 100644 bee_wallet/src/services/mod.rs
create mode 100644 bee_wallet/src/services/multifactor/cmd.rs
create mode 100644 bee_wallet/src/services/multifactor/jwk.rs
create mode 100644 bee_wallet/src/services/multifactor/key_derivation.rs
create mode 100644 bee_wallet/src/services/multifactor/mod.rs
create mode 100644 bee_wallet/src/services/multifactor/query.rs
create mode 100644 bee_wallet/src/services/multifactor/whitelist.rs
create mode 100644 bee_wallet/src/services/resolvers/mod.rs
create mode 100644 bee_wallet/src/services/tokens/mod.rs
create mode 100644 bee_wallet/src/services/tokens/sell_orders.rs
create mode 100644 bee_wallet/src/services/transaction/history.rs
create mode 100644 bee_wallet/src/services/transaction/mod.rs
create mode 100644 bee_wallet/src/services/validate_name.rs
create mode 100644 bee_wallet/src/services/zkp/auth.rs
create mode 100644 bee_wallet/src/services/zkp/client_login_complete.rs
create mode 100644 bee_wallet/src/services/zkp/client_login_prepare.rs
create mode 100644 bee_wallet/src/services/zkp/mod.rs
create mode 100644 bee_wallet/src/services/zkp/poseidon_lite_constants_1.json
create mode 100644 bee_wallet/src/services/zkp/poseidon_lite_constants_2.json
create mode 100644 bee_wallet/src/services/zkp/poseidon_lite_constants_4.json
create mode 100644 bee_wallet/src/services/zkp/poseidon_lite_constants_5.json
create mode 100644 bee_wallet/src/services/zkp/utils/error.rs
create mode 100644 bee_wallet/src/services/zkp/utils/mod.rs
create mode 100644 bee_wallet/src/services/zkp/utils/provider.rs
create mode 100644 bee_wallet/src/services/zkp/utils/zk_login.rs
create mode 100644 bee_wallet/src/types.rs
create mode 100644 bee_wallet/tests/dex_flows/common/context.rs
create mode 100644 bee_wallet/tests/dex_flows/common/keys.rs
create mode 100644 bee_wallet/tests/dex_flows/common/misc.rs
create mode 100644 bee_wallet/tests/dex_flows/common/mod.rs
create mode 100644 bee_wallet/tests/dex_flows/common/pn.rs
create mode 100644 bee_wallet/tests/dex_flows/common/voucher.rs
create mode 100644 bee_wallet/tests/dex_flows/common/wallet.rs
create mode 100644 bee_wallet/tests/dex_flows/flows.rs
create mode 100644 bee_wallet/tests/dex_flows/main.rs
create mode 100644 bee_wallet/tests/hello.abi.json
create mode 100644 bee_wallet/tests/hello.sol
create mode 100644 bee_wallet/tests/hello.tvc
create mode 100644 bee_wallet/tests/integration.rs
create mode 100644 examples/javascript/miner-react/public/nackl.svg
create mode 100755 scripts/build_wasm.sh
diff --git a/.cargo/config.toml b/.cargo/config.toml
index c91c3f3..4292deb 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -1,2 +1,21 @@
+[build]
+# Required by `dark_dex_halo2_private_witness_export_lib` → acki-nacki
+# `telemetry_utils` (calls `RuntimeMetrics::spawned_tasks_count`, gated
+# behind tokio's unstable cfg). Without this flag, the live witness
+# export crate fails to build.
+rustflags = ["--cfg", "tokio_unstable"]
+
+# NOTE: do NOT switch the linker to ld64.lld here. The dep-graph pulls in
+# two tvm-sdk versions (via ackinacki-kit and via acki-nacki/node) whose
+# `tvm_client` crates both export the same `extern "C"` symbols
+# (`tc_create_context`, `tc_request_*`, ...). Apple's system ld64 silently
+# picks one; ld64.lld errors out on duplicate symbols. Stick with ld64.
+
+# NOTE: `rustc-wrapper = "sccache"` is intentionally not configured here.
+# sccache is a personal/CI-side speedup; baking it into the project config
+# breaks builds for contributors who don't have sccache installed (cargo
+# fails if the wrapper binary is missing). Put it in your own
+# `~/.cargo/config.toml` instead.
+
[net]
git-fetch-with-cli = true
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7976705..ed2484b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,16 +21,40 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- - name: Setup Rust
+ # nightly is only for `cargo fmt` (rustfmt.toml uses nightly-only options).
+ # stable is installed last, so it is the default for build/clippy/test.
+ - name: Setup Rust (nightly, rustfmt only)
+ uses: dtolnay/rust-toolchain@nightly
+ with:
+ components: rustfmt
+ - name: Setup Rust (stable)
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
+ # `gosh_tls_lib` is a private dependency (bee_wallet -> tls_connect); fetch
+ # it over https with a read-only token. `dodex-sdk` is a dev-dependency used
+ # only by the `dex_flows` integration tests (not run in CI) and pulls a heavy
+ # halo2 graph, so strip it to keep the resolve light. Every other git
+ # dependency is public.
+ - name: Configure git for private dep
+ env:
+ PAT: ${{ secrets.GH_PAT }}
+ run: |
+ git config --global url."https://oauth2:${PAT}@github.com/".insteadOf "ssh://git@github.com/"
+ sed -i '/^dodex-sdk = /d' bee_wallet/Cargo.toml
+
- name: Cache Rust
uses: Swatinem/rust-cache@v2
- - name: Cargo check (workspace except verifier)
- run: cargo check --workspace --exclude bee-verifier
+ - name: Rustfmt
+ run: cargo +nightly fmt --all -- --check
+
+ # bee-sdk is the wasm aggregator (forces single-wasm on its deps, which leaks
+ # wasm-only code into native builds via feature unification); bee-verifier
+ # targets wasm32-wasip2. Both are covered on their own targets below.
+ - name: Cargo check (workspace, native)
+ run: cargo check --workspace --exclude bee-sdk --exclude bee-verifier
- name: Cargo check (verifier)
run: cargo check -p bee-verifier
@@ -38,14 +62,24 @@ jobs:
- name: Cargo check (miner wasm feature)
run: cargo check -p bee-miner --no-default-features --features wasm
- - name: Cargo clippy (workspace)
- run: cargo clippy --workspace --all-targets -- -D warnings
+ - name: Cargo check (sdk, wasm target)
+ run: |
+ rustup target add wasm32-unknown-unknown
+ cargo check -p bee-sdk --target wasm32-unknown-unknown
+
+ - name: Cargo clippy (workspace libs)
+ run: cargo clippy --workspace --exclude bee-sdk --exclude bee-verifier --lib -- -D warnings -A clippy::result_large_err
- - name: Cargo test (workspace native)
- run: cargo test --workspace --exclude bee-verifier --exclude bee-sdk
+ # `--lib` only: bee_wallet's integration tests hit shellnet / need local
+ # halo2 params, so they are run manually, not in CI.
+ - name: Cargo test (workspace libs)
+ run: cargo test --workspace --exclude bee-sdk --exclude bee-verifier --lib
- name: Cargo test (verifier lib)
run: cargo test -p bee-verifier --lib
+ # Skip the deep-link test: it drives wasm-bindgen futures that panic when
+ # the wasm feature is exercised on the host target (run it under
+ # wasm-pack instead). release.yml skips the same test.
- name: Cargo test (miner wasm feature on host)
- run: cargo test -p bee-miner --no-default-features --features wasm --lib
+ run: cargo test -p bee-miner --no-default-features --features wasm --lib -- --skip core::keys::tests::gen_mining_keys_generates_keys_and_valid_deep_link
diff --git a/.github/workflows/release-rc.yml b/.github/workflows/release-rc.yml
index d147b33..75706a0 100644
--- a/.github/workflows/release-rc.yml
+++ b/.github/workflows/release-rc.yml
@@ -46,6 +46,17 @@ jobs:
with:
components: clippy
+ # `gosh_tls_lib` is a private dependency (bee_wallet -> tls_connect); fetch
+ # it over https with a read-only token. `dodex-sdk` is a dev-dependency for
+ # the `dex_flows` integration tests only (not built here) and pulls a heavy
+ # halo2 graph, so strip it. Every other git dependency is public.
+ - name: Configure git for private dep
+ env:
+ PAT: ${{ secrets.GH_PAT }}
+ run: |
+ git config --global url."https://oauth2:${PAT}@github.com/".insteadOf "ssh://git@github.com/"
+ sed -i '/^dodex-sdk = /d' bee_wallet/Cargo.toml
+
- name: Setup Node
uses: actions/setup-node@v4
with:
@@ -58,8 +69,8 @@ jobs:
- name: Install wasm-pack
run: cargo install wasm-pack --locked
- - name: Cargo check (workspace except verifier)
- run: cargo check --workspace --exclude bee-verifier
+ - name: Cargo check (workspace, native)
+ run: cargo check --workspace --exclude bee-sdk --exclude bee-verifier
- name: Cargo check (verifier)
run: cargo check -p bee-verifier
@@ -67,11 +78,11 @@ jobs:
- name: Cargo check (miner wasm feature)
run: cargo check -p bee-miner --no-default-features --features wasm
- - name: Cargo clippy (workspace)
- run: cargo clippy --workspace --all-targets -- -D warnings
+ - name: Cargo clippy (workspace libs)
+ run: cargo clippy --workspace --exclude bee-sdk --exclude bee-verifier --lib -- -D warnings -A clippy::result_large_err
- - name: Cargo test (workspace native)
- run: cargo test --workspace --exclude bee-verifier --exclude bee-sdk
+ - name: Cargo test (workspace libs)
+ run: cargo test --workspace --exclude bee-sdk --exclude bee-verifier --lib
- name: Cargo test (verifier lib)
run: cargo test -p bee-verifier --lib
@@ -84,9 +95,11 @@ jobs:
shell: bash
run: |
set -euo pipefail
- VERSION="$(grep -E '^version\s*=' bee_sdk/Cargo.toml | head -n1 | sed -E 's/version\s*=\s*"([^"]+)"/\1/')"
+ # bee_sdk inherits its version from [workspace.package] in the root
+ # Cargo.toml, so resolve it there.
+ VERSION="$(grep -E '^version\s*=' Cargo.toml | head -n1 | sed -E 's/version\s*=\s*"([^"]+)"/\1/')"
if [ -z "${VERSION}" ]; then
- echo "Failed to resolve bee-sdk version from bee_sdk/Cargo.toml"
+ echo "Failed to resolve workspace version from Cargo.toml"
exit 1
fi
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 675bf7e..cdf75e6 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -46,6 +46,17 @@ jobs:
with:
components: clippy
+ # `gosh_tls_lib` is a private dependency (bee_wallet -> tls_connect); fetch
+ # it over https with a read-only token. `dodex-sdk` is a dev-dependency for
+ # the `dex_flows` integration tests only (not built here) and pulls a heavy
+ # halo2 graph, so strip it. Every other git dependency is public.
+ - name: Configure git for private dep
+ env:
+ PAT: ${{ secrets.GH_PAT }}
+ run: |
+ git config --global url."https://oauth2:${PAT}@github.com/".insteadOf "ssh://git@github.com/"
+ sed -i '/^dodex-sdk = /d' bee_wallet/Cargo.toml
+
- name: Setup Node
uses: actions/setup-node@v4
with:
@@ -58,8 +69,8 @@ jobs:
- name: Install wasm-pack
run: cargo install wasm-pack --locked
- - name: Cargo check (workspace except verifier)
- run: cargo check --workspace --exclude bee-verifier
+ - name: Cargo check (workspace, native)
+ run: cargo check --workspace --exclude bee-sdk --exclude bee-verifier
- name: Cargo check (verifier)
run: cargo check -p bee-verifier
@@ -67,11 +78,11 @@ jobs:
- name: Cargo check (miner wasm feature)
run: cargo check -p bee-miner --no-default-features --features wasm
- - name: Cargo clippy (workspace)
- run: cargo clippy --workspace --all-targets -- -D warnings
+ - name: Cargo clippy (workspace libs)
+ run: cargo clippy --workspace --exclude bee-sdk --exclude bee-verifier --lib -- -D warnings -A clippy::result_large_err
- - name: Cargo test (workspace native)
- run: cargo test --workspace --exclude bee-verifier --exclude bee-sdk
+ - name: Cargo test (workspace libs)
+ run: cargo test --workspace --exclude bee-sdk --exclude bee-verifier --lib
- name: Cargo test (verifier lib)
run: cargo test -p bee-verifier --lib
@@ -84,9 +95,11 @@ jobs:
shell: bash
run: |
set -euo pipefail
- VERSION="$(grep -E '^version\s*=' bee_sdk/Cargo.toml | head -n1 | sed -E 's/version\s*=\s*"([^"]+)"/\1/')"
+ # bee_sdk inherits its version from [workspace.package] in the root
+ # Cargo.toml, so resolve it there.
+ VERSION="$(grep -E '^version\s*=' Cargo.toml | head -n1 | sed -E 's/version\s*=\s*"([^"]+)"/\1/')"
if [ -z "${VERSION}" ]; then
- echo "Failed to resolve bee-sdk version from bee_sdk/Cargo.toml"
+ echo "Failed to resolve workspace version from Cargo.toml"
exit 1
fi
diff --git a/.gitleaks.toml b/.gitleaks.toml
new file mode 100644
index 0000000..0ecf0f2
--- /dev/null
+++ b/.gitleaks.toml
@@ -0,0 +1,71 @@
+title = "Gitleaks Config"
+
+[detect]
+redact = true
+
+# ======== Generic Rules ========
+
+[[rules]]
+id = "generic-api-key"
+description = "Generic API Key"
+regex = '''(?i)(api[_-]?key|secret|token|auth)[=:]["']?[0-9a-zA-Z-_]{16,}["']?'''
+tags = ["key", "API"]
+
+[[rules]]
+id = "github-token"
+description = "GitHub Personal Access Token"
+regex = '''ghp_[0-9A-Za-z]{36}'''
+tags = ["github", "token"]
+
+[[rules]]
+id = "gitlab-token"
+description = "GitLab Personal Access Token"
+regex = '''glpat-[0-9a-zA-Z\-\_]{20,}'''
+tags = ["gitlab", "token"]
+
+[[rules]]
+id = "slack-token"
+description = "Slack Token"
+regex = '''xox[baprs]-[0-9A-Za-z-]{10,48}'''
+tags = ["slack", "token"]
+
+[[rules]]
+id = "aws-access-key"
+description = "AWS Access Key ID"
+regex = '''AKIA[0-9A-Z]{16}'''
+tags = ["AWS", "key"]
+
+[[rules]]
+id = "aws-secret-key"
+description = "AWS Secret Access Key"
+regex = '''(?i)aws(.{0,20})?(secret|private)?(.{0,20})?(key|token)['"][0-9a-zA-Z\/+=]{40}['"]'''
+tags = ["AWS", "key"]
+
+[[rules]]
+id = "private-key"
+description = "Private Key (PEM)"
+regex = '''-----BEGIN (RSA|DSA|EC|OPENSSH|PGP) PRIVATE KEY-----'''
+tags = ["key", "private"]
+
+[[rules]]
+id = "db-connection-string"
+description = "Database Connection String"
+regex = '''(?i)(?:postgres|mysql|mongodb|redis|sqlserver):\/\/([^ \n]+)'''
+tags = ["database", "credentials"]
+
+# ======== Allowlist ========
+
+[allowlist]
+description = "Ignore common non-sensitive locations"
+paths = [
+ '''.*tests?/.*''',
+ '''.*examples?/.*''',
+ '''target/.*''',
+ '''docs?/.*''',
+ '''\.git/.*''',
+ '''\.idea/.*''',
+ '''\.vscode/.*''',
+]
+regexes = [
+ '''(?i)dummy|example|sample|test|placeholder|mock''',
+]
diff --git a/Cargo.toml b/Cargo.toml
index 90a9bc7..b030910 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,37 @@
[workspace]
resolver = "3"
-members = ["bee_sdk", "bee_miner", "bee_shared", "bee_verifier", "bee_infra"]
+members = [
+ "bee_sdk",
+ "bee_miner",
+ "bee_shared",
+ "bee_verifier",
+ "bee_wallet",
+ "bee_connect",
+ "bee_infra",
+ "bee_crypto",
+ "bee_errors",
+]
+# `bee_sdk` is the wasm-aggregator (forces `single-wasm` on its deps, which
+# leaks wasm-only code into native builds via feature unification).
+# `bee_verifier` targets `wasm32-wasip2` and won't build under the host target.
+# Excluding both from default-members keeps `cargo run|build|test` from the
+# workspace root scoped to crates that compile cleanly natively.
+default-members = [
+ "bee_miner",
+ "bee_shared",
+ "bee_wallet",
+ "bee_connect",
+ "bee_infra",
+ "bee_crypto",
+ "bee_errors",
+]
+
+[workspace.package]
+version = "3.0.0"
+edition = "2024"
+license = "LicenseRef-Acki-Nacki-Node-License"
+license-file = "LICENSE.md"
[workspace.dependencies]
blake3 = "1.8.2"
@@ -15,3 +45,8 @@ serde_json = "1"
# Tell `rustc` to optimize for small code size.
opt-level = "s"
panic = "abort"
+
+[profile.dev]
+# Line-tables-only: backtraces with file:line + symbol names, no locals.
+# Cuts debuginfo gen + link time noticeably on a halo2 + acki-nacki node tree.
+debug = 1
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..be3f7b2
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
diff --git a/NOTICE.md b/NOTICE.md
new file mode 100644
index 0000000..be839cb
--- /dev/null
+++ b/NOTICE.md
@@ -0,0 +1,31 @@
+# Bee-engine
+
+Copyright (C) 2026 GOSH TECHNOLOGY LTD.
+
+Bee-engine is free software: you can redistribute it and/or modify it under
+the terms of the **GNU Affero General Public License**, version 3, as
+published by the Free Software Foundation. The full license text is in
+[LICENSE.md](LICENSE.md).
+
+Bee-engine is distributed in the hope that it will be useful, but **WITHOUT
+ANY WARRANTY**; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
+for more details.
+
+## Runtime dependency: Acki Nacki node
+
+Bee-engine is a client SDK (mining, wallet management, proof generation and
+verification, and cryptographic operations) for the **Acki Nacki** chain. It
+does not run a chain itself — it signs and dispatches transactions to, and
+reads contract and account state from, an Acki Nacki node.
+
+The Acki Nacki node software is published by **GOSH TECHNOLOGY LTD.** under a
+separate license — the **Acki Nacki Node License (ANNL)**, a Business Source
+License with a two-year change date to GNU AGPL-3.0. The ANNL covers the node
+software itself, not Bee-engine. Refer to the node repository for its current
+license text and terms.
+
+The AGPL terms of Bee-engine apply only to Bee-engine itself — the project's
+own source code, builds, deployments, and modifications. They do not extend
+to or override the licensing of any separately distributed software (such as
+the Acki Nacki node) that Bee-engine interoperates with at runtime.
diff --git a/README.md b/README.md
index 2c7f466..39d9550 100644
--- a/README.md
+++ b/README.md
@@ -1,106 +1,105 @@
# Bee-engine
-**TODO:** Add a description of the project’s purpose and high-level architecture.
+Rust workspace for blockchain mining, proof verification, wallet management, and cryptographic operations. Targets native Rust, browser WASM (`wasm32-unknown-unknown`), and WASI 2.0 (`wasm32-wasip2`).
+## Crates
+```
+bee_sdk WASM aggregator — re-exports all crates into a single .wasm bundle
+bee_wallet Wallet operations (send tokens, accumulator, connect, deploy)
+bee_crypto Signing, encryption, mnemonics, hashing
+bee_miner Mining: proof generation, merkle trees, WASM workers
+bee_connect dApp ↔ wallet connect protocol, encrypted session management
+bee_verifier Proof verifier (WASI component, separate target)
+bee_shared Shared Borsh-serialized types
+bee_infra Async sleep/poll utilities
+```
## Prerequisites
-Before building the project, ensure the following tools are installed:
+- **Rust** 1.86+
+- **LLVM 21+** (for WASM compilation of `blst` dependency)
+- **wasm-pack**: `cargo install wasm-pack`
+- **cargo-component**: `cargo install cargo-component`
-1. **LLVM** — version **21 or higher**
-2. **wasm-pack**
- `cargo install wasm-pack`
-3. **cargo-component** `cargo install cargo-component`
-
+## Build
+
+Both WASM artifacts are built via the script — it remaps machine paths out of
+the binaries (`--remap-path-prefix`) and fails the build if a `/Users/...` or
+`/home//...` path survives into the artifact:
-## Building engine-sdk (WASM)
-This step builds the SDK targeting browser-compatible WebAssembly.
```bash
-cd engine_sdk
-rm -rf pkg && wasm-pack build --target web
+scripts/build_wasm.sh sdk # bee_sdk → bee_sdk/pkg (wasm-pack, browser)
+scripts/build_wasm.sh verifier # bee_verifier → wasm32-wasip2 (WASI component)
+scripts/build_wasm.sh # both
```
-Output artifacts are generated in the `pkg/` directory.
-
-## Building the Verifier (WASI)
+If `bee_verifier/wit` was updated, run `cargo component bindings` in
+`bee_verifier/` first (see `bee_verifier/README.md`).
-**TODO:** Verify the build commands below.
+## Tests
```bash
-cd engine
-
-# Generate WIT bindings
-cargo component bindings
-
-# Build the WASI-compatible WASM binary
-cargo +nightly build \
- -Zbuild-std=std,panic_abort \
- -Zbuild-std-features=panic_immediate_abort \
- --target wasm32-wasip2 \
- --release
+cargo test --workspace # all native tests
+cargo test -p bee-wallet # single crate
+cargo test -p bee-wallet -- test_name # single test
+cargo test -p bee-wallet -- --test-threads=1 # bee_wallet integration tests (sequential)
+wasm-pack test --headless --chrome -p bee_miner # WASM tests (browser)
```
-This produces a WASM component intended to run inside a WASI runtime.
+`bee_wallet` integration tests hit shellnet and share on-chain state (accumulator queues). Run with `--test-threads=1` to avoid flaky failures.
+## Lint & Format
+
+```bash
+cargo clippy --workspace
+cargo fmt --all -- --check
+```
+Formatting uses nightly rustfmt features (`imports_granularity = "Item"`, `group_imports = "StdExternalCrate"`).
-## Language Bindings
+## Examples
-**TODO:** Verify the build commands below.
+### miner-react
-### JavaScript
+React dApp demonstrating wallet connect, mining key setup, and wallet ownership verification.
```bash
-bun i
-bunx jco transpile \
- engine/target/wasm32-wasip2/release/bee_engine_verifier.wasm \
- -o ./bindings/js
+cd bee_sdk && rm -rf pkg && wasm-pack build --target web
+cd ../examples/javascript/miner-react
+npm install
+npm run dev
```
-This command generates JavaScript bindings from the WASI component.
-
----
-
## Troubleshooting
-### Build errors
+### `blst` build errors
-If you encounter errors similar to the following when running the `wasm-pack build --target web` command:
```
warning: blst@0.3.16: error: unable to create target:
'No available targets are compatible with triple "wasm32-unknown-unknown"'
-error: failed to run custom build command for `blst v0.3.16`
```
-This usually indicates that your system `clang` is **too old** and does not support WebAssembly targets.
-
----
-
-### Fix: Upgrade LLVM (macOS)
-
-Install a newer LLVM version using Homebrew:
+System `clang` is too old. Install LLVM 21+:
```bash
brew install llvm
```
-Then add the following lines to the end of your `~/.zshrc`:
+Add to `~/.zshrc`:
```bash
-# LLVM / clang configuration
export PATH="/opt/homebrew/opt/llvm/bin:$PATH"
export LDFLAGS="-L/opt/homebrew/opt/llvm/lib"
export CPPFLAGS="-I/opt/homebrew/opt/llvm/include"
export CMAKE_PREFIX_PATH="/opt/homebrew/opt/llvm"
```
-Reload your shell configuration:
+Verify: `clang --version` reports LLVM 21+, `which clang` returns `/opt/homebrew/opt/llvm/bin/clang`.
-```bash
-source ~/.zshrc
-```
+## License
-Verify the installation:
- - `clang --version` reports LLVM **21+** or higher
- - `which clang` returns `/opt/homebrew/opt/llvm/bin/clang`
+Bee-engine is licensed under the **GNU Affero General Public License v3.0** —
+see [LICENSE.md](LICENSE.md). The AGPL terms apply only to Bee-engine itself;
+the Acki Nacki node software it interoperates with at runtime is licensed
+separately. See [NOTICE.md](NOTICE.md) for details.
diff --git a/bee_connect/Cargo.toml b/bee_connect/Cargo.toml
new file mode 100644
index 0000000..b074167
--- /dev/null
+++ b/bee_connect/Cargo.toml
@@ -0,0 +1,37 @@
+[package]
+name = "bee-connect"
+version.workspace = true
+license.workspace = true
+edition.workspace = true
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[features]
+default = ["ackinacki-kit/default", "bee-errors/default"]
+wasm = ["dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:serde-wasm-bindgen", "dep:js-sys", "bee-infra/wasm", "ackinacki-kit/wasm", "bee-errors/wasm"]
+single-wasm = ["wasm"]
+
+[dependencies]
+ackinacki-kit = { git = "https://github.com/gosh-sh/ackinacki-kit.git", tag = "v3.0.0", default-features = false, features = ["contracts"] }
+base64 = "0.22.1"
+bee-errors = { path = "../bee_errors", default-features = false }
+bee-infra = { path = "../bee_infra", default-features = false, features = ["tokio"] }
+chacha20poly1305 = "0.10.1"
+getrandom = { version = "0.3.4", default-features = false, features = ["wasm_js"] }
+hex = "0.4.3"
+hkdf = "0.12.4"
+js-sys = { optional = true, version = "0.3.85" }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+serde-wasm-bindgen = { optional = true, version = "0.6.5" }
+ed25519-dalek = "2"
+sha2 = "0.10.9"
+x25519-dalek = { version = "2", features = ["static_secrets"] }
+zeroize = { version = "1.8", features = ["serde"] }
+wasm-bindgen = { optional = true, version = "0.2.106" }
+wasm-bindgen-futures = { optional = true, version = "0.4.56" }
+
+[dev-dependencies]
+serial_test = "3"
+tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
diff --git a/bee_connect/PROTOCOL.md b/bee_connect/PROTOCOL.md
new file mode 100644
index 0000000..47869bc
--- /dev/null
+++ b/bee_connect/PROTOCOL.md
@@ -0,0 +1,483 @@
+# bee_connect Protocol — DH Key Exchange Flow
+
+## Overview
+
+Connect protocol (`bee_connect.msg/1`) provides a secure channel between the client (dApp/miner) and wallet via an on-chain AuthProfile. All messages are encrypted. Secrets are **never transmitted** through URLs, QR codes, or other observable channels.
+
+Instead of passing a session secret in the URL, ephemeral X25519 Diffie-Hellman key exchange is used. Both sides independently derive identical session keys.
+
+---
+
+## Participants
+
+- **Client** — dApp or miner, initiates the session
+- **Wallet** — mobile or desktop wallet, accepts the session
+- **AuthProfile** — on-chain contract for exchanging encrypted messages
+- **links.gosh.sh** — deeplink URL resolver (sees only the public DH key)
+
+---
+
+## Phase 1: Session Creation (Client)
+
+```
+Client: create_shared_key_session(app_id, ttl_secs)
+```
+
+1. Generates `session_id`, `description`, `expires_at`
+2. Generates ephemeral **X25519 keypair** (`client_dh_public`, `client_dh_secret`)
+3. Builds deeplink URL:
+ ```
+ https://links.gosh.sh/deeplinks/wallet/v1/connect
+ ?payload=
+ &client_dh_public=<64 hex chars>
+ ```
+4. Caller receives `ResultOfCreateSharedKeySession`:
+ - `session_id`, `description`, `payload_b64url`
+ - `client_dh_public` — public key (in URL)
+ - `client_dh_secret` — secret key (**caller stores**, never transmitted)
+ - `deep_link` — URL with no secrets
+
+```
+┌────────────────────────────────────────────────────┐
+│ URL contains ONLY client_dh_public │
+│ links.gosh.sh does NOT see any secrets │
+│ Intercepting the URL is useless without │
+│ client_dh_secret │
+└────────────────────────────────────────────────────┘
+```
+
+---
+
+## Phase 2: Session Acceptance (Wallet)
+
+```
+Wallet: accept_shared_key_connect(payload, client_dh_public, wallet_name, wallet_address)
+```
+
+1. Wallet parses `payload` and `client_dh_public` from the deeplink URL
+2. Generates wallet ephemeral **X25519 keypair** (`wallet_dh_public`, `wallet_dh_secret`)
+3. **DH Key Agreement**:
+ ```
+ shared_secret = X25519(wallet_dh_secret, client_dh_public) // 32 bytes
+ ```
+4. **Key Derivation** (HKDF-SHA256, salt = session_id):
+ ```
+ signing_seed = HKDF(shared_secret, info="bee_connect.signing.v1")
+ encryption_root = HKDF(shared_secret, info="bee_connect.encryption.v1")
+
+ signing_key = Ed25519::SigningKey(signing_seed) // for on-chain transactions
+ // encryption_root is used as input for per-message HKDF
+ ```
+5. **Deploy AuthProfile** on-chain with `signing_key.public`
+6. **wallet_hello** — first encrypted message:
+ ```json
+ {
+ "v": "bee_connect.msg/1",
+ "session_id": "...",
+ "dir": "w2c",
+ "seq": 1700000000000,
+ "type": "wallet_hello",
+ "ts": 1700000000,
+ "dh_public": "",
+ "enc": {
+ "alg": "xchacha20poly1305-hkdf-sha256",
+ "nonce": "",
+ "salt": ""
+ },
+ "body": ""
+ }
+ ```
+ - `dh_public` — **cleartext** top-level field so client can read it before decryption
+ - `body` is encrypted with `encryption_root`
+ - On-chain transaction signature uses `signing_key`
+
+7. Wallet receives `ResultOfAcceptConnect`:
+ - `session_state: ConnectSessionState` — **wallet stores** for all subsequent operations
+ (contains signing keys, encryption root, DH keys for forward secrecy)
+
+---
+
+## Phase 3: DH Finalization (Client)
+
+```
+Client: wait_wallet_hello(session_id, description, client_dh_secret)
+```
+
+1. Client polls AuthProfile on-chain, waiting for `wallet_hello`
+2. Extracts `dh_public` (wallet's public key) from the envelope
+3. **DH Key Agreement** (mirrors wallet):
+ ```
+ shared_secret = X25519(client_dh_secret, wallet_dh_public)
+ // Identical to shared_secret on the wallet side!
+ ```
+4. **Key Derivation** — identical HKDF:
+ ```
+ signing_seed = HKDF(shared_secret, info="bee_connect.signing.v1") // == wallet's
+ encryption_root = HKDF(shared_secret, info="bee_connect.encryption.v1") // == wallet's
+ ```
+5. Decrypts `body` using `encryption_root` — success verifies the DH exchange
+6. Client receives `ResultOfWaitWalletHello`:
+ - `wallet_name`, `wallet_address` — from decrypted body
+ - `session_state: ConnectSessionState` — **client stores** for all subsequent operations
+ (contains signing keys, encryption root, DH keys for forward secrecy)
+
+---
+
+## Phase 4: Message Exchange (Forward Secrecy)
+
+After handshake both sides store a `ConnectSessionState`:
+
+```rust
+ConnectSessionState {
+ encryption_root: Zeroizing, // rotated every message, zeroed on drop
+ my_dh_secret: Zeroizing, // rotated on every outbound message, zeroed on drop
+ peer_dh_public: String, // updated on every inbound message
+ signing_public: String, // fixed for session lifetime
+ signing_secret: Zeroizing, // fixed for session lifetime, zeroed on drop
+ created_at: u64, // UNIX epoch seconds, set at handshake
+ expires_at: u64, // UNIX epoch seconds, default = created_at + 24h
+}
+```
+
+**Every outbound message performs a DH re-key for forward secrecy:**
+
+1. Sender generates new ephemeral X25519 keypair
+2. Computes `new_shared = X25519(new_secret, peer_dh_public)`
+3. Derives `new_root = HKDF(old_root || new_shared, salt=session_id, info="bee_connect.rekey.v1")`
+4. Encrypts message body with `new_root`
+5. Includes `dh_public` (new public key) in the envelope
+6. Updates local state: `encryption_root = new_root`, `my_dh_secret = new_secret`
+7. Old root is discarded → forward secrecy
+
+**Receiver mirrors the re-key:**
+
+1. Reads `dh_public` from envelope
+2. Computes `new_shared = X25519(my_dh_secret, peer_new_dh_public)`
+3. Derives same `new_root = HKDF(old_root || new_shared, ...)`
+4. Decrypts message body with `new_root`
+5. Updates local state: `encryption_root = new_root`, `peer_dh_public = peer_new_dh_public`
+
+### Client → Wallet: set_mining_keys
+
+```
+Client: request_set_mining_keys(session_state, app_id, owner_public) → updated_session_state
+```
+
+Caller MUST persist `updated_session_state` and discard old state.
+
+Envelope example (after re-key):
+
+```json
+{
+ "v": "bee_connect.msg/1",
+ "session_id": "...",
+ "dir": "c2w",
+ "seq": 1700000001000,
+ "type": "set_mining_keys",
+ "ts": 1700000001,
+ "dh_public": "",
+ "enc": {
+ "alg": "xchacha20poly1305-hkdf-sha256",
+ "nonce": "",
+ "salt": ""
+ },
+ "body": ""
+}
+```
+
+The `dh_public` field contains the sender's new ephemeral public key. The receiver uses it to perform `rekey_inbound` before decryption.
+
+### Client → Wallet: sign_challenge (Backend Auth)
+
+```
+Client: request_sign_challenge(session_state, nonce) → updated_session_state
+```
+
+Proves wallet ownership to a backend. The flow:
+
+1. **Backend** generates a random nonce (hex string) and gives it to the dApp client
+2. **Client** sends `sign_challenge` (c2w) with the nonce through the connect session
+3. **Wallet** receives the challenge, signs the nonce with its EPK key, and responds with `challenge_response` (w2c)
+4. **Client** receives `challenge_response` containing `{ nonce, signature, wallet_address }`
+5. **Client** forwards `signature` + `wallet_address` to the backend
+6. **Backend** verifies the Ed25519 signature against the wallet's on-chain public key
+
+Body (encrypted):
+```json
+{ "nonce": "" }
+```
+
+### Wallet → Client: challenge_response
+
+```
+Wallet: (responds to sign_challenge via query_session_messages)
+```
+
+The wallet signs the nonce from `sign_challenge` using its EPK key (`sign_detached_hex(nonce, epk_secret)`).
+
+Body (encrypted):
+```json
+{
+ "nonce": "",
+ "signature": "",
+ "wallet_address": "",
+ "epk_public": ""
+}
+```
+
+`epk_public` is the Ed25519 public key the wallet used to sign the nonce. The field is optional for backward compatibility with older wallets (`#[serde(default)]`).
+
+**Backend verification (Python example):**
+```python
+from nacl.signing import VerifyKey # pip install PyNaCl
+
+# 1. Verify signature (offline, no chain access)
+nonce_bytes = bytes.fromhex(nonce)
+signature_bytes = bytes.fromhex(signature)
+verify_key = VerifyKey(bytes.fromhex(epk_public))
+verify_key.verify(nonce_bytes, signature_bytes) # raises BadSignatureError
+
+# 2. Confirm EPK is registered in multifactor contract (one RPC call, cacheable)
+# Call get_epk_expire_at(wallet_address, epk_public) on chain
+# If returns valid non-expired timestamp → EPK belongs to this wallet
+```
+
+**State persistence:** The dApp MUST NOT persist `updated_session_state` from `request_sign_challenge` until `wait_challenge_response` succeeds. If the wait times out, the wallet has already advanced its DH chain (inbound rekey for `sign_challenge` + outbound rekey for `challenge_response`), while the dApp only did one outbound rekey. Persisting early makes the desync permanent. By keeping the pre-challenge state on timeout, the dApp can re-establish the session cleanly.
+
+### Client → Wallet: disconnect
+
+```
+Client: disconnect_session(session_state, reason) → updated_session_state
+```
+
+### Wallet → query
+
+```
+Wallet: query_session_messages(session_id, description, session_state) → messages, updated_session_state
+```
+
+The wallet re-keys for every c2w message with `dh_public` to maintain the chain.
+
+---
+
+## Per-Message Encryption
+
+Each message is encrypted individually:
+
+```
+1. random salt (32 bytes) + random nonce (24 bytes)
+2. per_message_key = HKDF-SHA256(ikm=encryption_root, salt=random_salt, info="bee_connect.msg.key.v1")
+3. ciphertext = XChaCha20-Poly1305(key=per_message_key, nonce=random_nonce, plaintext=body, aad=envelope_metadata)
+```
+
+AAD (Associated Authenticated Data) includes: `v`, `session_id`, `dir`, `seq`, `type`, `ts` — protects against metadata substitution.
+
+### Per-Message DH Re-Key (Forward Secrecy)
+
+Before encrypting, the sender performs a DH re-key:
+
+```
+new_dh = X25519::generate()
+shared = X25519(new_dh.secret, peer_dh_public)
+ikm = old_encryption_root || shared (64 bytes)
+new_encryption_root = HKDF-SHA256(ikm, salt=session_id, info="bee_connect.rekey.v1")
+```
+
+The `new_encryption_root` replaces `encryption_root` in the per-message encryption step above.
+The `new_dh.public` is included in the envelope's `dh_public` field.
+
+This ensures:
+- **Forward secrecy**: compromising the current state reveals nothing about past messages
+- **Post-compromise security**: each message uses fresh DH material
+
+---
+
+## Cryptographic Primitives
+
+| Purpose | Primitive | Key Size |
+|---------|-----------|----------|
+| DH key exchange | X25519 | 32 bytes |
+| Key derivation | HKDF-SHA256 | — |
+| On-chain signing | Ed25519 (verify_strict) | 32 bytes |
+| Message encryption | XChaCha20-Poly1305 | 32 bytes |
+| Replay protection | seq = now_millis() | u64 |
+
+---
+
+## MITM Analysis
+
+| Attack | Result |
+|--------|--------|
+| Passive URL intercept | Sees `client_dh_public` — useless without `client_dh_secret` |
+| Substitute `client_dh_public` in URL | Client and Wallet derive different shared_secret → wallet_hello decryption fails → session not established (DoS, not silent MITM) |
+| Intercept wallet_hello | `dh_public` is public, body is encrypted → useless |
+| Message replay | Different `seq` (millisecond timestamp) → AAD mismatch → AEAD rejects |
+
+---
+
+## Security Considerations
+
+### Session Timeout
+
+The bee_connect protocol uses two distinct TTL values:
+
+**Connect session TTL** (`DEFAULT_CONNECT_TTL_SECS = 300`, 5 minutes)
+Controls how long the deeplink / QR code is valid for wallet scanning.
+Set via `ttl_secs` in `create_shared_key_session`. Callers SHOULD keep
+this short — it is only the window for the wallet to scan and respond,
+not the lifetime of the established session.
+Maximum recommended value: **86400 seconds (24 hours)**. Values above
+this are accepted by the API but strongly discouraged.
+
+**Active session TTL** (`DEFAULT_SESSION_TTL_SECS = 86400`, 24 hours)
+The lifetime of an established `ConnectSessionState` after the DH
+handshake completes. Both `rekey_outbound` and `rekey_inbound` reject
+operations on expired states.
+
+Every `ConnectSessionState` carries `created_at` and `expires_at` timestamps (UNIX epoch seconds).
+
+- `rekey_outbound` and `rekey_inbound` check `expires_at` before performing any crypto — expired sessions are rejected with an error.
+- Legacy states (deserialized from JSON without these fields) have `expires_at = 0`, which is treated as **non-expiring** for backward compatibility.
+- Callers can set a custom `expires_at` on the state after `create_initial_state` if a shorter or longer TTL is needed.
+
+**Wallet app requirements:**
+
+- On session accept: persist `created_at` and `expires_at` from the returned state.
+- Before any operation with session state: check `state.is_expired()`. If expired, show "Session expired" to user and destroy the profile.
+- On app foreground: check all stored session states, destroy expired ones.
+- Display remaining session time in UI so user knows when to reconnect.
+
+### State Persistence
+
+`ConnectSessionState` contains secret key material. Callers are responsible for:
+
+- Storing the JSON-serialized state in a secure location (OS keychain, encrypted storage)
+- Overwriting or deleting old state after every re-key operation
+- Never logging the state in plaintext
+
+### Forward Secrecy Guarantees
+
+After a re-key, old `encryption_root` and `my_dh_secret` are discarded. Even if the current state is compromised, past messages cannot be decrypted because the old DH secrets needed to reverse the HKDF chain are gone.
+
+Forward secrecy holds only if:
+1. The caller actually discards old state (does not keep backups of previous `ConnectSessionState`)
+2. The runtime zeroes memory on drop (`ConnectSessionState` secret fields use `Zeroizing` which handles this)
+
+### Limitations
+
+- `ConnectSessionState` secret fields (`encryption_root`, `my_dh_secret`, `signing_secret`) are wrapped in `Zeroizing` — their memory is zeroed on drop. The `zeroize` crate's `serde` feature provides transparent JSON serialization.
+- The protocol does not authenticate the initial DH exchange beyond decryption success. An active MITM at session creation causes a DoS (decryption failure), not silent interception.
+- **Signing keys are fixed for the session lifetime** (see [Signing Key Lifetime](#signing-key-lifetime) below).
+- The protocol uses symmetric authentication (AEAD). Both sides share the same keys, so non-repudiation (proving which side sent a specific message) is not provided.
+- **Wallet identity is not cryptographically pinned.** The `wallet_address` in `wallet_hello` is self-reported by the wallet and authenticated only by AEAD decryption success — see [Wallet Identity](#wallet-identity) below.
+
+### Signing Key Lifetime
+
+The Ed25519 signing keypair (`signing_public`, `signing_secret`) is derived once during the DH handshake (Phase 2/3) and remains constant for the entire session. Unlike `encryption_root` and `my_dh_secret`, which rotate on every message via the DH re-key mechanism, signing keys are **never rotated**.
+
+**Why this matters:**
+
+- If `signing_secret` is leaked (memory dump, insecure state persistence, etc.), an attacker can forge on-chain AuthProfile transactions (e.g. `set_mining_keys`, `client_disconnect`) for the remainder of the session.
+- Forward secrecy does **not** cover signing keys — compromising the current `ConnectSessionState` reveals the signing key for all past and future on-chain signatures within that session.
+
+**Why this is acceptable for the current design:**
+
+1. **Narrow scope.** Signing keys are only used to authorize AuthProfile context additions on-chain. They do not protect message confidentiality (that is handled by the AEAD encryption layer with per-message re-keying).
+2. **Session-scoped impact.** A compromised signing key cannot be used outside the session's AuthProfile contract. Destroying the profile (`destroy_connect_profile`) invalidates the key on-chain.
+3. **Rotation cost.** Rotating signing keys would require an on-chain transaction to update the AuthProfile's public key, adding latency and gas cost to every message — disproportionate for the threat model.
+
+**Mitigations for callers:**
+
+- Keep sessions short-lived. Do not reuse a `ConnectSessionState` across app restarts if avoidable.
+- Destroy the AuthProfile (`destroy_connect_profile`) as soon as the session is no longer needed, to invalidate the signing key on-chain.
+- Store `ConnectSessionState` in OS keychain or encrypted storage, not in plaintext files.
+
+### Wallet Identity
+
+The `wallet_address` field in the `wallet_hello` message body is **self-reported** by the wallet. The protocol does not perform independent on-chain verification that the wallet actually controls this address.
+
+**Why additional pinning is not needed:**
+
+1. **DH authentication is sufficient.** An attacker who does not possess the `client_dh_secret` (never transmitted — kept by the dApp) cannot compute the shared secret, and therefore cannot produce a `wallet_hello` that the client can decrypt. A successful decryption proves the wallet participated in the same DH exchange.
+
+2. **MITM requires URL interception AND replacement.** To impersonate a different wallet, an attacker would need to intercept the deeplink URL, replace `client_dh_public` with their own, and forward it to a target wallet. But then the real client holds `client_dh_secret` for the original keypair — the attacker's `wallet_hello` (encrypted with the attacker's shared secret) will fail decryption on the client. This is a **DoS** (session fails to establish), not a silent impersonation.
+
+3. **No secret exfiltration path.** Even if an attacker deploys their own AuthProfile and writes a `wallet_hello` with a fake `wallet_address`, the client will reject it because the DH shared secrets won't match.
+
+**Recommendation for wallet apps:**
+
+- After `wait_wallet_hello` succeeds, display the received `wallet_name` and `wallet_address` to the user for visual confirmation.
+- If the dApp expects a specific wallet, compare `wallet_address` from the decrypted `wallet_hello` against the expected value at the application layer.
+
+---
+
+## Diagram
+
+```
+Client links.gosh.sh Wallet
+ │ │ │
+ │ generate X25519 │ │
+ │ (client_dh_pub, _sec) │ │
+ │ │ │
+ │── URL: payload + pub ──────>│────> deeplink ────────>│
+ │ (NO SECRETS!) │ │
+ │ │ │ generate X25519
+ │ │ │ (wallet_dh_pub, _sec)
+ │ │ │
+ │ │ │ shared = DH(w_sec, c_pub)
+ │ │ │ signing, encryption = HKDF(shared)
+ │ │ │
+ │ │ │ deploy AuthProfile(signing.pub)
+ │ │ │ wallet_hello:
+ │ │ │ dh_public: wallet_dh_pub
+ │ │ │ body: encrypted(name, addr)
+ │ │ │
+ │<── poll AuthProfile ─────────────────────────────────│
+ │ │ │
+ │ extract wallet_dh_pub │ │
+ │ shared = DH(c_sec, w_pub) │ │
+ │ signing, encryption = HKDF │ │
+ │ decrypt body ✓ │ │
+ │ │ │
+ │ │ │
+ │ rekey_outbound: │ │
+ │ new DH, new root │ │
+ │── set_mining_keys ──────────────────────────────────>│
+ │ (encrypted with new_root, │ │ rekey_inbound:
+ │ dh_public: new_pub) │ │ DH(my_sec, new_pub) → new root
+ │ │ │
+ │ rekey_outbound again │ │
+ │── disconnect ───────────────────────────────────────>│
+ │ (encrypted with newer_root,│ │ rekey_inbound again
+ │ dh_public: newer_pub) │ │
+ │ │ │
+```
+
+---
+
+## API Reference
+
+### Client-side (bee_connect)
+
+| Method | Input | Output |
+|--------|-------|--------|
+| `create_shared_key_session` | `app_id`, `ttl_secs` | `session_id`, `client_dh_public`, `client_dh_secret`, `deep_link` |
+| `decode_connect_payload_b64url` | `payload_b64url` | `ConnectPayload` |
+| `get_profile_address` | `endpoints`, `description` | `profile_address` |
+| `is_session_profile_deployed` | `endpoints`, `description` | `bool` |
+| `wait_wallet_hello` | `session_id`, `description`, `client_dh_secret` | `wallet_name`, `wallet_address`, `session_state` |
+| `request_set_mining_keys` | `session_state`, `app_id`, `owner_public` | `message_id`, `updated_session_state` |
+| `wait_set_mining_keys_request` | `session_state` | `app_id`, `owner_public`, `updated_session_state` |
+| `request_sign_challenge` | `session_state`, `nonce` | `message_id`, `updated_session_state` |
+| `wait_challenge_response` | `session_state` | `nonce`, `signature`, `wallet_address`, `updated_session_state` |
+| `disconnect_session` | `session_state`, `reason` | `message_id`, `updated_session_state` |
+| `query_active_sessions_by_multifactor` | `multifactor_address`, `app_id?`, `before?` | `sessions[]`, `next_before` |
+
+### Wallet-side (bee_wallet)
+
+| Method | Input | Output |
+|--------|-------|--------|
+| `decode_connect_payload_b64url` | `payload_b64url` | `ConnectPayload` |
+| `accept_shared_key_connect` | `payload`, `client_dh_public`, `wallet_name`, `wallet_address` | `profile_address`, `session_state` |
+| `query_session_messages` | `session_id`, `description`, `session_state` | `messages[]`, `updated_session_state` |
+| `destroy_connect_profile` | `profile_address`, `multifactor_address`, `signer_keys` | — |
diff --git a/bee_connect/README.md b/bee_connect/README.md
new file mode 100644
index 0000000..fa97bf1
--- /dev/null
+++ b/bee_connect/README.md
@@ -0,0 +1,398 @@
+# bee_connect
+
+Client-side helpers for AckiNacki wallet connect flows over `ackinacki-kit` `authservice`.
+
+`bee_connect` is intended for the dApp/client side.
+Wallet-side acceptance/deploy/send-first-message flow lives in `bee_wallet` (`connect` module).
+
+## What It Does
+
+`bee_connect` helps the dApp:
+
+- create a connect session with ephemeral X25519 Diffie-Hellman key exchange
+- derive deterministic `AuthProfile` address from `description`
+- wait for the first wallet message (`wallet_hello`) and finalize DH key agreement
+- send encrypted protocol requests (`set_mining_keys`, `client_disconnect`)
+- query active sessions by multifactor wallet
+
+## Protocol: `shared_key` (bidirectional, DH-authenticated)
+
+Secure bidirectional channel over one `AuthProfile`. No secrets are transmitted
+in URLs or deeplinks — only the client's X25519 public key.
+
+### Handshake Flow
+
+```
+Client links.gosh.sh Wallet
+ │ │ │
+ │ generate X25519 keypair │ │
+ │ │ │
+ │── URL: payload + dh_pub ───>│────> deeplink ────────>│
+ │ (NO SECRETS IN URL!) │ │
+ │ │ │ generate X25519 keypair
+ │ │ │ shared_secret = DH(w_sec, c_pub)
+ │ │ │ signing, encryption = HKDF(shared)
+ │ │ │
+ │ │ │ deploy AuthProfile(signing.pub)
+ │ │ │ wallet_hello:
+ │ │ │ dh_public: wallet_dh_pub
+ │ │ │ body: encrypted(name, addr)
+ │ │ │
+ │<── poll AuthProfile ─────────────────────────────────│
+ │ shared_secret = DH(c_sec, w_pub) │
+ │ signing, encryption = HKDF(shared) │
+ │ decrypt body ✓ │
+ │ │ │
+ │ rekey_outbound: │ │
+ │ new DH, new root │ │
+ │── set_mining_keys ──────────────────────────────────>│
+ │ (encrypted with new_root, │ │ rekey_inbound:
+ │ dh_public: new_pub) │ │ DH(my_sec, new_pub) → new root
+ │ │ │
+ │── disconnect ───────────────────────────────────────>│
+ │ (re-keyed again) │ │ rekey_inbound again
+```
+
+### Step by Step
+
+1. **Client** calls `create_shared_key_session(app_id, ttl_secs)`
+ - Generates ephemeral X25519 keypair
+ - Returns `client_dh_public` (in URL) and `client_dh_secret` (caller stores)
+ - Deeplink URL: `?payload=&client_dh_public=`
+
+2. **Wallet** receives deeplink, calls `accept_shared_key_connect(payload, client_dh_public, ...)`
+ - Generates wallet X25519 keypair
+ - Computes `shared_secret = X25519(wallet_secret, client_dh_public)`
+ - Derives session keys (`signing_keys` + `encryption_root`) via HKDF-SHA256
+ - Returns `session_state: ConnectSessionState` (caller persists)
+ - Deploys AuthProfile with derived signing public key
+ - Sends `wallet_hello` with `dh_public` field (wallet's public key, cleartext)
+
+3. **Client** calls `wait_wallet_hello(session_id, description, client_dh_secret)`
+ - Polls AuthProfile for `wallet_hello`
+ - Extracts `dh_public` from envelope
+ - Computes same `shared_secret = X25519(client_secret, wallet_dh_public)`
+ - Derives identical session keys
+ - Decrypts and verifies wallet_hello body
+ - Returns `session_state: ConnectSessionState` for subsequent operations
+
+4. **Messaging** — each outbound message performs DH re-key (forward secrecy), encrypts with new root, includes `dh_public` in envelope. Caller persists `updated_session_state` after each send.
+
+### Key Derivation
+
+```
+shared_secret = X25519(my_secret, their_public)
+
+signing_seed = HKDF-SHA256(ikm=shared_secret, salt=session_id, info="bee_connect.signing.v1")
+encryption_root = HKDF-SHA256(ikm=shared_secret, salt=session_id, info="bee_connect.encryption.v1")
+
+signing_key = Ed25519::SigningKey(signing_seed)
+```
+
+### Re-Key (Forward Secrecy)
+
+Every outbound message performs a DH re-key before encryption:
+
+```
+new_dh = X25519::generate()
+shared = X25519(new_dh.secret, peer_dh_public)
+ikm = old_encryption_root || shared (64 bytes)
+new_root = HKDF-SHA256(ikm, salt=session_id, info="bee_connect.rekey.v1")
+```
+
+The `new_root` replaces `encryption_root` in the session state. The `new_dh.public` is included in the envelope's `dh_public` field. Old roots are discarded — forward secrecy.
+
+### All HKDF Info Labels
+
+| Purpose | Info | IKM |
+|---------|------|-----|
+| Signing key | `bee_connect.signing.v1` | DH shared secret (32 bytes) |
+| Encryption root | `bee_connect.encryption.v1` | DH shared secret (32 bytes) |
+| Per-message re-key | `bee_connect.rekey.v1` | old_root \|\| new_DH_shared (64 bytes) |
+| Per-message encryption key | `bee_connect.msg.key.v1` | current encryption_root |
+
+All use HKDF-SHA256 with `salt = session_id` (except per-message key which uses `salt = random_32_bytes`).
+
+### MITM Protection
+
+- **Passive intercept**: URL contains only `client_dh_public` — useless without secret
+- **Active MITM** (public key substitution): client and wallet derive different shared secrets → wallet_hello decryption fails → session not established (DoS, not silent interception)
+- **Low-order point attack**: DH shared secret is checked for all-zero bytes — rejected before key derivation
+
+## Message Envelope (`bee_connect.msg/1`)
+
+All messages are encrypted. Unencrypted (`alg: "none"`) messages are rejected.
+
+### Envelope shape
+
+```json
+{
+ "v": "bee_connect.msg/1",
+ "session_id": "base64url-random",
+ "dir": "w2c",
+ "seq": 1700000000000,
+ "type": "wallet_hello",
+ "ts": 1700000000,
+ "dh_public": "",
+ "enc": {
+ "alg": "xchacha20poly1305-hkdf-sha256",
+ "nonce": "",
+ "salt": ""
+ },
+ "body": ""
+}
+```
+
+Fields:
+- `v` — protocol version (`bee_connect.msg/1`)
+- `session_id` — session identifier
+- `dir` — direction: `w2c` (wallet→client) or `c2w` (client→wallet)
+- `seq` — monotonic sequence number (millisecond timestamp for replay protection)
+- `type` — message type: `wallet_hello`, `set_mining_keys`, `client_disconnect`
+- `ts` — timestamp (seconds)
+- `dh_public` — ephemeral X25519 public key for DH re-key; present in `wallet_hello` and all c2w messages
+- `enc` — encryption metadata
+- `body` — encrypted message body (base64url)
+
+### Per-Message Encryption
+
+```
+per_message_key = HKDF-SHA256(ikm=encryption_root, salt=random_salt, info="bee_connect.msg.key.v1")
+ciphertext = XChaCha20-Poly1305(key=per_message_key, nonce=random, plaintext=body, aad=envelope_metadata)
+```
+
+AAD includes: `v`, `session_id`, `dir`, `seq`, `type`, `ts`.
+
+## Deeplink Payload (`bee_connect.dl/1`)
+
+```json
+{
+ "v": "bee_connect.dl/1",
+ "session_id": "base64url-random",
+ "description": "bee_connect:v1:::",
+ "expires_at": 1730000600,
+ "app_id": "0x0000000000000000000000000000000000000000000000000000000000000001"
+}
+```
+
+Deeplink URL query parameters:
+- `payload=` — encoded ConnectPayload
+- `client_dh_public=` — client X25519 public key (64 hex chars)
+
+## Cryptographic Primitives
+
+| Purpose | Primitive | Key Size |
+|---------|-----------|----------|
+| DH key exchange | X25519 | 32 bytes |
+| Key derivation | HKDF-SHA256 | — |
+| On-chain signing | Ed25519 (verify_strict) | 32 bytes |
+| Message encryption | XChaCha20-Poly1305 | 32 bytes |
+| Replay protection | seq = now_millis() | u64 |
+
+## Rust API
+
+Core methods on `ConnectClient`:
+
+- `create_shared_key_session(app_id, ttl_secs)` → session + DH keypair
+- `decode_connect_payload_b64url(payload_b64url)` → parsed payload
+- `get_profile_address(endpoints, description)` → profile address
+- `is_session_profile_deployed(endpoints, description)` → bool
+- `wait_wallet_hello(endpoints, session_id, description, client_dh_secret, ...)` → wallet info + session_state
+- `request_set_mining_keys(endpoints, session_id, description, session_state, ...)` → message_id, updated_session_state
+- `wait_set_mining_keys_request(endpoints, session_id, description, session_state, ...)` → app_id, owner_public, updated_session_state
+- `disconnect_session(endpoints, session_id, description, session_state, ...)` → message_id, updated_session_state
+- `query_active_sessions_by_multifactor(endpoints, multifactor_address, ...)` → sessions
+
+DH helpers (`bee_connect::dh`):
+
+- `generate_dh_keypair()` → DhKeyPair (public_hex, secret_hex)
+- `compute_shared_secret(my_secret_hex, their_public_hex)` → shared_secret_hex
+- `derive_session_keys(shared_secret_hex, session_id)` → DhSessionKeys (signing + encryption)
+- `create_initial_state(session_keys, my_dh_secret, peer_dh_public)` → ConnectSessionState
+- `rekey_outbound(state, session_id)` → RekeyResult (new root, new dh_public, updated state)
+- `rekey_inbound(state, peer_new_dh_public, session_id)` → RekeyResult (new root, updated state)
+
+## Data Contracts
+
+### `ConnectSessionState`
+
+Persisted by both client and wallet after every operation. JSON-serializable.
+
+```rust
+pub struct ConnectSessionState {
+ pub encryption_root: Zeroizing, // hex, 32 bytes — rotated every message
+ pub my_dh_secret: Zeroizing, // hex, 32 bytes — rotated on outbound
+ pub peer_dh_public: String, // hex, 32 bytes — updated on inbound
+ pub signing_public: String, // hex, 32 bytes — fixed for session
+ pub signing_secret: Zeroizing, // hex, 32 bytes — fixed for session
+ pub created_at: u64, // UNIX epoch seconds
+ pub expires_at: u64, // UNIX epoch seconds (default: created_at + 24h)
+}
+```
+
+- Secret fields are `Zeroizing` — zeroed on drop.
+- `created_at` / `expires_at` — set at handshake, carried through re-keys.
+- `expires_at == 0` means legacy state (no expiration).
+- `rekey_outbound` / `rekey_inbound` reject expired sessions.
+- Default TTL: `DEFAULT_SESSION_TTL_SECS = 86400` (24 hours).
+
+### `ResultOfWaitWalletHello`
+
+Returned by `wait_wallet_hello` after successful DH finalization.
+
+```rust
+pub struct ResultOfWaitWalletHello {
+ pub profile_address: String, // on-chain AuthProfile address
+ pub event_id: String, // wallet_hello event ID
+ pub event_created_at: u64, // event timestamp (seconds)
+ pub wallet_name: String, // from decrypted body
+ pub wallet_address: String, // from decrypted body
+ pub raw_message_json: String, // full envelope JSON for audit
+ pub session_state: ConnectSessionState, // initial state for subsequent operations
+}
+```
+
+### `ResultOfRequestSetMiningKeys`
+
+```rust
+pub struct ResultOfRequestSetMiningKeys {
+ pub profile_address: String,
+ pub message_id: Option,
+ pub updated_session_state: ConnectSessionState,
+}
+```
+
+### `ResultOfWaitSetMiningKeysRequest`
+
+```rust
+pub struct ResultOfWaitSetMiningKeysRequest {
+ pub profile_address: String,
+ pub event_id: String,
+ pub event_created_at: u64,
+ pub app_id: String,
+ pub owner_public: String,
+ pub raw_message_json: String,
+ pub updated_session_state: Option,
+}
+```
+
+## Wallet-Side API (`bee_wallet`)
+
+The wallet implements the other side of the protocol:
+
+| Method | What it does |
+|--------|-------------|
+| `decode_connect_payload_b64url(payload)` | Parse deeplink payload |
+| `accept_connect_shared_key(params)` | DH handshake + deploy AuthProfile + send wallet_hello |
+| `query_connect_session_messages(params)` | Poll for c2w messages with automatic re-key |
+| `destroy_connect_profile(params)` | Destroy AuthProfile on-chain |
+
+See `bee_wallet` crate and `PROTOCOL.md` for full wallet-side documentation.
+
+## TypeScript / WASM
+
+Session state is passed as JSON string across the WASM boundary. Parse with:
+
+```typescript
+import type { TConnectSessionState } from "@teamgosh/bee-sdk";
+
+const state: TConnectSessionState = JSON.parse(sessionStateJson);
+
+// Check expiration in UI
+if (state.expires_at > 0 && Date.now() / 1000 >= state.expires_at) {
+ // Session expired — destroy profile and reconnect
+}
+
+// Pass back to WASM
+const json = JSON.stringify(state);
+```
+
+The `TConnectSessionState` type is auto-generated by `wasm-bindgen` and exported from both `bee_connect` and `bee_wallet` WASM packages.
+
+## Input Normalization
+
+Hex inputs are normalized before use:
+
+- **`app_id`**: Stripped of `0x`/`0X` prefix, left-padded to 64 chars, lowercased, prepended with `0x`.
+ Example: `"0x1"` → `"0x0000000000000000000000000000000000000000000000000000000000000001"`
+- **`owner_public`**: Stripped of `0x`/`0X` prefix, lowercased. Must be exactly 64 hex chars (32 bytes).
+ Example: `"0xAABB...CC"` → `"aabb...cc"`
+
+These normalizations are applied by both `bee_connect` and `bee_wallet` via shared functions in `bee_connect::message`.
+
+## Polling Limits
+
+Methods that poll on-chain state (`wait_wallet_hello`, `wait_set_mining_keys_request`, etc.) accept `max_attempts` as `u32`. Internally, `wait_account` (from `ackinacki-kit`) accepts `u8`, so values above **255** are clamped silently.
+
+## Minimal Usage
+
+```rust
+use bee_connect::{ConnectClient, ParamsOfCreateSharedKeySession, ParamsOfWaitWalletHello, ParamsOfRequestSetMiningKeys};
+
+let client = ConnectClient::new();
+
+// Phase 1: Create session (generates X25519 DH keypair)
+let session = client.create_shared_key_session(ParamsOfCreateSharedKeySession {
+ app_id: "0x1".to_string(),
+ ttl_secs: Some(300),
+})?;
+
+// session.deep_link — open in wallet app (contains only client_dh_public)
+// session.client_dh_secret — store for phase 3
+
+// Phase 3: Wait for wallet_hello + DH finalize
+let hello = client.wait_wallet_hello(ParamsOfWaitWalletHello {
+ endpoints: vec!["https://your-endpoint".to_string()],
+ session_id: session.session_id.clone(),
+ description: session.description.clone(),
+ client_dh_secret: session.client_dh_secret.clone(),
+ created_at_from: Some(session.created_at),
+ max_attempts: Some(120),
+ interval_ms: Some(1000),
+}).await?;
+
+// hello.session_state — persist this for all subsequent operations
+// Contains signing keys, encryption root, DH keys for forward secrecy
+
+// Phase 4: Send message (re-keys automatically for forward secrecy)
+let result = client.request_set_mining_keys(ParamsOfRequestSetMiningKeys {
+ endpoints: vec!["https://your-endpoint".to_string()],
+ session_id: session.session_id.clone(),
+ description: session.description.clone(),
+ session_state: hello.session_state, // ← pass current state
+ app_id: "0x1".to_string(),
+ owner_public: "your-mining-public-key-hex".to_string(),
+ max_attempts: Some(30),
+ interval_ms: Some(1000),
+}).await?;
+
+// MUST persist updated state — old state is no longer valid
+let session_state = result.updated_session_state;
+```
+
+## Security Properties
+
+- **Forward secrecy**: per-message DH re-key discards old roots — past messages are unrecoverable even if current state is compromised.
+- **Mutual authentication**: successful `wallet_hello` decryption proves both sides share the same DH secret.
+- **Replay protection**: `seq` (millisecond timestamp) is included in AEAD AAD.
+- **Metadata integrity**: `session_id`, `dir`, `type`, `ts` are AEAD-authenticated.
+- **Session timeout**: `ConnectSessionState` carries `expires_at` — operations reject expired sessions (default 24h TTL).
+- **Memory safety**: secret fields use `Zeroizing` — zeroed on drop.
+- **Low-order point rejection**: all-zero DH shared secrets are rejected.
+
+See `PROTOCOL.md` for full security analysis including signing key lifetime, wallet identity, and threat model.
+
+## Build
+
+Native:
+
+```bash
+cargo check -p bee-connect
+cargo test -p bee-connect
+```
+
+WASM:
+
+```bash
+cargo check -p bee-connect --target wasm32-unknown-unknown --no-default-features --features single-wasm
+```
diff --git a/bee_connect/src/client.rs b/bee_connect/src/client.rs
new file mode 100644
index 0000000..c4cdb1f
--- /dev/null
+++ b/bee_connect/src/client.rs
@@ -0,0 +1,2821 @@
+use std::collections::HashMap;
+use std::collections::HashSet;
+use std::sync::Arc;
+#[cfg(not(all(feature = "wasm", target_arch = "wasm32")))]
+use std::time::SystemTime;
+#[cfg(not(all(feature = "wasm", target_arch = "wasm32")))]
+use std::time::UNIX_EPOCH;
+
+use ackinacki_kit::contracts::account::AccountStatus;
+use ackinacki_kit::contracts::account::ParamsOfWaitAccount;
+use ackinacki_kit::contracts::authservice::profile::AuthProfile;
+use ackinacki_kit::contracts::authservice::profile::ParamsOfQueryProfileEvents;
+use ackinacki_kit::contracts::authservice::root::AuthServiceRoot;
+use ackinacki_kit::contracts::authservice::root::ParamsOfQueryProfilesByMultifactor;
+use ackinacki_kit::contracts::traits::AccountAccessor;
+use ackinacki_kit::contracts::traits::ContextAccessor;
+use ackinacki_kit::tvm_client::abi::Signer;
+use ackinacki_kit::tvm_client::crypto::KeyPair;
+use ackinacki_kit::tvm_client::processing::ResultOfSendMessage;
+use ackinacki_kit::tvm_client::ClientConfig;
+use ackinacki_kit::tvm_client::ClientContext;
+use base64::engine::general_purpose::URL_SAFE_NO_PAD;
+use base64::Engine;
+use serde::Deserialize;
+use serde::Serialize;
+
+use crate::message::connect_message_aad;
+use crate::message::decrypt_connect_body;
+use crate::message::encrypt_connect_body;
+use crate::message::normalize_owner_public_hex;
+use crate::message::normalize_uint256_hex;
+use crate::message::CONNECT_DEEPLINK_VERSION;
+use crate::message::CONNECT_MESSAGE_ENC_NONE;
+use crate::message::CONNECT_MESSAGE_ENC_XCHACHA20POLY1305_HKDF_SHA256;
+use crate::message::CONNECT_MESSAGE_TYPE_CHALLENGE_RESPONSE;
+use crate::message::CONNECT_MESSAGE_TYPE_CLIENT_DISCONNECT;
+use crate::message::CONNECT_MESSAGE_TYPE_SET_MINING_KEYS;
+use crate::message::CONNECT_MESSAGE_TYPE_SIGN_CHALLENGE;
+use crate::message::CONNECT_MESSAGE_TYPE_WALLET_HELLO;
+use crate::message::CONNECT_MESSAGE_VERSION;
+
+const CONNECT_DEEPLINK_RESOLVER_URL: &str = "https://links.gosh.sh";
+const CONNECT_DEEPLINK_PATH: &str = "/deeplinks/wallet/v1/connect";
+const DEFAULT_CONNECT_TTL_SECS: u64 = 300;
+const ACTIVE_SESSIONS_CHUNK_SIZE: u32 = 10;
+const CONNECT_DESCRIPTION_PREFIX: &str = "bee_connect:";
+const CONNECT_DESCRIPTION_VERSION: &str = "v1";
+
+#[derive(Debug, Clone, Default)]
+pub struct ConnectClient {
+ rate_limiter: Option,
+ /// Optional BM API token, threaded into every `ClientContext` this client
+ /// builds (see `root_with_endpoints`). `None` → anonymous requests, which
+ /// is the intended default for the wasm/browser path (token is optional
+ /// there). Native callers set it via [`ConnectClient::with_api_token`].
+ api_token: Option,
+}
+
+/// Parameters for creating a bidirectional `shared_key` connect session.
+///
+/// The client generates an ephemeral X25519 DH keypair. Only the public key
+/// is included in the deeplink URL. The wallet will generate its own DH keypair
+/// and both sides derive shared session keys via Diffie-Hellman key agreement.
+#[derive(Debug, Clone)]
+pub struct ParamsOfCreateSharedKeySession {
+ /// dApp identifier as `uint256` hex string (`0x...` or raw hex).
+ pub app_id: String,
+ /// Session TTL in seconds. Defaults to a short connect timeout.
+ pub ttl_secs: Option,
+ /// Optional challenge nonce (hex). When present, it is embedded in the
+ /// deeplink payload so the wallet can sign it and include the signature
+ /// in `wallet_hello`. This eliminates the separate `sign_challenge` /
+ /// `challenge_response` roundtrip.
+ pub nonce: Option,
+}
+
+/// Result of creating a `shared_key` connect session.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ResultOfCreateSharedKeySession {
+ /// Connect session identifier used in all protocol messages.
+ pub session_id: String,
+ /// Random profile description used to deterministically derive profile
+ /// address.
+ pub description: String,
+ /// Session creation timestamp (unix seconds).
+ pub created_at: u64,
+ /// Session expiration timestamp (unix seconds).
+ pub expires_at: u64,
+ /// Normalized `app_id` (`0x` + 64 lowercase hex chars).
+ pub app_id: String,
+ /// Payload JSON before base64url encoding.
+ pub payload_json: String,
+ /// Deeplink URL for opening the wallet app. Contains only
+ /// `client_dh_public` — no secrets.
+ pub deep_link: String,
+ /// Base64url-encoded payload for QR transfer / app bridge.
+ pub payload_b64url: String,
+ /// Client X25519 DH public key (hex). Transmitted in the deeplink URL.
+ pub client_dh_public: String,
+ /// Client X25519 DH secret key (hex). Caller MUST store this for DH
+ /// finalization in `wait_wallet_hello`. NEVER transmitted.
+ pub client_dh_secret: String,
+}
+
+/// Parameters for resolving a deterministic `AuthProfile` address by
+/// `description`.
+#[derive(Debug, Clone)]
+pub struct ParamsOfResolveProfileAddress {
+ /// TVM RPC endpoints used to create a client context.
+ pub endpoints: Vec,
+ /// Connect profile description (rendezvous id).
+ pub description: String,
+}
+
+/// Parameters for waiting for the first wallet message (`wallet_hello`).
+#[derive(Debug, Clone)]
+pub struct ParamsOfWaitWalletHello {
+ /// TVM RPC endpoints used to create a client context.
+ pub endpoints: Vec,
+ /// Session id that must match the received message envelope.
+ pub session_id: String,
+ /// Connect profile description used to resolve the profile address.
+ pub description: String,
+ /// Client X25519 DH secret key (hex) from `create_shared_key_session`.
+ /// Used to compute shared secret with wallet's DH public key from
+ /// wallet_hello.
+ pub client_dh_secret: String,
+ /// Optional lower bound for event timestamps (unix seconds).
+ pub created_at_from: Option,
+ /// Polling attempts for profile activation and event lookup.
+ pub max_attempts: Option,
+ /// Polling interval in milliseconds.
+ pub interval_ms: Option,
+}
+
+/// Parameters for sending a client-side disconnect request.
+#[derive(Debug, Clone)]
+pub struct ParamsOfDisconnectSession {
+ /// TVM RPC endpoints used to create a client context.
+ pub endpoints: Vec,
+ /// Session id that must be included in the message envelope.
+ pub session_id: String,
+ /// Connect profile description used to resolve the profile address.
+ pub description: String,
+ /// Current session state (signing keys, encryption root, DH keys).
+ pub session_state: crate::dh::ConnectSessionState,
+ /// Optional disconnect reason.
+ pub reason: Option,
+ /// Polling attempts for profile activation.
+ pub max_attempts: Option,
+ /// Polling interval in milliseconds.
+ pub interval_ms: Option,
+}
+
+/// Parameters for sending `set_mining_keys` request from client to wallet.
+#[derive(Debug, Clone)]
+pub struct ParamsOfRequestSetMiningKeys {
+ /// TVM RPC endpoints used to create a client context.
+ pub endpoints: Vec,
+ /// Session id that must be included in the message envelope.
+ pub session_id: String,
+ /// Connect profile description used to resolve the profile address.
+ pub description: String,
+ /// Current session state (signing keys, encryption root, DH keys).
+ pub session_state: crate::dh::ConnectSessionState,
+ /// Miner app id (`uint256` hex string).
+ pub app_id: String,
+ /// Mining owner public key (hex).
+ pub owner_public: String,
+ /// Polling attempts for profile activation.
+ pub max_attempts: Option,
+ /// Polling interval in milliseconds.
+ pub interval_ms: Option,
+}
+
+/// Parameters for querying active connect sessions by multifactor wallet.
+#[derive(Debug, Clone)]
+pub struct ParamsOfQueryActiveSessionsByMultifactor {
+ /// TVM RPC endpoints used to create a client context.
+ pub endpoints: Vec,
+ /// Multifactor wallet address.
+ pub multifactor_address: String,
+ /// Optional `app_id` filter (`uint256` hex string).
+ ///
+ /// If set, only sessions with successfully parsed matching `app_id` are
+ /// returned. Malformed descriptions (`app_id = None`) are skipped.
+ pub app_id: Option,
+ /// Optional lower bound for deploy event timestamps (unix seconds).
+ pub created_at_from: Option,
+ /// Reverse-pagination cursor for next chunk.
+ pub before: Option,
+}
+
+/// One active connect session resolved from `AuthProfileDeployed`.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ActiveConnectSession {
+ /// AuthProfile address.
+ pub profile_address: String,
+ /// Connect description (`bee_connect:v1:::...`).
+ pub description: String,
+ /// Parsed `app_id` from description (`bee_connect:v1::...`).
+ ///
+ /// `None` means description doesn't match current protocol format.
+ pub app_id: Option,
+ /// Parsed `session_id` from description (if parseable).
+ pub session_id: Option,
+ /// GraphQL event id of `AuthProfileDeployed`.
+ pub deployed_event_id: String,
+ /// Deploy event timestamp (unix seconds).
+ pub deployed_at: u64,
+}
+
+/// Result page for active connect sessions.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ResultOfQueryActiveSessionsByMultifactor {
+ /// Active sessions (at most 10 per call).
+ pub sessions: Vec,
+ /// Cursor for the next page request.
+ pub next_before: Option,
+ /// `true` means there are no more active connect sessions in older pages.
+ pub exhausted_active: bool,
+}
+
+/// Wallet metadata received in the first `wallet_hello` message.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct WalletHelloMetadata {
+ /// Human-readable wallet name.
+ pub wallet_name: String,
+ /// Wallet account address shown to the dApp.
+ pub wallet_address: String,
+}
+
+/// Result returned when the dApp receives the first `wallet_hello`.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ResultOfWaitWalletHello {
+ /// Resolved `AuthProfile` address.
+ pub profile_address: String,
+ /// GraphQL event id of the matched `ContextAdded` event.
+ pub event_id: String,
+ /// Event timestamp (unix seconds).
+ pub event_created_at: u64,
+ /// Wallet name from the `wallet_hello` body.
+ pub wallet_name: String,
+ /// Wallet address from the `wallet_hello` body.
+ pub wallet_address: String,
+ /// Raw JSON envelope received from the chain.
+ pub raw_message_json: String,
+ /// Initial session state after DH key exchange.
+ /// Contains signing keys, encryption root, and DH keys.
+ /// Caller MUST persist this for subsequent operations.
+ pub session_state: crate::dh::ConnectSessionState,
+ /// Inline challenge nonce (present when the wallet responded to a nonce in
+ /// the deeplink).
+ pub nonce: Option,
+ /// Inline challenge signature (present when the wallet signed the nonce).
+ pub signature: Option,
+ /// EPK public key used to sign the nonce (hex).
+ pub epk_public: Option,
+}
+
+/// Result of sending `client_disconnect`.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ResultOfDisconnectSession {
+ /// Resolved `AuthProfile` address.
+ pub profile_address: String,
+ /// Outbound message hash returned by TVM processing (if available).
+ pub message_id: Option,
+ /// Raw JSON envelope sent to the chain.
+ pub raw_message_json: String,
+ /// Updated session state after DH re-key. Caller MUST persist this.
+ pub updated_session_state: crate::dh::ConnectSessionState,
+}
+
+/// Result of sending `set_mining_keys`.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ResultOfRequestSetMiningKeys {
+ /// Resolved `AuthProfile` address.
+ pub profile_address: String,
+ /// Outbound message hash returned by TVM processing (if available).
+ pub message_id: Option,
+ /// Normalized `app_id`.
+ pub app_id: String,
+ /// Normalized mining owner public key (lowercase hex without `0x`).
+ pub owner_public: String,
+ /// Raw JSON envelope sent to the chain.
+ pub raw_message_json: String,
+ /// Updated session state after DH re-key. Caller MUST persist this.
+ pub updated_session_state: crate::dh::ConnectSessionState,
+}
+
+/// Parameters for waiting `set_mining_keys` request (wallet-side poll helper).
+#[derive(Debug, Clone)]
+pub struct ParamsOfWaitSetMiningKeysRequest {
+ /// TVM RPC endpoints used to create a client context.
+ pub endpoints: Vec,
+ /// Session id that must match the received message envelope.
+ pub session_id: String,
+ /// Connect profile description used to resolve the profile address.
+ pub description: String,
+ /// Session state for decrypting and re-keying incoming messages.
+ pub session_state: Option,
+ /// Optional lower bound for event timestamps (unix seconds).
+ pub created_at_from: Option,
+ /// Polling attempts for profile activation and event lookup.
+ pub max_attempts: Option,
+ /// Polling interval in milliseconds.
+ pub interval_ms: Option,
+}
+
+/// Result of waiting `set_mining_keys` request.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ResultOfWaitSetMiningKeysRequest {
+ /// Resolved `AuthProfile` address.
+ pub profile_address: String,
+ /// GraphQL event id of the matched `ContextAdded` event.
+ pub event_id: String,
+ /// Event timestamp (unix seconds).
+ pub event_created_at: u64,
+ /// Normalized `app_id`.
+ pub app_id: String,
+ /// Normalized mining owner public key.
+ pub owner_public: String,
+ /// Raw JSON envelope received from the chain.
+ pub raw_message_json: String,
+ /// Updated session state after re-keying. Caller SHOULD persist this.
+ /// `None` if no `session_state` was provided in params.
+ pub updated_session_state: Option,
+}
+
+/// Parameters for sending `sign_challenge` request from client to wallet.
+///
+/// The client (dApp backend) generates a random nonce and asks the wallet to
+/// sign it with the multifactor's EPK key. The resulting signature proves
+/// wallet ownership to the backend.
+#[derive(Debug, Clone)]
+pub struct ParamsOfRequestSignChallenge {
+ /// TVM RPC endpoints used to create a client context.
+ pub endpoints: Vec,
+ /// Session id that must be included in the message envelope.
+ pub session_id: String,
+ /// Connect profile description used to resolve the profile address.
+ pub description: String,
+ /// Current session state (signing keys, encryption root, DH keys).
+ pub session_state: crate::dh::ConnectSessionState,
+ /// Random nonce generated by the backend (hex string).
+ pub nonce: String,
+ /// Polling attempts for profile activation.
+ pub max_attempts: Option,
+ /// Polling interval in milliseconds.
+ pub interval_ms: Option,
+}
+
+/// Result of sending `sign_challenge`.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ResultOfRequestSignChallenge {
+ /// Resolved `AuthProfile` address.
+ pub profile_address: String,
+ /// Outbound message hash returned by TVM processing (if available).
+ pub message_id: Option,
+ /// Nonce that was sent (echoed back for convenience).
+ pub nonce: String,
+ /// Raw JSON envelope sent to the chain.
+ pub raw_message_json: String,
+ /// Updated session state after DH re-key. Caller MUST persist this.
+ pub updated_session_state: crate::dh::ConnectSessionState,
+ /// Timestamp (unix seconds) when the challenge was sent.
+ /// Use as `created_at_from` in `wait_challenge_response` for precise event
+ /// filtering.
+ pub sent_at: u64,
+}
+
+/// Parameters for waiting `challenge_response` from wallet (client-side poll).
+#[derive(Debug, Clone)]
+pub struct ParamsOfWaitChallengeResponse {
+ /// TVM RPC endpoints used to create a client context.
+ pub endpoints: Vec,
+ /// Session id that must match the received message envelope.
+ pub session_id: String,
+ /// Connect profile description used to resolve the profile address.
+ pub description: String,
+ /// Session state for decrypting and re-keying incoming messages.
+ pub session_state: Option,
+ /// Optional lower bound for event timestamps (unix seconds).
+ pub created_at_from: Option,
+ /// Polling attempts for profile activation and event lookup.
+ pub max_attempts: Option,
+ /// Polling interval in milliseconds.
+ pub interval_ms: Option,
+}
+
+/// Result of waiting `challenge_response`.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ResultOfWaitChallengeResponse {
+ /// Resolved `AuthProfile` address.
+ pub profile_address: String,
+ /// GraphQL event id of the matched `ContextAdded` event.
+ pub event_id: String,
+ /// Event timestamp (unix seconds).
+ pub event_created_at: u64,
+ /// The nonce that was signed.
+ pub nonce: String,
+ /// Ed25519 signature of the nonce (hex).
+ pub signature: String,
+ /// Wallet address that signed the challenge.
+ pub wallet_address: String,
+ /// EPK public key used to sign the nonce (hex).
+ /// `None` if the wallet uses an older protocol version without this field.
+ pub epk_public: Option,
+ /// Raw JSON envelope received from the chain.
+ pub raw_message_json: String,
+ /// Updated session state after re-keying. Caller SHOULD persist this.
+ /// `None` if no `session_state` was provided in params.
+ pub updated_session_state: Option,
+}
+
+/// Body of `sign_challenge` message (c2w).
+/// Sent by the dApp to ask the wallet to sign a nonce for backend auth.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct SignChallengeBody {
+ /// Random hex nonce generated by the backend.
+ pub nonce: String,
+}
+
+/// Body of `challenge_response` message (w2c).
+/// Sent by the wallet in response to `sign_challenge`.
+/// Backend verifies the signature to confirm wallet ownership.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ChallengeResponseBody {
+ /// Echoed nonce from the challenge.
+ pub nonce: String,
+ /// Ed25519 detached signature of the nonce (hex).
+ pub signature: String,
+ /// Multifactor address of the signing wallet.
+ pub wallet_address: String,
+ /// EPK public key used to sign the nonce (hex, 64 chars).
+ /// Backend uses this to verify the signature and then confirms
+ /// the key is registered in the multifactor contract via
+ /// `get_epk_expire_at(wallet_address, epk_public)`.
+ #[serde(default)]
+ pub epk_public: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ConnectPayload {
+ pub v: String,
+ pub session_id: String,
+ pub description: String,
+ pub expires_at: u64,
+ pub app_id: String,
+ /// Optional challenge nonce for inline wallet ownership verification.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub nonce: Option,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+struct ConnectMessageEnvelope {
+ v: String,
+ session_id: String,
+ dir: String,
+ seq: u64,
+ #[serde(rename = "type")]
+ msg_type: String,
+ #[serde(default)]
+ ts: Option,
+ #[serde(default)]
+ dh_public: Option,
+ #[serde(default)]
+ enc: Option,
+ #[serde(default)]
+ body: serde_json::Value,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+struct ConnectMessageEnc {
+ alg: String,
+ #[serde(default)]
+ nonce: Option,
+ #[serde(default)]
+ salt: Option,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+struct WalletHelloBody {
+ wallet_name: String,
+ wallet_address: String,
+ /// Signed nonce (inline challenge response).
+ #[serde(default)]
+ nonce: Option,
+ #[serde(default)]
+ signature: Option,
+ #[serde(default)]
+ epk_public: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct SetMiningKeysBody {
+ app_id: String,
+ owner_public: String,
+}
+
+impl ConnectClient {
+ /// Creates a new client-side helper for `bee_connect`.
+ pub fn new() -> Self {
+ Self { rate_limiter: None, api_token: None }
+ }
+
+ /// Creates a client with rate limiting (max requests per second).
+ pub fn with_rate_limit(max_rps: u32) -> Self {
+ Self { rate_limiter: Some(bee_infra::RateLimiter::new(max_rps)), api_token: None }
+ }
+
+ /// Sets the BM API token used for all requests this client makes.
+ /// Chainable with [`Self::new`] / [`Self::with_rate_limit`], e.g.
+ /// `ConnectClient::with_rate_limit(rps).with_api_token(token)`. Native
+ /// apps pass `Some(BM_API_TOKEN)`; the wasm/browser path leaves it unset.
+ pub fn with_api_token(mut self, api_token: Option) -> Self {
+ self.api_token = api_token;
+ self
+ }
+
+ async fn acquire(&self) {
+ bee_infra::maybe_acquire(self.rate_limiter.as_ref()).await;
+ }
+
+ /// Small health-check helper for integration smoke tests.
+ pub fn ping(&self) -> &'static str {
+ "bee_connect:stub"
+ }
+
+ /// Decodes and validates base64url connect payload (`payload` query
+ /// value).
+ pub fn decode_connect_payload_b64url(
+ &self,
+ payload_b64url: impl AsRef,
+ ) -> Result {
+ decode_connect_payload_b64url(payload_b64url)
+ }
+
+ /// Creates a `shared_key` session and generates an ephemeral X25519 DH
+ /// keypair.
+ ///
+ /// The returned `client_dh_public` is included in the deeplink URL.
+ /// The returned `client_dh_secret` MUST be stored by the caller and passed
+ /// to `wait_wallet_hello` for DH key agreement.
+ ///
+ /// **No secrets are transmitted in the URL.**
+ pub fn create_shared_key_session(
+ &self,
+ params: ParamsOfCreateSharedKeySession,
+ ) -> Result {
+ let common = self.create_session_common(params.app_id, params.ttl_secs, params.nonce)?;
+ let dh_keypair = crate::dh::generate_dh_keypair()?;
+ let deep_link = format!(
+ "{CONNECT_DEEPLINK_RESOLVER_URL}{CONNECT_DEEPLINK_PATH}?payload={}&client_dh_public={}",
+ common.payload_b64url, dh_keypair.public_hex
+ );
+
+ Ok(ResultOfCreateSharedKeySession {
+ session_id: common.session_id,
+ description: common.description,
+ created_at: common.created_at,
+ expires_at: common.expires_at,
+ app_id: common.app_id,
+ payload_json: common.payload_json,
+ deep_link,
+ payload_b64url: common.payload_b64url,
+ client_dh_public: dh_keypair.public_hex,
+ client_dh_secret: dh_keypair.secret_hex.to_string(),
+ })
+ }
+
+ /// Resolves deterministic `AuthProfile` address from a connect
+ /// `description`.
+ pub async fn get_profile_address(
+ &self,
+ params: ParamsOfResolveProfileAddress,
+ ) -> Result {
+ self.acquire().await;
+ let root = root_with_endpoints(params.endpoints, self.api_token.clone())?;
+ let result = root
+ .get_profile_address(
+ ackinacki_kit::contracts::authservice::root::ParamsOfGetProfileAddress {
+ description: params.description,
+ },
+ )
+ .await
+ .map_err(|e| {
+ crate::errors::AppError::from(e)
+ .with_context("AuthServiceRoot::get_profile_address")
+ })?;
+ Ok(result.profile)
+ }
+
+ /// Checks whether session profile resolved from `description` is currently
+ /// deployed on chain.
+ pub async fn is_session_profile_deployed(
+ &self,
+ params: ParamsOfResolveProfileAddress,
+ ) -> Result {
+ self.acquire().await;
+ let root = root_with_endpoints(params.endpoints, self.api_token.clone())?;
+ let profile_address = root
+ .get_profile_address(
+ ackinacki_kit::contracts::authservice::root::ParamsOfGetProfileAddress {
+ description: params.description,
+ },
+ )
+ .await
+ .map_err(|e| {
+ crate::errors::AppError::from(e)
+ .with_context("AuthServiceRoot::get_profile_address")
+ })?
+ .profile;
+
+ let profile = AuthProfile::new_default(root.context().clone(), &profile_address);
+ Ok(profile.is_deployed().await)
+ }
+
+ /// Waits until the wallet deploys the profile and sends the first
+ /// `wallet_hello`.
+ ///
+ /// Validates message envelope fields:
+ /// - `v = bee_connect.msg/1`
+ /// - `dir = w2c`
+ /// - `seq > 0`
+ /// - `type = wallet_hello`
+ /// - `dh_public` present (wallet X25519 public key)
+ /// - `session_id` matches the requested session
+ ///
+ /// Performs DH key agreement using `client_dh_secret` and wallet's
+ /// `dh_public` from the envelope, derives session signing and encryption
+ /// keys, and decrypts the `wallet_hello` body.
+ pub async fn wait_wallet_hello(
+ &self,
+ params: ParamsOfWaitWalletHello,
+ ) -> Result {
+ self.acquire().await;
+ let root = root_with_endpoints(params.endpoints, self.api_token.clone())?;
+ let profile_address = root
+ .get_profile_address(
+ ackinacki_kit::contracts::authservice::root::ParamsOfGetProfileAddress {
+ description: params.description.clone(),
+ },
+ )
+ .await
+ .map_err(|e| {
+ crate::errors::AppError::from(e)
+ .with_context("AuthServiceRoot::get_profile_address")
+ })?
+ .profile;
+
+ let profile = AuthProfile::new_default(root.context().clone(), &profile_address);
+ wait_profile_active(&profile, params.max_attempts, params.interval_ms).await?;
+
+ let created_at_from = params
+ .created_at_from
+ .unwrap_or_else(|| now_secs().unwrap_or_default().saturating_sub(300));
+
+ let client_dh_secret = params.client_dh_secret.clone();
+ let rl = self.rate_limiter.clone();
+ let found = bee_infra::poll_until(
+ || {
+ let profile = profile.clone();
+ let session_id = params.session_id.clone();
+ let client_dh_secret = client_dh_secret.clone();
+ let rl = rl.clone();
+ async move {
+ bee_infra::maybe_acquire(rl.as_ref()).await;
+ find_wallet_hello_event(
+ &profile,
+ &session_id,
+ &client_dh_secret,
+ created_at_from,
+ )
+ .await
+ .map_err(|e| e.with_context("Query wallet_hello"))
+ }
+ },
+ |found| found.is_some(),
+ params.max_attempts,
+ params.interval_ms,
+ )
+ .await?
+ .ok_or_else(|| "wait_wallet_hello: no wallet_hello event found".to_string())?;
+
+ Ok(ResultOfWaitWalletHello {
+ profile_address,
+ event_id: found.event_id,
+ event_created_at: found.event_created_at,
+ wallet_name: found.wallet_name,
+ wallet_address: found.wallet_address,
+ raw_message_json: found.raw_message_json,
+ session_state: found.session_state,
+ nonce: found.nonce,
+ signature: found.signature,
+ epk_public: found.epk_public,
+ })
+ }
+
+ /// Waits until client->wallet `set_mining_keys` request is written to
+ /// profile context.
+ pub async fn wait_set_mining_keys_request(
+ &self,
+ params: ParamsOfWaitSetMiningKeysRequest,
+ ) -> Result {
+ self.acquire().await;
+ let root = root_with_endpoints(params.endpoints, self.api_token.clone())?;
+ let profile_address = root
+ .get_profile_address(
+ ackinacki_kit::contracts::authservice::root::ParamsOfGetProfileAddress {
+ description: params.description.clone(),
+ },
+ )
+ .await
+ .map_err(|e| {
+ crate::errors::AppError::from(e)
+ .with_context("AuthServiceRoot::get_profile_address")
+ })?
+ .profile;
+
+ let profile = AuthProfile::new_default(root.context().clone(), &profile_address);
+ wait_profile_active(&profile, params.max_attempts, params.interval_ms).await?;
+
+ let created_at_from = params
+ .created_at_from
+ .unwrap_or_else(|| now_secs().unwrap_or_default().saturating_sub(300));
+
+ let rl = self.rate_limiter.clone();
+ let found = bee_infra::poll_until(
+ || {
+ let profile = profile.clone();
+ let session_id = params.session_id.clone();
+ let session_state = params.session_state.clone();
+ let rl = rl.clone();
+ async move {
+ bee_infra::maybe_acquire(rl.as_ref()).await;
+ find_set_mining_keys_event(
+ &profile,
+ &session_id,
+ session_state.as_ref(),
+ created_at_from,
+ )
+ .await
+ .map_err(|e| e.with_context("Query set_mining_keys"))
+ }
+ },
+ |found| found.is_some(),
+ params.max_attempts,
+ params.interval_ms,
+ )
+ .await?
+ .ok_or_else(|| {
+ "wait_set_mining_keys_request: no set_mining_keys event found".to_string()
+ })?;
+
+ Ok(ResultOfWaitSetMiningKeysRequest {
+ profile_address,
+ event_id: found.event_id,
+ event_created_at: found.event_created_at,
+ app_id: found.app_id,
+ owner_public: found.owner_public,
+ raw_message_json: found.raw_message_json,
+ updated_session_state: found.updated_session_state,
+ })
+ }
+
+ /// Sends `client_disconnect` (`dir = c2w`, `type = client_disconnect`) to
+ /// the connected profile using shared session owner keys.
+ pub async fn disconnect_session(
+ &self,
+ params: ParamsOfDisconnectSession,
+ ) -> Result {
+ self.acquire().await;
+ if params.session_id.trim().is_empty() {
+ return Err("disconnect_session session_id is empty".to_string().into());
+ }
+
+ let root = root_with_endpoints(params.endpoints, self.api_token.clone())?;
+ let profile_address = root
+ .get_profile_address(
+ ackinacki_kit::contracts::authservice::root::ParamsOfGetProfileAddress {
+ description: params.description,
+ },
+ )
+ .await
+ .map_err(|e| {
+ crate::errors::AppError::from(e)
+ .with_context("AuthServiceRoot::get_profile_address")
+ })?
+ .profile;
+
+ let profile = AuthProfile::new_default(root.context().clone(), &profile_address);
+ wait_profile_active(&profile, params.max_attempts, params.interval_ms).await?;
+
+ let body = params
+ .reason
+ .as_deref()
+ .map(str::trim)
+ .filter(|v| !v.is_empty())
+ .map(|reason| serde_json::json!({ "reason": reason }))
+ .unwrap_or_else(|| serde_json::json!({}));
+
+ // DH re-key for forward secrecy
+ let seq = params.session_state.next_outbound_seq().map_err(|e| {
+ crate::errors::AppError::from(e).with_context("disconnect_session next_outbound_seq")
+ })?;
+ let rekey = crate::dh::rekey_outbound(¶ms.session_state, ¶ms.session_id, seq)
+ .map_err(|e| {
+ crate::errors::AppError::from(e).with_context("disconnect_session rekey_outbound")
+ })?;
+
+ let signing_keys = KeyPair {
+ public: params.session_state.signing_public.clone(),
+ secret: params.session_state.signing_secret.to_string(),
+ };
+
+ let raw_message_json = encode_connect_message(
+ ¶ms.session_id,
+ "c2w",
+ rekey.outbound_seq,
+ CONNECT_MESSAGE_TYPE_CLIENT_DISCONNECT,
+ body,
+ Some(&rekey.message_encryption_root),
+ rekey.new_dh_public.as_deref(),
+ )?;
+ let send_result = profile
+ .add_context_text(&raw_message_json, Signer::Keys { keys: signing_keys })
+ .await
+ .map_err(|e| {
+ crate::errors::AppError::from(e)
+ .with_context("AuthProfile::add_context_text client_disconnect")
+ })?;
+ let send_result = ensure_tx_success(send_result, "client_disconnect add_context")?;
+
+ Ok(ResultOfDisconnectSession {
+ profile_address,
+ message_id: send_result.message_hash,
+ raw_message_json,
+ updated_session_state: rekey.updated_state,
+ })
+ }
+
+ /// Sends `set_mining_keys` (`dir = c2w`) to the connected profile.
+ ///
+ /// Wallet app can read this request and execute
+ /// `bee_wallet.set_mining_keys`.
+ pub async fn request_set_mining_keys(
+ &self,
+ params: ParamsOfRequestSetMiningKeys,
+ ) -> Result {
+ self.acquire().await;
+ if params.session_id.trim().is_empty() {
+ return Err("request_set_mining_keys session_id is empty".to_string().into());
+ }
+
+ let app_id = normalize_uint256_hex(¶ms.app_id)?;
+ let owner_public = normalize_owner_public_hex(¶ms.owner_public)?;
+
+ let root = root_with_endpoints(params.endpoints, self.api_token.clone())?;
+ let profile_address = root
+ .get_profile_address(
+ ackinacki_kit::contracts::authservice::root::ParamsOfGetProfileAddress {
+ description: params.description,
+ },
+ )
+ .await
+ .map_err(|e| {
+ crate::errors::AppError::from(e)
+ .with_context("AuthServiceRoot::get_profile_address")
+ })?
+ .profile;
+
+ let profile = AuthProfile::new_default(root.context().clone(), &profile_address);
+ wait_profile_active(&profile, params.max_attempts, params.interval_ms).await?;
+
+ let body = serde_json::to_value(SetMiningKeysBody {
+ app_id: app_id.clone(),
+ owner_public: owner_public.clone(),
+ })
+ .map_err(|e| {
+ crate::errors::AppError::from(e).with_context("Serialize set_mining_keys body")
+ })?;
+ // DH re-key for forward secrecy
+ let seq = params.session_state.next_outbound_seq().map_err(|e| {
+ crate::errors::AppError::from(e)
+ .with_context("request_set_mining_keys next_outbound_seq")
+ })?;
+ let rekey = crate::dh::rekey_outbound(¶ms.session_state, ¶ms.session_id, seq)
+ .map_err(|e| {
+ crate::errors::AppError::from(e)
+ .with_context("request_set_mining_keys rekey_outbound")
+ })?;
+
+ let signing_keys = KeyPair {
+ public: params.session_state.signing_public.clone(),
+ secret: params.session_state.signing_secret.to_string(),
+ };
+
+ let raw_message_json = encode_connect_message(
+ ¶ms.session_id,
+ "c2w",
+ rekey.outbound_seq,
+ CONNECT_MESSAGE_TYPE_SET_MINING_KEYS,
+ body,
+ Some(&rekey.message_encryption_root),
+ rekey.new_dh_public.as_deref(),
+ )?;
+ let send_result = profile
+ .add_context_text(&raw_message_json, Signer::Keys { keys: signing_keys })
+ .await
+ .map_err(|e| {
+ crate::errors::AppError::from(e)
+ .with_context("AuthProfile::add_context_text set_mining_keys")
+ })?;
+ let send_result = ensure_tx_success(send_result, "set_mining_keys add_context")?;
+
+ Ok(ResultOfRequestSetMiningKeys {
+ profile_address,
+ message_id: send_result.message_hash,
+ app_id,
+ owner_public,
+ raw_message_json,
+ updated_session_state: rekey.updated_state,
+ })
+ }
+
+ /// Sends `sign_challenge` (`dir = c2w`) to the connected wallet.
+ ///
+ /// The wallet should sign the nonce with its EPK keys and respond
+ /// with a `challenge_response` message. The backend can then verify
+ /// the signature to confirm wallet ownership.
+ pub async fn request_sign_challenge(
+ &self,
+ params: ParamsOfRequestSignChallenge,
+ ) -> Result {
+ self.acquire().await;
+ if params.session_id.trim().is_empty() {
+ return Err("request_sign_challenge session_id is empty".to_string().into());
+ }
+ if params.nonce.trim().is_empty() {
+ return Err("request_sign_challenge nonce is empty".to_string().into());
+ }
+
+ let root = root_with_endpoints(params.endpoints, self.api_token.clone())?;
+ let profile_address = root
+ .get_profile_address(
+ ackinacki_kit::contracts::authservice::root::ParamsOfGetProfileAddress {
+ description: params.description,
+ },
+ )
+ .await
+ .map_err(|e| {
+ crate::errors::AppError::from(e)
+ .with_context("AuthServiceRoot::get_profile_address")
+ })?
+ .profile;
+
+ let profile = AuthProfile::new_default(root.context().clone(), &profile_address);
+ wait_profile_active(&profile, params.max_attempts, params.interval_ms).await?;
+
+ let body = serde_json::to_value(SignChallengeBody { nonce: params.nonce.clone() })
+ .map_err(|e| {
+ crate::errors::AppError::from(e).with_context("Serialize sign_challenge body")
+ })?;
+
+ let seq = params.session_state.next_outbound_seq().map_err(|e| {
+ crate::errors::AppError::from(e)
+ .with_context("request_sign_challenge next_outbound_seq")
+ })?;
+ let rekey = crate::dh::rekey_outbound(¶ms.session_state, ¶ms.session_id, seq)
+ .map_err(|e| {
+ crate::errors::AppError::from(e)
+ .with_context("request_sign_challenge rekey_outbound")
+ })?;
+
+ let signing_keys = KeyPair {
+ public: params.session_state.signing_public.clone(),
+ secret: params.session_state.signing_secret.to_string(),
+ };
+
+ let raw_message_json = encode_connect_message(
+ ¶ms.session_id,
+ "c2w",
+ rekey.outbound_seq,
+ CONNECT_MESSAGE_TYPE_SIGN_CHALLENGE,
+ body,
+ Some(&rekey.message_encryption_root),
+ rekey.new_dh_public.as_deref(),
+ )?;
+ let send_result = profile
+ .add_context_text(&raw_message_json, Signer::Keys { keys: signing_keys })
+ .await
+ .map_err(|e| {
+ crate::errors::AppError::from(e)
+ .with_context("AuthProfile::add_context_text sign_challenge")
+ })?;
+ let send_result = ensure_tx_success(send_result, "sign_challenge add_context")?;
+
+ Ok(ResultOfRequestSignChallenge {
+ profile_address,
+ message_id: send_result.message_hash,
+ nonce: params.nonce,
+ raw_message_json,
+ updated_session_state: rekey.updated_state,
+ sent_at: now_secs()?,
+ })
+ }
+
+ /// Waits for `challenge_response` (`dir = w2c`) from the wallet.
+ ///
+ /// Polls the AuthProfile until a `challenge_response` message appears,
+ /// then returns the nonce, signature, and wallet address for backend
+ /// verification.
+ pub async fn wait_challenge_response(
+ &self,
+ params: ParamsOfWaitChallengeResponse,
+ ) -> Result {
+ self.acquire().await;
+ if params.session_id.trim().is_empty() {
+ return Err("wait_challenge_response session_id is empty".to_string().into());
+ }
+
+ let root = root_with_endpoints(params.endpoints, self.api_token.clone())?;
+ let profile_address = root
+ .get_profile_address(
+ ackinacki_kit::contracts::authservice::root::ParamsOfGetProfileAddress {
+ description: params.description.clone(),
+ },
+ )
+ .await
+ .map_err(|e| {
+ crate::errors::AppError::from(e)
+ .with_context("AuthServiceRoot::get_profile_address")
+ })?
+ .profile;
+
+ let profile = AuthProfile::new_default(root.context().clone(), &profile_address);
+ wait_profile_active(&profile, params.max_attempts, params.interval_ms).await?;
+
+ let created_at_from = params
+ .created_at_from
+ .unwrap_or_else(|| now_secs().unwrap_or_default().saturating_sub(300));
+
+ let rl = self.rate_limiter.clone();
+ let found = bee_infra::poll_until(
+ || {
+ let profile = profile.clone();
+ let session_id = params.session_id.clone();
+ let session_state = params.session_state.clone();
+ let rl = rl.clone();
+ async move {
+ bee_infra::maybe_acquire(rl.as_ref()).await;
+ find_challenge_response_event(
+ &profile,
+ &session_id,
+ session_state.as_ref(),
+ created_at_from,
+ )
+ .await
+ .map_err(|e| e.with_context("Query challenge_response"))
+ }
+ },
+ |found| found.is_some(),
+ params.max_attempts,
+ params.interval_ms,
+ )
+ .await?
+ .ok_or_else(|| "wait_challenge_response: no challenge_response event found".to_string())?;
+
+ Ok(ResultOfWaitChallengeResponse {
+ profile_address,
+ event_id: found.event_id,
+ event_created_at: found.event_created_at,
+ nonce: found.nonce,
+ signature: found.signature,
+ wallet_address: found.wallet_address,
+ epk_public: found.epk_public,
+ raw_message_json: found.raw_message_json,
+ updated_session_state: found.updated_session_state,
+ })
+ }
+
+ /// Queries active `bee_connect` sessions for a multifactor wallet.
+ ///
+ /// The method scans one root-event chunk (`10` records) and returns only
+ /// profiles that are currently deployed (`is_deployed == true`).
+ ///
+ /// Stop condition for active-session scan:
+ /// - when the scanned chunk contains no deployed connect profiles, or
+ /// - when there are no more root events.
+ pub async fn query_active_sessions_by_multifactor(
+ &self,
+ params: ParamsOfQueryActiveSessionsByMultifactor,
+ ) -> Result {
+ self.acquire().await;
+ let app_id_filter = normalize_optional_app_id(params.app_id)?;
+ let root = root_with_endpoints(params.endpoints, self.api_token.clone())?;
+ let mut sessions = Vec::new();
+ let mut seen_session_keys = HashSet::new();
+ let mut deployed_cache = HashMap::new();
+ let mut has_deployed_connect_profile = false;
+ let mut next_before = None;
+ let mut before = params.before.clone();
+ let mut cursor_stalled = false;
+
+ while sessions.len() < ACTIVE_SESSIONS_CHUNK_SIZE as usize {
+ let before_for_query = before.clone();
+ let query_result = root
+ .query_profiles_by_multifactor(ParamsOfQueryProfilesByMultifactor {
+ multifactor: params.multifactor_address.clone(),
+ created_at_from: params.created_at_from,
+ limit: Some(ACTIVE_SESSIONS_CHUNK_SIZE),
+ before: before_for_query.clone(),
+ })
+ .await
+ .map_err(|e| {
+ crate::errors::AppError::from(e)
+ .with_context("AuthServiceRoot::query_profiles_by_multifactor")
+ })?;
+ let records = &query_result.records;
+
+ if records.is_empty() {
+ break;
+ }
+
+ for record in records {
+ if !is_connect_description(&record.data.description) {
+ continue;
+ }
+
+ let parsed = parse_connect_description(&record.data.description);
+ let is_deployed = match deployed_cache.get(&record.data.profile).copied() {
+ Some(cached) => cached,
+ None => {
+ let profile =
+ AuthProfile::new_default(root.context().clone(), &record.data.profile);
+ let value = profile.is_deployed().await;
+ deployed_cache.insert(record.data.profile.clone(), value);
+ value
+ }
+ };
+ if !is_deployed {
+ continue;
+ }
+
+ has_deployed_connect_profile = true;
+ if let Some(filter) = app_id_filter.as_deref() {
+ if parsed.app_id.as_deref() != Some(filter) {
+ continue;
+ }
+ }
+
+ let session = ActiveConnectSession {
+ profile_address: record.data.profile.clone(),
+ description: record.data.description.clone(),
+ app_id: parsed.app_id,
+ session_id: parsed.session_id,
+ deployed_event_id: record.event.id.clone(),
+ deployed_at: record.event.created_at,
+ };
+ push_unique_active_session(&mut sessions, &mut seen_session_keys, session);
+
+ if sessions.len() >= ACTIVE_SESSIONS_CHUNK_SIZE as usize {
+ break;
+ }
+ }
+
+ let Some(cursor) = query_result.oldest_cursor else {
+ break;
+ };
+ if before_for_query.as_deref() == Some(cursor.as_str()) {
+ cursor_stalled = true;
+ next_before = None;
+ break;
+ }
+
+ next_before = Some(cursor.clone());
+ before = Some(cursor);
+
+ if records.len() < ACTIVE_SESSIONS_CHUNK_SIZE as usize {
+ break;
+ }
+ }
+
+ Ok(ResultOfQueryActiveSessionsByMultifactor {
+ sessions,
+ next_before,
+ exhausted_active: cursor_stalled || !has_deployed_connect_profile,
+ })
+ }
+}
+
+/// Decodes and validates base64url connect payload (`payload` query value).
+pub fn decode_connect_payload_b64url(
+ payload_b64url: impl AsRef,
+) -> Result {
+ let payload_b64url = payload_b64url.as_ref().trim();
+ if payload_b64url.is_empty() {
+ return Err("connect payload is empty".to_string().into());
+ }
+
+ let payload_bytes = URL_SAFE_NO_PAD.decode(payload_b64url).map_err(|e| {
+ crate::errors::AppError::from(e.to_string())
+ .with_context("Decode connect payload base64url")
+ })?;
+ let payload_json = String::from_utf8(payload_bytes).map_err(|e| {
+ crate::errors::AppError::from(e.to_string()).with_context("Decode connect payload utf8")
+ })?;
+ let mut payload: ConnectPayload = serde_json::from_str(&payload_json).map_err(|e| {
+ crate::errors::AppError::from(e).with_context("Deserialize connect payload")
+ })?;
+
+ validate_connect_payload_shape(&payload)?;
+ payload.app_id = normalize_uint256_hex(&payload.app_id)?;
+
+ let parsed_description = parse_connect_description(&payload.description);
+ if parsed_description.app_id.as_deref() != Some(payload.app_id.as_str()) {
+ return Err("connect payload description app_id mismatch".to_string().into());
+ }
+ if parsed_description.session_id.as_deref() != Some(payload.session_id.as_str()) {
+ return Err("connect payload description session_id mismatch".to_string().into());
+ }
+
+ Ok(payload)
+}
+
+struct SessionCommon {
+ session_id: String,
+ description: String,
+ created_at: u64,
+ expires_at: u64,
+ app_id: String,
+ payload_json: String,
+ payload_b64url: String,
+}
+
+impl ConnectClient {
+ fn create_session_common(
+ &self,
+ app_id: String,
+ ttl_secs: Option,
+ nonce: Option,
+ ) -> Result {
+ let now_secs = now_secs()?;
+ let ttl_secs = ttl_secs.unwrap_or(DEFAULT_CONNECT_TTL_SECS).max(1);
+ let expires_at = now_secs.saturating_add(ttl_secs);
+ let app_id = normalize_uint256_hex(&app_id)?;
+ let session_id = random_token_b64url(16)?;
+ let description = format!(
+ "{CONNECT_DESCRIPTION_PREFIX}{CONNECT_DESCRIPTION_VERSION}:{app_id}:{session_id}:{}",
+ random_token_b64url(16)?,
+ );
+
+ let payload = ConnectPayload {
+ v: CONNECT_DEEPLINK_VERSION.to_string(),
+ session_id: session_id.clone(),
+ description: description.clone(),
+ expires_at,
+ app_id: app_id.clone(),
+ nonce,
+ };
+ let payload_json = serde_json::to_string(&payload).map_err(|e| {
+ crate::errors::AppError::from(e).with_context("Serialize connect payload")
+ })?;
+ let payload_b64url = URL_SAFE_NO_PAD.encode(payload_json.as_bytes());
+
+ Ok(SessionCommon {
+ session_id,
+ description,
+ created_at: now_secs,
+ expires_at,
+ app_id,
+ payload_json,
+ payload_b64url,
+ })
+ }
+}
+
+#[derive(Debug, Clone)]
+struct WalletHelloFound {
+ event_id: String,
+ event_created_at: u64,
+ wallet_name: String,
+ wallet_address: String,
+ raw_message_json: String,
+ session_state: crate::dh::ConnectSessionState,
+ nonce: Option,
+ signature: Option,
+ epk_public: Option,
+}
+
+#[derive(Debug, Clone)]
+struct SetMiningKeysFound {
+ event_id: String,
+ event_created_at: u64,
+ app_id: String,
+ owner_public: String,
+ raw_message_json: String,
+ updated_session_state: Option,
+}
+
+#[derive(Debug, Clone)]
+struct ChallengeResponseFound {
+ event_id: String,
+ event_created_at: u64,
+ nonce: String,
+ signature: String,
+ wallet_address: String,
+ epk_public: Option,
+ raw_message_json: String,
+ updated_session_state: Option,
+}
+
+async fn find_wallet_hello_event(
+ profile: &AuthProfile,
+ session_id: &str,
+ client_dh_secret: &str,
+ created_at_from: u64,
+) -> Result