Compare commits

...

12 Commits

Author SHA1 Message Date
pierre 649e763732 fix constant declaration duplication 2023-07-21 14:49:41 +02:00
pierre 03ef5254cc switch to dentenvy 2023-07-21 10:47:37 +02:00
pierre e2f8611876 add env vars during ci build 2023-07-20 18:38:01 +02:00
pierre 57d6f1dcfa wip 2023-07-20 18:22:18 +02:00
pierre b79f774c48 remove hardcoded dsn urls
pass them as env vars
2023-07-20 18:19:44 +02:00
Pierre Dommerc 415fe4605c feat(nc-desktop): add sentry to backend (#3652) 2023-07-20 13:17:49 +02:00
Pierre Dommerc 70e6539298 refactor(nc-desktop): add privacy level user settings (#3664) 2023-07-19 15:56:00 +02:00
pierre fe76ba68a0 ci: fix connect-desktop-ci workflow 2023-07-19 15:41:00 +02:00
pierre 863580a6f2 ci: fix workflow for strapi data publish 2023-07-18 19:23:31 +02:00
pierre 4c558db08e add alephium to supported wallets 2023-07-18 17:08:20 +02:00
Mark Sinclair 9d5b582908 SDK - fix unsubscribe function for events (#3659)
Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
2023-07-18 14:12:00 +01:00
Fouad c12b20f1d6 Feature/sdk browser extension examples (#3673)
* New TypeScript SDK example: Google Chrome

Example of a simple manifest v3 extension to load a Nym client within  a
popup view.

* Firefox Extension example for Nym TypeScript SDK

* Fix typo

* Add basic install script

* fix up html files

* remove unnecessary background page

* bundle extensions with webpack

* use icons from shared assets

* ignore sdk index file

* import sdk lib

* create new build script for non-inline workers

* import workers and build as separate files using webpack

* update html titles

* create react app node tester example

* add readme file

* update readme for FF extension

* code formatting

---------

Co-authored-by: Nadim Kobeissi <nadim@symbolic.software>
Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
2023-07-18 14:09:40 +01:00
81 changed files with 2784 additions and 478 deletions
+6 -1
View File
@@ -23,7 +23,12 @@ jobs:
node-version: 18
- name: Install Yarn
run: npm install -g yarn
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Install project dependencies
run: cd ../.. && yarn --network-timeout 100000
@@ -19,7 +19,7 @@ jobs:
outputs:
release_id: ${{ steps.create-release.outputs.id }}
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].published_at }}
release_date: ${{ fromJSON(steps.create-release.outputs.assets[0].published_at) }}
version: ${{ steps.release-info.outputs.version }}
filename: ${{ steps.release-info.outputs.filename }}
file_hash: ${{ steps.release-info.outputs.file_hash }}
@@ -78,6 +78,8 @@ jobs:
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
SENTRY_DSN_RUST: ${{ secrets.SENTRY_DSN_RUST }}
SENTRY_DSN_JS: ${{ secrets.SENTRY_DSN_JS }}
run: yarn && yarn build
- name: Upload Artifact
@@ -19,7 +19,7 @@ jobs:
outputs:
release_id: ${{ steps.create-release.outputs.id }}
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].published_at }}
release_date: ${{ fromJSON(steps.create-release.outputs.assets[0].published_at) }}
version: ${{ steps.release-info.outputs.version }}
filename: ${{ steps.release-info.outputs.filename }}
file_hash: ${{ steps.release-info.outputs.file_hash }}
@@ -61,6 +61,8 @@ jobs:
env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
SENTRY_DSN_RUST: ${{ secrets.SENTRY_DSN_RUST }}
SENTRY_DSN_JS: ${{ secrets.SENTRY_DSN_JS }}
- name: Upload Artifact
uses: actions/upload-artifact@v3
@@ -19,7 +19,7 @@ jobs:
outputs:
release_id: ${{ steps.create-release.outputs.id }}
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].published_at }}
release_date: ${{ fromJSON(steps.create-release.outputs.assets[0].published_at) }}
version: ${{ steps.release-info.outputs.version }}
filename: ${{ steps.release-info.outputs.filename }}
file_hash: ${{ steps.release-info.outputs.file_hash }}
@@ -79,6 +79,8 @@ jobs:
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
SENTRY_DSN_RUST: ${{ secrets.SENTRY_DSN_RUST }}
SENTRY_DSN_JS: ${{ secrets.SENTRY_DSN_JS }}
run: yarn build
- name: Upload Artifact
@@ -1,104 +0,0 @@
name: Publish Nym Connect - desktop (MacOS)
on:
workflow_dispatch:
inputs:
release_tag:
required: true
description: Release tag (nym-connect-s-v*)
type: string
default: 'nym-connect-s-v1.0.0'
push:
tags:
- nym-connect-s-v*
defaults:
run:
working-directory: nym-connect/desktop
jobs:
publish-tauri:
if: ${{ (startsWith(github.ref, 'refs/tags/nym-connect-s-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
strategy:
fail-fast: false
matrix:
platform: [macos-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v2
- name: Node
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Install the Apple developer certificate for code signing
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
# create variables
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# import certificate and provisioning profile from secrets
echo -n "$APPLE_CERTIFICATE" | base64 --decode --output $CERTIFICATE_PATH
# create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# import certificate to keychain
security import $CERTIFICATE_PATH -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
- name: Create env file
uses: timheuer/base64-to-file@v1.2
with:
fileName: '.env'
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
- name: Prepare for build
env:
RELEASE_TAG: ${{ github.ref_name || inputs.release_tag }}
run: ./scripts/pre_medium_build.sh
- name: Install app dependencies and build it
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_IDENTITY_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
NYM_CONNECT_ENABLE_MEDIUM: 1
run: yarn && yarn build
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: nym-connect-s_1.0.0_x64.dmg
path: nym-connect/desktop/target/release/bundle/dmg/nym-connect-s_1*_x64.dmg
retention-days: 30
- name: Clean up keychain
if: ${{ always() }}
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
- id: create-release
name: Upload to release based on tag name
uses: softprops/action-gh-release@v1
if: github.event_name == 'release'
with:
files: |
nym-connect/desktop/target/release/bundle/dmg/*.dmg
nym-connect/desktop/target/release/bundle/macos/*.app.tar.gz*
@@ -1,85 +0,0 @@
name: Publish NymConnect S - desktop (Ubuntu)
on:
workflow_dispatch:
inputs:
release_tag:
required: true
description: Release tag (nym-connect-s-v*)
type: string
default: 'nym-connect-s-v1.0.0'
push:
tags:
- nym-connect-s-v*
defaults:
run:
working-directory: nym-connect/desktop
jobs:
publish-tauri:
if: ${{ (startsWith(github.ref, 'refs/tags/nym-connect-s-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
strategy:
fail-fast: false
matrix:
platform: [custom-runner-linux]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v2
- name: Tauri dependencies
run: >
sudo apt-get update &&
sudo apt-get install -y webkit2gtk-4.0 libayatana-appindicator3-dev
continue-on-error: true
- name: Node
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Install project dependencies
shell: bash
run: cd .. && yarn --network-timeout 100000
- name: Install app dependencies
run: yarn
- name: Create env file
uses: timheuer/base64-to-file@v1.2
with:
fileName: '.env'
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
- name: Prepare for build
env:
RELEASE_TAG: ${{ github.ref_name || inputs.release_tag }}
run: ./scripts/pre_medium_build.sh
- name: Build app
run: yarn build
env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
NYM_CONNECT_ENABLE_MEDIUM: 1
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: nym-connect-s.AppImage.tar.gz
path: nym-connect/desktop/target/release/bundle/appimage/nym-connect-s_1*_amd64.AppImage
retention-days: 30
- id: create-release
name: Upload to release based on tag name
uses: softprops/action-gh-release@v1
if: github.event_name == 'release'
with:
files: |
nym-connect/desktop/target/release/bundle/appimage/*.AppImage
nym-connect/desktop/target/release/bundle/appimage/*.AppImage.tar.gz*
@@ -1,104 +0,0 @@
name: Publish Nym Connect - desktop (Windows 10)
on:
workflow_dispatch:
inputs:
release_tag:
required: true
description: Release tag (nym-connect-s-v*)
type: string
default: 'nym-connect-s-v1.0.0'
push:
tags:
- nym-connect-s-v*
defaults:
run:
working-directory: nym-connect/desktop
jobs:
publish-tauri:
if: ${{ (startsWith(github.ref, 'refs/tags/nym-connect-s-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
strategy:
fail-fast: false
matrix:
platform: [windows10]
runs-on: ${{ matrix.platform }}
steps:
- name: Clean up first
continue-on-error: true
working-directory: .
run: |
cd ..
del /s /q /A:H nym
rmdir /s /q nym
- uses: actions/checkout@v3
- name: Import signing certificate
env:
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
run: |
New-Item -ItemType directory -Path certificate
Set-Content -Path certificate/tempCert.txt -Value $env:WINDOWS_CERTIFICATE
certutil -decode certificate/tempCert.txt certificate/certificate.pfx
Remove-Item -path certificate -include tempCert.txt
Import-PfxCertificate -FilePath certificate/certificate.pfx -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
- name: Node
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Create env file
uses: timheuer/base64-to-file@v1.2
with:
fileName: '.env'
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
- name: Install project dependencies
shell: bash
run: cd .. && yarn --network-timeout 100000
- name: Install app dependencies
shell: bash
run: yarn --network-timeout 100000
- name: Prepare for build
env:
RELEASE_TAG: ${{ github.ref_name || inputs.release_tag }}
run: ./scripts/pre_medium_build.sh
- name: Build and sign it
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ENABLE_CODE_SIGNING: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
NYM_CONNECT_ENABLE_MEDIUM: 1
run: yarn build
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: nym-connect-s_1.0.0_x64_en-US.msi
path: nym-connect/desktop/target/release/bundle/msi/nym-connect-s_1*_x64_en-US.msi
retention-days: 30
- id: create-release
name: Upload to release based on tag name
uses: softprops/action-gh-release@v1
if: github.event_name == 'release'
with:
files: |
nym-connect/desktop/target/release/bundle/msi/*.msi
nym-connect/desktop/target/release/bundle/msi/*.msi.zip*
+302 -16
View File
@@ -8,6 +8,15 @@ version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]]
name = "addr2line"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
@@ -88,6 +97,9 @@ name = "anyhow"
version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
dependencies = [
"backtrace",
]
[[package]]
name = "arrayref"
@@ -193,6 +205,21 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "backtrace"
version = "0.3.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide 0.7.1",
"object",
"rustc-demangle",
]
[[package]]
name = "base16ct"
version = "0.1.1"
@@ -1238,6 +1265,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "debugid"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
dependencies = [
"serde",
"uuid 1.3.0",
]
[[package]]
name = "der"
version = "0.6.1"
@@ -1377,9 +1414,9 @@ checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
[[package]]
name = "dotenvy"
version = "0.15.6"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "dtoa"
@@ -1532,6 +1569,12 @@ dependencies = [
"termcolor",
]
[[package]]
name = "equivalent"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
[[package]]
name = "errno"
version = "0.2.8"
@@ -1640,6 +1683,18 @@ dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "findshlibs"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64"
dependencies = [
"cc",
"lazy_static",
"libc",
"winapi",
]
[[package]]
name = "fix-path-env"
version = "0.1.0"
@@ -1656,7 +1711,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
dependencies = [
"crc32fast",
"miniz_oxide",
"miniz_oxide 0.6.2",
]
[[package]]
@@ -1985,6 +2040,12 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "gimli"
version = "0.27.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
[[package]]
name = "gio"
version = "0.15.12"
@@ -2217,7 +2278,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap",
"indexmap 1.9.2",
"slab",
"tokio",
"tokio-util",
@@ -2256,6 +2317,12 @@ dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
[[package]]
name = "hashlink"
version = "0.7.0"
@@ -2391,6 +2458,17 @@ dependencies = [
"digest 0.10.6",
]
[[package]]
name = "hostname"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
dependencies = [
"libc",
"match_cfg",
"winapi",
]
[[package]]
name = "html5ever"
version = "0.25.2"
@@ -2618,6 +2696,16 @@ dependencies = [
"serde",
]
[[package]]
name = "indexmap"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
"equivalent",
"hashbrown 0.14.0",
]
[[package]]
name = "infer"
version = "0.7.0"
@@ -3025,6 +3113,12 @@ dependencies = [
"tendril",
]
[[package]]
name = "match_cfg"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
[[package]]
name = "matchers"
version = "0.1.0"
@@ -3082,6 +3176,15 @@ dependencies = [
"adler",
]
[[package]]
name = "miniz_oxide"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
dependencies = [
"adler",
]
[[package]]
name = "mio"
version = "0.8.6"
@@ -3429,6 +3532,7 @@ dependencies = [
"anyhow",
"bip39",
"dirs 4.0.0",
"dotenvy",
"eyre",
"fern",
"fix-path-env",
@@ -3449,6 +3553,8 @@ dependencies = [
"rand 0.8.5",
"reqwest",
"rust-embed",
"sentry",
"sentry-log",
"serde",
"serde_json",
"serde_repr",
@@ -3462,6 +3568,7 @@ dependencies = [
"thiserror",
"time",
"tokio",
"toml 0.7.4",
"ts-rs",
"url",
"yaml-rust",
@@ -4076,6 +4183,15 @@ dependencies = [
"objc",
]
[[package]]
name = "object"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.17.1"
@@ -4165,6 +4281,17 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "os_info"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e"
dependencies = [
"log",
"serde",
"winapi",
]
[[package]]
name = "os_str_bytes"
version = "6.5.0"
@@ -4530,7 +4657,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590"
dependencies = [
"base64 0.21.2",
"indexmap",
"indexmap 1.9.2",
"line-wrap",
"quick-xml 0.28.1",
"serde",
@@ -4546,7 +4673,7 @@ dependencies = [
"bitflags 1.3.2",
"crc32fast",
"flate2",
"miniz_oxide",
"miniz_oxide 0.6.2",
]
[[package]]
@@ -5017,6 +5144,12 @@ dependencies = [
"walkdir",
]
[[package]]
name = "rustc-demangle"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustc_version"
version = "0.4.0"
@@ -5129,7 +5262,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f"
dependencies = [
"dyn-clone",
"indexmap",
"indexmap 1.9.2",
"schemars_derive",
"serde",
"serde_json",
@@ -5263,6 +5396,136 @@ dependencies = [
"pest",
]
[[package]]
name = "sentry"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01b0ad16faa5d12372f914ed40d00bda21a6d1bdcc99264c5e5e1c9495cf3654"
dependencies = [
"httpdate",
"native-tls",
"reqwest",
"sentry-anyhow",
"sentry-backtrace",
"sentry-contexts",
"sentry-core",
"sentry-debug-images",
"sentry-panic",
"sentry-tracing",
"tokio",
"ureq",
]
[[package]]
name = "sentry-anyhow"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f3a571f02f9982af445af829c4837fe4857568a431bd2bed9f7cf88de4a6c44"
dependencies = [
"anyhow",
"sentry-backtrace",
"sentry-core",
]
[[package]]
name = "sentry-backtrace"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11f2ee8f147bb5f22ac59b5c35754a759b9a6f6722402e2a14750b2a63fc59bd"
dependencies = [
"backtrace",
"once_cell",
"regex",
"sentry-core",
]
[[package]]
name = "sentry-contexts"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcd133362c745151eeba0ac61e3ba8350f034e9fe7509877d08059fe1d7720c6"
dependencies = [
"hostname",
"libc",
"os_info",
"rustc_version",
"sentry-core",
"uname",
]
[[package]]
name = "sentry-core"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7163491708804a74446642ff2c80b3acd668d4b9e9f497f85621f3d250fd012b"
dependencies = [
"once_cell",
"rand 0.8.5",
"sentry-types",
"serde",
"serde_json",
]
[[package]]
name = "sentry-debug-images"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a5003d7ff08aa3b2b76994080b183e8cfa06c083e280737c9cee02ca1c70f5e"
dependencies = [
"findshlibs",
"once_cell",
"sentry-core",
]
[[package]]
name = "sentry-log"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2558fc4a85326e6063711b45ce82ed6b18cdacd0732580c1567da914ac1df33e"
dependencies = [
"log",
"sentry-core",
]
[[package]]
name = "sentry-panic"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4dfe8371c9b2e126a8b64f6fefa54cef716ff2a50e63b5558a48b899265bccd"
dependencies = [
"sentry-backtrace",
"sentry-core",
]
[[package]]
name = "sentry-tracing"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aca8b88978677a27ee1a91beafe4052306c474c06f582321fde72d2e2cc2f7f"
dependencies = [
"sentry-backtrace",
"sentry-core",
"tracing-core",
"tracing-subscriber",
]
[[package]]
name = "sentry-types"
version = "0.31.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e7a88e0c1922d19b3efee12a8215f6a8a806e442e665ada71cc222cab72985f"
dependencies = [
"debugid",
"getrandom 0.2.10",
"hex",
"serde",
"serde_json",
"thiserror",
"time",
"url",
"uuid 1.3.0",
]
[[package]]
name = "serde"
version = "1.0.158"
@@ -5345,9 +5608,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "0.6.2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d"
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
dependencies = [
"serde",
]
@@ -5677,7 +5940,7 @@ dependencies = [
"futures-util",
"hashlink 0.7.0",
"hex",
"indexmap",
"indexmap 1.9.2",
"itoa 1.0.6",
"libc",
"libsqlite3-sys",
@@ -5723,7 +5986,7 @@ dependencies = [
"futures-util",
"hashlink 0.8.1",
"hex",
"indexmap",
"indexmap 1.9.2",
"itoa 1.0.6",
"libc",
"libsqlite3-sys",
@@ -6580,20 +6843,20 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.6.2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.19.10"
version = "0.19.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739"
checksum = "c500344a19072298cd05a7224b3c0c629348b78692bf48466c5238656e315a78"
dependencies = [
"indexmap",
"indexmap 2.0.0",
"serde",
"serde_spanned",
"toml_datetime",
@@ -6750,6 +7013,15 @@ dependencies = [
"static_assertions",
]
[[package]]
name = "uname"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8"
dependencies = [
"libc",
]
[[package]]
name = "unicode-bidi"
version = "0.3.13"
@@ -6805,6 +7077,19 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "ureq"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9"
dependencies = [
"base64 0.21.2",
"log",
"native-tls",
"once_cell",
"url",
]
[[package]]
name = "url"
version = "2.3.1"
@@ -6842,6 +7127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79"
dependencies = [
"getrandom 0.2.10",
"serde",
]
[[package]]
+4 -1
View File
@@ -43,7 +43,10 @@ time = { version = "0.3.17", features = ["local-offset"] }
tokio = { version = "1.24.1", features = ["sync", "time"] }
url = "2.2"
yaml-rust = "0.4"
toml = "0.7"
sentry = { version = "0.31.5", features = [ "anyhow" ] }
sentry-log = "0.31.5"
dotenvy = "0.15.7"
nym-client-core = { path = "../../../common/client-core" }
nym-api-requests = { path = "../../../nym-api/nym-api-requests" }
+11 -5
View File
@@ -1,10 +1,16 @@
use std::env;
fn main() {
if env::var_os("NYM_CONNECT_ENABLE_MEDIUM").is_some() {
println!("cargo:rustc-cfg=medium_enabled");
}
println!("cargo:rerun-if-changed=build.rs");
mod constants;
use constants::{SENTRY_DSN_JS, SENTRY_DSN_RUST};
fn main() {
// set these env vars at compile time
if let Ok(dsn) = env::var(SENTRY_DSN_RUST) {
println!("cargo:rustc-env={}={}", SENTRY_DSN_RUST, dsn);
}
if let Ok(dsn) = env::var(SENTRY_DSN_JS) {
println!("cargo:rustc-env={}={}", SENTRY_DSN_JS, dsn);
}
tauri_build::build();
}
@@ -26,6 +26,10 @@ mod old_config_v1_1_20_2;
mod persistence;
mod template;
mod upgrade;
mod user_data;
pub use user_data::PrivacyLevel;
pub use user_data::UserData;
static SOCKS5_CONFIG_ID: &str = "nym-connect";
@@ -0,0 +1,67 @@
use eyre::{eyre, Context, Result};
use log::error;
use serde::{Deserialize, Serialize};
use std::{fs, str};
use tauri::api::path::data_dir;
const DATA_DIR: &str = "nym-connect";
const DATA_FILE: &str = "user-data.toml";
#[derive(Serialize, Deserialize, Debug, Copy, Clone, Default)]
pub enum PrivacyLevel {
#[default]
High,
Medium,
}
// User data is read from and write on disk
// Linux: $XDG_DATA_HOME or $HOME/.local/share/
// macOS: $HOME/Library/Application Support
// Windows: {FOLDERID_RoamingAppData}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct UserData {
pub monitoring: Option<bool>,
pub privacy_level: Option<PrivacyLevel>,
}
fn create_directory_path() -> Result<()> {
let mut data_dir = data_dir().ok_or(eyre!("Failed to retrieve data directory"))?;
data_dir.push(DATA_DIR);
fs::create_dir_all(&data_dir).context(format!(
"Failed to create user data directory path {}",
data_dir.display()
))
}
impl UserData {
pub fn read() -> Result<Self> {
// create the full directory path if it is missing
create_directory_path()?;
let mut data_path = data_dir().ok_or(eyre!("Failed to retrieve data directory"))?;
data_path.push(DATA_DIR);
data_path.push(DATA_FILE);
let content = fs::read(&data_path)
.context(format!("Failed to read user data {}", data_path.display()))?;
toml::from_str::<UserData>(str::from_utf8(&content)?).map_err(|e| {
error!("{}", e);
eyre!("{e}")
})
}
pub fn write(&self) -> Result<()> {
// create the full directory path if it is missing
create_directory_path()?;
let mut data_path = data_dir().ok_or(eyre!("Failed to retrieve data directory"))?;
data_path.push(DATA_DIR);
data_path.push(DATA_FILE);
let toml = toml::to_string(&self)?;
fs::write(data_path, toml)?;
Ok(())
}
}
@@ -0,0 +1,3 @@
// env var keys
pub const SENTRY_DSN_RUST: &str = "SENTRY_DSN_RUST";
pub const SENTRY_DSN_JS: &str = "SENTRY_DSN_JS";
@@ -70,6 +70,8 @@ pub enum BackendError {
NewWindowError,
#[error("unable to parse the specified gateway")]
UnableToParseGateway,
#[error("unable to write user data to disk")]
UserDataWriteError,
#[error("unable to load keys: {source}")]
UnableToLoadKeys {
+21 -1
View File
@@ -1,4 +1,6 @@
use fern::colors::{Color, ColoredLevelConfig};
use log::Level;
use sentry::Level as SentryLevel;
use serde::Serialize;
use serde_repr::{Deserialize_repr, Serialize_repr};
use std::str::FromStr;
@@ -22,7 +24,10 @@ fn formatted_time() -> String {
_now.format(&format).unwrap()
}
pub fn setup_logging(app_handle: tauri::AppHandle) -> Result<(), log::SetLoggerError> {
pub fn setup_logging(
app_handle: tauri::AppHandle,
monitoring: bool,
) -> Result<(), log::SetLoggerError> {
let colors = ColoredLevelConfig::new()
.trace(Color::Magenta)
.debug(Color::Blue)
@@ -61,6 +66,21 @@ pub fn setup_logging(app_handle: tauri::AppHandle) -> Result<(), log::SetLoggerE
level: record.level().into(),
};
app_handle.emit_all("log://log", msg).unwrap();
}))
.chain(fern::Output::call(move |record| {
if !monitoring {
return;
}
let level = match record.level() {
Level::Error => SentryLevel::Error,
Level::Warn => SentryLevel::Warning,
Level::Info => SentryLevel::Info,
_ => SentryLevel::Debug,
};
// only send error and warn logs to sentry
if let Level::Error | Level::Warn = record.level() {
sentry::capture_message(&record.args().to_string(), level);
};
}));
base_config
+35 -11
View File
@@ -3,50 +3,75 @@
windows_subsystem = "windows"
)]
use std::env;
use std::sync::Arc;
use nym_config::defaults::setup_env;
use tauri::Manager;
use tokio::sync::RwLock;
use crate::config::UserData;
use crate::menu::{create_tray_menu, tray_menu_event_handler};
use crate::state::{is_medium_enabled, State};
use crate::state::State;
use crate::window::window_toggle;
mod config;
mod constants;
mod error;
mod events;
mod logging;
mod menu;
mod models;
mod monitoring;
mod operations;
mod state;
mod tasks;
mod window;
fn main() {
if is_medium_enabled() {
println!("medium mode enabled");
std::env::set_var("NYM_CONNECT_DISABLE_COVER", "1");
std::env::set_var("NYM_CONNECT_ENABLE_MIXED_SIZE_PACKETS", "1");
std::env::set_var("NYM_CONNECT_DISABLE_PER_HOP_DELAYS", "1");
}
dotenvy::dotenv().ok();
setup_env(None);
println!("Starting up...");
// As per breaking change description here
// https://github.com/tauri-apps/tauri/blob/feac1d193c6d618e49916ad0707201f43d5cdd36/tooling/bundler/CHANGELOG.md
if let Err(error) = fix_path_env::fix() {
log::warn!("Failed to fix PATH: {error}");
println!("Failed to fix PATH: {error}");
}
let user_data = UserData::read().unwrap_or_else(|e| {
println!("{}", e);
println!("Fallback to default");
UserData::default()
});
let monitoring = user_data.monitoring.unwrap_or(false);
let mut _sentry_guard;
if monitoring {
match monitoring::init() {
Ok(guard) => {
println!("Monitoring and error reporting enabled");
// we must keep the sentry guard in scope during app lifetime
_sentry_guard = guard;
}
Err(e) => {
println!("Unable to init monitoring: {e}");
}
}
}
let context = tauri::generate_context!();
tauri::Builder::default()
.manage(Arc::new(RwLock::new(State::new())))
.manage(Arc::new(RwLock::new(State::new(user_data))))
.invoke_handler(tauri::generate_handler![
crate::operations::config::get_config_file_location,
crate::operations::config::get_config_id,
crate::operations::common::get_env,
crate::operations::common::get_user_data,
crate::operations::common::set_monitoring,
crate::operations::common::set_privacy_level,
crate::operations::connection::connect::get_gateway,
crate::operations::connection::connect::get_service_provider,
crate::operations::connection::connect::set_gateway,
@@ -57,7 +82,6 @@ fn main() {
crate::operations::connection::status::get_connection_status,
crate::operations::connection::status::get_gateway_connection_status,
crate::operations::connection::status::start_connection_health_check_task,
crate::operations::connection::status::is_medium_mode_enabled,
crate::operations::directory::get_services,
crate::operations::directory::get_gateways_detailed,
crate::operations::export::export_keys,
@@ -83,7 +107,7 @@ fn main() {
);
}
})
.setup(|app| Ok(crate::logging::setup_logging(app.app_handle())?))
.setup(move |app| Ok(crate::logging::setup_logging(app.app_handle(), monitoring)?))
.system_tray(create_tray_menu())
.on_system_tray_event(tray_menu_event_handler)
.run(context)
@@ -0,0 +1,38 @@
use std::env;
use anyhow::{Context, Result};
use sentry::ClientInitGuard;
use crate::constants::{SENTRY_DSN_JS, SENTRY_DSN_RUST};
pub fn init() -> Result<ClientInitGuard> {
// if these env vars were set at compile time, use their value
if let Some(v) = option_env!("SENTRY_DSN_RUST") {
env::set_var(SENTRY_DSN_RUST, v);
}
if let Some(v) = option_env!("SENTRY_DSN_JS") {
env::set_var(SENTRY_DSN_JS, v);
}
let dsn = env::var(SENTRY_DSN_RUST).context(format!("{} env var not set", SENTRY_DSN_RUST))?;
println!("using DSN {dsn}");
let guard = sentry::init((
dsn,
sentry::ClientOptions {
release: sentry::release_name!(),
sample_rate: 1.0, // TODO lower this in prod
traces_sample_rate: 1.0,
..Default::default() // TODO add data scrubbing
// see https://docs.sentry.io/platforms/rust/data-management/sensitive-data/
},
));
sentry::configure_scope(|scope| {
scope.set_user(Some(sentry::User {
id: Some("nym".into()),
..Default::default()
}));
});
Ok(guard)
}
@@ -1,4 +1,9 @@
use crate::config::PrivacyLevel;
use crate::error::Result;
use crate::{config::UserData, state::State};
use std::env;
use std::sync::Arc;
use tokio::sync::RwLock;
#[tauri::command]
pub async fn get_env(variable: String) -> Option<String> {
@@ -7,3 +12,27 @@ pub async fn get_env(variable: String) -> Option<String> {
var
}
#[tauri::command]
pub async fn get_user_data(state: tauri::State<'_, Arc<RwLock<State>>>) -> Result<UserData> {
let guard = state.read().await;
Ok(guard.get_user_data().clone())
}
#[tauri::command]
pub async fn set_monitoring(
enabled: bool,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<()> {
let mut guard = state.write().await;
guard.set_monitoring(enabled)
}
#[tauri::command]
pub async fn set_privacy_level(
privacy_level: PrivacyLevel,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<()> {
let mut guard = state.write().await;
guard.set_privacy_level(privacy_level)
}
@@ -12,6 +12,7 @@ pub async fn start_connecting(
window: tauri::Window<tauri::Wry>,
) -> Result<ConnectResult> {
log::trace!("Start connecting");
let (msg_receiver, exit_status_receiver) = {
let mut state_w = state.write().await;
state_w.start_connecting(&window).await?
@@ -5,7 +5,7 @@ use std::sync::Arc;
use tokio::sync::RwLock;
use crate::models::{ConnectionStatusKind, ConnectivityTestResult, GatewayConnectionStatusKind};
use crate::state::{is_medium_enabled, State};
use crate::state::State;
#[tauri::command]
pub async fn get_connection_status(
@@ -42,8 +42,3 @@ pub fn start_connection_health_check_task(
) {
tasks::start_connection_check(state.inner().clone(), window);
}
#[tauri::command]
pub async fn is_medium_mode_enabled(_state: tauri::State<'_, Arc<RwLock<State>>>) -> Result<bool> {
Ok(is_medium_enabled())
}
@@ -1,12 +1,15 @@
use itertools::Itertools;
use crate::config::PrivacyLevel;
use crate::error::Result;
use crate::models::{
DirectoryService, DirectoryServiceProvider, HarbourMasterService, PagedResult,
};
use crate::state::is_medium_enabled;
use crate::state::State;
use nym_api_requests::models::GatewayBondAnnotated;
use nym_contracts_common::types::Percent;
use std::sync::Arc;
use tokio::sync::RwLock;
static SERVICE_PROVIDER_WELLKNOWN_URL: &str =
"https://nymtech.net/.wellknown/connect/service-providers.json";
@@ -20,21 +23,20 @@ static HARBOUR_MASTER_URL: &str = "https://harbourmaster.nymtech.net/v1/services
static GATEWAYS_DETAILED_URL: &str =
"https://validator.nymtech.net/api/v1/status/gateways/detailed";
fn get_services_url() -> &'static str {
if is_medium_enabled() {
return SERVICE_PROVIDER_WELLKNOWN_URL_MEDIUM;
}
SERVICE_PROVIDER_WELLKNOWN_URL
}
#[tauri::command]
pub async fn get_services() -> Result<Vec<DirectoryServiceProvider>> {
pub async fn get_services(
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<Vec<DirectoryServiceProvider>> {
log::trace!("Fetching services");
let all_services = fetch_services().await?;
let guard = state.read().await;
let privacy_level = guard.get_user_data().privacy_level.unwrap_or_default();
let all_services = fetch_services(&privacy_level).await?;
log::trace!("Received: {:#?}", all_services);
// Early return if we're running with medium toggle enabled
if is_medium_enabled() {
if let PrivacyLevel::Medium = privacy_level {
return Ok(all_services.into_iter().flat_map(|sp| sp.items).collect());
}
@@ -100,8 +102,12 @@ fn filter_out_poor_gateways(
.collect()
}
async fn fetch_services() -> Result<Vec<DirectoryService>> {
let services_url = get_services_url();
async fn fetch_services(privacy_level: &PrivacyLevel) -> Result<Vec<DirectoryService>> {
let services_url = match privacy_level {
PrivacyLevel::Medium => SERVICE_PROVIDER_WELLKNOWN_URL_MEDIUM,
_ => SERVICE_PROVIDER_WELLKNOWN_URL,
};
let services_res = reqwest::get(services_url)
.await?
.json::<Vec<DirectoryService>>()
+30 -6
View File
@@ -1,4 +1,5 @@
use futures::SinkExt;
use log::error;
use nym_client_core::error::ClientCoreStatusMessage;
use nym_socks5_client_core::{Socks5ControlMessage, Socks5ControlMessageSender};
use std::time::Duration;
@@ -7,6 +8,8 @@ use tauri::Manager;
use tokio::time::Instant;
use crate::config::Config;
use crate::config::PrivacyLevel;
use crate::config::UserData;
use crate::{
config::{self, socks5_config_id_appended_with},
error::{BackendError, Result},
@@ -65,10 +68,13 @@ pub struct State {
/// The latest end-to-end connectivity test result. The first test is initiated on connection
/// established. Additional tests can be triggered.
connectivity_test_result: ConnectivityTestResult,
/// User data saved on disk, like user settings
user_data: UserData,
}
impl State {
pub fn new() -> Self {
pub fn new(user_data: UserData) -> Self {
State {
status: ConnectionStatusKind::Disconnected,
service_provider: None,
@@ -76,6 +82,7 @@ impl State {
socks5_client_sender: None,
gateway_connectivity: GatewayConnectivity::Good,
connectivity_test_result: ConnectivityTestResult::NotAvailable,
user_data,
}
}
@@ -93,6 +100,26 @@ impl State {
self.gateway_connectivity
}
pub fn get_user_data(&self) -> &UserData {
&self.user_data
}
pub fn set_monitoring(&mut self, enabled: bool) -> Result<()> {
self.user_data.monitoring = Some(enabled);
self.user_data.write().map_err(|e| {
error!("Failed to write user data to disk {e}");
BackendError::UserDataWriteError
})
}
pub fn set_privacy_level(&mut self, privacy_level: PrivacyLevel) -> Result<()> {
self.user_data.privacy_level = Some(privacy_level);
self.user_data.write().map_err(|e| {
error!("Failed to write user data to disk {e}");
BackendError::UserDataWriteError
})
}
pub fn set_gateway_connectivity(&mut self, gateway_connectivity: GatewayConnectivity) {
self.gateway_connectivity = gateway_connectivity
}
@@ -200,8 +227,9 @@ impl State {
&mut self,
) -> Result<(nym_task::StatusReceiver, ExitStatusReceiver)> {
let id = self.get_config_id()?;
let privacy_level = self.user_data.privacy_level.unwrap_or_default();
let (control_tx, msg_rx, exit_status_rx, used_gateway) =
tasks::start_nym_socks5_client(&id).await?;
tasks::start_nym_socks5_client(&id, &privacy_level).await?;
self.socks5_client_sender = Some(control_tx);
self.gateway = Some(used_gateway.gateway_id);
Ok((msg_rx, exit_status_rx))
@@ -244,7 +272,3 @@ impl State {
self.set_state(ConnectionStatusKind::Disconnected, window);
}
}
pub fn is_medium_enabled() -> bool {
cfg!(medium_enabled) || std::env::var("NYM_CONNECT_ENABLE_MEDIUM").is_ok()
}
+6 -8
View File
@@ -10,7 +10,7 @@ use std::sync::Arc;
use tap::TapFallible;
use tokio::sync::RwLock;
use crate::config::Config;
use crate::config::{Config, PrivacyLevel};
use crate::{
error::Result,
events::{self, emit_event, emit_status_event},
@@ -30,23 +30,20 @@ pub enum Socks5ExitStatusMessage {
Failed(Box<dyn std::error::Error + Send>),
}
fn override_config_from_env(config: &mut Config) {
fn override_config_from_env(config: &mut Config, privacy_level: &PrivacyLevel) {
// Disable both the loop cover traffic that runs in the background as well as the Poisson
// process that injects cover traffic into the traffic stream.
if std::env::var("NYM_CONNECT_DISABLE_COVER").is_ok() {
if let PrivacyLevel::Medium = privacy_level {
log::info!("Running in Medium privacy level");
log::warn!("Disabling cover traffic");
config.core.base.set_no_cover_traffic_with_keepalive();
}
if std::env::var("NYM_CONNECT_ENABLE_MIXED_SIZE_PACKETS").is_ok() {
log::warn!("Enabling mixed size packets");
config
.core
.base
.set_secondary_packet_size(Some(PacketSize::ExtendedPacket16));
}
if std::env::var("NYM_CONNECT_DISABLE_PER_HOP_DELAYS").is_ok() {
log::warn!("Disabling per-hop delay");
config.core.base.set_no_per_hop_delays();
}
@@ -55,6 +52,7 @@ fn override_config_from_env(config: &mut Config) {
/// The main SOCKS5 client task. It loads the configuration from file determined by the `id`.
pub async fn start_nym_socks5_client(
id: &str,
privacy_level: &PrivacyLevel,
) -> Result<(
Socks5ControlMessageSender,
nym_task::StatusReceiver,
@@ -65,7 +63,7 @@ pub async fn start_nym_socks5_client(
let mut config = Config::read_from_default_path(id)
.tap_err(|_| log::warn!("Failed to load configuration file"))?;
override_config_from_env(&mut config);
override_config_from_env(&mut config, privacy_level);
log::trace!("Configuration used: {:#?}", config);
@@ -16,7 +16,7 @@ const ConnectionStatusContent: FCWithChildren<{
serviceProvider?: ServiceProvider;
gatewayError: boolean;
}> = ({ status, serviceProvider, gatewayError }) => {
const { speedMode } = useClientContext();
const { userData } = useClientContext();
if (gatewayError) {
return (
@@ -49,10 +49,10 @@ const ConnectionStatusContent: FCWithChildren<{
</Typography>
</Box>
</Tooltip>
{speedMode === 'medium' && (
{userData?.privacy_level === 'Medium' && (
<Stack alignItems="center" color="warning.main">
<Typography variant="caption" color="grey.400">
Speedy mode activated
Speed boost activated
</Typography>
</Stack>
)}
+34 -27
View File
@@ -3,23 +3,24 @@ import { DateTime } from 'luxon';
import { invoke } from '@tauri-apps/api';
import { Error } from 'src/types/error';
import { getVersion } from '@tauri-apps/api/app';
import * as Sentry from '@sentry/react';
import { useEvents } from 'src/hooks/events';
import { UserDefinedGateway, UserDefinedSPAddress } from 'src/types/service-provider';
import { getItemFromStorage, setItemInStorage } from 'src/utils';
import { ConnectionStatusKind, GatewayPerformance, SpeedMode } from '../types';
import { ConnectionStatusKind, GatewayPerformance, PrivacyLevel, UserData } from '../types';
import { ConnectionStatsItem } from '../components/ConnectionStats';
import { ServiceProvider } from '../types/directory';
import initSentry from '../sentry';
const FORAGE_GATEWAY_KEY = 'nym-connect-user-gateway';
const FORAGE_SP_KEY = 'nym-connect-user-sp';
const FORAGE_MONITORING_ENABLED = 'nym-connect-monitoring-enabled';
type ModeType = 'light' | 'dark';
export type TClientContext = {
mode: ModeType;
appVersion?: string;
userData?: UserData;
connectionStatus: ConnectionStatusKind;
connectionStats?: ConnectionStatsItem[];
connectedSince?: DateTime;
@@ -32,7 +33,6 @@ export type TClientContext = {
serviceProviders?: ServiceProvider[];
setMode: (mode: ModeType) => void;
clearError: () => void;
monitoringEnabled: boolean;
setConnectionStatus: (connectionStatus: ConnectionStatusKind) => void;
setConnectionStats: (connectionStats: ConnectionStatsItem[] | undefined) => void;
setConnectedSince: (connectedSince: DateTime | undefined) => void;
@@ -43,7 +43,7 @@ export type TClientContext = {
setUserDefinedGateway: React.Dispatch<React.SetStateAction<UserDefinedGateway>>;
setUserDefinedSPAddress: React.Dispatch<React.SetStateAction<UserDefinedSPAddress>>;
setMonitoring: (value: boolean) => Promise<void>;
speedMode: SpeedMode;
setPrivacyLevel: (value: PrivacyLevel) => Promise<void>;
};
export const ClientContext = createContext({} as TClientContext);
@@ -67,19 +67,26 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
isActive: false,
address: undefined,
});
const [monitoringEnabled, setMonitoringEnabled] = useState(false);
const [speedMode, setspeedMode] = useState<SpeedMode>('slow');
const [userData, setUserData] = useState<UserData>();
const getAppVersion = async () => {
const version = await getVersion();
return version;
};
const getUserData = async () => {
const data = await invoke<UserData>('get_user_data');
if (!data.privacy_level) {
data.privacy_level = 'High';
}
setUserData(data);
return data;
};
useEffect(() => {
const initSentryClient = async () => {
const monitoring = await getItemFromStorage({ key: FORAGE_MONITORING_ENABLED });
setMonitoringEnabled(Boolean(monitoring));
if (monitoring === true) {
const data = await getUserData();
if (data.monitoring) {
await initSentry();
}
};
@@ -87,17 +94,6 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
initSentryClient();
}, []);
useEffect(() => {
const initSpeedMode = async () => {
const isEnabled = await invoke<boolean>('is_medium_mode_enabled');
if (isEnabled) {
setspeedMode('medium');
}
};
initSpeedMode();
}, []);
useEffect(() => {
setItemInStorage({ key: FORAGE_GATEWAY_KEY, value: userDefinedGateway });
}, [userDefinedGateway]);
@@ -124,7 +120,10 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
};
useEvents({
onError: (e) => setError(e),
onError: (e) => {
setError(e);
Sentry.captureException(e);
},
onGatewayPerformanceChange: (performance) => setGatewayPerformance(performance),
onStatusChange: (status) => setConnectionStatus(status),
});
@@ -147,6 +146,7 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
} catch (e) {
setError({ title: 'Could not connect', message: e as string });
console.log(e);
Sentry.captureException(e);
}
}, []);
@@ -155,6 +155,7 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
await invoke('start_disconnecting');
} catch (e) {
console.log(e);
Sentry.captureException(e);
}
}, []);
@@ -197,8 +198,15 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
const clearError = () => setError(undefined);
const setMonitoring = async (value: boolean) => {
setMonitoringEnabled(value);
await setItemInStorage({ key: FORAGE_MONITORING_ENABLED, value });
await invoke('set_monitoring', { enabled: value });
// refresh user data
await getUserData();
};
const setPrivacyLevel = async (value: PrivacyLevel) => {
await invoke('set_privacy_level', { privacyLevel: value });
// refresh user data
await getUserData();
};
const contextValue = useMemo(
@@ -216,7 +224,7 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
selectedProvider,
serviceProviders,
connectedSince,
monitoringEnabled,
userData,
setConnectedSince,
setSerivceProvider,
startConnecting,
@@ -228,7 +236,7 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
setUserDefinedGateway,
setUserDefinedSPAddress,
setMonitoring,
speedMode,
setPrivacyLevel,
}),
[
mode,
@@ -244,8 +252,7 @@ export const ClientContextProvider: FCWithChildren = ({ children }) => {
selectedProvider,
userDefinedGateway,
userDefinedSPAddress,
monitoringEnabled,
speedMode,
userData,
],
);
@@ -11,8 +11,7 @@ const mockValues: TClientContext = {
showInfoModal: false,
userDefinedGateway: { isActive: false, gateway: '' },
userDefinedSPAddress: { isActive: false, address: '' },
monitoringEnabled: false,
speedMode: 'slow',
userData: { monitoring: false, privacy_level: 'High' },
setShowInfoModal: () => {},
setMode: () => {},
clearError: () => {},
@@ -25,6 +24,7 @@ const mockValues: TClientContext = {
setUserDefinedGateway: () => {},
setUserDefinedSPAddress: () => {},
setMonitoring: async () => {},
setPrivacyLevel: async () => {},
};
export const MockProvider: FCWithChildren<{
+5 -1
View File
@@ -1,5 +1,6 @@
import { useEffect, useRef } from 'react';
import { listen, UnlistenFn } from '@tauri-apps/api/event';
import * as Sentry from '@sentry/react';
import { ConnectionStatusKind, GatewayPerformance } from 'src/types';
import { Error } from 'src/types/error';
import { TauriEvent } from 'src/types/event';
@@ -29,7 +30,10 @@ export const useEvents = ({
.then((result) => {
unlisten.push(result);
})
.catch((e) => console.log(e));
.catch((e) => {
console.log(e);
Sentry.captureException(e);
});
listen('socks5-event', (e: TauriEvent) => {
console.log(e);
@@ -1,5 +1,6 @@
import React from 'react';
import { forage } from '@tauri-apps/tauri-forage';
import * as Sentry from '@sentry/react';
import { DateTime } from 'luxon';
import { useClientContext } from 'src/context/main';
import { useTauriEvents } from 'src/utils';
@@ -28,12 +29,14 @@ export const ConnectionPage = () => {
// eslint-disable-next-line default-case
switch (currentStatus) {
case 'disconnected':
Sentry.captureMessage('start connect', 'info');
await context.setSerivceProvider();
await context.startConnecting();
context.setConnectedSince(DateTime.now());
context.setShowInfoModal(true);
break;
case 'connected':
Sentry.captureMessage('start disconnect', 'info');
await context.startDisconnecting();
context.setConnectedSince(undefined);
break;
+1 -1
View File
@@ -4,7 +4,7 @@ import { Box } from '@mui/system';
const appsSchema = {
messagingApps: ['Matrix', 'Telegram', 'Keybase'],
wallets: ['Monero', 'Blockstream', 'Electrum'],
wallets: ['Monero', 'Blockstream', 'Electrum', 'Alephium'],
};
export const CompatibleApps = () => (
+2 -1
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { Apps, HelpOutline, Settings, BugReport } from '@mui/icons-material';
import { Apps, HelpOutline, Settings, BugReport, PrivacyTip } from '@mui/icons-material';
import { Stack, Link, List, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
import { AppVersion } from 'src/components/AppVersion';
@@ -7,6 +7,7 @@ import { AppVersion } from 'src/components/AppVersion';
const menuSchema = [
{ title: 'Supported apps', icon: Apps, path: 'apps' },
{ title: 'How to connect guide', icon: HelpOutline, path: 'guide' },
{ title: 'Privacy level', icon: PrivacyTip, path: 'privacy-level' },
{ title: 'Please help us improve the app', icon: BugReport, path: 'monitoring' },
{ title: 'Settings', icon: Settings, path: 'settings' },
];
@@ -4,8 +4,8 @@ import { Box, FormControl, FormControlLabel, FormHelperText, Stack, Switch, Typo
import { useClientContext } from 'src/context/main';
export const MonitoringSettings = () => {
const { monitoringEnabled, setMonitoring } = useClientContext();
const [enabled, setEnabled] = useState(monitoringEnabled);
const { userData, setMonitoring } = useClientContext();
const [enabled, setEnabled] = useState(userData?.monitoring || false);
const [loading, setLoading] = useState(false);
const handleChange = async (e: ChangeEvent<HTMLInputElement>) => {
@@ -25,7 +25,13 @@ export const MonitoringSettings = () => {
<FormControl fullWidth>
<FormControlLabel
control={
<Switch checked={enabled} onChange={handleChange} disabled={loading} size="small" sx={{ ml: 1 }} />
<Switch
checked={enabled}
onChange={handleChange}
disabled={loading}
size="small"
sx={{ ml: 1, mr: 1 }}
/>
}
label="Enable"
/>
@@ -0,0 +1,48 @@
import React, { ChangeEvent, useState } from 'react';
import * as Sentry from '@sentry/react';
import { Box, FormControl, FormControlLabel, FormHelperText, Stack, Switch, Typography } from '@mui/material';
import { useClientContext } from 'src/context/main';
export const PrivacyLevelSettings = () => {
const { userData, setPrivacyLevel } = useClientContext();
const [speedBoost, setSpeedBoost] = useState(userData?.privacy_level !== 'High');
const [loading, setLoading] = useState(false);
const handleChange = async (e: ChangeEvent<HTMLInputElement>) => {
setLoading(true);
setSpeedBoost(e.target.checked);
Sentry.captureMessage(`privacy level switched to ${e.target.checked ? 'Medium' : 'High'}`, 'info');
await setPrivacyLevel(e.target.checked ? 'Medium' : 'High');
setLoading(false);
};
return (
<Box height="100%">
<Stack justifyContent="space-between" height="100%">
<Box>
<Typography fontWeight="bold" variant="body2" mb={2}>
Speed boost
</Typography>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={speedBoost}
onChange={handleChange}
disabled={loading}
size="small"
sx={{ ml: 1, mr: 1 }}
/>
}
label="Enable"
/>
<FormHelperText sx={{ m: 0, my: 2 }}>
By activating this option, the connection speed will be relatively faster in exchange of relaxing some
protections
</FormHelperText>
</FormControl>
</Box>
</Stack>
</Box>
);
};
+4 -2
View File
@@ -9,14 +9,15 @@ import { SettingsMenu } from 'src/pages/menu/settings';
import { GatewaySettings } from 'src/pages/menu/settings/GatewaySettings';
import { ServiceProviderSettings } from 'src/pages/menu/settings/ServiceProviderSettings';
import { MonitoringSettings } from '../pages/menu/settings/MonitoringSettings';
import { PrivacyLevelSettings } from '../pages/menu/settings/PrivacyLevelSettings';
import { useClientContext } from '../context/main';
const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);
export const AppRoutes = () => {
const { monitoringEnabled } = useClientContext();
const { userData } = useClientContext();
const RoutesContainer = monitoringEnabled ? SentryRoutes : Routes;
const RoutesContainer = userData?.monitoring ? SentryRoutes : Routes;
return (
<RoutesContainer>
@@ -25,6 +26,7 @@ export const AppRoutes = () => {
<Route index element={<Menu />} />
<Route path="apps" element={<CompatibleApps />} />
<Route path="guide" element={<HelpGuide />} />
<Route path="privacy-level" element={<PrivacyLevelSettings />} />
<Route path="monitoring" element={<MonitoringSettings />} />
<Route path="settings">
<Route index element={<SettingsMenu />} />
+10 -2
View File
@@ -1,17 +1,25 @@
import React from 'react';
import { createRoutesFromChildren, matchRoutes, useLocation, useNavigationType } from 'react-router-dom';
import { invoke } from '@tauri-apps/api';
import * as Sentry from '@sentry/react';
import { CaptureConsole } from '@sentry/integrations';
import { getVersion } from '@tauri-apps/api/app';
const SENTRY_DSN = 'https://625e2658da4945a7a253f3ee04413a31@o967446.ingest.sentry.io/4505306292289536';
const SENTRY_DSN = 'SENTRY_DSN_JS';
async function initSentry() {
console.log('⚠ performance monitoring and error reporting enabled');
console.log('initializing sentry');
const dsn = await invoke<string | undefined>('get_env', { variable: SENTRY_DSN });
if (!dsn) {
console.warn(`unable to initialize sentry, ${SENTRY_DSN} env var not set`);
return;
}
Sentry.init({
dsn: SENTRY_DSN,
dsn,
integrations: [
new Sentry.BrowserTracing({
// Set `tracePropagationTargets` to control for which URLs distributed tracing should be enabled
-1
View File
@@ -1 +0,0 @@
export type SpeedMode = 'fast' | 'medium' | 'slow';
+1 -1
View File
@@ -1,3 +1,3 @@
export * from './rust';
export * from './connection';
export * from './common';
export * from './user-data';
@@ -0,0 +1,6 @@
export type PrivacyLevel = 'High' | 'Medium';
export type UserData = {
monitoring?: boolean;
privacy_level?: PrivacyLevel;
};
@@ -0,0 +1,22 @@
# Nym Chrome Extension Example
This is an example of how Nym can be used within the context of a Chrome extension.
## Running the example
1. Copy a build of the Nym TypeScript SDK (ESM version) into `./sdk`.
2. Navigate to `chrome://extensions` in Google Chrome.
3. Enable "Developer mode" (top right of the page).
4. Click on "Load unpacked" (top left of the page).
5. Load this extension folder.
## How does it work?
The Nym Mixnet Client runs a [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) that wraps
a WASM library that builds and encrypts Sphinx packets in the browser to send over the Nym mixnet:
![Sphinx packet](../docs/worker.svg)
The WASM code encrypts each layer of the Sphinx packet in the browser, before sending the Sphinx packet over a websocket to the ingress gateway:
![Sphinx packet](../docs/sphinx.svg)
@@ -0,0 +1,17 @@
{
"name": "Nym Chrome Extension Example",
"description": "An example demonstrating how to integrate the Nym TypeScript SDK in the context of a Google Chrome browser extension.",
"version": "1.0",
"manifest_version": 3,
"icons": {
"48": "icon.png"
},
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
},
"action": {
"default_title": "Nym Chrome Extension Example",
"default_icon": "icon.png",
"default_popup": "popup.html"
}
}
@@ -0,0 +1,20 @@
{
"name": "chrome-extension",
"version": "1.0.0",
"description": "This is an example of how Nym can be used within the context of a Chrome extension.",
"main": "index.js",
"scripts": {
"build": "webpack"
},
"author": "",
"license": "ISC",
"devDependencies": {
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^11.0.0",
"webpack": "^5.88.1",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"@nymproject/sdk": "^1.1.8"
}
}
@@ -0,0 +1,8 @@
body {
width: 800px;
min-height: 400px;
}
#editdialog input {
width: 100%;
}
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="popup.css" />
<script type="module" src="main.js"></script>
</head>
<body>
<p><label>Sender:</label><input disabled="true" size="85" id="sender" value="" /></p>
<p><label>Recipient:</label><input size="85" id="recipient" value="" /></p>
<p><label>Message:</label><input id="message" value="Hello mixnet!" /></p>
<p><button id="send-button">Send</button></p>
<p>Send messages from your browser, through the mixnet, and to the recipient using the "send" button.</p>
<p>
<span style="color: blue">Sent</span> messages show in blue, <span style="color: green">received</span> messages
show in green.
</p>
<hr />
<p></p>
<div id="output"></div>
<p></p>
</body>
</html>
@@ -0,0 +1,66 @@
// dom-utils.js
// Contains utility functionality to help manipulate the DOM elements necessary to demonstrate the Nym example.
/**
* Create a Sphinx packet and send it to the mixnet through the gateway node.
*
* Message and recipient are taken from the values in the user interface.
*
* @param {Client} nymClient the nym client to use for message sending
*/
async function sendMessageTo(nym) {
const message = document.getElementById('message').value;
const recipient = document.getElementById('recipient').value;
await nym.client.send({
payload: {
message,
mimeType: 'text/plain'
},
recipient
});
displaySend(message);
}
/**
* Display messages that have been sent up the websocket. Colours them blue.
*
* @param {string} message
*/
function displaySend(message) {
const timestamp = new Date().toISOString().substr(11, 12);
const sendDiv = document.createElement('div');
const paragraph = document.createElement('p');
paragraph.setAttribute('style', 'color: blue');
const paragraphContent = document.createTextNode(`${timestamp} sent >>> ${message}`);
paragraph.appendChild(paragraphContent);
sendDiv.appendChild(paragraph);
document.getElementById('output')?.appendChild(sendDiv);
}
/**
* Display received text messages in the browser. Colour them green.
*
* @param {string} message
*/
function displayReceived(message) {
const content = message;
const timestamp = new Date().toLocaleTimeString();
const receivedDiv = document.createElement('div');
const paragraph = document.createElement('p');
paragraph.setAttribute('style', 'color: green');
const paragraphContent = document.createTextNode(`${timestamp} received >>> ${content}`);
paragraph.appendChild(paragraphContent);
receivedDiv.appendChild(paragraph);
document.getElementById('output')?.appendChild(receivedDiv);
}
/**
* Display the nymClient's sender address in the user interface
*
* @param {Client} nymClient
*/
function displaySenderAddress(address) {
document.getElementById('sender').value = address;
}
export { sendMessageTo, displaySend, displayReceived , displaySenderAddress }
@@ -0,0 +1,53 @@
// main.js
// Simple example of how to load Nym's TypeScript SDK and bind it to a DOM.
// Look at dom-utils.js for the DOM utility functionality referenced here.
// Import the Nym mixnet ESM module.
import { createNymMixnetClient } from '@nymproject/sdk';
// Import the DOM utility functionality.
import { displaySenderAddress, displayReceived, sendMessageTo } from './dom-utils.js';
async function main() {
// Initialize the Nym mixnet client.
let nymClient = await createNymMixnetClient();
if (!nymClient) {
console.error('Oh no! Could not create client');
return;
}
const nymApiUrl = 'https://validator.nymtech.net/api';
const preferredGatewayIdentityKey = 'E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM';
// subscribe to connect event, so that we can show the client's address
nymClient.events.subscribeToConnected((e) => {
if (e.args.address) {
displaySenderAddress(e.args.address);
}
});
// subscribe to message received events and show any string messages received
nymClient.events.subscribeToTextMessageReceivedEvent((e) => {
displayReceived(e.args.payload);
});
const sendButton = document.querySelector('#send-button');
if (sendButton) {
sendButton.onclick = function () {
if (nymClient) {
sendMessageTo(nymClient);
}
};
}
nymClient.events.subscribeToRawMessageReceivedEvent((e) => console.log('Received: ', e.args.payload));
await nymClient.client.start({
clientId: 'My awesome client',
nymApiUrl,
preferredGatewayIdentityKey,
});
}
window.addEventListener('DOMContentLoaded', () => {
main();
});
@@ -0,0 +1,25 @@
// Webpack configuration for the Chrome extension example
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
mode: 'production',
entry: {
main: './src/main.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
},
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [
'manifest.json',
'popup.html',
{ from: path.resolve(__dirname, '../../../../assets/favicon/favicon.png'), to: 'icon.png' },
],
}),
],
};
@@ -0,0 +1 @@
sdk/index.js
@@ -0,0 +1,35 @@
# Nym Firefox Extension Example
This is an example of how Nym can be used within the context of a Mozilla Firefox extension.
## Running the example
First, build the Nym SDK:
From the SDK directory `sdk/typescript/packages/sdk` run:
```js
npm run build:local
```
Then, from the example directory `sdk/typescript/examples/firefox-extension` run:
```js
npm install
npm run build
```
## Workers
Firefox browser extensions cannot run inline web workers. In order to overcome this limitation, the Nym Firefox Extension Example imports workers from the SDK and uses Webpack's `worker-loader` to allow the worker's to be bundled inline into the extension. In order for webpack to include the workers in the build, they are imported as modules in the `src/index.js` file:
## How does it work?
The Nym Mixnet Client runs a [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) that wraps
a WASM library that builds and encrypts Sphinx packets in the browser to send over the Nym mixnet:
![Sphinx packet](../docs/worker.svg)
The WASM code encrypts each layer of the Sphinx packet in the browser, before sending the Sphinx packet over a websocket to the ingress gateway:
![Sphinx packet](../docs/sphinx.svg)
@@ -0,0 +1,23 @@
{
"manifest_version": 3,
"name": "Nym Firefox Extension Example",
"version": "1.0",
"description": "An example demonstrating how to integrate the Nym TypeScript SDK in the context of a Mozilla Firefox browser extension.",
"icons": {
"48": "icon.png"
},
"permissions": [],
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval';"
},
"background": {
"scripts": ["background.js"]
},
"action": {
"default_icon": {
"32": "icon.png"
},
"default_title": "Nym Firefox Extension Example",
"default_popup": "popup.html"
}
}
@@ -0,0 +1,19 @@
{
"name": "firefox-extension",
"version": "1.0.0",
"description": "This is an example of how Nym can be used within the context of a Firefox extension.",
"main": "index.js",
"author": "",
"license": "ISC",
"devDependencies": {
"copy-webpack-plugin": "^11.0.0",
"webpack": "^5.88.1",
"webpack-cli": "^5.1.4"
},
"scripts": {
"build": "yarn webpack"
},
"dependencies": {
"worker-loader": "^3.0.8"
}
}
@@ -0,0 +1,8 @@
body {
width: 800px;
min-height: 400px;
}
#editdialog input {
width: 100%;
}
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="popup.css" />
<script type="module" src="popup.js"></script>
</head>
<body>
<p><label>Sender:</label><input disabled="true" size="85" id="sender" value="" /></p>
<p><label>Recipient:</label><input size="85" id="recipient" value="" /></p>
<p><label>Message:</label><input id="message" value="Hello mixnet!" /></p>
<p><button id="send-button">Send</button></p>
<p>Send messages from your browser, through the mixnet, and to the recipient using the "send" button.</p>
<p>
<span style="color: blue">Sent</span> messages show in blue, <span style="color: green">received</span> messages
show in green.
</p>
<hr />
<p></p>
<div id="output"></div>
<p></p>
</body>
</html>
@@ -0,0 +1,107 @@
// main.js
// Simple example of how to load Nym's TypeScript SDK and bind it to a DOM.
// Look at dom-utils.js for the DOM utility functionality referenced here.
// Import the Nym mixnet ESM module.
// Import The web workers for the Nym mixnet ESM module.These are required for to run the Nym mixnet client.
import { createNymMixnetClient } from '../../../packages/sdk/dist/full-fat/index.js';
import '../../../packages/sdk/dist/full-fat/web-worker-0.js';
import '../../../packages/sdk/dist/full-fat/web-worker-1.js';
const backgroundState = {
isReady: false,
address: '',
recipient: '',
messageLog: [],
};
async function initBackground() {
// Initialize the Nym mixnet client.
let nymClient = await createNymMixnetClient().catch((err) => {
console.log(err);
});
if (!nymClient) {
console.error('Oh no! Could not create client');
return;
}
const nymApiUrl = 'https://validator.nymtech.net/api';
const preferredGatewayIdentityKey = 'E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM';
// subscribe to connect event, so that we can show the client's address
nymClient.events.subscribeToConnected((e) => {
if (e.args.address) {
backgroundState.address = e.args.address;
browser.runtime.sendMessage({
type: 'displaySenderAddress',
message: backgroundState.address,
});
}
});
// subscribe to message received events and show any string messages received
nymClient.events.subscribeToTextMessageReceivedEvent((e) => {
backgroundState.messageLog.push({
type: 'received',
message: e.args.payload,
});
browser.runtime.sendMessage({
type: 'displayReceived',
message: e.args.payload,
});
});
nymClient.events.subscribeToRawMessageReceivedEvent((e) => console.log('Received: ', e.args.payload));
await nymClient.client.start({
clientId: 'My awesome client',
nymApiUrl,
preferredGatewayIdentityKey,
});
browser.runtime.onMessage.addListener(async (data) => {
switch (data.type) {
case 'nymClientSendMessage':
if (nymClient) {
await nymClient.client.send({
payload: {
message: data.message,
mimeType: 'text/plain',
},
recipient: data.recipient,
});
backgroundState.messageLog.push({
type: 'sent',
message: data.message,
});
break;
}
}
});
backgroundState.isReady = true;
}
window.addEventListener('DOMContentLoaded', () => {
browser.runtime.onMessage.addListener((data) => {
switch (data.type) {
case 'popupReady':
if (backgroundState.isReady) {
browser.runtime.sendMessage({
type: 'displaySenderAddress',
message: backgroundState.address,
});
browser.runtime.sendMessage({
type: 'displayMessageLog',
message: backgroundState.messageLog,
});
browser.runtime.sendMessage({
type: 'updateRecipient',
message: backgroundState.recipient,
});
} else {
initBackground();
}
break;
case 'updateRecipient':
backgroundState.recipient = data.message;
}
});
});
@@ -0,0 +1,74 @@
// dom-utils.js
// Contains utility functionality to help manipulate the DOM elements necessary to demonstrate the Nym example.
/**
* Create a Sphinx packet and send it to the mixnet through the gateway node.
*
* Message and recipient are taken from the values in the user interface.
*
* @param {Client} nymClient the nym client to use for message sending
*/
async function sendMessageTo() {
const message = document.getElementById('message').value;
const recipient = document.getElementById('recipient').value;
browser.runtime.sendMessage({
type: 'nymClientSendMessage',
message,
recipient,
});
displaySend(message);
}
/**
* Display messages that have been sent up the websocket. Colours them blue.
*
* @param {string} message
*/
function displaySend(message) {
const timestamp = new Date().toISOString().substr(11, 12);
const sendDiv = document.createElement('div');
const paragraph = document.createElement('p');
paragraph.setAttribute('style', 'color: blue');
const paragraphContent = document.createTextNode(`${timestamp} sent >>> ${message}`);
paragraph.appendChild(paragraphContent);
sendDiv.appendChild(paragraph);
document.getElementById('output')?.appendChild(sendDiv);
}
/**
* Display received text messages in the browser. Colour them green.
*
* @param {string} message
*/
function displayReceived(message) {
const content = message;
const timestamp = new Date().toLocaleTimeString();
const receivedDiv = document.createElement('div');
const paragraph = document.createElement('p');
paragraph.setAttribute('style', 'color: green');
const paragraphContent = document.createTextNode(`${timestamp} received >>> ${content}`);
paragraph.appendChild(paragraphContent);
receivedDiv.appendChild(paragraph);
document.getElementById('output')?.appendChild(receivedDiv);
}
/**
* Display the nymClient's sender address in the user interface
*
* @param {Client} nymClient
*/
function displaySenderAddress(address) {
document.getElementById('sender').value = address;
}
function displayMessageLog(messageLog) {
for (let i = 0; i < messageLog.length; i++) {
if (messageLog[i].type === 'sent') {
displaySend(messageLog[i].message);
} else if (messageLog[i].type === 'received') {
displayReceived(messageLog[i].message);
}
}
}
export { sendMessageTo, displaySend, displayReceived, displaySenderAddress, displayMessageLog };
@@ -0,0 +1,40 @@
// Import the DOM utility functionality.
import { displaySenderAddress, displayReceived, sendMessageTo, displayMessageLog } from './dom-utils.js';
window.addEventListener('DOMContentLoaded', () => {
const sendButton = document.querySelector('#send-button');
if (sendButton) {
sendButton.onclick = function () {
sendMessageTo();
};
}
const recipient = document.getElementById('recipient');
recipient.onchange = () => {
browser.runtime.sendMessage({
type: 'updateRecipient',
message: recipient.value,
});
};
browser.runtime.onMessage.addListener((data) => {
switch (data.type) {
case 'displaySenderAddress':
displaySenderAddress(data.message);
break;
case 'displayReceived':
displayReceived(data.message);
break;
case 'sendMessageTo':
sendMessageTo(data.message);
break;
case 'displayMessageLog':
displayMessageLog(data.message);
break;
case 'updateRecipient':
recipient.value = data.message;
}
});
browser.runtime.sendMessage({
type: 'popupReady',
message: '',
});
});
@@ -0,0 +1,38 @@
// Webpack configuration for the Firefox extension example
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
mode: 'production',
entry: {
background: './src/background.js',
popup: './src/popup.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
},
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [
'manifest.json',
'popup.html',
{ from: path.resolve(__dirname, '../../../../assets/favicon/favicon.png'), to: 'icon.png' },
],
}),
],
module: {
rules: [
{
test: /web-worker.*\.js$/,
loader: 'worker-loader',
options: {
filename: '[name].js',
inline: 'fallback',
},
},
],
},
};
@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nym WebAssembly Demo</title>
<title>Nym Node Tester Demo</title>
</head>
<body>
@@ -1,3 +0,0 @@
{
"presets": ["@babel/env", "@babel/react"]
}
@@ -1,5 +0,0 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nym WebAssembly Demo</title>
<title>Nym Node Tester Demo</title>
</head>
<body>
@@ -0,0 +1,14 @@
# Nym Node Tester - React
This is an example of using the Nym Mixnet node tester.
You can use this example as a seed for a new project.
## Running the example
Try out the node tester app by running:
```
npm install
npm start
```
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="shortcut icon" href="../../../../../assets/favicon/favicon.png" />
<title>Nym Node Tester Example</title>
</head>
<body>
<div id="root"></div>
<script src="src/index.tsx" type="module"></script>
</body>
</html>
@@ -0,0 +1,19 @@
{
"name": "@nymproject/sdk-example-node-tester-react",
"description": "An example project that uses WASM node tester and React",
"version": "1.0.0",
"license": "Apache-2.0",
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.0",
"@mui/material": "^5.14.0",
"@nymproject/sdk": "1",
"parcel": "^2.9.3",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"scripts": {
"start": "parcel index.html"
}
}
@@ -0,0 +1,127 @@
import React, { useState } from 'react';
import {
Button,
Card,
CardActions,
CardContent,
CardHeader,
CircularProgress,
Grid,
List,
ListItem,
ListItemText,
TextField,
Typography,
} from '@mui/material';
import { NodeTestResultResponse } from '@nymproject/sdk';
import { ScoreIndicator } from 'src/components/ScoreIndicator';
import { useNodeTesterClient } from 'src/hooks/useNodeTesterClient';
import { BasicPageLayout } from 'src/layouts';
import { TestStatusLabel } from 'src/components/TestStatusLabel';
import Icon from '../../../../../../assets/appicon/appicon.png';
export const App = () => {
const { testState, error, testNode, disconnectFromGateway, reconnectToGateway } = useNodeTesterClient();
const [mixnodeIdentity, setMixnodeIdentity] = useState<string>('');
const [results, setResults] = React.useState<NodeTestResultResponse>();
console.log({ testState, error, testNode });
const handleTestNode = async () => {
setResults(undefined);
try {
const result = await testNode(mixnodeIdentity);
setResults(result);
} catch (e) {
console.error(e);
}
};
return (
<BasicPageLayout>
<Card variant="outlined" sx={{ mt: 15, p: 4 }}>
<CardHeader
title={<Typography variant="h6">Nym Mixnode Testnet Node Tester</Typography>}
action={<TestStatusLabel state={testState} />}
avatar={<img src={Icon} width={40} />}
/>
<CardContent sx={{ mb: 2 }}>
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<ScoreIndicator score={results?.score || 0} />
</Grid>
<Grid item xs={12} sm={6}>
<List>
<ListItem>
<ListItemText primary="Packets sent" secondary={results?.sentPackets.toString() || '-'} />
</ListItem>
<ListItem>
<ListItemText primary="Packets received" secondary={results?.receivedPackets.toString() || '-'} />
</ListItem>
<ListItem>
<ListItemText
primary="Duplicate packets received"
secondary={results?.duplicatePackets.toString() || '-'}
/>
</ListItem>
</List>
</Grid>
</Grid>
</CardContent>
<CardActions>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
label="Enter a Mixnode Identity to test"
value={mixnodeIdentity}
onChange={(e) => {
setMixnodeIdentity(e.target.value);
}}
fullWidth
/>
</Grid>
<Grid item xs={12} sm={4}>
<Button
disabled={!disconnectFromGateway || testState === 'Disconnected' || testState === 'Testing'}
onClick={disconnectFromGateway}
variant="outlined"
disableElevation
size="large"
fullWidth
sx={{ mr: 2 }}
>
Disconnect
</Button>
</Grid>
<Grid item xs={12} sm={4}>
<Button
disabled={!reconnectToGateway || testState === 'Ready' || testState === 'Testing'}
onClick={reconnectToGateway}
variant="outlined"
disableElevation
size="large"
fullWidth
sx={{ mr: 2 }}
>
Reconnect
</Button>
</Grid>
<Grid item xs={12} sm={4}>
<Button
disabled={!testNode || !mixnodeIdentity || testState === 'Testing' || testState === 'Disconnected'}
onClick={handleTestNode}
variant="contained"
disableElevation
fullWidth
size="large"
endIcon={testState === 'Testing' && <CircularProgress size={25} />}
>
Start test
</Button>
</Grid>
</Grid>
</CardActions>
</Card>
</BasicPageLayout>
);
};
@@ -0,0 +1,61 @@
import React from 'react';
import { Box, CircularProgress, CircularProgressProps, Stack, Typography } from '@mui/material';
const getPerformanceDescriptionAndColor = (score: number) => {
const res: { description: string; color: CircularProgressProps['color'] } = { description: '', color: 'warning' };
if (score >= 90) {
res.description = 'Reliable node';
res.color = 'success';
}
if (score >= 75 && score < 90) {
res.description = 'Average node';
res.color = 'warning';
}
if (score > 0 && score < 75) {
res.description = 'Unreliable node';
res.color = 'error';
}
return res;
};
export const ScoreIndicator = ({ score }) => {
const { color } = getPerformanceDescriptionAndColor(score);
return (
<Box
sx={{
display: 'flex',
position: 'relative',
width: 250,
height: 250,
justifyContent: 'center',
alignItems: 'center',
mx: 'auto',
mt: 4,
}}
>
<CircularProgress
variant="determinate"
value={100}
size={250}
sx={{ position: 'absolute', top: 0, left: 0, color: 'grey.200' }}
/>
<CircularProgress
variant="determinate"
value={score}
size={250}
sx={{ position: 'absolute', top: 0, left: 0 }}
color={color}
/>
<Stack alignItems="center" gap={1}>
<Typography fontWeight="bold" variant="h4">
{Math.round(score)}%
</Typography>
<Typography>Performance Score</Typography>
</Stack>
</Box>
);
};
@@ -0,0 +1,34 @@
import React from 'react';
import { Chip } from '@mui/material';
import { HourglassTop, ErrorOutline, CheckCircleOutline, WarningAmber } from '@mui/icons-material';
import { TestState } from 'src/hooks/useNodeTesterClient';
const getColor = (state: TestState) => {
switch (state) {
case 'Connecting':
return 'warning';
case 'Error':
return 'error';
case 'Ready':
return 'success';
default:
return 'warning';
}
};
const getIcon = (state: TestState) => {
switch (state) {
case 'Connecting':
return <HourglassTop />;
case 'Error':
return <ErrorOutline />;
case 'Ready':
return <CheckCircleOutline />;
default:
return <WarningAmber />;
}
};
export const TestStatusLabel = ({ state }: { state: TestState }) => (
<Chip label={state} color={getColor(state)} icon={getIcon(state)} sx={{ color: 'white' }} />
);
@@ -0,0 +1,71 @@
import { useState, useEffect } from 'react';
import { createNodeTesterClient, NodeTester } from '@nymproject/sdk';
export type TestState = 'Ready' | 'Connecting' | 'Disconnected' | 'Disconnecting' | 'Error' | 'Testing' | 'Stopped';
export const useNodeTesterClient = () => {
const [client, setClient] = useState<NodeTester>();
const [error, setError] = useState<string>();
const [testState, setTestState] = useState<TestState>('Disconnected');
const createClient = async () => {
setTestState('Connecting');
try {
const validator = 'https://validator.nymtech.net/api';
const nodeTesterClient = await createNodeTesterClient();
await nodeTesterClient.tester.init(validator);
setClient(nodeTesterClient);
} catch (e) {
console.log(e);
setError('Failed to load node tester client, please try again');
} finally {
setTestState('Ready');
}
};
useEffect(() => {
createClient();
}, []);
const testNode = !client
? undefined
: async (mixnodeIdentity: string) => {
try {
setTestState('Testing');
const result = await client.tester.startTest(mixnodeIdentity);
setTestState('Ready');
return result;
} catch (e) {
console.log(e);
setError('Failed to test node, please try again');
setTestState('Error');
}
};
const disconnectFromGateway = !client
? undefined
: async () => {
setTestState('Disconnecting');
await client.tester.disconnectFromGateway();
setTestState('Disconnected');
};
const reconnectToGateway = !client
? undefined
: async () => {
setTestState('Connecting');
await client.tester.reconnectToGateway();
setTestState('Ready');
};
const terminateWorker = !client
? undefined
: async () => {
setTestState('Disconnecting');
await client.terminate();
setTestState('Disconnected');
};
return { testNode, disconnectFromGateway, reconnectToGateway, terminateWorker, testState, error };
};
@@ -0,0 +1,14 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';
import { NymThemeProvider } from './theme/theme';
const rootDOMElem = document.getElementById('root');
if (!rootDOMElem) throw new Error('Root element not found');
const root = createRoot(rootDOMElem);
root.render(
<NymThemeProvider>
<App />
</NymThemeProvider>,
);
@@ -0,0 +1,6 @@
import React from 'react';
import { Container } from '@mui/material';
export const BasicPageLayout = ({ children }: { children: React.ReactNode }) => (
<Container maxWidth="md">{children}</Container>
);
@@ -0,0 +1,28 @@
import * as React from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { CssBaseline } from '@mui/material';
export const NymThemeProvider = ({ children }: { children: React.ReactNode }) => {
const theme = createTheme({
palette: {
mode: 'dark',
primary: {
main: '#FB6E4E',
},
success: {
main: '#21D073',
},
background: {
default: '#1D2125',
paper: '#292E34',
},
},
});
return (
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
);
};
@@ -0,0 +1,4 @@
declare module '*.png' {
const content: any;
export default content;
}
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"jsx": "react",
"baseUrl": ".",
"paths": {
"react": ["node_modules/react"],
"react-dom": ["node_modules/react-dom"]
},
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"include": ["src"],
"paths": {
"@assets/*": ["../../../../../../assets"]
}
}
+3
View File
@@ -0,0 +1,3 @@
{
"presets": ["@babel/env"]
}
@@ -0,0 +1,15 @@
import preset from 'ts-jest/presets/index.js'
/** @type {import('ts-jest').JestConfigWithTsJest} */
export default {
...preset.defaults,
transform: {
'^.+\\.(ts|tsx)$': [
'ts-jest',
{
tsconfig: 'tsconfig.jest.json',
useESM: true,
},
],
},
}
+14 -3
View File
@@ -27,16 +27,22 @@
"prebuild:dev": "yarn build:dependencies",
"build:dev": "yarn build:dev:only-this",
"build:dev:only-this": "scripts/build.sh",
"build:local": "run-s build:dependencies:nym-client-wasm build:dev:only-this"
"build:local": "run-s build:dependencies:nym-client-wasm build:dev:only-this",
"test": "node --experimental-vm-modules --no-warnings node_modules/jest/bin/jest.js -c=jest.config.mjs --no-cache"
},
"dependencies": {
"@npmcli/node-gyp": "^3.0.0",
"@nymproject/nym-client-wasm": "1",
"@nymproject/nym-client-wasm": "1.0.0",
"comlink": "^4.3.1",
"lerna": "^6.6.2",
"node-gyp": "^9.3.1"
},
"devDependencies": {
"@babel/core": "^7.15.0",
"@babel/plugin-transform-async-to-generator": "^7.14.5",
"@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.15.0",
"@nymproject/eslint-config-react-typescript": "^1.0.0",
"@rollup/plugin-commonjs": "^24.0.1",
"@rollup/plugin-inject": "^5.0.3",
@@ -48,6 +54,8 @@
"@rollup/plugin-wasm": "^6.1.1",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.13.0",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.13",
"eslint": "^8.10.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^16.1.0",
@@ -63,6 +71,9 @@
"rollup": "^3.9.1",
"rollup-plugin-base64": "^1.0.1",
"rollup-plugin-web-worker-loader": "^1.6.1",
"typescript": "^4.8.4"
"typescript": "^4.8.4",
"jest": "^29.5.0",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.2"
}
}
@@ -0,0 +1,28 @@
// This is the rollup config for the full-fat SDK package.
// The config is similar to the esm config, but exports web workers as separate files.
// This can be necessary for implentations that do not support inline web workers.
import typescript from '@rollup/plugin-typescript';
import resolve from '@rollup/plugin-node-resolve';
import webWorkerLoader from 'rollup-plugin-web-worker-loader';
const extensions = ['.js', '.jsx', '.ts', '.tsx'];
export default {
input: 'src/index.ts',
output: {
dir: 'dist/full-fat',
format: 'es',
},
plugins: [
webWorkerLoader({
targetPlatform: 'browser',
inline: false,
}),
resolve({ extensions }),
typescript({
exclude: ['mixnet/wasm/worker.ts', 'mixnet/node-tester/worker.ts'],
compilerOptions: { outDir: 'dist/full-fat' },
}),
],
};
@@ -48,6 +48,13 @@ rollup -c rollup-esm.config.mjs
# build the SDK as a CommonJS bundle
rollup -c rollup-cjs.config.mjs
#-------------------------------------------------------
# FULL FAT
#-------------------------------------------------------
# build the SDK as a ESM bundle
rollup -c rollup-full-fat.config.mjs
#-------------------------------------------------------
# CLEAN UP
#-------------------------------------------------------
@@ -3,16 +3,16 @@ import InlineWasmWebWorker from 'web-worker:./worker';
import {
BinaryMessageReceivedEvent,
ConnectedEvent,
EventHandlerFn,
EventKinds,
IWebWorker,
IWebWorkerAsync,
IWebWorkerEvents,
LoadedEvent,
MimeTypes,
StringMessageReceivedEvent,
RawMessageReceivedEvent,
StringMessageReceivedEvent,
} from './types';
import { createSubscriptions } from './subscriptions';
/**
* Client for the Nym mixnet.
@@ -33,25 +33,13 @@ export const createNymMixnetClient = async (options?: {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
const worker = await createWorker();
// stores the subscriptions for events
const subscriptions: {
[key: string]: Array<EventHandlerFn<unknown>>;
} = {};
/**
* Helper method to get typed subscriptions
*/
const getSubscriptions = <E>(key: EventKinds): Array<EventHandlerFn<E>> => {
if (!subscriptions[key]) {
subscriptions[key] = [];
}
return subscriptions[key] as Array<EventHandlerFn<E>>;
};
const subscriptions = createSubscriptions();
const { getSubscriptions, addSubscription } = subscriptions;
// listen to messages from the worker, parse them and let the subscribers handle them, catching any unhandled exceptions
worker.addEventListener('message', (msg) => {
if (msg.data && msg.data.kind) {
const subscribers = subscriptions[msg.data.kind];
const subscribers = getSubscriptions(msg.data.kind);
(subscribers || []).forEach((s) => {
try {
// let the subscriber handle the message
@@ -66,36 +54,14 @@ export const createNymMixnetClient = async (options?: {
// manage the subscribers, returning self-unsubscribe methods
const events: IWebWorkerEvents = {
subscribeToConnected: (handler) => {
getSubscriptions<ConnectedEvent>(EventKinds.Connected).push(handler);
return () => {
getSubscriptions<ConnectedEvent>(EventKinds.Connected).unshift(handler);
};
},
subscribeToLoaded: (handler) => {
getSubscriptions<LoadedEvent>(EventKinds.Loaded).push(handler);
return () => {
getSubscriptions<LoadedEvent>(EventKinds.Loaded).unshift(handler);
};
},
subscribeToTextMessageReceivedEvent: (handler) => {
getSubscriptions<StringMessageReceivedEvent>(EventKinds.StringMessageReceived).push(handler);
return () => {
getSubscriptions<StringMessageReceivedEvent>(EventKinds.StringMessageReceived).unshift(handler);
};
},
subscribeToBinaryMessageReceivedEvent: (handler) => {
getSubscriptions<BinaryMessageReceivedEvent>(EventKinds.BinaryMessageReceived).push(handler);
return () => {
getSubscriptions<BinaryMessageReceivedEvent>(EventKinds.BinaryMessageReceived).unshift(handler);
};
},
subscribeToRawMessageReceivedEvent: (handler) => {
getSubscriptions<RawMessageReceivedEvent>(EventKinds.RawMessageReceived).push(handler);
return () => {
getSubscriptions<RawMessageReceivedEvent>(EventKinds.RawMessageReceived).unshift(handler);
};
},
subscribeToConnected: (handler) => addSubscription<ConnectedEvent>(EventKinds.Connected, handler),
subscribeToLoaded: (handler) => addSubscription<LoadedEvent>(EventKinds.Loaded, handler),
subscribeToTextMessageReceivedEvent: (handler) =>
addSubscription<StringMessageReceivedEvent>(EventKinds.StringMessageReceived, handler),
subscribeToBinaryMessageReceivedEvent: (handler) =>
addSubscription<BinaryMessageReceivedEvent>(EventKinds.BinaryMessageReceived, handler),
subscribeToRawMessageReceivedEvent: (handler) =>
addSubscription<RawMessageReceivedEvent>(EventKinds.RawMessageReceived, handler),
};
// let comlink handle interop with the web worker
@@ -0,0 +1,98 @@
import { createSubscriptions } from './subscriptions';
import { EventKinds, MimeTypes, StringMessageReceivedEvent } from './types';
describe('wasm subscription manager', () => {
test('works with default values', () => {
const { getSubscriptions, fireEvent, addSubscription } = createSubscriptions();
expect(getSubscriptions(EventKinds.StringMessageReceived)).toHaveLength(0);
// the event should fire and not fail
fireEvent(EventKinds.StringMessageReceived, {});
// mock a handler, fire events and check that it was called
const mockHandler = jest.fn();
addSubscription(EventKinds.StringMessageReceived, mockHandler);
fireEvent(EventKinds.StringMessageReceived, {});
expect(mockHandler).toHaveBeenCalled();
});
test('adding and removing subscriptions works as expected', () => {
const { addSubscription, getSubscriptions, fireEvent } = createSubscriptions();
expect(getSubscriptions(EventKinds.StringMessageReceived)).toHaveLength(0);
const callStats: number[] = [0, 0, 0];
const showDebug = false;
const handler1 = (e: StringMessageReceivedEvent) => {
if (showDebug) {
console.log('handler1', e);
}
callStats[0] += 1;
};
const handler2 = (e: StringMessageReceivedEvent) => {
if (showDebug) {
console.log('handler2', e);
}
callStats[1] += 1;
};
const handler3 = (e: StringMessageReceivedEvent) => {
if (showDebug) {
console.log('handler3', e);
}
callStats[2] += 1;
};
const unsubcribeFn1 = addSubscription(EventKinds.StringMessageReceived, handler1);
const unsubcribeFn2 = addSubscription(EventKinds.StringMessageReceived, handler2);
const unsubcribeFn3 = addSubscription(EventKinds.StringMessageReceived, handler3);
const event: StringMessageReceivedEvent = {
kind: EventKinds.StringMessageReceived,
args: {
payload: 'Testing',
mimeType: MimeTypes.TextPlain,
payloadRaw: new Uint8Array(),
},
};
// fire and expect all handlers to get message
fireEvent(EventKinds.StringMessageReceived, event);
expect(callStats[0]).toBe(1);
expect(callStats[1]).toBe(1);
expect(callStats[2]).toBe(1);
expect(getSubscriptions(EventKinds.StringMessageReceived)).toHaveLength(3);
// unscribe and fire again
unsubcribeFn2();
fireEvent(EventKinds.StringMessageReceived, event);
expect(callStats[0]).toBe(2);
expect(callStats[1]).toBe(1);
expect(callStats[2]).toBe(2);
expect(getSubscriptions(EventKinds.StringMessageReceived)).toHaveLength(2);
// unscribe and fire again
unsubcribeFn3();
fireEvent(EventKinds.StringMessageReceived, event);
expect(callStats[0]).toBe(3);
expect(callStats[1]).toBe(1);
expect(callStats[2]).toBe(2);
expect(getSubscriptions(EventKinds.StringMessageReceived)).toHaveLength(1);
// unscribe and fire again
unsubcribeFn1();
fireEvent(EventKinds.StringMessageReceived, event);
expect(callStats[0]).toBe(3);
expect(callStats[1]).toBe(1);
expect(callStats[2]).toBe(2);
expect(getSubscriptions(EventKinds.StringMessageReceived)).toHaveLength(0);
// nothing is subscribed, so fire again and check
fireEvent(EventKinds.StringMessageReceived, event);
expect(callStats[0]).toBe(3);
expect(callStats[1]).toBe(1);
expect(callStats[2]).toBe(2);
});
});
@@ -0,0 +1,69 @@
import type { EventHandlerFn } from './types';
import { EventKinds } from './types';
type ISubscriptions = {
[key: string]: Array<EventHandlerFn<unknown>>;
};
/**
* Creates a subscription manager.
*/
export const createSubscriptions = () => {
// stores the subscriptions for events
const subscriptions: ISubscriptions = {};
/**
* Helper method to get typed subscriptions.
*/
const getSubscriptions = <E>(key: EventKinds): Array<EventHandlerFn<E>> => {
if (!subscriptions[key]) {
subscriptions[key] = [];
}
return subscriptions[key] as Array<EventHandlerFn<E>>;
};
/**
* Remove a subscription.
*/
const removeSubscription = <E>(key: EventKinds, handler: EventHandlerFn<E>) => {
if (!subscriptions[key]) {
subscriptions[key] = [];
}
const items: Array<EventHandlerFn<unknown>> = (subscriptions[key] as Array<EventHandlerFn<unknown>>).filter(
(h) => h !== handler,
);
subscriptions[key] = items;
};
/**
* Add typed subscription.
*/
const addSubscription = <E>(key: EventKinds, handler: EventHandlerFn<E>) => {
getSubscriptions(key).push(handler as EventHandlerFn<unknown>);
return () => {
removeSubscription(key, handler);
};
};
/**
* Fires an event.
*/
const fireEvent = <E>(key: EventKinds, event: E) => {
getSubscriptions(key).forEach((handler) => {
try {
handler(event);
} catch (e: any) {
console.error(`Unhandled exception in handler for ${key}: `, e);
}
});
};
return {
getSubscriptions,
addSubscription,
removeSubscription,
fireEvent,
subscriptions,
};
};
@@ -0,0 +1,42 @@
{
"compileOnSave": false,
"compilerOptions": {
"lib": [
"es2021",
"dom",
"dom.iterable",
"esnext",
"webworker"
],
"module": "CommonJS",
"target": "es5",
"strict": true,
"moduleResolution": "node",
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"declaration": true,
"baseUrl": ".",
"esModuleInterop": true,
"allowJs": true
},
"include": [
"src/**/*.ts"
],
"exclude": [
"jest.*",
"webpack.config.js",
"webpack.prod.js",
"webpack.common.js",
"node_modules",
"**/node_modules",
"dist",
"**/dist",
"scripts",
"jest",
"__tests__",
"**/__tests__",
"__jest__",
"**/__jest__",
"config/*"
]
}
+768 -10
View File
File diff suppressed because it is too large Load Diff