Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 649e763732 | |||
| 03ef5254cc | |||
| e2f8611876 | |||
| 57d6f1dcfa | |||
| b79f774c48 | |||
| 415fe4605c | |||
| 70e6539298 | |||
| fe76ba68a0 | |||
| 863580a6f2 | |||
| 4c558db08e | |||
| 9d5b582908 | |||
| c12b20f1d6 |
@@ -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*
|
||||
Generated
+302
-16
@@ -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]]
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>>()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = () => (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 />} />
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
export type SpeedMode = 'fast' | 'medium' | 'slow';
|
||||
@@ -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:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
@@ -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:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -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/*"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user