Merge pull request #5687 from nymtech/feature/test-v2
Tauri V2 - Wallet Migration
@@ -16,8 +16,12 @@ jobs:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
steps:
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y libdbus-1-dev libmnl-dev libnftnl-dev \
|
||||
libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev jq \
|
||||
libgtk-3-dev squashfs-tools libayatana-appindicator3-dev make libfuse2 unzip librsvg2-dev file \
|
||||
libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
|
||||
continue-on-error: true
|
||||
|
||||
- name: Check out repository code
|
||||
|
||||
@@ -18,11 +18,7 @@ jobs:
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
outputs:
|
||||
release_id: ${{ steps.create-release.outputs.id }}
|
||||
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].created_at }}
|
||||
version: ${{ steps.release-info.outputs.version }}
|
||||
filename: ${{ steps.release-info.outputs.filename }}
|
||||
file_hash: ${{ steps.release-info.outputs.file_hash }}
|
||||
release_tag: ${{ github.ref_name }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -33,10 +29,16 @@ jobs:
|
||||
node-version: 21
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Add Rust target for x86_64-apple-darwin
|
||||
run: rustup target add x86_64-apple-darwin
|
||||
|
||||
- name: Set Cargo build target to x86_64
|
||||
run: echo "CARGO_BUILD_TARGET=x86_64-apple-darwin" >> $GITHUB_ENV
|
||||
|
||||
- name: Install the Apple developer certificate for code signing
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
@@ -66,12 +68,6 @@ jobs:
|
||||
fileName: '.env'
|
||||
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
|
||||
|
||||
- name: Add Rust target for x86_64-apple-darwin
|
||||
run: rustup target add x86_64-apple-darwin
|
||||
|
||||
- name: Set Cargo build target to x86_64
|
||||
run: echo "CARGO_BUILD_TARGET=x86_64-apple-darwin" >> $GITHUB_ENV
|
||||
|
||||
- name: Yarn cache clean
|
||||
shell: bash
|
||||
run: cd .. && yarn cache clean
|
||||
@@ -94,10 +90,22 @@ jobs:
|
||||
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 }}
|
||||
# Tauri v2 specific environment variables
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
TAURI_NOTARIZATION_USERNAME: ${{ secrets.APPLE_ID }}
|
||||
TAURI_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
TAURI_NOTARIZATION_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: |
|
||||
yarn build-macx86
|
||||
yarn build-macx86
|
||||
|
||||
- name: Create app tarball
|
||||
run: |
|
||||
# Navigate to where the app bundle is and create the tarball
|
||||
cd target/x86_64-apple-darwin/release/bundle/macos
|
||||
echo "Creating tarball from app bundle"
|
||||
tar -czf nym-wallet.app.tar.gz NymWallet.app
|
||||
cd -
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -120,22 +128,10 @@ jobs:
|
||||
nym-wallet/target/x86_64-apple-darwin/release/bundle/dmg/*.dmg
|
||||
nym-wallet/target/x86_64-apple-darwin/release/bundle/macos/*.app.tar.gz*
|
||||
|
||||
- name: Deploy artifacts to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-avzr"
|
||||
SOURCE: "nym-wallet/target/x86_64-apple-darwin/release/bundle/macos/nym-wallet.app.tar.gz"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/${{ github.ref_name }}/nym-wallet
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
|
||||
push-release-data:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
uses: ./.github/workflows/release-calculate-hash.yml
|
||||
needs: publish-tauri
|
||||
with:
|
||||
release_tag: ${{ github.ref_name }}
|
||||
secrets: inherit
|
||||
release_tag: ${{ needs.publish-tauri.outputs.release_tag || github.ref_name }}
|
||||
secrets: inherit
|
||||
@@ -3,71 +3,108 @@ on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: nym-wallet
|
||||
|
||||
jobs:
|
||||
publish-tauri:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [custom-ubuntu-22.04]
|
||||
platform: [ubuntu-22.04]
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
outputs:
|
||||
release_id: ${{ steps.create-release.outputs.id }}
|
||||
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].created_at }}
|
||||
version: ${{ steps.release-info.outputs.version }}
|
||||
filename: ${{ steps.release-info.outputs.filename }}
|
||||
file_hash: ${{ steps.release-info.outputs.file_hash }}
|
||||
|
||||
release_tag: ${{ github.ref_name }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Tauri dependencies
|
||||
run: >
|
||||
sudo apt-get update &&
|
||||
sudo apt-get install -y webkit2gtk-4.0
|
||||
continue-on-error: true
|
||||
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y libdbus-1-dev libmnl-dev libnftnl-dev \
|
||||
libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev jq \
|
||||
libgtk-3-dev squashfs-tools libayatana-appindicator3-dev make libfuse2 unzip librsvg2-dev file \
|
||||
libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
|
||||
|
||||
- name: Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
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: Build app
|
||||
run: yarn build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
- name: Check bundle directory
|
||||
run: |
|
||||
echo "Checking bundle directory structure"
|
||||
ls -la target/release/bundle || echo "Bundle directory not found"
|
||||
if [ -d "target/release/bundle/appimage" ]; then
|
||||
echo "AppImage bundle directory exists, checking contents:"
|
||||
ls -la target/release/bundle/appimage
|
||||
else
|
||||
echo "AppImage bundle directory not found, checking alternatives:"
|
||||
find target/release/bundle -type d -name "*appimage*" -o -name "*AppImage*" || echo "No AppImage directories found"
|
||||
find target/release/bundle -name "*.AppImage" -o -name "*.appimage" || echo "No AppImage files found"
|
||||
fi
|
||||
|
||||
- name: Create AppImage tarball if needed
|
||||
run: |
|
||||
# Find the AppImage file
|
||||
APPIMAGE_FILE=$(find target/release/bundle -name "*.AppImage" | head -n 1)
|
||||
if [ -n "$APPIMAGE_FILE" ]; then
|
||||
echo "Found AppImage file: $APPIMAGE_FILE"
|
||||
APPIMAGE_DIR=$(dirname "$APPIMAGE_FILE")
|
||||
APPIMAGE_NAME=$(basename "$APPIMAGE_FILE")
|
||||
|
||||
# Create tarball if it doesn't exist
|
||||
if [ ! -f "${APPIMAGE_FILE}.tar.gz" ]; then
|
||||
echo "Creating tarball for $APPIMAGE_NAME"
|
||||
cd "$APPIMAGE_DIR"
|
||||
tar -czf "${APPIMAGE_NAME}.tar.gz" "$APPIMAGE_NAME"
|
||||
cd -
|
||||
echo "Created tarball: ${APPIMAGE_FILE}.tar.gz"
|
||||
else
|
||||
echo "Tarball already exists: ${APPIMAGE_FILE}.tar.gz"
|
||||
fi
|
||||
else
|
||||
echo "WARNING: No AppImage file found!"
|
||||
fi
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nym-wallet_1.0.0_amd64.AppImage.tar.gz
|
||||
path: nym-wallet/target/release/bundle/appimage/nym-wallet*.AppImage.tar.gz
|
||||
name: nym-wallet-appimage.tar.gz
|
||||
path: |
|
||||
nym-wallet/target/release/bundle/appimage/*.AppImage.tar.gz
|
||||
nym-wallet/target/release/bundle/*/nym-wallet*.AppImage.tar.gz
|
||||
retention-days: 30
|
||||
|
||||
|
||||
- id: create-release
|
||||
name: Upload to release based on tag name
|
||||
uses: softprops/action-gh-release@v2
|
||||
@@ -75,24 +112,26 @@ jobs:
|
||||
with:
|
||||
files: |
|
||||
nym-wallet/target/release/bundle/appimage/*.AppImage
|
||||
nym-wallet/target/release/bundle/appimage/*.AppImage.tar.gz*
|
||||
|
||||
- name: Deploy artifacts to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-avzr"
|
||||
SOURCE: "nym-wallet/target/release/bundle/appimage/nym-wallet*.AppImage.tar.gz"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/${{ github.ref_name }}/nym-wallet
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
|
||||
nym-wallet/target/release/bundle/appimage/*.AppImage.tar.gz
|
||||
nym-wallet/target/release/bundle/*/nym-wallet*.AppImage
|
||||
nym-wallet/target/release/bundle/*/nym-wallet*.AppImage.tar.gz
|
||||
|
||||
- name: Find AppImage tarball path for deployment
|
||||
id: find-appimage
|
||||
run: |
|
||||
APPIMAGE_TARBALL=$(find target/release/bundle -name "*.AppImage.tar.gz" | head -n 1)
|
||||
if [ -n "$APPIMAGE_TARBALL" ]; then
|
||||
echo "Found AppImage tarball: $APPIMAGE_TARBALL"
|
||||
echo "appimage_path=$APPIMAGE_TARBALL" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "WARNING: No AppImage tarball found for deployment!"
|
||||
echo "appimage_path=target/release/bundle/appimage/nym-wallet*.AppImage.tar.gz" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
push-release-data:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
uses: ./.github/workflows/release-calculate-hash.yml
|
||||
needs: publish-tauri
|
||||
with:
|
||||
release_tag: ${{ github.ref_name }}
|
||||
secrets: inherit
|
||||
release_tag: ${{ needs.publish-tauri.outputs.release_tag || github.ref_name }}
|
||||
secrets: inherit
|
||||
@@ -1,6 +1,12 @@
|
||||
name: publish-nym-wallet-win11
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
sign:
|
||||
description: "Sign this build using SSL.com. Signing is billed per signature so be careful"
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
@@ -18,53 +24,61 @@ jobs:
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
outputs:
|
||||
release_id: ${{ steps.create-release.outputs.id }}
|
||||
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].created_at }}
|
||||
version: ${{ steps.release-info.outputs.version }}
|
||||
filename: ${{ steps.release-info.outputs.filename }}
|
||||
file_hash: ${{ steps.release-info.outputs.file_hash }}
|
||||
release_tag: ${{ github.ref_name }}
|
||||
|
||||
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@v4
|
||||
|
||||
- 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: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Setup MSBuild.exe
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
|
||||
- name: Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
|
||||
- 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 Yarn
|
||||
run: npm install -g yarn
|
||||
|
||||
- name: Download EV CodeSignTool from ssl.com
|
||||
working-directory: nym-wallet/src-tauri
|
||||
if: ${{ inputs.sign }}
|
||||
shell: bash
|
||||
run: |
|
||||
curl -L0 https://www.ssl.com/download/codesigntool-for-linux-and-macos/ -o codesigntool.zip
|
||||
unzip codesigntool.zip
|
||||
- name: Get EV certificate credential id
|
||||
working-directory: nym-wallet/src-tauri
|
||||
if: ${{ inputs.sign }}
|
||||
id: get_credential_ids
|
||||
shell: bash
|
||||
run: |
|
||||
echo "SSL_COM_CREDENTIAL_ID=$(./CodeSignTool.sh get_credential_ids -username=${{ secrets.SSL_COM_USERNAME }} -password=${{ secrets.SSL_COM_PASSWORD }} | sed -n '1!p' | sed 's/- //')" >> "$GITHUB_OUTPUT"
|
||||
- name: Add custom sign command to tauri.conf.json
|
||||
working-directory: nym-wallet/src-tauri
|
||||
if: ${{ inputs.sign }}
|
||||
shell: bash
|
||||
run: |
|
||||
yq eval --inplace '.bundle.windows +=
|
||||
{
|
||||
"signCommand": {
|
||||
"cmd": "C:\Program Files\Git\bin\bash.EXE",
|
||||
"args": [
|
||||
"/c/actions-runner/_work/nym/nym/nym-wallet/src-tauri/CodeSignTool.sh",
|
||||
"sign",
|
||||
"-username ${{ secrets.SSL_COM_USERNAME }}",
|
||||
"-password ${{ secrets.SSL_COM_PASSWORD }}",
|
||||
"-credential_id ${{ steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}",
|
||||
"-totp_secret ${{ secrets.SSL_COM_TOTP_SECRET }}",
|
||||
"-program_name NymWallet",
|
||||
"-input_file_path",
|
||||
"%1",
|
||||
"-override"
|
||||
]
|
||||
}
|
||||
}' tauri.conf.json
|
||||
- name: Install project dependencies
|
||||
shell: bash
|
||||
run: cd .. && yarn --network-timeout 100000
|
||||
@@ -77,18 +91,50 @@ jobs:
|
||||
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 }}
|
||||
run: yarn build
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
SSL_COM_USERNAME: ${{ inputs.sign && secrets.SSL_COM_USERNAME }}
|
||||
SSL_COM_PASSWORD: ${{ inputs.sign && secrets.SSL_COM_PASSWORD }}
|
||||
SSL_COM_CREDENTIAL_ID: ${{ inputs.sign && steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
|
||||
SSL_COM_TOTP_SECRET: ${{ inputs.sign && secrets.SSL_COM_TOTP_SECRET }}
|
||||
run: |
|
||||
echo "Starting build process..."
|
||||
yarn build
|
||||
|
||||
- name: Check bundle directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Checking bundle directory structure"
|
||||
|
||||
# Check standard location
|
||||
if [ -d "target/release/bundle" ]; then
|
||||
echo "Found bundle directory at standard location"
|
||||
ls -la target/release/bundle || echo "Failed to list bundle directory"
|
||||
fi
|
||||
|
||||
# Check src-tauri location
|
||||
if [ -d "src-tauri/target/release/bundle" ]; then
|
||||
echo "Found bundle directory in src-tauri"
|
||||
ls -la src-tauri/target/release/bundle || echo "Failed to list src-tauri bundle directory"
|
||||
|
||||
# Use this path for future steps
|
||||
echo "BUNDLE_PATH=src-tauri/target/release/bundle" >> $GITHUB_ENV
|
||||
else
|
||||
echo "Using standard bundle path"
|
||||
echo "BUNDLE_PATH=target/release/bundle" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# Check for MSI files in any location
|
||||
find . -name "*.msi" -type f
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nym-wallet_1.0.0_x64_en-US.msi
|
||||
path: nym-wallet/target/release/bundle/msi/nym-wallet_1.*.msi
|
||||
name: nym-wallet.msi
|
||||
path: |
|
||||
nym-wallet/${{ env.BUNDLE_PATH }}/msi/*.msi
|
||||
nym-wallet/${{ env.BUNDLE_PATH }}/*/nym-wallet*.msi
|
||||
nym-wallet/src-tauri/target/release/bundle/msi/*.msi
|
||||
retention-days: 30
|
||||
|
||||
- id: create-release
|
||||
@@ -97,25 +143,28 @@ jobs:
|
||||
if: github.event_name == 'release'
|
||||
with:
|
||||
files: |
|
||||
nym-wallet/target/release/bundle/msi/*.msi
|
||||
nym-wallet/target/release/bundle/msi/*.msi.zip*
|
||||
|
||||
- name: Deploy artifacts to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-avzr"
|
||||
SOURCE: "nym-wallet/target/release/bundle/msi/nym-wallet_1.*.msi"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/${{ github.ref_name }}/nym-wallet
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
nym-wallet/${{ env.BUNDLE_PATH }}/msi/*.msi
|
||||
nym-wallet/${{ env.BUNDLE_PATH }}/msi/*.msi.zip*
|
||||
nym-wallet/${{ env.BUNDLE_PATH }}/*/nym-wallet*.msi
|
||||
nym-wallet/src-tauri/target/release/bundle/msi/*.msi
|
||||
|
||||
- name: Find MSI path for deployment
|
||||
id: find-msi
|
||||
shell: bash
|
||||
run: |
|
||||
MSI_FILE=$(find . -name "*.msi" -type f | head -n 1)
|
||||
if [ -n "$MSI_FILE" ]; then
|
||||
echo "Found MSI file: $MSI_FILE"
|
||||
echo "msi_path=$MSI_FILE" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "WARNING: No MSI file found for deployment!"
|
||||
echo "msi_path=${{ env.BUNDLE_PATH }}/msi/nym-wallet*.msi" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
push-release-data:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
uses: ./.github/workflows/release-calculate-hash.yml
|
||||
needs: publish-tauri
|
||||
with:
|
||||
release_tag: ${{ github.ref_name }}
|
||||
secrets: inherit
|
||||
release_tag: ${{ needs.publish-tauri.outputs.release_tag || github.ref_name }}
|
||||
secrets: inherit
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@nymproject/nym-wallet-app",
|
||||
"version": "1.2.17",
|
||||
"version": "1.2.18",
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "run-s webpack:prod tauri:build",
|
||||
"dev": "run-p tauri:dev webpack:dev",
|
||||
"build-macx86": "run-s webpack:prod tauri:buildx86",
|
||||
"dev": "run-p tauri:dev webpack:dev",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"prebuild": "yarn --cwd .. build",
|
||||
@@ -15,14 +15,15 @@
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"storybook:build": "build-storybook",
|
||||
"tauri:build": "yarn tauri build",
|
||||
"tauri:buildx86": "yarn tauri build --target x86_64-apple-darwin",
|
||||
"tauri:dev": "yarn tauri dev",
|
||||
"tauri:buildx86": "yarn tauri build --target x86_64-apple-darwin",
|
||||
"tsc": "tsc --noEmit true",
|
||||
"tsc:watch": "tsc --noEmit true --watch",
|
||||
"webpack:dev": "yarn webpack serve --config webpack.dev.js",
|
||||
"webpack:prod": "yarn webpack --progress --config webpack.prod.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-simple-access": "^7.25.9",
|
||||
"@emotion/react": "^11.7.0",
|
||||
"@emotion/styled": "^11.6.0",
|
||||
"@hookform/resolvers": "^2.8.0",
|
||||
@@ -35,7 +36,11 @@
|
||||
"@nymproject/react": "^1.0.0",
|
||||
"@nymproject/types": "^1.0.0",
|
||||
"@storybook/react": "^6.5.15",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"@tauri-apps/api": "^2.4.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
|
||||
"@tauri-apps/plugin-opener": "^2.2.6",
|
||||
"@tauri-apps/plugin-shell": "^2.2.1",
|
||||
"@tauri-apps/plugin-updater": "^2.0.0",
|
||||
"@tauri-apps/tauri-forage": "^1.0.0-beta.2",
|
||||
"big.js": "^6.2.1",
|
||||
"bs58": "^4.0.1",
|
||||
@@ -69,7 +74,7 @@
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
|
||||
"@storybook/react": "^6.5.15",
|
||||
"@svgr/webpack": "^6.1.1",
|
||||
"@tauri-apps/cli": "^1.0.5",
|
||||
"@tauri-apps/cli": "^2.4.0",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
"@types/big.js": "^6.1.6",
|
||||
@@ -127,4 +132,4 @@
|
||||
"webpack-merge": "^5.8.0"
|
||||
},
|
||||
"private": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[capabilities]
|
||||
shell = { open = true }
|
||||
@@ -1,25 +1,27 @@
|
||||
[package]
|
||||
name = "nym_wallet"
|
||||
version = "1.2.17"
|
||||
name = "NymWallet"
|
||||
version = "1.2.18"
|
||||
description = "Nym Native Wallet"
|
||||
authors = ["Nym Technologies SA"]
|
||||
license = ""
|
||||
repository = ""
|
||||
default-run = "nym_wallet"
|
||||
default-run = "NymWallet"
|
||||
edition = "2021"
|
||||
build = "src/build.rs"
|
||||
rust-version = "1.76"
|
||||
rust-version = "1.85"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "=1.2.1", features = [] }
|
||||
|
||||
tauri-codegen = "=1.2.1"
|
||||
tauri-macros = "=1.2.1"
|
||||
tauri-build = { version = "2.1.1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.68"
|
||||
tauri-plugin-updater = "2.7.0"
|
||||
tauri-plugin-clipboard-manager = "2.0.0"
|
||||
tauri-plugin-shell = "2.2.1"
|
||||
tauri-plugin-process = "2.2.1"
|
||||
tauri-plugin-opener = "2.2.6"
|
||||
bip39 = { version = "2.0.0", features = ["zeroize", "rand"] }
|
||||
cfg-if = "1.0.0"
|
||||
colored = "2.0"
|
||||
@@ -31,6 +33,7 @@ futures = "0.3.15"
|
||||
itertools = "0.10"
|
||||
log = { version = "0.4", features = ["serde"] }
|
||||
once_cell = "1.7.2"
|
||||
open = "5.3.2"
|
||||
pretty_env_logger = "0.4"
|
||||
reqwest = { version = "0.12.4", features = ["json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@@ -38,7 +41,7 @@ serde_json = "1.0"
|
||||
serde_repr = "0.1"
|
||||
strum = { version = "0.23", features = ["derive"] }
|
||||
tap = "1"
|
||||
tauri = { version = "=1.2.3", features = ["clipboard-all", "shell-open", "updater", "window-maximize", "window-print"] }
|
||||
tauri = { version = "2", features = [] }
|
||||
#tendermint-rpc = "0.23.0"
|
||||
time = { version = "0.3.30", features = ["local-offset"] }
|
||||
thiserror = "1.0"
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "main-capability",
|
||||
"description": "Default capability for Nym Wallet main window",
|
||||
"windows": [
|
||||
"main",
|
||||
"nymWalletApp",
|
||||
"log"
|
||||
],
|
||||
"platforms": [
|
||||
"linux",
|
||||
"macOS",
|
||||
"windows"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:path:default",
|
||||
"core:event:default",
|
||||
"core:window:default",
|
||||
"core:app:default",
|
||||
"core:resources:default",
|
||||
"core:menu:default",
|
||||
"core:tray:default",
|
||||
"opener:allow-open-url",
|
||||
"opener:allow-default-urls",
|
||||
"core:window:allow-set-title",
|
||||
"core:app:allow-version",
|
||||
"clipboard-manager:allow-read-text",
|
||||
"clipboard-manager:allow-write-text",
|
||||
"updater:default",
|
||||
"updater:allow-check",
|
||||
"updater:allow-download",
|
||||
"updater:allow-download-and-install",
|
||||
"updater:allow-install",
|
||||
"core:event:allow-listen"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"main-capability":{"identifier":"main-capability","description":"Default capability for Nym Wallet main window","local":true,"windows":["main","nymWalletApp","log"],"permissions":["core:default","core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","opener:allow-open-url","opener:allow-default-urls","core:window:allow-set-title","core:app:allow-version","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text","updater:default","updater:allow-check","updater:allow-download","updater:allow-download-and-install","updater:allow-install","core:event:allow-listen"],"platforms":["linux","macOS","windows"]}}
|
||||
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 557 B After Width: | Height: | Size: 1008 B |
@@ -1,24 +0,0 @@
|
||||
# Regenerating icons
|
||||
|
||||
> **Note**: This is likely to be temporary until `tauri icon` is put back into the CLI.
|
||||
|
||||
The Tauri Docs say to use the CLI to generate icons: https://tauri.studio/docs/api/cli/#icon. However `1.0.0-rc.X` appears to not have this command. `1.0.0-beta.6` does 🎉!
|
||||
|
||||
Do the following to regenerate the icons:
|
||||
|
||||
```
|
||||
cd ~
|
||||
git clone nym ...
|
||||
cd nym
|
||||
docker run -v "$(pwd)":/workspace -it node:16 /bin/bash
|
||||
npm i -g @tauri-apps/cli@1.0.0-beta.6
|
||||
cd /workspace/nym-wallet/src-tauri
|
||||
tauri icon /workspace/assets/appicon/appicon.png
|
||||
exit
|
||||
```
|
||||
|
||||
Reasons to use docker:
|
||||
|
||||
- you can't destroy your dev environments `npm` cache
|
||||
- if you mess it up, kill the container, try again
|
||||
- inside the `src-tauri` directory, `node` will resolve to the nearest `node_modules` directory and you'll get the wrong `tauri` cli
|
||||
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 502 B |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 701 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 765 B |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 401 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 401 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
@@ -105,7 +105,17 @@ impl NetworkConfig {
|
||||
|
||||
impl Config {
|
||||
fn root_directory() -> PathBuf {
|
||||
tauri::api::path::config_dir().expect("Failed to get config directory")
|
||||
// tauri v1 (via `tauri::api::path::config_dir()`) was internally calling `dirs_next::config_dir()`
|
||||
// which ultimately was getting resolved to
|
||||
// - **Linux:** Resolves to `$XDG_CONFIG_HOME` or `$HOME/.config`.
|
||||
// - **macOS:** Resolves to `$HOME/Library/Application Support`.
|
||||
// - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`.
|
||||
//
|
||||
// tauri v2 calls `dirs::config_dir().ok_or(Error::UnknownPath)` which ultimately does the same thing,
|
||||
// however, it changed its API so that it's called on a `PathResolver`.
|
||||
// but, to instantiate one here would be a hassle as we don't need those specific functionalities,
|
||||
// so let's just recreate tauri's behaviour
|
||||
dirs::config_dir().expect("Failed to get config directory")
|
||||
}
|
||||
|
||||
fn config_directory() -> PathBuf {
|
||||
@@ -243,6 +253,7 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::to_string_in_format_args)]
|
||||
pub fn set_default_nyxd_url(&mut self, nyxd_url: Url, network: &WalletNetwork) {
|
||||
log::debug!(
|
||||
"set default nyxd URL for {network} {}",
|
||||
@@ -300,7 +311,7 @@ impl Config {
|
||||
self.networks.get(&network.as_key()).and_then(|config| {
|
||||
log::debug!(
|
||||
"get selected nyxd url for {} {:?}",
|
||||
network.to_string(),
|
||||
network,
|
||||
config.selected_nyxd_url,
|
||||
);
|
||||
config.selected_nyxd_url.clone()
|
||||
@@ -311,7 +322,7 @@ impl Config {
|
||||
self.networks.get(&network.as_key()).and_then(|config| {
|
||||
log::debug!(
|
||||
"get default nyxd url for {} {:?}",
|
||||
network.to_string(),
|
||||
network,
|
||||
config.default_nyxd_url,
|
||||
);
|
||||
config.default_nyxd_url.clone()
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::str::FromStr;
|
||||
use fern::colors::{Color, ColoredLevelConfig};
|
||||
use serde::Serialize;
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use tauri::Manager;
|
||||
use tauri::Emitter;
|
||||
use time::{format_description, OffsetDateTime};
|
||||
|
||||
fn formatted_time() -> String {
|
||||
@@ -61,7 +61,7 @@ pub fn setup_logging(app_handle: tauri::AppHandle) -> Result<(), log::SetLoggerE
|
||||
message: record.args().to_string(),
|
||||
level: record.level().into(),
|
||||
};
|
||||
app_handle.emit_all("log://log", msg).unwrap();
|
||||
app_handle.emit("log://log", msg).unwrap();
|
||||
}));
|
||||
|
||||
base_config
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use tauri::{Manager, Menu};
|
||||
|
||||
use nym_mixnet_contract_common::{Gateway, MixNode};
|
||||
use tauri::menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder};
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_opener::init as init_opener;
|
||||
use tauri_plugin_shell::init as init_shell;
|
||||
use tauri_plugin_updater::Builder as UpdaterBuilder;
|
||||
|
||||
use crate::menu::AddDefaultSubmenus;
|
||||
use crate::menu::SHOW_LOG_WINDOW;
|
||||
use crate::operations::app;
|
||||
use crate::operations::help;
|
||||
use crate::operations::mixnet;
|
||||
@@ -34,8 +37,13 @@ fn main() {
|
||||
|
||||
let context = tauri::generate_context!();
|
||||
tauri::Builder::default()
|
||||
.plugin(init_shell())
|
||||
.plugin(init_opener())
|
||||
.plugin(UpdaterBuilder::new().build())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.manage(WalletState::default())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
app::link::open_url,
|
||||
app::version::check_version,
|
||||
mixnet::account::add_account_for_password,
|
||||
mixnet::account::archive_wallet_file,
|
||||
@@ -121,7 +129,6 @@ fn main() {
|
||||
nym_api::status::compute_mixnode_reward_estimation,
|
||||
nym_api::status::gateway_core_node_status,
|
||||
nym_api::status::mixnode_core_node_status,
|
||||
nym_api::status::mixnode_inclusion_probability,
|
||||
nym_api::status::mixnode_reward_estimation,
|
||||
nym_api::status::mixnode_stake_saturation,
|
||||
nym_api::status::mixnode_status,
|
||||
@@ -210,13 +217,31 @@ fn main() {
|
||||
app::react::set_react_state,
|
||||
app::react::get_react_state,
|
||||
])
|
||||
.menu(Menu::os_default(&context.package_info().name).add_default_app_submenus())
|
||||
.on_menu_event(|event| {
|
||||
if event.menu_item_id() == menu::SHOW_LOG_WINDOW {
|
||||
let _r = help::log::help_log_toggle_window(event.window().app_handle());
|
||||
.menu(|app| {
|
||||
// Create a menu builder
|
||||
let menu_builder = MenuBuilder::new(app);
|
||||
if ::std::env::var("NYM_WALLET_ENABLE_LOG").is_ok() {
|
||||
let help_text = MenuItemBuilder::with_id(SHOW_LOG_WINDOW, "Show logs")
|
||||
.build(app)
|
||||
.expect("Failed to create menu item");
|
||||
|
||||
let submenu = SubmenuBuilder::new(app, "Help")
|
||||
.items(&[&help_text])
|
||||
.build()
|
||||
.expect("Failed to create help submenu");
|
||||
|
||||
menu_builder.item(&submenu).build()
|
||||
} else {
|
||||
// Build a default menu without the submenu
|
||||
menu_builder.build()
|
||||
}
|
||||
})
|
||||
.setup(|app| Ok(log::setup_logging(app.app_handle())?))
|
||||
.on_menu_event(|app, event| {
|
||||
if event.id() == SHOW_LOG_WINDOW {
|
||||
let _r = help::log::help_log_toggle_window(app.app_handle().clone());
|
||||
}
|
||||
})
|
||||
.setup(|app| Ok(log::setup_logging(app.app_handle().clone())?))
|
||||
.run(context)
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
use tauri::Menu;
|
||||
use tauri::{CustomMenuItem, Submenu};
|
||||
use tauri::menu::Menu;
|
||||
use tauri::menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder};
|
||||
|
||||
pub const SHOW_LOG_WINDOW: &str = "show_log_window";
|
||||
|
||||
pub trait AddDefaultSubmenus {
|
||||
#[allow(dead_code)]
|
||||
fn add_default_app_submenus(self) -> Self;
|
||||
}
|
||||
|
||||
impl AddDefaultSubmenus for Menu {
|
||||
impl<R: tauri::Runtime> AddDefaultSubmenus for Menu<R> {
|
||||
#[allow(dead_code)]
|
||||
fn add_default_app_submenus(self) -> Self {
|
||||
if ::std::env::var("NYM_WALLET_ENABLE_LOG").is_ok() {
|
||||
let submenu = Submenu::new(
|
||||
"Help",
|
||||
Menu::new().add_item(CustomMenuItem::new(SHOW_LOG_WINDOW, "Show logs")),
|
||||
);
|
||||
return self.add_submenu(submenu);
|
||||
let app_handle = self.app_handle();
|
||||
|
||||
let help_text = MenuItemBuilder::with_id(SHOW_LOG_WINDOW, "Show logs")
|
||||
.build(app_handle)
|
||||
.expect("Failed to create menu item");
|
||||
|
||||
let submenu = SubmenuBuilder::new(app_handle, "Help")
|
||||
.items(&[&help_text])
|
||||
.build()
|
||||
.expect("Failed to create help submenu");
|
||||
|
||||
let menu_builder = MenuBuilder::new(app_handle);
|
||||
|
||||
match menu_builder.item(&submenu).build() {
|
||||
Ok(new_menu) => new_menu,
|
||||
Err(_) => self,
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_url(url: String, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
println!("Opening URL: {}", url);
|
||||
|
||||
match app_handle.opener().open_url(&url, None::<&str>) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("Failed to open URL: {}", err)),
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod link;
|
||||
pub mod react;
|
||||
pub mod version;
|
||||
pub mod window;
|
||||
|
||||
@@ -3,27 +3,46 @@
|
||||
|
||||
use crate::error::BackendError;
|
||||
use nym_wallet_types::app::AppVersion;
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_version(handle: tauri::AppHandle) -> Result<AppVersion, BackendError> {
|
||||
log::info!(">>> Getting app version info");
|
||||
let res = tauri::updater::builder(handle)
|
||||
.check()
|
||||
.await
|
||||
.map(|u| AppVersion {
|
||||
current_version: u.current_version().to_string(),
|
||||
latest_version: u.latest_version().to_owned(),
|
||||
is_update_available: u.is_update_available(),
|
||||
|
||||
let updater = handle.updater().map_err(|e| {
|
||||
log::error!("Failed to get updater: {}", e);
|
||||
BackendError::CheckAppVersionError
|
||||
})?;
|
||||
|
||||
// Then check for updates
|
||||
let update_info = updater.check().await.map_err(|e| {
|
||||
log::error!("An error occurred while checking for app update {}", e);
|
||||
BackendError::CheckAppVersionError
|
||||
})?;
|
||||
|
||||
// Process the result
|
||||
if let Some(update) = update_info {
|
||||
log::debug!(
|
||||
"<<< update available: [true], current version {}, latest version {}",
|
||||
update.current_version,
|
||||
update.version
|
||||
);
|
||||
Ok(AppVersion {
|
||||
current_version: update.current_version.to_string(),
|
||||
latest_version: update.version,
|
||||
is_update_available: true,
|
||||
})
|
||||
.map_err(|e| {
|
||||
log::error!("An error ocurred while checking for app update {}", e);
|
||||
BackendError::CheckAppVersionError
|
||||
})?;
|
||||
log::debug!(
|
||||
"<<< update available: [{}], current version {}, latest version {}",
|
||||
res.is_update_available,
|
||||
res.current_version,
|
||||
res.latest_version
|
||||
);
|
||||
Ok(res)
|
||||
} else {
|
||||
// No update available
|
||||
let current_version = handle.package_info().version.to_string();
|
||||
log::debug!(
|
||||
"<<< update available: [false], current version {}",
|
||||
current_version
|
||||
);
|
||||
Ok(AppVersion {
|
||||
current_version: current_version.clone(),
|
||||
latest_version: current_version,
|
||||
is_update_available: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,30 +26,30 @@ async fn create_window(
|
||||
) -> Result<(), BackendError> {
|
||||
// create the new window first, to stop the app process from exiting
|
||||
log::info!("Creating {} window...", new_window_label);
|
||||
match tauri::WindowBuilder::new(
|
||||
match tauri::WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
new_window_label,
|
||||
tauri::WindowUrl::App(new_window_url.into()),
|
||||
tauri::WebviewUrl::App(new_window_url.into()),
|
||||
)
|
||||
.title("Nym Wallet")
|
||||
.build()
|
||||
{
|
||||
Ok(window) => {
|
||||
if let Err(err) = window.set_focus() {
|
||||
log::error!("Unable to focus log window: {err}");
|
||||
log::error!("Unable to focus window: {err}");
|
||||
}
|
||||
if let Err(err) = window.maximize() {
|
||||
log::error!("Could not maximize window: {err}");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Unable to create log window: {err}");
|
||||
log::error!("Unable to create window: {err}");
|
||||
return Err(BackendError::NewWindowError);
|
||||
}
|
||||
}
|
||||
|
||||
// close the old window
|
||||
match app_handle.windows().get(try_close_window_label) {
|
||||
match app_handle.get_webview_window(try_close_window_label) {
|
||||
Some(try_close_window) => {
|
||||
if let Err(err) = try_close_window.close() {
|
||||
log::error!("Could not close window: {err}")
|
||||
|
||||
@@ -3,7 +3,7 @@ use tauri::Manager;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn help_log_toggle_window(app_handle: tauri::AppHandle) -> Result<(), BackendError> {
|
||||
if let Some(current_log_window) = app_handle.windows().get("log") {
|
||||
if let Some(current_log_window) = app_handle.get_webview_window("log") {
|
||||
log::info!("Closing log window...");
|
||||
if let Err(err) = current_log_window.close() {
|
||||
log::error!("Unable to close log window: {err}");
|
||||
@@ -12,9 +12,13 @@ pub fn help_log_toggle_window(app_handle: tauri::AppHandle) -> Result<(), Backen
|
||||
}
|
||||
|
||||
log::info!("Creating log window...");
|
||||
match tauri::WindowBuilder::new(&app_handle, "log", tauri::WindowUrl::App("log.html".into()))
|
||||
.title("Nym Wallet Logs")
|
||||
.build()
|
||||
match tauri::WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
"log",
|
||||
tauri::WebviewUrl::App("log.html".into()),
|
||||
)
|
||||
.title("Nym Wallet Logs")
|
||||
.build()
|
||||
{
|
||||
Ok(window) => {
|
||||
if let Err(err) = window.set_focus() {
|
||||
|
||||
@@ -407,14 +407,22 @@ pub async fn get_all_mix_delegations(
|
||||
d.height
|
||||
);
|
||||
let timestamp = client
|
||||
.nyxd
|
||||
.get_block_timestamp(Some(d.height as u32))
|
||||
.await
|
||||
.tap_err(|err| {
|
||||
.nyxd
|
||||
.get_block_timestamp(Some(d.height as u32))
|
||||
.await
|
||||
.tap_err(|err| {
|
||||
let error_message = err.to_string();
|
||||
// Check if the error is related to height not being available (pruning)
|
||||
if error_message.contains("height") && error_message.contains("not available") {
|
||||
let str_err = "Due to pruning strategies from validators, please navigate to the Settings tab and change your RPC node for your validator to retrieve your delegations.";
|
||||
log::error!(" <<< {}", str_err);
|
||||
error_strings.push(str_err.to_string());
|
||||
} else {
|
||||
let str_err = format!("Failed to get block timestamp for height = {} for delegation to mix_id = {}. Error: {}", d.height, d.mix_id, err);
|
||||
log::error!(" <<< {}", str_err);
|
||||
error_strings.push(str_err);
|
||||
}).ok();
|
||||
}
|
||||
}).ok();
|
||||
let delegated_on_iso_datetime = timestamp.map(|ts| ts.to_rfc3339());
|
||||
log::trace!(
|
||||
" <<< timestamp = {:?}, delegated_on_iso_datetime = {:?}",
|
||||
|
||||
@@ -32,7 +32,7 @@ pub async fn send(
|
||||
.nyxd
|
||||
.send(&to_address, vec![amount_base], memo, fee)
|
||||
.await?;
|
||||
log::info!("<<< tx hash = {}", raw_res.hash.to_string());
|
||||
log::info!("<<< tx hash = {}", raw_res.hash);
|
||||
let res = SendTxResult::new(
|
||||
raw_res,
|
||||
TransactionDetails::new(amount, from_address, to_address.to_string()),
|
||||
|
||||
@@ -109,18 +109,6 @@ pub async fn mixnode_stake_saturation(
|
||||
.await?)
|
||||
}
|
||||
|
||||
// TODO: fix later (yeah...)
|
||||
#[allow(deprecated)]
|
||||
#[tauri::command]
|
||||
pub async fn mixnode_inclusion_probability(
|
||||
mix_id: NodeId,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<nym_validator_client::models::InclusionProbabilityResponse, BackendError> {
|
||||
Ok(api_client!(state)
|
||||
.get_mixnode_inclusion_probability(mix_id)
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_nymnode_role(
|
||||
node_id: NodeId,
|
||||
|
||||
@@ -40,7 +40,17 @@ pub(crate) const DEFAULT_LOGIN_ID: &str = "default";
|
||||
pub(crate) const DEFAULT_FIRST_ACCOUNT_NAME: &str = "Account 1";
|
||||
|
||||
fn get_storage_directory() -> Result<PathBuf, BackendError> {
|
||||
tauri::api::path::local_data_dir()
|
||||
// tauri v1 (via `tauri::api::path::local_data_dir()`) was internally calling `dirs_next::data_local_dir()`
|
||||
// which ultimately was getting resolved to
|
||||
// - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`.
|
||||
// - **macOS:** Resolves to `$HOME/Library/Application Support`.
|
||||
// - **Windows:** Resolves to `{FOLDERID_LocalAppData}`.
|
||||
//
|
||||
// tauri v2 calls `dirs::data_local_dir().ok_or(Error::UnknownPath)` which ultimately does the same thing,
|
||||
// however, it changed its API so that it's called on a `PathResolver`.
|
||||
// but, to instantiate one here would be a hassle as we don't need those specific functionalities,
|
||||
// so let's just recreate tauri's behaviour
|
||||
dirs::data_local_dir()
|
||||
.map(|dir| dir.join(STORAGE_DIR_NAME))
|
||||
.ok_or(BackendError::UnknownStorageDirectory)
|
||||
}
|
||||
|
||||
@@ -1,78 +1,70 @@
|
||||
{
|
||||
"package": {
|
||||
"productName": "nym-wallet",
|
||||
"version": "1.2.17"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "../dist",
|
||||
"devPath": "http://localhost:9000",
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": ""
|
||||
},
|
||||
"tauri": {
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"identifier": "net.nymtech.wallet",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": [],
|
||||
"externalBin": [],
|
||||
"copyright": "Copyright © 2021-2023 Nym Technologies SA",
|
||||
"category": "Business",
|
||||
"shortDescription": "Nym desktop wallet allows you to manage your NYM tokens",
|
||||
"longDescription": "",
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"windows": {
|
||||
"certificateThumbprint": "6DB77B1F529A0804FE0E6843A3EB8A8CECFFD408",
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": "http://timestamp.comodoca.com"
|
||||
},
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": [],
|
||||
"externalBin": [],
|
||||
"copyright": "Copyright © 2021-2025 Nym Technologies SA",
|
||||
"category": "Business",
|
||||
"shortDescription": "Nym desktop wallet allows you to manage your NYM tokens",
|
||||
"longDescription": "",
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
"minimumSystemVersion": "15.3.2",
|
||||
"exceptionDomain": "",
|
||||
"signingIdentity": "Developer ID Application: Nym Technologies SA (VW5DZLFHM5)",
|
||||
"entitlements": null
|
||||
},
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": []
|
||||
},
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
"minimumSystemVersion": "",
|
||||
"exceptionDomain": "",
|
||||
"signingIdentity": "Developer ID Application: Nym Technologies SA (VW5DZLFHM5)",
|
||||
"entitlements": null
|
||||
},
|
||||
"windows": {
|
||||
"certificateThumbprint": "6DB77B1F529A0804FE0E6843A3EB8A8CECFFD408",
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": "http://timestamp.comodoca.com"
|
||||
}
|
||||
},
|
||||
"createUpdaterArtifacts": "v1Compatible"
|
||||
},
|
||||
"build": {
|
||||
"beforeBuildCommand": "",
|
||||
"frontendDist": "../dist",
|
||||
"beforeDevCommand": "",
|
||||
"devUrl": "http://localhost:9000"
|
||||
},
|
||||
"productName": "NymWallet",
|
||||
"mainBinaryName": "NymWallet",
|
||||
"version": "1.2.18",
|
||||
"identifier": "net.nymtech.wallet",
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"active": true,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IENCNzQ2M0E5N0VFODE2NApSV1JrZ2U2WE9rYTNETTg1OTBKdE5uWUEra0hML2syOVUvQ2lxZmFZRzZ1T3NWbGM0eVRzUTVhVwo=",
|
||||
"endpoints": [
|
||||
"https://nymtech.net/.wellknown/wallet/updater.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"security": {
|
||||
"capabilities": [
|
||||
"main-capability"
|
||||
],
|
||||
"dialog": true,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IENCNzQ2M0E5N0VFODE2NApSV1JrZ2U2WE9rYTNETTg1OTBKdE5uWUEra0hML2syOVUvQ2lxZmFZRzZ1T3NWbGM0eVRzUTVhVwo="
|
||||
},
|
||||
"allowlist": {
|
||||
"window": {
|
||||
"maximize": true,
|
||||
"print": true
|
||||
},
|
||||
"clipboard": {
|
||||
"all": true
|
||||
},
|
||||
"shell": {
|
||||
"open": true
|
||||
}
|
||||
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'; connect-src ipc: http://ipc.localhost"
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"title": "Nym Wallet",
|
||||
"width": 1268,
|
||||
"height": 768,
|
||||
"resizable": true
|
||||
"resizable": true,
|
||||
"useHttpsScheme": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useContext } from 'react';
|
||||
import EditIcon from '@mui/icons-material/Create';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import {
|
||||
Box,
|
||||
IconButton,
|
||||
@@ -9,6 +10,8 @@ import {
|
||||
ListItemText,
|
||||
Tooltip,
|
||||
Typography,
|
||||
alpha,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import { useClipboard } from 'use-clipboard-copy';
|
||||
import { AccountsContext } from 'src/context';
|
||||
@@ -25,29 +28,94 @@ export const AccountItem = ({
|
||||
}) => {
|
||||
const { selectedAccount, setDialogToDisplay, setAccountMnemonic, handleAccountToEdit } = useContext(AccountsContext);
|
||||
const { copy, copied } = useClipboard({ copiedTimeout: 1000 });
|
||||
const theme = useTheme();
|
||||
|
||||
const isSelected = selectedAccount?.id === name;
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
disablePadding
|
||||
disableGutters
|
||||
sx={selectedAccount?.id === name ? { bgcolor: 'rgba(33, 208, 115, 0.1)' } : {}}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
my: 0.5,
|
||||
mx: 1,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
bgcolor: isSelected
|
||||
? alpha(theme.palette.nym.highlight, theme.palette.mode === 'dark' ? 0.15 : 0.08)
|
||||
: 'transparent',
|
||||
borderLeft: isSelected ? `3px solid ${theme.palette.nym.highlight}` : '3px solid transparent',
|
||||
}}
|
||||
secondaryAction={
|
||||
<IconButton
|
||||
sx={{ mr: 2, color: 'nym.text.dark' }}
|
||||
onClick={() => {
|
||||
handleAccountToEdit(name);
|
||||
setDialogToDisplay('Edit');
|
||||
}}
|
||||
>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{isSelected && (
|
||||
<CheckCircleIcon
|
||||
sx={{
|
||||
color: theme.palette.nym.highlight,
|
||||
fontSize: 18,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
sx={{
|
||||
mr: 1.5,
|
||||
color: theme.palette.mode === 'dark' ? 'nym.text.dark' : theme.palette.text.primary,
|
||||
backgroundColor: alpha(theme.palette.text.primary, 0.05),
|
||||
'&:hover': {
|
||||
backgroundColor: alpha(theme.palette.text.primary, 0.1),
|
||||
},
|
||||
width: 30,
|
||||
height: 30,
|
||||
}}
|
||||
onClick={() => {
|
||||
handleAccountToEdit(name);
|
||||
setDialogToDisplay('Edit');
|
||||
}}
|
||||
>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<ListItemButton disableRipple onClick={onSelectAccount}>
|
||||
<ListItemAvatar sx={{ minWidth: 0, mr: 2 }}>
|
||||
<ListItemButton
|
||||
disableRipple
|
||||
onClick={onSelectAccount}
|
||||
sx={{
|
||||
py: 1,
|
||||
transition: 'background-color 0.2s',
|
||||
'&:hover': {
|
||||
backgroundColor: isSelected
|
||||
? alpha(theme.palette.nym.highlight, theme.palette.mode === 'dark' ? 0.2 : 0.12)
|
||||
: alpha(theme.palette.nym.nymWallet.hover.background, 0.5),
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Account avatar with box wrapper to apply styling */}
|
||||
<ListItemAvatar
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
mr: 2,
|
||||
'& .MuiAvatar-root': {
|
||||
border: isSelected ? `2px solid ${theme.palette.nym.highlight}` : '2px solid transparent',
|
||||
transition: 'all 0.2s',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AccountAvatar name={name} />
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={name}
|
||||
primary={
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{
|
||||
fontWeight: isSelected ? 600 : 400,
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
<Box>
|
||||
<Tooltip title={copied ? 'Copied!' : `Click to copy address ${address}`}>
|
||||
@@ -58,7 +126,18 @@ export const AccountItem = ({
|
||||
e.stopPropagation();
|
||||
copy(address);
|
||||
}}
|
||||
sx={{ '&:hover': { color: 'grey.900' } }}
|
||||
sx={{
|
||||
fontFamily: 'monospace',
|
||||
cursor: 'pointer',
|
||||
color:
|
||||
theme.palette.mode === 'dark'
|
||||
? theme.palette.nym.nymWallet.text.muted
|
||||
: alpha(theme.palette.text.primary, 0.7),
|
||||
'&:hover': {
|
||||
color: theme.palette.text.primary,
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{address}
|
||||
</Typography>
|
||||
@@ -67,7 +146,19 @@ export const AccountItem = ({
|
||||
<Typography
|
||||
variant="body2"
|
||||
component="span"
|
||||
sx={{ textDecoration: 'underline', mb: 0.5, '&:hover': { color: 'primary.main' } }}
|
||||
sx={{
|
||||
textDecoration: 'underline',
|
||||
mb: 0.5,
|
||||
cursor: 'pointer',
|
||||
color:
|
||||
theme.palette.mode === 'dark'
|
||||
? alpha(theme.palette.nym.highlight, 0.9)
|
||||
: theme.palette.nym.highlight,
|
||||
'&:hover': {
|
||||
color: theme.palette.nym.highlight,
|
||||
fontWeight: 500,
|
||||
},
|
||||
}}
|
||||
onClick={(e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
setDialogToDisplay('Mnemonic');
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { useContext, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Paper,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
@@ -10,9 +9,11 @@ import {
|
||||
IconButton,
|
||||
Typography,
|
||||
Divider,
|
||||
alpha,
|
||||
useTheme,
|
||||
List,
|
||||
} from '@mui/material';
|
||||
import { Add, ArrowDownwardSharp, Close } from '@mui/icons-material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Add, ArrowDownwardSharp, Close, SwapHorizOutlined } from '@mui/icons-material';
|
||||
import { AccountsContext } from 'src/context';
|
||||
import { AccountItem } from '../AccountItem';
|
||||
import { ConfirmPasswordModal } from './ConfirmPasswordModal';
|
||||
@@ -52,23 +53,91 @@ export const AccountsModal = () => {
|
||||
open={dialogToDisplay === 'Accounts'}
|
||||
onClose={handleClose}
|
||||
fullWidth
|
||||
maxWidth="sm"
|
||||
PaperProps={{
|
||||
style: { border: `1px solid ${theme.palette.nym.nymWallet.modal.border}` },
|
||||
elevation: 4,
|
||||
sx: {
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
border: `1px solid ${theme.palette.nym.nymWallet.modal.border}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxHeight: '80vh', // Limit maximum height
|
||||
...(theme.palette.mode === 'dark' && {
|
||||
backgroundImage: 'linear-gradient(180deg, rgba(50, 55, 61, 0.8), rgba(36, 43, 45, 0.95))',
|
||||
}),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Paper>
|
||||
<DialogTitle>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h6">Accounts</Typography>
|
||||
<IconButton onClick={handleClose}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
<DialogTitle sx={{ pb: 1, flexShrink: 0 }}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<SwapHorizOutlined
|
||||
sx={{
|
||||
color: theme.palette.nym.highlight,
|
||||
backgroundColor: alpha(theme.palette.nym.highlight, 0.1),
|
||||
borderRadius: '50%',
|
||||
p: 0.5,
|
||||
fontSize: 24,
|
||||
}}
|
||||
/>
|
||||
<Typography variant="h6" fontWeight={600}>
|
||||
Accounts
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography fontSize="small" sx={{ color: 'grey.600' }}>
|
||||
Switch between accounts
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ padding: 0 }}>
|
||||
<IconButton
|
||||
onClick={handleClose}
|
||||
size="small"
|
||||
sx={{
|
||||
backgroundColor: alpha(theme.palette.text.primary, 0.05),
|
||||
'&:hover': {
|
||||
backgroundColor: alpha(theme.palette.text.primary, 0.1),
|
||||
},
|
||||
width: 30,
|
||||
height: 30,
|
||||
}}
|
||||
>
|
||||
<Close fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
color:
|
||||
theme.palette.mode === 'dark'
|
||||
? theme.palette.nym.nymWallet.text.muted
|
||||
: alpha(theme.palette.text.primary, 0.6),
|
||||
pl: 4.5,
|
||||
}}
|
||||
>
|
||||
Switch between accounts
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent
|
||||
sx={{
|
||||
px: 1,
|
||||
pt: 0,
|
||||
flexGrow: 1,
|
||||
overflowY: 'auto', // Enable vertical scrolling
|
||||
minHeight: '100px', // Ensure minimum height for content
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'dark'
|
||||
? alpha(theme.palette.nym.nymWallet.background.greyStroke, 0.8)
|
||||
: alpha(theme.palette.nym.nymWallet.background.greyStroke, 0.5),
|
||||
borderRadius: '4px',
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<List sx={{ py: 1 }}>
|
||||
{accounts?.map(({ id, address }) => (
|
||||
<AccountItem
|
||||
name={id}
|
||||
@@ -81,22 +150,64 @@ export const AccountsModal = () => {
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</DialogContent>
|
||||
<Divider variant="middle" sx={{ mt: 3 }} />
|
||||
<DialogActions sx={{ p: 3 }}>
|
||||
<Button startIcon={<ArrowDownwardSharp />} onClick={() => setDialogToDisplay('Import')}>
|
||||
</List>
|
||||
</DialogContent>
|
||||
|
||||
<Box sx={{ flexShrink: 0 }}>
|
||||
<Divider
|
||||
variant="middle"
|
||||
sx={{
|
||||
my: 1.5,
|
||||
opacity: 0.6,
|
||||
}}
|
||||
/>
|
||||
|
||||
<DialogActions
|
||||
sx={{
|
||||
p: 3,
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
startIcon={<ArrowDownwardSharp />}
|
||||
onClick={() => setDialogToDisplay('Import')}
|
||||
sx={{
|
||||
borderRadius: 1.5,
|
||||
transition: 'all 0.2s',
|
||||
px: 2,
|
||||
py: 0.75,
|
||||
color: theme.palette.text.primary,
|
||||
'&:hover': {
|
||||
backgroundColor: alpha(theme.palette.text.primary, 0.05),
|
||||
},
|
||||
}}
|
||||
>
|
||||
Import account
|
||||
</Button>
|
||||
<Button
|
||||
disableElevation
|
||||
variant="contained"
|
||||
startIcon={<Add fontSize="small" />}
|
||||
startIcon={<Add fontSize="medium" />}
|
||||
onClick={() => setDialogToDisplay('Add')}
|
||||
sx={{
|
||||
px: 2,
|
||||
py: 0.75,
|
||||
borderRadius: 1.5,
|
||||
background: theme.palette.nym.nymWallet.gradients.primary || theme.palette.nym.highlight,
|
||||
fontWeight: 600,
|
||||
boxShadow: 'none',
|
||||
transition: 'all 0.2s',
|
||||
'&:hover': {
|
||||
boxShadow:
|
||||
theme.palette.mode === 'dark' ? '0 4px 12px rgba(0, 0, 0, 0.2)' : '0 4px 12px rgba(0, 0, 0, 0.1)',
|
||||
transform: 'translateY(-1px)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Create account
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
import React, { useContext } from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Box, Stack, Tooltip, Typography } from '@mui/material';
|
||||
import { Box, Stack, Tooltip, Typography, Skeleton } from '@mui/material';
|
||||
import { format } from 'date-fns';
|
||||
import { AppContext } from 'src/context';
|
||||
|
||||
@@ -17,7 +16,10 @@ const Marker: FCWithChildren<{ tooltipText: string; color: string; position: str
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
export const VestingTimeline: FCWithChildren<{ percentageComplete: number }> = ({ percentageComplete }) => {
|
||||
export const VestingTimeline: FCWithChildren<{ percentageComplete: number; isLoading?: boolean }> = ({
|
||||
percentageComplete,
|
||||
isLoading,
|
||||
}) => {
|
||||
const {
|
||||
userBalance: { currentVestingPeriod, vestingAccountInfo },
|
||||
} = useContext(AppContext);
|
||||
@@ -32,34 +34,48 @@ export const VestingTimeline: FCWithChildren<{ percentageComplete: number }> = (
|
||||
return (
|
||||
<Box>
|
||||
<Stack direction="row" gap={1} alignItems="center">
|
||||
<Typography variant="body2">{percentageComplete}%</Typography>
|
||||
<svg width="100%" height="12">
|
||||
<rect y="2" width="100%" height="6" rx="0" fill="#E6E6E6" />
|
||||
<rect y="2" width={`${percentageComplete}%`} height="6" rx="0" fill={theme.palette.success.main} />
|
||||
{vestingAccountInfo?.periods.map((period, i, arr) => (
|
||||
<Marker
|
||||
position={`${calculateMarkerPosition(arr.length, i)}%`}
|
||||
color={
|
||||
Math.ceil(+percentageComplete) >= calculateMarkerPosition(arr.length, i)
|
||||
? theme.palette.success.main
|
||||
: '#B9B9B9'
|
||||
}
|
||||
tooltipText={format(new Date(Number(period.start_time) * 1000), 'HH:mm do MMM yyyy')}
|
||||
key={i}
|
||||
/>
|
||||
))}
|
||||
<Marker
|
||||
position="calc(100% - 4px)"
|
||||
color={percentageComplete === 100 ? theme.palette.success.main : '#B9B9B9'}
|
||||
tooltipText="End of vesting schedule"
|
||||
/>
|
||||
</svg>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Skeleton width={40} />
|
||||
<Skeleton width="100%" height={12} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography variant="body2">{percentageComplete}%</Typography>
|
||||
<svg width="100%" height="12">
|
||||
<rect y="2" width="100%" height="6" rx="0" fill="#E6E6E6" />
|
||||
<rect y="2" width={`${percentageComplete}%`} height="6" rx="0" fill={theme.palette.success.main} />
|
||||
{vestingAccountInfo?.periods.map((period, i, arr) => (
|
||||
<Marker
|
||||
position={`${calculateMarkerPosition(arr.length, i)}%`}
|
||||
color={
|
||||
Math.ceil(+percentageComplete) >= calculateMarkerPosition(arr.length, i)
|
||||
? theme.palette.success.main
|
||||
: '#B9B9B9'
|
||||
}
|
||||
tooltipText={format(new Date(Number(period.start_time) * 1000), 'HH:mm do MMM yyyy')}
|
||||
key={`period-${period.start_time}`}
|
||||
/>
|
||||
))}
|
||||
<Marker
|
||||
position="calc(100% - 4px)"
|
||||
color={percentageComplete === 100 ? theme.palette.success.main : '#B9B9B9'}
|
||||
tooltipText="End of vesting schedule"
|
||||
/>
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
{!!nextPeriod && (
|
||||
{!!nextPeriod && !isLoading && (
|
||||
<Typography variant="caption" sx={{ color: 'nym.text.muted', ml: 6 }}>
|
||||
Next vesting period: {format(new Date(nextPeriod * 1000), 'HH:mm do MMM yyyy')}
|
||||
</Typography>
|
||||
)}
|
||||
{isLoading && (
|
||||
<Box sx={{ ml: 6, mt: 1 }}>
|
||||
<Skeleton width={240} height={14} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Card, Stack, Button } from '@mui/material';
|
||||
import { Card, Stack, Button, Skeleton } from '@mui/material';
|
||||
import { ModalListItem } from 'src/components/Modals/ModalListItem';
|
||||
|
||||
export const TokenTransfer = ({
|
||||
@@ -7,20 +7,27 @@ export const TokenTransfer = ({
|
||||
unlockedTokens,
|
||||
unlockedRewards,
|
||||
unlockedTransferable,
|
||||
isLoading,
|
||||
}: {
|
||||
unlockedTokens?: string;
|
||||
unlockedRewards?: string;
|
||||
unlockedTransferable?: string;
|
||||
onTransfer: () => void;
|
||||
isLoading?: boolean;
|
||||
}) => (
|
||||
<Card variant="outlined" sx={{ p: 3, height: '100%' }}>
|
||||
<Stack justifyContent="space-between" sx={{ height: '100%' }}>
|
||||
<Stack gap={1} sx={{ mb: 2 }}>
|
||||
<ModalListItem label="Unlocked tokens" value={unlockedTokens} />
|
||||
<ModalListItem label="Unlocked rewards" value={unlockedRewards} divider />
|
||||
<ModalListItem fontSize={16} label="Transferable tokens" value={unlockedTransferable} fontWeight={600} />
|
||||
<ModalListItem label="Unlocked tokens" value={isLoading ? <Skeleton width={80} /> : unlockedTokens} />
|
||||
<ModalListItem label="Unlocked rewards" value={isLoading ? <Skeleton width={80} /> : unlockedRewards} divider />
|
||||
<ModalListItem
|
||||
fontSize={16}
|
||||
label="Transferable tokens"
|
||||
value={isLoading ? <Skeleton width={100} /> : unlockedTransferable}
|
||||
fontWeight={600}
|
||||
/>
|
||||
</Stack>
|
||||
<Button size="large" fullWidth variant="contained" onClick={onTransfer} disableElevation>
|
||||
<Button size="large" fullWidth variant="contained" onClick={onTransfer} disableElevation disabled={isLoading}>
|
||||
Transfer
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Typography,
|
||||
TableCellProps,
|
||||
Card,
|
||||
Skeleton,
|
||||
} from '@mui/material';
|
||||
import { Period } from '@nymproject/types';
|
||||
import { AppContext } from 'src/context';
|
||||
@@ -28,7 +29,7 @@ const vestingPeriod = (current?: Period, original?: number) => {
|
||||
return 'N/A';
|
||||
};
|
||||
|
||||
export const VestingSchedule = () => {
|
||||
export const VestingSchedule = ({ isLoading }: { isLoading?: boolean }) => {
|
||||
const { userBalance, clientDetails } = useContext(AppContext);
|
||||
const [vestedPercentage, setVestedPercentage] = useState(0);
|
||||
|
||||
@@ -73,8 +74,14 @@ export const VestingSchedule = () => {
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
{userBalance.tokenAllocation?.vesting || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '}
|
||||
{clientDetails?.display_mix_denom.toUpperCase()}
|
||||
{isLoading ? (
|
||||
<Skeleton width={100} />
|
||||
) : (
|
||||
<>
|
||||
{userBalance.tokenAllocation?.vesting || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '}
|
||||
{clientDetails?.display_mix_denom.toUpperCase()}
|
||||
</>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="left"
|
||||
@@ -83,7 +90,11 @@ export const VestingSchedule = () => {
|
||||
borderBottom: 'none',
|
||||
}}
|
||||
>
|
||||
{vestingPeriod(userBalance.currentVestingPeriod, userBalance.originalVesting?.number_of_periods)}
|
||||
{isLoading ? (
|
||||
<Skeleton width={40} />
|
||||
) : (
|
||||
vestingPeriod(userBalance.currentVestingPeriod, userBalance.originalVesting?.number_of_periods)
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
@@ -93,8 +104,14 @@ export const VestingSchedule = () => {
|
||||
}}
|
||||
align="right"
|
||||
>
|
||||
{userBalance.tokenAllocation?.vested || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '}
|
||||
{clientDetails?.display_mix_denom.toUpperCase()}
|
||||
{isLoading ? (
|
||||
<Skeleton width={100} />
|
||||
) : (
|
||||
<>
|
||||
{userBalance.tokenAllocation?.vested || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '}
|
||||
{clientDetails?.display_mix_denom.toUpperCase()}
|
||||
</>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
@@ -103,7 +120,7 @@ export const VestingSchedule = () => {
|
||||
<Typography variant="body2" sx={{ color: 'nym.text.muted', mb: 3 }}>
|
||||
Percentage
|
||||
</Typography>
|
||||
<VestingTimeline percentageComplete={vestedPercentage} />
|
||||
<VestingTimeline percentageComplete={vestedPercentage} isLoading={isLoading} />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
import { ConfirmationModal } from 'src/components';
|
||||
|
||||
export type TTransactionDetails = { amount: string; url: string };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
import { Box, Button, Typography } from '@mui/material';
|
||||
import { NymCard } from '../NymCard';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Box, Button, Stack, Tooltip, Typography } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
import { urls } from 'src/context';
|
||||
import { NymCard } from 'src/components';
|
||||
import { Network } from 'src/types';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Box, Button, Chip, Stack, Tooltip, Typography } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
import { Network } from 'src/types';
|
||||
import { urls } from 'src/context';
|
||||
import { NymCard } from 'src/components';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Box, Button, Chip, Stack, Tooltip, Typography } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
import { Network } from 'src/types';
|
||||
import { urls } from 'src/context';
|
||||
import { NymCard } from 'src/components';
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { NodeTypeSelector } from 'src/components';
|
||||
import { CurrencyDenom, TNodeType } from '@nymproject/types';
|
||||
import { GatewayAmount, GatewayData } from 'src/pages/bonding/types';
|
||||
import GatewayInitForm from './GatewayInitForm';
|
||||
import GatewayAmountForm from './GatewayAmountForm';
|
||||
|
||||
export const BondGatewayForm = ({
|
||||
step,
|
||||
denom,
|
||||
gatewayData,
|
||||
amountData,
|
||||
hasVestingTokens,
|
||||
onSelectNodeType,
|
||||
onValidateGatewayData,
|
||||
onValidateAmountData,
|
||||
}: {
|
||||
step: 1 | 2 | 3 | 4;
|
||||
gatewayData: GatewayData;
|
||||
amountData: GatewayAmount;
|
||||
denom: CurrencyDenom;
|
||||
hasVestingTokens: boolean;
|
||||
onSelectNodeType: (nodeType: TNodeType) => void;
|
||||
onValidateGatewayData: (data: GatewayData) => void;
|
||||
onValidateAmountData: (data: GatewayAmount) => Promise<void>;
|
||||
}) => (
|
||||
<>
|
||||
{step === 1 && (
|
||||
<>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<NodeTypeSelector disabled={false} setNodeType={onSelectNodeType} nodeType="gateway" />
|
||||
</Box>
|
||||
<GatewayInitForm onNext={onValidateGatewayData} gatewayData={gatewayData} />
|
||||
</>
|
||||
)}
|
||||
{step === 2 && (
|
||||
<GatewayAmountForm
|
||||
denom={denom}
|
||||
amountData={amountData}
|
||||
hasVestingTokens={hasVestingTokens}
|
||||
onNext={onValidateAmountData}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -1,84 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { CurrencyDenom } from '@nymproject/types';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { yupResolver } from '@hookform/resolvers/yup/dist/yup';
|
||||
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
|
||||
import { Box, Stack } from '@mui/material';
|
||||
import { amountSchema } from './gatewayValidationSchema';
|
||||
import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from '../../../../utils';
|
||||
import { GatewayAmount } from '../../../../pages/bonding/types';
|
||||
import { TokenPoolSelector } from '../../../TokenPoolSelector';
|
||||
|
||||
const GatewayAmountForm = ({
|
||||
denom,
|
||||
amountData,
|
||||
hasVestingTokens,
|
||||
onNext,
|
||||
}: {
|
||||
denom: CurrencyDenom;
|
||||
amountData: GatewayAmount;
|
||||
hasVestingTokens: boolean;
|
||||
onNext: (data: any) => void;
|
||||
}) => {
|
||||
const {
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
setValue,
|
||||
getValues,
|
||||
setError,
|
||||
} = useForm({ resolver: yupResolver(amountSchema), defaultValues: amountData });
|
||||
|
||||
const handleRequestValidation = async (event: { detail: { step: number } }) => {
|
||||
let hasSufficientTokens = true;
|
||||
const values = getValues();
|
||||
|
||||
if (values.tokenPool === 'balance') {
|
||||
hasSufficientTokens = await checkHasEnoughFunds(values.amount.amount);
|
||||
}
|
||||
|
||||
if (values.tokenPool === 'locked') {
|
||||
hasSufficientTokens = await checkHasEnoughLockedTokens(values.amount.amount);
|
||||
}
|
||||
|
||||
if (event.detail.step === 2 && hasSufficientTokens) {
|
||||
handleSubmit(onNext)();
|
||||
} else {
|
||||
setError('amount.amount', { message: 'Not enough tokens' });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('validate_bond_gateway_step' as any, handleRequestValidation);
|
||||
return () => window.removeEventListener('validate_bond_gateway_step' as any, handleRequestValidation);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack gap={3}>
|
||||
<Box display="flex" gap={3} justifyContent="center" sx={{ mt: 2 }}>
|
||||
{hasVestingTokens && <TokenPoolSelector disabled={false} onSelect={(pool) => setValue('tokenPool', pool)} />}
|
||||
<CurrencyFormField
|
||||
required
|
||||
fullWidth
|
||||
label="Amount"
|
||||
autoFocus
|
||||
onChanged={(newValue) => setValue('amount', newValue, { shouldValidate: true })}
|
||||
validationError={errors.amount?.amount?.message}
|
||||
denom={denom}
|
||||
initialValue={amountData.amount.amount}
|
||||
/>
|
||||
<CurrencyFormField
|
||||
required
|
||||
fullWidth
|
||||
label="Operator Cost"
|
||||
autoFocus
|
||||
onChanged={(newValue) => setValue('operatorCost', newValue, { shouldValidate: true })}
|
||||
validationError={errors.operatorCost?.amount?.message}
|
||||
denom={denom}
|
||||
initialValue={amountData.operatorCost.amount}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default GatewayAmountForm;
|
||||
@@ -1,124 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { clean } from 'semver';
|
||||
import { Checkbox, FormControlLabel, Stack, TextField } from '@mui/material';
|
||||
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
|
||||
import { yupResolver } from '@hookform/resolvers/yup/dist/yup';
|
||||
import { GatewayData } from '../../../../pages/bonding/types';
|
||||
import { gatewayValidationSchema } from './gatewayValidationSchema';
|
||||
|
||||
const GatewayInitForm = ({
|
||||
gatewayData,
|
||||
onNext,
|
||||
}: {
|
||||
gatewayData: GatewayData;
|
||||
onNext: (data: GatewayData) => void;
|
||||
}) => {
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
setValue,
|
||||
} = useForm({ resolver: yupResolver(gatewayValidationSchema), defaultValues: gatewayData });
|
||||
|
||||
const handleRequestValidation = (event: { detail: { step: number } }) => {
|
||||
if (event.detail.step === 1) {
|
||||
handleSubmit((data) => {
|
||||
const validatedData = {
|
||||
...data,
|
||||
version: clean(data.version) as string,
|
||||
};
|
||||
onNext(validatedData);
|
||||
})();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('validate_bond_gateway_step' as any, handleRequestValidation);
|
||||
return () => window.removeEventListener('validate_bond_gateway_step' as any, handleRequestValidation);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack gap={3}>
|
||||
<IdentityKeyFormField
|
||||
required
|
||||
fullWidth
|
||||
label="Identity Key"
|
||||
initialValue={gatewayData?.identityKey}
|
||||
errorText={errors.identityKey?.message}
|
||||
onChanged={(value) => setValue('identityKey', value)}
|
||||
showTickOnValid={false}
|
||||
/>
|
||||
<TextField
|
||||
{...register('sphinxKey')}
|
||||
name="sphinxKey"
|
||||
label="Sphinx key"
|
||||
error={Boolean(errors.sphinxKey)}
|
||||
helperText={errors.sphinxKey?.message}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<TextField
|
||||
{...register('location')}
|
||||
name="location"
|
||||
label="Location"
|
||||
error={Boolean(errors.location)}
|
||||
helperText={errors.location?.message}
|
||||
required
|
||||
InputLabelProps={{ shrink: true }}
|
||||
sx={{ flexBasis: '50%' }}
|
||||
/>
|
||||
<Stack direction="row" gap={3}>
|
||||
<TextField
|
||||
{...register('host')}
|
||||
name="host"
|
||||
label="Host"
|
||||
error={Boolean(errors.host)}
|
||||
helperText={errors.host?.message}
|
||||
required
|
||||
InputLabelProps={{ shrink: true }}
|
||||
sx={{ flexBasis: '50%' }}
|
||||
/>
|
||||
<TextField
|
||||
{...register('version')}
|
||||
name="version"
|
||||
label="Version"
|
||||
error={Boolean(errors.version)}
|
||||
helperText={errors.version?.message}
|
||||
required
|
||||
InputLabelProps={{ shrink: true }}
|
||||
sx={{ flexBasis: '50%' }}
|
||||
/>
|
||||
</Stack>
|
||||
<FormControlLabel
|
||||
control={<Checkbox onChange={() => setShowAdvancedOptions((show) => !show)} checked={showAdvancedOptions} />}
|
||||
label="Show advanced options"
|
||||
/>
|
||||
{showAdvancedOptions && (
|
||||
<Stack direction="row" gap={3} sx={{ mb: 2 }}>
|
||||
<TextField
|
||||
{...register('mixPort')}
|
||||
name="mixPort"
|
||||
label="Mix port"
|
||||
error={Boolean(errors.mixPort)}
|
||||
helperText={errors.mixPort?.message}
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<TextField
|
||||
{...register('clientsPort')}
|
||||
name="clientsPort"
|
||||
label="Client WS API port"
|
||||
error={Boolean(errors.clientsPort)}
|
||||
helperText={errors.clientsPort?.message}
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default GatewayInitForm;
|
||||
@@ -1,125 +0,0 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Box, FormHelperText, Stack, TextField, Typography } from '@mui/material';
|
||||
import { yupResolver } from '@hookform/resolvers/yup/dist/yup';
|
||||
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
|
||||
import { CurrencyDenom } from '@nymproject/types';
|
||||
import { amountSchema } from './mixnodeValidationSchema';
|
||||
import { MixnodeAmount } from '../../../../pages/bonding/types';
|
||||
import { AppContext } from '../../../../context';
|
||||
import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from '../../../../utils';
|
||||
import { TokenPoolSelector } from '../../../TokenPoolSelector';
|
||||
import { ModalListItem } from '../../../Modals/ModalListItem';
|
||||
|
||||
const MixnodeAmountForm = ({
|
||||
amountData,
|
||||
hasVestingTokens,
|
||||
denom,
|
||||
onNext,
|
||||
}: {
|
||||
amountData: MixnodeAmount;
|
||||
hasVestingTokens: boolean;
|
||||
denom: CurrencyDenom;
|
||||
onNext: (data: MixnodeAmount) => void;
|
||||
}) => {
|
||||
const { mixnetContractParams } = useContext(AppContext);
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
setValue,
|
||||
getValues,
|
||||
setError,
|
||||
} = useForm({ resolver: yupResolver(amountSchema(mixnetContractParams)), defaultValues: amountData });
|
||||
|
||||
const { userBalance } = useContext(AppContext);
|
||||
|
||||
const handleRequestValidation = async (event: { detail: { step: number } }) => {
|
||||
let hasSufficientTokens = true;
|
||||
const values = getValues();
|
||||
|
||||
if (values.tokenPool === 'balance') {
|
||||
hasSufficientTokens = await checkHasEnoughFunds(values.amount.amount);
|
||||
}
|
||||
|
||||
if (values.tokenPool === 'locked') {
|
||||
hasSufficientTokens = await checkHasEnoughLockedTokens(values.amount.amount);
|
||||
}
|
||||
|
||||
if (event.detail.step === 2 && hasSufficientTokens) {
|
||||
handleSubmit(onNext)();
|
||||
} else {
|
||||
setError('amount.amount', { message: 'Not enough tokens' });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('validate_bond_mixnode_step' as any, handleRequestValidation);
|
||||
return () => window.removeEventListener('validate_bond_mixnode_step' as any, handleRequestValidation);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack gap={3}>
|
||||
<Box display="flex" gap={3} justifyContent="center">
|
||||
{hasVestingTokens && <TokenPoolSelector disabled={false} onSelect={(pool) => setValue('tokenPool', pool)} />}
|
||||
<CurrencyFormField
|
||||
required
|
||||
fullWidth
|
||||
label="Amount"
|
||||
autoFocus
|
||||
onChanged={(newValue) => {
|
||||
setValue('amount', newValue, { shouldValidate: true });
|
||||
}}
|
||||
validationError={errors.amount?.amount?.message}
|
||||
denom={denom}
|
||||
initialValue={amountData.amount.amount}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<CurrencyFormField
|
||||
required
|
||||
fullWidth
|
||||
label="Operating cost"
|
||||
onChanged={(newValue) => {
|
||||
setValue('operatorCost', newValue, { shouldValidate: true });
|
||||
}}
|
||||
validationError={errors.operatorCost?.amount?.message}
|
||||
denom={denom}
|
||||
initialValue={amountData.operatorCost.amount}
|
||||
/>
|
||||
<FormHelperText>
|
||||
Monthly operational costs of running your node. If your node is in the active set the amount will be paid back
|
||||
to you from the rewards.
|
||||
</FormHelperText>
|
||||
</Box>
|
||||
<Box>
|
||||
<TextField
|
||||
{...register('profitMargin')}
|
||||
name="profitMargin"
|
||||
label="Profit margin"
|
||||
error={Boolean(errors.profitMargin)}
|
||||
helperText={errors.profitMargin?.message}
|
||||
fullWidth
|
||||
/>
|
||||
<FormHelperText>
|
||||
The percentage of node rewards that you as the node operator take before rewards are distributed to operator
|
||||
and delegators.
|
||||
</FormHelperText>
|
||||
</Box>
|
||||
<Box sx={{ mb: 1 }}>
|
||||
{!hasVestingTokens && (
|
||||
<ModalListItem
|
||||
divider
|
||||
label="Account balance"
|
||||
value={userBalance.balance?.printable_balance.toUpperCase()}
|
||||
fontWeight={600}
|
||||
/>
|
||||
)}
|
||||
<Typography variant="body2">Est. fee for this transaction will be calculated in the next page</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default MixnodeAmountForm;
|
||||
@@ -1,117 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { clean } from 'semver';
|
||||
import { Checkbox, FormControlLabel, Stack, TextField } from '@mui/material';
|
||||
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
|
||||
import { yupResolver } from '@hookform/resolvers/yup/dist/yup';
|
||||
import { mixnodeValidationSchema } from './mixnodeValidationSchema';
|
||||
import { MixnodeData } from '../../../../pages/bonding/types';
|
||||
|
||||
const MixnodeInitForm = ({ mixnodeData, onNext }: { mixnodeData: MixnodeData; onNext: (data: any) => void }) => {
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
setValue,
|
||||
} = useForm({ resolver: yupResolver(mixnodeValidationSchema), defaultValues: mixnodeData });
|
||||
|
||||
const handleRequestValidation = (event: { detail: { step: number } }) => {
|
||||
if (event.detail.step === 1) {
|
||||
handleSubmit((data) => {
|
||||
const validatedData = {
|
||||
...data,
|
||||
version: clean(data.version),
|
||||
};
|
||||
onNext(validatedData);
|
||||
})();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('validate_bond_mixnode_step' as any, handleRequestValidation);
|
||||
return () => window.removeEventListener('validate_bond_mixnode_step' as any, handleRequestValidation);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack gap={3}>
|
||||
<IdentityKeyFormField
|
||||
required
|
||||
fullWidth
|
||||
label="Identity Key"
|
||||
initialValue={mixnodeData?.identityKey}
|
||||
errorText={errors.identityKey?.message}
|
||||
onChanged={(value) => setValue('identityKey', value)}
|
||||
showTickOnValid={false}
|
||||
/>
|
||||
<TextField
|
||||
{...register('sphinxKey')}
|
||||
name="sphinxKey"
|
||||
label="Sphinx key"
|
||||
error={Boolean(errors.sphinxKey)}
|
||||
helperText={errors.sphinxKey?.message}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<Stack direction="row" gap={3}>
|
||||
<TextField
|
||||
{...register('host')}
|
||||
name="host"
|
||||
label="Host"
|
||||
error={Boolean(errors.host)}
|
||||
helperText={errors.host?.message}
|
||||
required
|
||||
InputLabelProps={{ shrink: true }}
|
||||
sx={{ flexBasis: '50%' }}
|
||||
/>
|
||||
<TextField
|
||||
{...register('version')}
|
||||
name="version"
|
||||
label="Version"
|
||||
error={Boolean(errors.version)}
|
||||
helperText={errors.version?.message}
|
||||
required
|
||||
InputLabelProps={{ shrink: true }}
|
||||
sx={{ flexBasis: '50%' }}
|
||||
/>
|
||||
</Stack>
|
||||
<FormControlLabel
|
||||
control={<Checkbox onChange={() => setShowAdvancedOptions((show) => !show)} checked={showAdvancedOptions} />}
|
||||
label="Show advanced options"
|
||||
/>
|
||||
{showAdvancedOptions && (
|
||||
<Stack direction="row" gap={3} sx={{ mb: 2 }}>
|
||||
<TextField
|
||||
{...register('mixPort')}
|
||||
name="mixPort"
|
||||
label="Mix port"
|
||||
error={Boolean(errors.mixPort)}
|
||||
helperText={errors.mixPort?.message}
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<TextField
|
||||
{...register('verlocPort')}
|
||||
name="verlocPort"
|
||||
label="Verloc port"
|
||||
error={Boolean(errors.verlocPort)}
|
||||
helperText={errors.verlocPort?.message}
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<TextField
|
||||
{...register('httpApiPort')}
|
||||
name="httpApiPort"
|
||||
label="HTTP api port"
|
||||
error={Boolean(errors.httpApiPort)}
|
||||
helperText={errors.httpApiPort?.message}
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default MixnodeInitForm;
|
||||
@@ -1,91 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
import {
|
||||
isLessThan,
|
||||
isValidHostname,
|
||||
validateAmount,
|
||||
validateKey,
|
||||
validateLocation,
|
||||
validateRawPort,
|
||||
validateVersion,
|
||||
} from 'src/utils';
|
||||
|
||||
export const gatewayValidationSchema = Yup.object().shape({
|
||||
identityKey: Yup.string()
|
||||
.required('An identity key is required')
|
||||
.test('valid-id-key', 'A valid identity key is required', (value) => validateKey(value || '', 32)),
|
||||
|
||||
sphinxKey: Yup.string()
|
||||
.required('A sphinx key is required')
|
||||
.test('valid-sphinx-key', 'A valid sphinx key is required', (value) => validateKey(value || '', 32)),
|
||||
|
||||
host: Yup.string()
|
||||
.required('A host is required')
|
||||
.test('no-whitespace', 'Host cannot contain whitespace', (value) => !/\s/.test(value || ''))
|
||||
.test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)),
|
||||
|
||||
version: Yup.string()
|
||||
.required('A version is required')
|
||||
.test('no-whitespace', 'A version cannot contain whitespace', (value) => !/\s/.test(value || ''))
|
||||
.test('valid-version', 'A valid version is required', (value) => (value ? validateVersion(value) : false)),
|
||||
|
||||
location: Yup.string()
|
||||
.required('A location is required')
|
||||
.test('valid-location', 'A valid location is required', (locationValueTest) =>
|
||||
locationValueTest ? validateLocation(locationValueTest) : false,
|
||||
),
|
||||
|
||||
mixPort: Yup.number()
|
||||
.required('A mixport is required')
|
||||
.test('valid-mixport', 'A valid mixport is required', (value) => (value ? validateRawPort(value) : false)),
|
||||
|
||||
clientsPort: Yup.number()
|
||||
.required('A clients port is required')
|
||||
.test('valid-clients', 'A valid clients port is required', (value) => (value ? validateRawPort(value) : false)),
|
||||
});
|
||||
|
||||
export const amountSchema = Yup.object().shape({
|
||||
amount: Yup.object().shape({
|
||||
amount: Yup.string()
|
||||
.required('An amount is required')
|
||||
.test('valid-amount', 'Pledge error', async function isValidAmount(this, value) {
|
||||
const isValid = await validateAmount(value || '', '100');
|
||||
if (!isValid) {
|
||||
return this.createError({ message: 'A valid amount is required (min 100)' });
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
}),
|
||||
operatorCost: Yup.object().shape({
|
||||
amount: Yup.string()
|
||||
.required('An operating cost is required')
|
||||
// eslint-disable-next-line
|
||||
.test('valid-operating-cost', 'A valid amount is required (min 40)', async function isValidAmount(this, value) {
|
||||
if (value && (!Number(value) || isLessThan(+value, 40))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const updateGatewayValidationSchema = Yup.object().shape({
|
||||
host: Yup.string()
|
||||
.required('A host is required')
|
||||
.test('no-whitespace', 'Host cannot contain whitespace', (value) => !/\s/.test(value || ''))
|
||||
.test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)),
|
||||
|
||||
mixPort: Yup.number()
|
||||
.required('A mixport is required')
|
||||
.test('valid-mixport', 'A valid mixport is required', (value) => (value ? validateRawPort(value) : false)),
|
||||
|
||||
httpApiPort: Yup.number()
|
||||
.required('A clients port is required')
|
||||
.test('valid-clients', 'A valid clients port is required', (value) => (value ? validateRawPort(value) : false)),
|
||||
location: Yup.string().test('valid-location', 'A valid location is required', (value) =>
|
||||
value ? validateLocation(value) : false,
|
||||
),
|
||||
version: Yup.string()
|
||||
.test('no-whitespace', 'A version cannot contain whitespace', (value) => !/\s/.test(value || ''))
|
||||
.test('valid-version', 'A valid version is required', (value) => (value ? validateVersion(value) : false)),
|
||||
});
|
||||
@@ -1,15 +1,16 @@
|
||||
import React from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import { Stack, TextField, FormControlLabel, Checkbox } from '@mui/material';
|
||||
import { Stack, FormControlLabel, Checkbox } from '@mui/material';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { SimpleModal } from 'src/components/Modals/SimpleModal';
|
||||
import { HookFormTextFieldWithPaste } from 'src/components/Clipboard/ClipboardFormFields';
|
||||
import { useFormContext } from './FormContext';
|
||||
import { settingsValidationSchema } from './settingsValidationSchema';
|
||||
|
||||
type NymNodeDataProps = {
|
||||
onClose: () => void;
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
onBack: () => void;
|
||||
onNext: () => Promise<void>;
|
||||
step: number;
|
||||
@@ -53,23 +54,24 @@ const NymNodeData = ({ onClose, onNext, step }: NymNodeDataProps) => {
|
||||
okDisabled={Object.keys(errors).length > 0}
|
||||
>
|
||||
<Stack gap={3}>
|
||||
<IdentityKeyFormField
|
||||
autoFocus
|
||||
{/* Identity Key Field with Paste Button */}
|
||||
<HookFormTextFieldWithPaste
|
||||
name="identity_key"
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
errors={errors}
|
||||
required
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
label="Identity Key"
|
||||
initialValue={nymNodeData.identity_key}
|
||||
errorText={errors.identity_key?.message?.toString()}
|
||||
onChanged={(value) => setValue('identity_key', value, { shouldValidate: true })}
|
||||
showTickOnValid={false}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
{...register('host')}
|
||||
{/* Host Field with Built-in Paste */}
|
||||
<HookFormTextFieldWithPaste
|
||||
name="host"
|
||||
label="Host"
|
||||
error={Boolean(errors.host)}
|
||||
helperText={errors.host?.message}
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
errors={errors}
|
||||
required
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
@@ -78,14 +80,15 @@ const NymNodeData = ({ onClose, onNext, step }: NymNodeDataProps) => {
|
||||
control={<Checkbox onChange={() => setShowAdvancedOptions((show) => !show)} checked={showAdvancedOptions} />}
|
||||
label="Show advanced options"
|
||||
/>
|
||||
|
||||
{showAdvancedOptions && (
|
||||
<Stack direction="row" gap={3} sx={{ mb: 2 }}>
|
||||
<TextField
|
||||
{...register('custom_http_port')}
|
||||
<HookFormTextFieldWithPaste
|
||||
name="custom_http_port"
|
||||
label="Custom HTTP port"
|
||||
error={Boolean(errors.custom_http_port)}
|
||||
helperText={errors.custom_http_port?.message}
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
errors={errors}
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import * as yup from 'yup';
|
||||
import { Stack, TextField, Typography } from '@mui/material';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { CopyToClipboard } from 'src/components/CopyToClipboard';
|
||||
import { CopyToClipboard, TextFieldWithPaste } from 'src/components/Clipboard';
|
||||
import { ErrorModal } from 'src/components/Modals/ErrorModal';
|
||||
import { SimpleModal } from 'src/components/Modals/SimpleModal';
|
||||
import { useBondingContext } from 'src/context';
|
||||
@@ -41,6 +41,7 @@ const NymNodeSignature = ({
|
||||
register,
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
setValue,
|
||||
} = useForm<Signature>({
|
||||
defaultValues: {
|
||||
signature,
|
||||
@@ -60,7 +61,6 @@ const NymNodeSignature = ({
|
||||
setMessage(msg);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError('Something went wrong while generating the payload signature');
|
||||
}
|
||||
};
|
||||
@@ -73,6 +73,11 @@ const NymNodeSignature = ({
|
||||
handleSubmit(onNext)();
|
||||
};
|
||||
|
||||
const handleSignatureChange = (value: string) => {
|
||||
setSignature(value);
|
||||
setValue('signature', value, { shouldValidate: true });
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return <ErrorModal open message={error} onClose={() => {}} />;
|
||||
}
|
||||
@@ -98,14 +103,18 @@ const NymNodeSignature = ({
|
||||
<br />
|
||||
Then paste the signature in the next field.
|
||||
</Typography>
|
||||
<TextField id="outlined-multiline-static" multiline rows={7} value={message} fullWidth disabled />
|
||||
|
||||
<TextFieldWithPaste id="outlined-multiline-static" multiline rows={7} value={message} fullWidth disabled />
|
||||
|
||||
<Stack direction="row" alignItems="center" gap={1} justifyContent="end">
|
||||
<Typography fontWeight={600}>Copy Message</Typography>
|
||||
{message && <CopyToClipboard text={message} iconButton />}
|
||||
</Stack>
|
||||
<TextField
|
||||
|
||||
<TextFieldWithPaste
|
||||
{...register('signature')}
|
||||
onChange={(e) => setSignature(e.target.value)}
|
||||
onPasteValue={handleSignatureChange}
|
||||
onChange={(e) => handleSignatureChange(e.target.value)}
|
||||
id="outlined-multiline-static"
|
||||
name="signature"
|
||||
rows={3}
|
||||
@@ -115,6 +124,7 @@ const NymNodeSignature = ({
|
||||
multiline
|
||||
fullWidth
|
||||
required
|
||||
value={signature}
|
||||
/>
|
||||
</Stack>
|
||||
</SimpleModal>
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { CurrencyDenom, TNodeType } from '@nymproject/types';
|
||||
import { ConfirmTx } from 'src/components/ConfirmTX';
|
||||
import { ModalListItem } from 'src/components/Modals/ModalListItem';
|
||||
import { SimpleModal } from 'src/components/Modals/SimpleModal';
|
||||
import { useGetFee } from 'src/hooks/useGetFee';
|
||||
import { GatewayAmount, GatewayData } from 'src/pages/bonding/types';
|
||||
import { BalanceWarning } from 'src/components/FeeWarning';
|
||||
import { AppContext } from 'src/context';
|
||||
import { BondGatewayForm } from '../forms/legacyForms/BondGatewayForm';
|
||||
|
||||
const defaultGatewayValues: GatewayData = {
|
||||
identityKey: '',
|
||||
sphinxKey: '',
|
||||
ownerSignature: '',
|
||||
location: '',
|
||||
host: '',
|
||||
version: '',
|
||||
mixPort: 1789,
|
||||
clientsPort: 9000,
|
||||
};
|
||||
|
||||
const defaultAmountValues = (denom: CurrencyDenom) => ({
|
||||
amount: { amount: '100', denom },
|
||||
operatorCost: { amount: '40', denom },
|
||||
tokenPool: 'balance',
|
||||
});
|
||||
|
||||
export const BondGatewayModal = ({
|
||||
denom,
|
||||
hasVestingTokens,
|
||||
onSelectNodeType,
|
||||
onClose,
|
||||
onError,
|
||||
}: {
|
||||
denom: CurrencyDenom;
|
||||
hasVestingTokens: boolean;
|
||||
onSelectNodeType: (type: TNodeType) => void;
|
||||
onClose: () => void;
|
||||
onError: (e: string) => void;
|
||||
}) => {
|
||||
const [step, setStep] = useState<1 | 2 | 3>(1);
|
||||
const [gatewayData, setGatewayData] = useState<GatewayData>(defaultGatewayValues);
|
||||
const [amountData, setAmountData] = useState<GatewayAmount>(defaultAmountValues(denom));
|
||||
|
||||
const { fee, resetFeeState, feeError } = useGetFee();
|
||||
const { userBalance } = useContext(AppContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (feeError) {
|
||||
onError(feeError);
|
||||
}
|
||||
}, [feeError]);
|
||||
|
||||
const validateStep = async (s: number) => {
|
||||
const event = new CustomEvent('validate_bond_gateway_step', { detail: { step: s } });
|
||||
window.dispatchEvent(event);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (step === 2) {
|
||||
setStep(1);
|
||||
} else if (step === 3) {
|
||||
setStep(2);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateGatwayData = (data: GatewayData) => {
|
||||
setGatewayData(data);
|
||||
setStep(2);
|
||||
};
|
||||
|
||||
const handleUpdateAmountData = async (data: GatewayAmount) => {
|
||||
setAmountData(data);
|
||||
setStep(3);
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {};
|
||||
|
||||
if (fee) {
|
||||
return (
|
||||
<ConfirmTx
|
||||
open
|
||||
header="Bond details"
|
||||
fee={fee}
|
||||
onClose={onClose}
|
||||
onPrev={resetFeeState}
|
||||
onConfirm={handleConfirm}
|
||||
>
|
||||
<ModalListItem label="Node identity key" value={gatewayData.identityKey} divider />
|
||||
<ModalListItem
|
||||
label="Amount"
|
||||
value={`${amountData.amount.amount} ${amountData.amount.denom.toUpperCase()}`}
|
||||
divider
|
||||
/>
|
||||
{fee.amount?.amount && userBalance.balance && (
|
||||
<BalanceWarning fee={fee.amount?.amount} tx={amountData.amount.amount} />
|
||||
)}
|
||||
</ConfirmTx>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SimpleModal
|
||||
open
|
||||
onOk={async () => {
|
||||
await validateStep(step);
|
||||
}}
|
||||
onBack={step === 2 ? handleBack : undefined}
|
||||
onClose={onClose}
|
||||
header="Bond gateway"
|
||||
subHeader={`Step ${step}/3`}
|
||||
okLabel="Next"
|
||||
>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<BondGatewayForm
|
||||
step={step}
|
||||
denom={denom}
|
||||
gatewayData={gatewayData}
|
||||
amountData={amountData}
|
||||
hasVestingTokens={hasVestingTokens}
|
||||
onValidateGatewayData={handleUpdateGatwayData}
|
||||
onValidateAmountData={handleUpdateAmountData}
|
||||
onSelectNodeType={onSelectNodeType}
|
||||
/>
|
||||
</Box>
|
||||
</SimpleModal>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Stack, Typography, SxProps } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
import { ConfirmationModal } from 'src/components/Modals/ConfirmationModal';
|
||||
import { ErrorModal } from 'src/components/Modals/ErrorModal';
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Stack, TextField, Typography } from '@mui/material';
|
||||
import { Stack, Typography, Box } from '@mui/material';
|
||||
import { useBuyContext } from 'src/context';
|
||||
import { SimpleModal } from '../Modals/SimpleModal';
|
||||
import { ErrorModal } from '../Modals/ErrorModal';
|
||||
import { CopyToClipboard } from '../CopyToClipboard';
|
||||
import { TextFieldWithPaste } from '../Clipboard/ClipboardFormFields';
|
||||
import { CopyToClipboard } from '../Clipboard/ClipboardActions';
|
||||
|
||||
export const SignMessageModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const [message, setMessage] = useState<string>();
|
||||
const [signature, setSignature] = useState<string>();
|
||||
const [message, setMessage] = useState<string>('');
|
||||
const [signature, setSignature] = useState<string>('');
|
||||
|
||||
const { signMessage, loading, refresh, error } = useBuyContext();
|
||||
|
||||
@@ -16,13 +17,17 @@ export const SignMessageModal = ({ onClose }: { onClose: () => void }) => {
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
signMessage(message).then((sig) => {
|
||||
setSignature(sig);
|
||||
});
|
||||
try {
|
||||
const sig = await signMessage(message);
|
||||
setSignature(sig || '');
|
||||
} catch (err) {
|
||||
console.error('Signing failed:', err);
|
||||
setSignature('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setMessage(event.target.value);
|
||||
const handleMessageChange = (value: string) => {
|
||||
setMessage(value);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
@@ -38,34 +43,57 @@ export const SignMessageModal = ({ onClose }: { onClose: () => void }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<SimpleModal open header="Sign message" okLabel="Sign" onOk={handleSign} onClose={onClose} okDisabled={loading}>
|
||||
<Stack gap={2}>
|
||||
<TextField
|
||||
id="outlined-multiline-static"
|
||||
label="Message"
|
||||
multiline
|
||||
rows={8}
|
||||
placeholder="Paste your message here"
|
||||
fullWidth
|
||||
value={message}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<TextField
|
||||
id="outlined-multiline-static"
|
||||
multiline
|
||||
rows={3}
|
||||
value={signature}
|
||||
placeholder="Signature"
|
||||
fullWidth
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Stack direction="row" alignItems="center" alignSelf="flex-end">
|
||||
<Typography variant="body2" component="span" fontWeight={600} sx={{ mr: 1, color: 'text.primary' }}>
|
||||
Copy signature
|
||||
<SimpleModal
|
||||
open
|
||||
header="Sign message"
|
||||
okLabel="Sign"
|
||||
onOk={handleSign}
|
||||
onClose={onClose}
|
||||
okDisabled={loading || !message}
|
||||
>
|
||||
<Stack spacing={3} sx={{ width: '100%' }}>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
||||
Message to Sign
|
||||
</Typography>
|
||||
<CopyToClipboard text={signature} iconButton />
|
||||
</Stack>
|
||||
<TextFieldWithPaste
|
||||
label=""
|
||||
multiline
|
||||
rows={8}
|
||||
placeholder="Enter or paste your message"
|
||||
fullWidth
|
||||
value={message}
|
||||
onPasteValue={handleMessageChange}
|
||||
onChange={(e) => handleMessageChange(e.target.value)}
|
||||
InputLabelProps={{ shrink: false }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
||||
Signature
|
||||
</Typography>
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<TextFieldWithPaste
|
||||
label=""
|
||||
multiline
|
||||
rows={3}
|
||||
value={signature}
|
||||
placeholder="Signature will appear here"
|
||||
fullWidth
|
||||
disabled
|
||||
InputLabelProps={{ shrink: false }}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
endAdornment: signature ? (
|
||||
<Box sx={{ position: 'absolute', top: 8, right: 8 }}>
|
||||
<CopyToClipboard text={signature} iconButton />
|
||||
</Box>
|
||||
) : undefined,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</SimpleModal>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { Button, IconButton, Tooltip } from '@mui/material';
|
||||
import { Check, ContentCopy, ContentPaste } from '@mui/icons-material';
|
||||
import { writeText, readText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
import { Console } from '../../utils/console';
|
||||
|
||||
/**
|
||||
* ClipboardActions component handles copy and paste operations
|
||||
* Can be used as standalone buttons or as icons
|
||||
*/
|
||||
export const ClipboardActions = ({
|
||||
text = '',
|
||||
iconButton = false,
|
||||
showCopy = true,
|
||||
showPaste = true,
|
||||
onPaste,
|
||||
pasteTooltip = 'Paste',
|
||||
copyTooltip = 'Copy',
|
||||
onCopy,
|
||||
fieldRef,
|
||||
}: {
|
||||
text?: string;
|
||||
iconButton?: boolean;
|
||||
showCopy?: boolean;
|
||||
showPaste?: boolean;
|
||||
onPaste?: (pastedText: string) => void;
|
||||
pasteTooltip?: string;
|
||||
copyTooltip?: string;
|
||||
onCopy?: () => void;
|
||||
fieldRef?: React.MutableRefObject<HTMLInputElement | HTMLTextAreaElement | null>;
|
||||
}) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [pasted, setPasted] = useState(false);
|
||||
|
||||
const handleCopy = async (_text: string) => {
|
||||
try {
|
||||
await writeText(_text);
|
||||
setCopied(true);
|
||||
onCopy?.();
|
||||
} catch (e) {
|
||||
Console.error(`failed to copy: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaste = async () => {
|
||||
try {
|
||||
// Try Tauri clipboard first
|
||||
const pastedText = await readText();
|
||||
|
||||
if (pastedText) {
|
||||
onPaste?.(pastedText);
|
||||
setPasted(true);
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback to browser clipboard
|
||||
try {
|
||||
const pastedText = await navigator.clipboard.readText();
|
||||
if (pastedText) {
|
||||
onPaste?.(pastedText);
|
||||
setPasted(true);
|
||||
}
|
||||
} catch (err) {
|
||||
Console.error(`paste failed: ${err}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const wasSelectAllPressed = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fieldRef) return;
|
||||
|
||||
const keydownHandler = async (e: KeyboardEvent) => {
|
||||
// Only handle if the associated field is focused
|
||||
const { activeElement } = document;
|
||||
if (fieldRef.current && activeElement === fieldRef.current) {
|
||||
// Handle Select All (Cmd+A or Ctrl+A)
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'a') {
|
||||
wasSelectAllPressed.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle paste (Cmd+V or Ctrl+V)
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'v' && onPaste) {
|
||||
e.preventDefault();
|
||||
await handlePaste();
|
||||
return;
|
||||
}
|
||||
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'c' && showCopy) {
|
||||
const field = fieldRef.current;
|
||||
|
||||
const selectedText = field.value.substring(field.selectionStart || 0, field.selectionEnd || 0);
|
||||
|
||||
if (selectedText || wasSelectAllPressed.current) {
|
||||
e.preventDefault();
|
||||
|
||||
if (wasSelectAllPressed.current && !selectedText) {
|
||||
field.select();
|
||||
const textToCopy = field.value;
|
||||
await writeText(textToCopy);
|
||||
} else {
|
||||
await writeText(selectedText);
|
||||
}
|
||||
|
||||
setCopied(true);
|
||||
onCopy?.();
|
||||
wasSelectAllPressed.current = false;
|
||||
} else if (text) {
|
||||
e.preventDefault();
|
||||
await writeText(text);
|
||||
setCopied(true);
|
||||
onCopy?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// When field loses focus, reset the Select All tracking
|
||||
const blurHandler = () => {
|
||||
wasSelectAllPressed.current = false;
|
||||
};
|
||||
|
||||
// Add keydown and blur event listeners
|
||||
document.addEventListener('keydown', keydownHandler);
|
||||
if (fieldRef.current) {
|
||||
fieldRef.current.addEventListener('blur', blurHandler);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => {
|
||||
document.removeEventListener('keydown', keydownHandler);
|
||||
if (fieldRef.current) {
|
||||
fieldRef.current.removeEventListener('blur', blurHandler);
|
||||
}
|
||||
};
|
||||
}, [onPaste, fieldRef, text, showCopy, onCopy]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setCopied(false);
|
||||
setPasted(false);
|
||||
}, 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [copied, pasted]);
|
||||
|
||||
if (iconButton) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{showCopy && (
|
||||
<Tooltip title={!copied ? copyTooltip : 'Copied!'} leaveDelay={500}>
|
||||
<IconButton
|
||||
onClick={() => handleCopy(text)}
|
||||
size="small"
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
mr: showPaste ? 0.5 : 0,
|
||||
}}
|
||||
disabled={!text}
|
||||
>
|
||||
{!copied ? <ContentCopy sx={{ fontSize: 14 }} /> : <Check color="success" sx={{ fontSize: 14 }} />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{showPaste && (
|
||||
<Tooltip title={!pasted ? pasteTooltip : 'Pasted!'} leaveDelay={500}>
|
||||
<IconButton
|
||||
onClick={handlePaste}
|
||||
size="small"
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
}}
|
||||
>
|
||||
{!pasted ? <ContentPaste sx={{ fontSize: 14 }} /> : <Check color="success" sx={{ fontSize: 14 }} />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
{showCopy && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
borderColor: 'text.primary',
|
||||
}}
|
||||
onClick={() => handleCopy(text)}
|
||||
endIcon={copied && <Check sx={{ color: (theme) => theme.palette.success.light }} />}
|
||||
disabled={!text}
|
||||
>
|
||||
{!copied ? 'Copy' : 'Copied'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showPaste && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
borderColor: 'text.primary',
|
||||
}}
|
||||
onClick={handlePaste}
|
||||
endIcon={pasted && <Check sx={{ color: (theme) => theme.palette.success.light }} />}
|
||||
>
|
||||
{!pasted ? 'Paste' : 'Pasted'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// For backward compatibility
|
||||
export const CopyToClipboard = ({
|
||||
text = '',
|
||||
iconButton,
|
||||
onPaste,
|
||||
fieldRef,
|
||||
}: {
|
||||
text?: string;
|
||||
iconButton?: boolean;
|
||||
onPaste?: (pastedText: string) => void;
|
||||
fieldRef?: React.MutableRefObject<HTMLInputElement | HTMLTextAreaElement | null>;
|
||||
}) => (
|
||||
<ClipboardActions text={text} iconButton={iconButton} onPaste={onPaste} showPaste={!!onPaste} fieldRef={fieldRef} />
|
||||
);
|
||||
|
||||
// Export a paste-only variant for convenience
|
||||
export const PasteFromClipboard = ({
|
||||
onPaste,
|
||||
iconButton = true,
|
||||
tooltip = 'Paste',
|
||||
fieldRef,
|
||||
}: {
|
||||
onPaste: (pastedText: string) => void;
|
||||
iconButton?: boolean;
|
||||
tooltip?: string;
|
||||
fieldRef?: React.MutableRefObject<HTMLInputElement | HTMLTextAreaElement | null>;
|
||||
}) => (
|
||||
<ClipboardActions
|
||||
iconButton={iconButton}
|
||||
showCopy={false}
|
||||
showPaste
|
||||
onPaste={onPaste}
|
||||
pasteTooltip={tooltip}
|
||||
fieldRef={fieldRef}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,463 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { TextField, InputAdornment, Box, TextFieldProps } from '@mui/material';
|
||||
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
|
||||
import { CurrencyDenom, DecCoin } from '@nymproject/types';
|
||||
import { UseFormRegister, UseFormSetValue, FieldValues, Path, FieldErrors } from 'react-hook-form';
|
||||
import { writeText, readText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
import { PasteFromClipboard } from './ClipboardActions';
|
||||
|
||||
export const useCopyAllSupport = (
|
||||
inputRef: React.MutableRefObject<HTMLInputElement | HTMLTextAreaElement | null>,
|
||||
onPasteValue?: (value: string) => void,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (!inputRef.current) return undefined;
|
||||
|
||||
const handleKeyDown = async (e: Event) => {
|
||||
const keyEvent = e as KeyboardEvent;
|
||||
|
||||
if (document.activeElement !== inputRef.current) return;
|
||||
|
||||
// Handle Cmd+A or Ctrl+A (Select All)
|
||||
if ((keyEvent.metaKey || keyEvent.ctrlKey) && keyEvent.key === 'a') {
|
||||
setTimeout(() => {
|
||||
if (document.activeElement === inputRef.current && inputRef.current) {
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// Handle Cmd+C or Ctrl+C (Copy)
|
||||
if ((keyEvent.metaKey || keyEvent.ctrlKey) && keyEvent.key === 'c') {
|
||||
if (inputRef.current && inputRef.current.selectionStart !== inputRef.current.selectionEnd) {
|
||||
const selectedText = inputRef.current.value.substring(
|
||||
inputRef.current.selectionStart || 0,
|
||||
inputRef.current.selectionEnd || 0,
|
||||
);
|
||||
|
||||
if (selectedText) {
|
||||
keyEvent.preventDefault();
|
||||
writeText(selectedText).catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to copy text:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Cmd+V or Ctrl+V (Paste)
|
||||
if ((keyEvent.metaKey || keyEvent.ctrlKey) && keyEvent.key === 'v' && onPasteValue) {
|
||||
try {
|
||||
keyEvent.preventDefault();
|
||||
const clipboardText = await readText();
|
||||
if (clipboardText) {
|
||||
onPasteValue(clipboardText);
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to paste text:', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const input = inputRef.current;
|
||||
input.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
if (input) {
|
||||
input.removeEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
};
|
||||
}, [inputRef.current, onPasteValue]);
|
||||
};
|
||||
|
||||
export const TextFieldWithPaste = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
TextFieldProps & {
|
||||
onPasteValue?: (value: string) => void;
|
||||
}
|
||||
>(({ onPasteValue, ...props }, ref) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useCopyAllSupport(inputRef, onPasteValue);
|
||||
|
||||
const handlePaste = (pastedText: string) => {
|
||||
onPasteValue?.(pastedText);
|
||||
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TextField
|
||||
{...props}
|
||||
ref={ref}
|
||||
inputRef={inputRef}
|
||||
InputProps={{
|
||||
...props.InputProps,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{onPasteValue && <PasteFromClipboard onPaste={handlePaste} fieldRef={inputRef} />}
|
||||
{props.InputProps?.endAdornment}
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
// Add defaultProps to fix the "require-default-props" warning
|
||||
TextFieldWithPaste.defaultProps = {
|
||||
onPasteValue: undefined,
|
||||
};
|
||||
|
||||
export const CurrencyFormFieldWithPaste = ({
|
||||
label,
|
||||
fullWidth,
|
||||
onChanged,
|
||||
initialValue,
|
||||
denom,
|
||||
required,
|
||||
autoFocus,
|
||||
validationError,
|
||||
}: {
|
||||
label: string;
|
||||
fullWidth?: boolean;
|
||||
onChanged: (value: DecCoin) => void;
|
||||
initialValue?: string;
|
||||
denom?: CurrencyDenom;
|
||||
required?: boolean;
|
||||
autoFocus?: boolean;
|
||||
validationError?: string;
|
||||
}) => {
|
||||
const fieldRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
// Process pasted text to clean the value
|
||||
const processPastedText = (pastedText: string) => {
|
||||
if (!pastedText) return;
|
||||
|
||||
let cleanedValue = pastedText.trim();
|
||||
|
||||
// Remove non-numeric characters except decimal point
|
||||
cleanedValue = cleanedValue.replace(/[^\d.]/g, '');
|
||||
|
||||
// Ensure only one decimal point
|
||||
const parts = cleanedValue.split('.');
|
||||
if (parts.length > 2) {
|
||||
cleanedValue = `${parts[0]}.${parts.slice(1).join('')}`;
|
||||
}
|
||||
|
||||
const decCoin: DecCoin = {
|
||||
amount: cleanedValue,
|
||||
denom: denom as any,
|
||||
};
|
||||
|
||||
onChanged(decCoin);
|
||||
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// Modified to pass the processPastedText function to useCopyAllSupport
|
||||
useEffect(() => {
|
||||
if (!inputRef.current) return undefined;
|
||||
|
||||
const handleKeyDown = async (e: Event) => {
|
||||
const keyEvent = e as KeyboardEvent;
|
||||
if (document.activeElement !== inputRef.current) return;
|
||||
|
||||
// Handle Cmd+A (Select All)
|
||||
if ((keyEvent.metaKey || keyEvent.ctrlKey) && keyEvent.key === 'a' && inputRef.current) {
|
||||
setTimeout(() => {
|
||||
if (inputRef.current) inputRef.current.select();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// Handle Cmd+C (Copy)
|
||||
if ((keyEvent.metaKey || keyEvent.ctrlKey) && keyEvent.key === 'c' && inputRef.current) {
|
||||
if (inputRef.current.selectionStart !== inputRef.current.selectionEnd) {
|
||||
const selectedText = inputRef.current.value.substring(
|
||||
inputRef.current.selectionStart || 0,
|
||||
inputRef.current.selectionEnd || 0,
|
||||
);
|
||||
|
||||
if (selectedText) {
|
||||
keyEvent.preventDefault();
|
||||
writeText(selectedText).catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to copy text:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Cmd+V (Paste)
|
||||
if ((keyEvent.metaKey || keyEvent.ctrlKey) && keyEvent.key === 'v') {
|
||||
try {
|
||||
keyEvent.preventDefault();
|
||||
const clipboardText = await readText();
|
||||
if (clipboardText) {
|
||||
processPastedText(clipboardText);
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to paste text:', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const input = inputRef.current;
|
||||
if (input) {
|
||||
input.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (input) {
|
||||
input.removeEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
};
|
||||
}, [inputRef.current, denom, onChanged]);
|
||||
|
||||
// Find the input element
|
||||
useEffect(() => {
|
||||
const findInputElement = () => {
|
||||
if (fieldRef.current) {
|
||||
inputRef.current = fieldRef.current.querySelector('input');
|
||||
}
|
||||
};
|
||||
|
||||
findInputElement();
|
||||
const timeoutId = setTimeout(findInputElement, 100);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box position="relative" width="100%" ref={fieldRef}>
|
||||
<CurrencyFormField
|
||||
label={label}
|
||||
fullWidth={fullWidth}
|
||||
onChanged={onChanged}
|
||||
initialValue={initialValue}
|
||||
denom={denom}
|
||||
required={required}
|
||||
autoFocus={autoFocus}
|
||||
validationError={validationError}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
right: '14px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<PasteFromClipboard onPaste={processPastedText} fieldRef={inputRef} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
type CurrencyFieldError = {
|
||||
amount?: {
|
||||
message?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const HookFormTextFieldWithPaste = <TFieldValues extends FieldValues>({
|
||||
name,
|
||||
label,
|
||||
register,
|
||||
setValue,
|
||||
errors,
|
||||
...props
|
||||
}: {
|
||||
name: Path<TFieldValues>;
|
||||
label: string;
|
||||
register: UseFormRegister<TFieldValues>;
|
||||
setValue: UseFormSetValue<TFieldValues>;
|
||||
errors: FieldErrors<TFieldValues>;
|
||||
} & Omit<TextFieldProps, 'name' | 'label'>) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handlePaste = (pastedText: string) => {
|
||||
setValue(name, pastedText as any, { shouldValidate: true });
|
||||
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// Pass handlePaste to useCopyAllSupport for Cmd+V handling
|
||||
useCopyAllSupport(inputRef, handlePaste);
|
||||
|
||||
return (
|
||||
<TextField
|
||||
{...register(name)}
|
||||
name={name}
|
||||
label={label}
|
||||
error={Boolean(errors[name])}
|
||||
helperText={errors[name]?.message?.toString()}
|
||||
inputRef={inputRef}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<PasteFromClipboard onPaste={handlePaste} fieldRef={inputRef} />
|
||||
</InputAdornment>
|
||||
),
|
||||
...props.InputProps,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const HookFormCurrencyFieldWithPaste = <TFieldValues extends FieldValues>({
|
||||
name,
|
||||
label,
|
||||
setValue,
|
||||
errors,
|
||||
denom,
|
||||
initialValue,
|
||||
...props
|
||||
}: {
|
||||
name: Path<TFieldValues>;
|
||||
label: string;
|
||||
setValue: UseFormSetValue<TFieldValues>;
|
||||
errors: FieldErrors<TFieldValues>;
|
||||
denom?: CurrencyDenom;
|
||||
initialValue?: string;
|
||||
}) => {
|
||||
const fieldRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
// Handle pasting with number formatting
|
||||
const handlePaste = (pastedText: string) => {
|
||||
if (!pastedText) return;
|
||||
|
||||
let cleanedValue = pastedText.trim();
|
||||
|
||||
cleanedValue = cleanedValue.replace(/[^\d.]/g, '');
|
||||
|
||||
const parts = cleanedValue.split('.');
|
||||
if (parts.length > 2) {
|
||||
cleanedValue = `${parts[0]}.${parts.slice(1).join('')}`;
|
||||
}
|
||||
|
||||
setValue(`${String(name)}.amount` as Path<TFieldValues>, cleanedValue as any, { shouldValidate: true });
|
||||
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// Enable copy-all support for this field with Cmd+V handling
|
||||
useEffect(() => {
|
||||
if (!inputRef.current) return undefined;
|
||||
|
||||
const handleKeyDown = async (e: Event) => {
|
||||
const keyEvent = e as KeyboardEvent;
|
||||
if (document.activeElement !== inputRef.current) return;
|
||||
|
||||
// Handle Cmd+A (Select All)
|
||||
if ((keyEvent.metaKey || keyEvent.ctrlKey) && keyEvent.key === 'a' && inputRef.current) {
|
||||
setTimeout(() => {
|
||||
if (inputRef.current) inputRef.current.select();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// Handle Cmd+C (Copy)
|
||||
if ((keyEvent.metaKey || keyEvent.ctrlKey) && keyEvent.key === 'c' && inputRef.current) {
|
||||
if (inputRef.current.selectionStart !== inputRef.current.selectionEnd) {
|
||||
const selectedText = inputRef.current.value.substring(
|
||||
inputRef.current.selectionStart || 0,
|
||||
inputRef.current.selectionEnd || 0,
|
||||
);
|
||||
|
||||
if (selectedText) {
|
||||
keyEvent.preventDefault();
|
||||
writeText(selectedText).catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to copy text:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Cmd+V (Paste)
|
||||
if ((keyEvent.metaKey || keyEvent.ctrlKey) && keyEvent.key === 'v') {
|
||||
try {
|
||||
keyEvent.preventDefault();
|
||||
const clipboardText = await readText();
|
||||
if (clipboardText) {
|
||||
handlePaste(clipboardText);
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to paste text:', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const input = inputRef.current;
|
||||
if (input) {
|
||||
input.addEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (input) {
|
||||
input.removeEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
};
|
||||
}, [inputRef.current, name, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
const findInputElement = () => {
|
||||
if (fieldRef.current) {
|
||||
inputRef.current = fieldRef.current.querySelector('input');
|
||||
}
|
||||
};
|
||||
|
||||
findInputElement();
|
||||
const timeoutId = setTimeout(findInputElement, 100);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, []);
|
||||
|
||||
// Safely access error message
|
||||
const getErrorMessage = (): string | undefined => {
|
||||
const fieldError = errors[name] as unknown as CurrencyFieldError | undefined;
|
||||
return fieldError?.amount?.message;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box position="relative" width="100%" ref={fieldRef}>
|
||||
<CurrencyFormField
|
||||
label={label}
|
||||
fullWidth
|
||||
onChanged={(value) => {
|
||||
setValue(`${String(name)}.amount` as Path<TFieldValues>, value.amount as any, { shouldValidate: true });
|
||||
}}
|
||||
initialValue={initialValue}
|
||||
denom={denom}
|
||||
validationError={getErrorMessage()}
|
||||
{...props}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
right: '14px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<PasteFromClipboard onPaste={handlePaste} fieldRef={inputRef} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './ClipboardActions';
|
||||
export * from './ClipboardFormFields';
|
||||
@@ -1,60 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, IconButton, Tooltip } from '@mui/material';
|
||||
import { Check, ContentCopy } from '@mui/icons-material';
|
||||
import { clipboard } from '@tauri-apps/api';
|
||||
import { Console } from '../utils/console';
|
||||
|
||||
export const CopyToClipboard = ({ text = '', iconButton }: { text?: string; iconButton?: boolean }) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = async (_text: string) => {
|
||||
try {
|
||||
await clipboard.writeText(_text);
|
||||
setCopied(true);
|
||||
} catch (e) {
|
||||
Console.error(`failed to copy: ${e}`);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (copied) {
|
||||
timer = setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 2000);
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, [copied]);
|
||||
|
||||
if (iconButton)
|
||||
return (
|
||||
<Tooltip title={!copied ? 'Copy' : 'Copied!'} leaveDelay={500}>
|
||||
<IconButton
|
||||
onClick={() => handleCopy(text)}
|
||||
size="small"
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
}}
|
||||
disabled={!text}
|
||||
>
|
||||
{!copied ? <ContentCopy sx={{ fontSize: 14 }} /> : <Check color="success" sx={{ fontSize: 14 }} />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
borderColor: 'text.primary',
|
||||
}}
|
||||
onClick={() => handleCopy(text)}
|
||||
endIcon={copied && <Check sx={{ color: (theme) => theme.palette.success.light }} />}
|
||||
disabled={!text}
|
||||
>
|
||||
{!copied ? 'Copy' : 'Copied'}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
|
||||
import { CurrencyDenom, DecCoin } from '@nymproject/types';
|
||||
import { PasteFromClipboard } from './Clipboard/ClipboardActions';
|
||||
|
||||
export const CurrencyFormFieldWithPaste = ({
|
||||
label,
|
||||
fullWidth,
|
||||
onChanged,
|
||||
initialValue,
|
||||
denom,
|
||||
required,
|
||||
autoFocus,
|
||||
validationError,
|
||||
}: {
|
||||
label: string;
|
||||
fullWidth?: boolean;
|
||||
onChanged: (value: DecCoin) => void;
|
||||
initialValue?: string;
|
||||
denom?: CurrencyDenom;
|
||||
required?: boolean;
|
||||
autoFocus?: boolean;
|
||||
validationError?: string;
|
||||
}) => {
|
||||
const fieldRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const processPastedText = (pastedText: string) => {
|
||||
if (!pastedText) return;
|
||||
|
||||
let cleanedValue = pastedText.trim();
|
||||
cleanedValue = cleanedValue.replace(/[^\d.]/g, '');
|
||||
|
||||
const parts = cleanedValue.split('.');
|
||||
if (parts.length > 2) {
|
||||
cleanedValue = `${parts[0]}.${parts.slice(1).join('')}`;
|
||||
}
|
||||
|
||||
const decCoin: DecCoin = {
|
||||
amount: cleanedValue,
|
||||
denom: denom as any,
|
||||
};
|
||||
|
||||
onChanged(decCoin);
|
||||
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = cleanedValue;
|
||||
|
||||
const inputEvent = new Event('input', { bubbles: true });
|
||||
inputRef.current.dispatchEvent(inputEvent);
|
||||
|
||||
const changeEvent = new Event('change', { bubbles: true });
|
||||
inputRef.current.dispatchEvent(changeEvent);
|
||||
|
||||
inputRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const pasteEventHandler = (e: ClipboardEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { clipboardData } = e;
|
||||
if (!clipboardData) return;
|
||||
|
||||
const pastedText = clipboardData.getData('text');
|
||||
if (!pastedText) return;
|
||||
|
||||
processPastedText(pastedText);
|
||||
};
|
||||
|
||||
const findInputElement = () => {
|
||||
if (fieldRef.current) {
|
||||
const input = fieldRef.current.querySelector('input');
|
||||
if (input) {
|
||||
inputRef.current = input;
|
||||
|
||||
// Set up paste event handler
|
||||
input.addEventListener('paste', pasteEventHandler as EventListener);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
findInputElement();
|
||||
const timeoutId = setTimeout(findInputElement, 200);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
if (inputRef.current) {
|
||||
inputRef.current.removeEventListener('paste', pasteEventHandler as EventListener);
|
||||
}
|
||||
};
|
||||
}, [denom, onChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = async (e: KeyboardEvent) => {
|
||||
if (inputRef.current && document.activeElement === inputRef.current) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'v') {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const clipboardText = await navigator.clipboard.readText();
|
||||
if (clipboardText) {
|
||||
processPastedText(clipboardText);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error accessing clipboard:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [denom, onChanged]);
|
||||
|
||||
return (
|
||||
<Box position="relative" width="100%" ref={fieldRef}>
|
||||
<CurrencyFormField
|
||||
label={label}
|
||||
fullWidth={fullWidth}
|
||||
onChanged={onChanged}
|
||||
initialValue={initialValue}
|
||||
denom={denom}
|
||||
required={required}
|
||||
autoFocus={autoFocus}
|
||||
validationError={validationError}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
right: '14px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<PasteFromClipboard onPaste={processPastedText} fieldRef={inputRef} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,5 @@
|
||||
import React, { useCallback, useContext, useState } from 'react';
|
||||
import { Box, Typography, SxProps } from '@mui/material';
|
||||
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
|
||||
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
|
||||
import { Box, SxProps } from '@mui/material';
|
||||
import { CurrencyDenom, FeeDetails, DecCoin, decimalToFloatApproximation } from '@nymproject/types';
|
||||
import { Console } from 'src/utils/console';
|
||||
import { useGetFee } from 'src/hooks/useGetFee';
|
||||
@@ -13,10 +11,10 @@ import { ModalListItem } from '../Modals/ModalListItem';
|
||||
import { checkTokenBalance, validateAmount, validateKey } from '../../utils';
|
||||
import { TokenPoolSelector, TPoolOption } from '../TokenPoolSelector';
|
||||
import { ConfirmTx } from '../ConfirmTX';
|
||||
|
||||
import { getMixnodeStakeSaturation } from '../../requests';
|
||||
import { ErrorModal } from '../Modals/ErrorModal';
|
||||
import { BalanceWarning } from '../FeeWarning';
|
||||
import { TextFieldWithPaste, CurrencyFormFieldWithPaste } from '../Clipboard/ClipboardFormFields';
|
||||
|
||||
const MIN_AMOUNT_TO_DELEGATE = 10;
|
||||
|
||||
@@ -255,36 +253,24 @@ export const DelegateModal: FCWithChildren<{
|
||||
backdropProps={backdropProps}
|
||||
>
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<IdentityKeyFormField
|
||||
required
|
||||
fullWidth
|
||||
<TextFieldWithPaste
|
||||
label="Node identity key"
|
||||
onChanged={handleIdentityKeyChanged}
|
||||
initialValue={identityKey}
|
||||
readOnly={Boolean(initialIdentityKey)}
|
||||
textFieldProps={{
|
||||
autoFocus: !initialIdentityKey,
|
||||
}}
|
||||
showTickOnValid={false}
|
||||
fullWidth
|
||||
value={identityKey}
|
||||
onChange={(e) => handleIdentityKeyChanged(e.target.value)}
|
||||
onPasteValue={handleIdentityKeyChanged}
|
||||
error={Boolean(errorIdentityKey)}
|
||||
helperText={errorIdentityKey}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</Box>
|
||||
<Typography
|
||||
component="div"
|
||||
textAlign="left"
|
||||
variant="caption"
|
||||
sx={{ color: 'error.main', mx: 2, mt: errorIdentityKey && 1 }}
|
||||
>
|
||||
{errorIdentityKey}
|
||||
</Typography>
|
||||
<Box display="flex" gap={2} alignItems="center" sx={{ mt: 3 }}>
|
||||
{hasVestingContract && <TokenPoolSelector disabled={false} onSelect={(pool) => setTokenPool(pool)} />}
|
||||
<CurrencyFormField
|
||||
required
|
||||
fullWidth
|
||||
<CurrencyFormFieldWithPaste
|
||||
label="Amount"
|
||||
initialValue={amount}
|
||||
autoFocus={Boolean(initialIdentityKey)}
|
||||
fullWidth
|
||||
onChanged={handleAmountChanged}
|
||||
initialValue={amount}
|
||||
denom={denom}
|
||||
validationError={errorAmount}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Box, Chip, IconButton, TableCell, TableRow, Tooltip, Typography } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
import { decimalToPercentage, DelegationWithEverything } from '@nymproject/types';
|
||||
import { LockOutlined, WarningAmberOutlined } from '@mui/icons-material';
|
||||
import { isDelegation, useDelegationContext } from 'src/context/delegations';
|
||||
@@ -100,7 +100,7 @@ export const DelegationItem = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: 'inherit' }}>
|
||||
<TableCell align="center" sx={{ color: 'inherit' }}>
|
||||
{!item.pending_events.length && !nodeIsUnbonded && (
|
||||
<DelegationsActionsMenu
|
||||
onActionClick={(action) => (onItemActionClick ? onItemActionClick(item, action) : undefined)}
|
||||
@@ -117,8 +117,15 @@ export const DelegationItem = ({
|
||||
<Tooltip
|
||||
title="Your changes will take effect when the new epoch starts. There is a new epoch every hour."
|
||||
arrow
|
||||
componentsProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
textAlign: 'left',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Chip label="Pending Events" />
|
||||
<Chip label="Pending events" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
import React from 'react';
|
||||
import { Box, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TableSortLabel } from '@mui/material';
|
||||
import {
|
||||
Alert,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Button,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableSortLabel,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { visuallyHidden } from '@mui/utils';
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
import { DelegationWithEverything } from '@nymproject/types';
|
||||
import { useSortDelegations } from 'src/hooks/useSortDelegations';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { DelegationListItemActions } from './DelegationActions';
|
||||
import { DelegationItem } from './DelegationItem';
|
||||
import { PendingDelegationItem } from './PendingDelegationItem';
|
||||
@@ -15,6 +29,35 @@ export type Order = 'asc' | 'desc';
|
||||
type AdditionalTypes = { profit_margin_percent: number; operating_cost: number };
|
||||
export type SortingKeys = keyof AdditionalTypes | keyof DelegationWithEverything;
|
||||
|
||||
// Helper function to check if a delegation item should be filtered
|
||||
const shouldBeFiltered = (item: any): boolean => {
|
||||
// For regular delegations, filter out placeholders
|
||||
if (isDelegation(item)) {
|
||||
// Check if node_identity is empty or just placeholders
|
||||
if (!item.node_identity || item.node_identity === '-' || item.node_identity === '...') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if uptime is a placeholder dash
|
||||
if (typeof item.avg_uptime_percent === 'string' && item.avg_uptime_percent === '-') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// For pending delegations, keep "Delegate" events but filter out "Undelegate" events with empty node_identity
|
||||
if (isPendingDelegation(item)) {
|
||||
// If it's an undelegate event with empty node_identity, filter it out
|
||||
if ((!item.node_identity || item.node_identity === '') && item.event && item.event.kind === 'Undelegate') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Keep all other pending events (including new delegation events)
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
interface EnhancedTableProps {
|
||||
onRequestSort: (event: React.MouseEvent<unknown>, property: string) => void;
|
||||
order: Order;
|
||||
@@ -27,23 +70,25 @@ interface HeadCell {
|
||||
sortable: boolean;
|
||||
disablePadding?: boolean;
|
||||
align: 'left' | 'center' | 'right';
|
||||
width?: string;
|
||||
}
|
||||
|
||||
const headCells: HeadCell[] = [
|
||||
{ id: 'node_identity', label: 'Node ID', sortable: true, align: 'left' },
|
||||
{ id: 'avg_uptime_percent', label: 'Routing score', sortable: true, align: 'left' },
|
||||
{ id: 'profit_margin_percent', label: 'Profit margin', sortable: true, align: 'left' },
|
||||
{ id: 'operating_cost', label: 'Operating Cost', sortable: true, align: 'left' },
|
||||
{ id: 'stake_saturation', label: 'Stake saturation', sortable: true, align: 'left' },
|
||||
{ id: 'node_identity', label: 'Node ID', sortable: true, align: 'left', width: '15%' },
|
||||
{ id: 'avg_uptime_percent', label: 'Routing score', sortable: true, align: 'left', width: '10%' },
|
||||
{ id: 'profit_margin_percent', label: 'Profit margin', sortable: true, align: 'left', width: '10%' },
|
||||
{ id: 'operating_cost', label: 'Operating Cost', sortable: true, align: 'left', width: '12%' },
|
||||
{ id: 'stake_saturation', label: 'Stake saturation', sortable: true, align: 'left', width: '10%' },
|
||||
{
|
||||
id: 'delegated_on_iso_datetime',
|
||||
label: 'Delegated on',
|
||||
sortable: true,
|
||||
align: 'left',
|
||||
width: '10%',
|
||||
},
|
||||
{ id: 'amount', label: 'Delegation', sortable: true, align: 'left' },
|
||||
{ id: 'unclaimed_rewards', label: 'Reward', sortable: true, align: 'left' },
|
||||
{ id: 'uses_locked_tokens', label: '', sortable: false, align: 'left' },
|
||||
{ id: 'amount', label: 'Delegation', sortable: true, align: 'left', width: '12%' },
|
||||
{ id: 'unclaimed_rewards', label: 'Reward', sortable: true, align: 'left', width: '10%' },
|
||||
{ id: 'uses_locked_tokens', label: '', sortable: false, align: 'left', width: '8%' },
|
||||
];
|
||||
|
||||
const EnhancedTableHead: FCWithChildren<EnhancedTableProps> = ({ order, orderBy, onRequestSort }) => {
|
||||
@@ -61,6 +106,13 @@ const EnhancedTableHead: FCWithChildren<EnhancedTableProps> = ({ order, orderBy,
|
||||
padding={headCell.disablePadding ? 'none' : 'normal'}
|
||||
sortDirection={orderBy === headCell.id ? order : false}
|
||||
color="secondary"
|
||||
sx={{
|
||||
width: headCell.width,
|
||||
minWidth: headCell.id === 'node_identity' ? '120px' : '80px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
<TableSortLabel
|
||||
active={orderBy === headCell.id}
|
||||
@@ -77,13 +129,34 @@ const EnhancedTableHead: FCWithChildren<EnhancedTableProps> = ({ order, orderBy,
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell />
|
||||
<TableCell
|
||||
align="center"
|
||||
sx={{
|
||||
width: '10%',
|
||||
minWidth: '100px',
|
||||
maxWidth: '120px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography noWrap align="center">
|
||||
Actions
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
);
|
||||
};
|
||||
|
||||
// Pin delegations on unbonded nodes to the top of the list
|
||||
const hasPruningError = (item: any): boolean => {
|
||||
if (!isDelegation(item) || !item.errors) return false;
|
||||
|
||||
return (
|
||||
(item.errors.includes('height') && item.errors.includes('not available')) ||
|
||||
item.errors.includes('Due to pruning strategies')
|
||||
);
|
||||
};
|
||||
|
||||
export const DelegationList: FCWithChildren<{
|
||||
isLoading?: boolean;
|
||||
@@ -94,6 +167,7 @@ export const DelegationList: FCWithChildren<{
|
||||
}> = ({ isLoading, items, onItemActionClick, explorerUrl }) => {
|
||||
const [order, setOrder] = React.useState<Order>('asc');
|
||||
const [orderBy, setOrderBy] = React.useState<SortingKeys>('delegated_on_iso_datetime');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { delegationItemErrors, setDelegationItemErrors } = useDelegationContext();
|
||||
|
||||
@@ -103,37 +177,124 @@ export const DelegationList: FCWithChildren<{
|
||||
setOrderBy(property);
|
||||
};
|
||||
|
||||
// Get sorted items
|
||||
const sorted = useSortDelegations(items, order, orderBy);
|
||||
|
||||
// Filter out empty placeholder rows
|
||||
const filteredItems = React.useMemo(() => {
|
||||
if (!sorted) return [];
|
||||
return sorted.filter((item) => !shouldBeFiltered(item));
|
||||
}, [sorted]);
|
||||
|
||||
// Check if any delegations have pruning errors
|
||||
const hasPruningErrors = React.useMemo(() => filteredItems?.some((item) => hasPruningError(item)), [filteredItems]);
|
||||
|
||||
// Navigate to settings page
|
||||
const navigateToSettings = () => {
|
||||
navigate('/settings');
|
||||
};
|
||||
|
||||
// Format error message for display
|
||||
const formatErrorMessage = (message: string) => {
|
||||
if (message.includes('height') && message.includes('not available')) {
|
||||
return 'Due to pruning strategies from validators, please navigate to the Settings tab and change your RPC node for your validator to retrieve your delegations.';
|
||||
}
|
||||
return message;
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
{isLoading && <LoadingModal text="Please wait. Refreshing..." />}
|
||||
<ErrorModal
|
||||
open={Boolean(delegationItemErrors)}
|
||||
title={`Delegation errors for Node ID ${delegationItemErrors?.nodeId || 'unknown'}`}
|
||||
message={delegationItemErrors?.errors || 'oops'}
|
||||
onClose={() => setDelegationItemErrors(undefined)}
|
||||
/>
|
||||
<Table sx={{ width: '100%' }}>
|
||||
<EnhancedTableHead order={order} orderBy={orderBy} onRequestSort={handleRequestSort} />
|
||||
<TableBody>
|
||||
{sorted?.length
|
||||
? sorted.map((item: any) => {
|
||||
if (isPendingDelegation(item)) return <PendingDelegationItem item={item} explorerUrl={explorerUrl} />;
|
||||
if (isDelegation(item))
|
||||
return (
|
||||
<DelegationItem
|
||||
item={item}
|
||||
explorerUrl={explorerUrl}
|
||||
nodeIsUnbonded={Boolean(!item.node_identity)}
|
||||
onItemActionClick={onItemActionClick}
|
||||
/>
|
||||
);
|
||||
return null;
|
||||
})
|
||||
: null}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<>
|
||||
{/* Display pruning error alert at the top if needed */}
|
||||
{hasPruningErrors && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
sx={{ mb: 2 }}
|
||||
action={
|
||||
<Button color="inherit" size="small" onClick={navigateToSettings}>
|
||||
Go to Settings
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<AlertTitle>Data Pruning Detected</AlertTitle>
|
||||
<Typography>
|
||||
Some delegation details cannot be retrieved because of data pruning on the validator. Please navigate to the
|
||||
Settings tab and change your RPC node to fix this issue.
|
||||
</Typography>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Add horizontal scrolling to the table container */}
|
||||
<TableContainer
|
||||
sx={{
|
||||
width: '100%',
|
||||
overflowX: 'auto',
|
||||
'& .MuiTable-root': {
|
||||
tableLayout: 'fixed',
|
||||
minWidth: 650,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{isLoading && <LoadingModal text="Please wait. Refreshing..." />}
|
||||
<ErrorModal
|
||||
open={Boolean(delegationItemErrors)}
|
||||
title={`Delegation errors for Node ID ${delegationItemErrors?.nodeId || 'unknown'}`}
|
||||
message={
|
||||
delegationItemErrors?.errors ? formatErrorMessage(delegationItemErrors.errors) : 'An unknown error occurred'
|
||||
}
|
||||
onClose={() => setDelegationItemErrors(undefined)}
|
||||
/>
|
||||
<Table>
|
||||
<EnhancedTableHead order={order} orderBy={orderBy} onRequestSort={handleRequestSort} />
|
||||
<TableBody>
|
||||
{filteredItems?.length
|
||||
? filteredItems.map((item: any, _index: number) => {
|
||||
if (isPendingDelegation(item)) {
|
||||
const pendingKey = `pending-${item.event.mix_id}-${
|
||||
item.event.address
|
||||
}-${Date.now()}-${Math.random()}`;
|
||||
|
||||
if (
|
||||
item.event &&
|
||||
item.event.kind === 'Delegate' &&
|
||||
(!item.node_identity || item.node_identity === '')
|
||||
) {
|
||||
return (
|
||||
<PendingDelegationItem
|
||||
key={pendingKey}
|
||||
item={{
|
||||
...item,
|
||||
node_identity: `Mix Identity Key ${item.event.mix_id}`,
|
||||
}}
|
||||
explorerUrl={explorerUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <PendingDelegationItem key={pendingKey} item={item} explorerUrl={explorerUrl} />;
|
||||
}
|
||||
|
||||
if (isDelegation(item)) {
|
||||
if (!item.node_identity || item.node_identity === '-' || item.node_identity === '...') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DelegationItem
|
||||
key={`delegation-${item.mix_id}`}
|
||||
item={item}
|
||||
explorerUrl={explorerUrl}
|
||||
nodeIsUnbonded={Boolean(!item.node_identity)}
|
||||
onItemActionClick={onItemActionClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
: null}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Typography, SxProps, Stack } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
import { LoadingModal } from '../Modals/LoadingModal';
|
||||
import { ConfirmationModal } from '../Modals/ConfirmationModal';
|
||||
import { ErrorModal } from '../Modals/ErrorModal';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { DelegationWithEverything } from '@nymproject/types';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
import { DelegationList } from './DelegationList';
|
||||
import { DelegationListItemActions } from './DelegationActions';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Chip, TableCell, TableRow, Tooltip } from '@mui/material';
|
||||
import { Box, Chip, TableCell, TableRow, Tooltip } from '@mui/material';
|
||||
import { WrappedDelegationEvent } from '@nymproject/types';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
|
||||
export const PendingDelegationItem = ({ item, explorerUrl }: { item: WrappedDelegationEvent; explorerUrl: string }) => (
|
||||
<TableRow key={item.node_identity}>
|
||||
@@ -19,17 +19,31 @@ export const PendingDelegationItem = ({ item, explorerUrl }: { item: WrappedDele
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell>
|
||||
<Box sx={{ textAlign: 'left' }}>{item.event.amount?.amount} NYM</Box>
|
||||
</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell align="right">
|
||||
<Tooltip
|
||||
title={`Your delegation of ${item.event.amount?.amount} ${item.event.amount?.denom} will take effect
|
||||
when the new epoch starts. There is a new
|
||||
epoch every hour.`}
|
||||
arrow
|
||||
>
|
||||
<Chip label="Pending Events" />
|
||||
</Tooltip>
|
||||
<TableCell sx={{ textAlign: 'center' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', width: '100%' }}>
|
||||
<Tooltip
|
||||
title={
|
||||
<div style={{ textAlign: 'center', width: '100%' }}>
|
||||
Your delegation of {item.event.amount?.amount} {item.event.amount?.denom} will take effect when the new
|
||||
epoch starts. There is a new epoch every hour.
|
||||
</div>
|
||||
}
|
||||
arrow
|
||||
PopperProps={{
|
||||
sx: {
|
||||
'& .MuiTooltip-tooltip': {
|
||||
textAlign: 'center',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Chip label="Pending Events" />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@@ -4,10 +4,10 @@ import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
|
||||
import { splice } from 'src/utils';
|
||||
|
||||
export const IdentityKey = ({ identityKey }: { identityKey: string }) => (
|
||||
<Stack direction="row">
|
||||
<Typography variant="body2" component="span" fontWeight={400} sx={{ mr: 1, color: 'text.primary' }}>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<Typography variant="body2" component="span" fontWeight={400} sx={{ color: 'text.primary' }}>
|
||||
{splice(6, identityKey)}
|
||||
</Typography>
|
||||
<CopyToClipboard value={identityKey} sx={{ fontSize: 18 }} />
|
||||
<CopyToClipboard value={identityKey} smallIcons />
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { AppContext } from 'src/context';
|
||||
import { Box, Stack, SxProps } from '@mui/material';
|
||||
import { Box, Stack, SxProps, Typography, alpha, useTheme } from '@mui/material';
|
||||
import QRCode from 'qrcode.react';
|
||||
import { ClientAddress } from '@nymproject/react/client-address/ClientAddress';
|
||||
import { ModalListItem } from '../Modals/ModalListItem';
|
||||
import { SimpleModal } from '../Modals/SimpleModal';
|
||||
|
||||
export const ReceiveModal = ({
|
||||
@@ -16,50 +15,158 @@ export const ReceiveModal = ({
|
||||
backdropProps?: object;
|
||||
}) => {
|
||||
const { clientDetails } = useContext(AppContext);
|
||||
const theme = useTheme();
|
||||
|
||||
const isLightMode = theme.palette.mode === 'light';
|
||||
const highlightColor = theme.palette.nym.highlight;
|
||||
const darkBgColor = theme.palette.background.default;
|
||||
|
||||
return (
|
||||
<SimpleModal
|
||||
header="Receive"
|
||||
open
|
||||
onClose={onClose}
|
||||
okLabel=""
|
||||
sx={sx}
|
||||
sx={{
|
||||
...sx,
|
||||
'& .MuiPaper-root': {
|
||||
overflow: 'hidden',
|
||||
borderRadius: '20px',
|
||||
boxShadow: '0 12px 48px rgba(0, 0, 0, 0.15)',
|
||||
maxWidth: '480px',
|
||||
},
|
||||
}}
|
||||
backdropProps={backdropProps}
|
||||
subHeaderStyles={{ mb: 0 }}
|
||||
subHeaderStyles={{ mb: 0, px: 3, pt: 2 }}
|
||||
>
|
||||
<Stack gap={3} sx={{ position: 'relative', top: '32px' }}>
|
||||
<ModalListItem
|
||||
label="Your address"
|
||||
value={
|
||||
clientDetails?.client_address && (
|
||||
<ClientAddress address={clientDetails?.client_address} withCopy showEntireAddress />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Stack
|
||||
alignItems="center"
|
||||
<Stack
|
||||
gap={4}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
px: 3,
|
||||
pb: 3,
|
||||
pt: 1,
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
mb: 1.5,
|
||||
color: 'text.secondary',
|
||||
fontWeight: 600,
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
Your address
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: alpha(theme.palette.primary.main, 0.04),
|
||||
borderRadius: '12px',
|
||||
border: `1px solid ${alpha(theme.palette.primary.main, 0.1)}`,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{clientDetails?.client_address && (
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: '0.9rem',
|
||||
fontFamily: 'monospace',
|
||||
letterSpacing: '0.5px',
|
||||
wordBreak: 'break-all',
|
||||
}}
|
||||
>
|
||||
<ClientAddress address={clientDetails?.client_address} withCopy showEntireAddress />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
left: '-32px',
|
||||
width: '598px',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
bgcolor: isLightMode ? alpha(highlightColor, 0.06) : alpha(darkBgColor, 0.7),
|
||||
borderRadius: '16px',
|
||||
py: 4,
|
||||
bgcolor: (t) => (t.palette.mode === 'dark' ? t.palette.background.default : 'rgba(251, 110, 78, 5%)'),
|
||||
borderRadius: '0px 0px 16px 16px',
|
||||
px: 2,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
border: (t) =>
|
||||
t.palette.mode === 'dark'
|
||||
? `1px solid ${t.palette.nym.nymWallet.modal.border}`
|
||||
: `1px solid ${t.palette.nym.highlight}`,
|
||||
bgcolor: (t) => (t.palette.mode === 'dark' ? 'transparent' : 'white'),
|
||||
borderRadius: 2,
|
||||
p: 3,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '240px',
|
||||
height: '240px',
|
||||
}}
|
||||
>
|
||||
{clientDetails && <QRCode data-testid="qr-code" value={clientDetails?.client_address} />}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '16px',
|
||||
background: `radial-gradient(circle, ${alpha(highlightColor, 0.15)} 0%, transparent 70%)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
p: 3,
|
||||
bgcolor: isLightMode ? 'white' : alpha(darkBgColor, 0.7),
|
||||
borderRadius: '16px',
|
||||
border: `2px solid ${isLightMode ? highlightColor : theme.palette.nym.nymWallet.modal.border}`,
|
||||
boxShadow: `0 10px 32px ${alpha(theme.palette.common.black, 0.1)}`,
|
||||
transition: 'transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.02)',
|
||||
boxShadow: `0 14px 36px ${alpha(theme.palette.common.black, 0.15)}`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{clientDetails && (
|
||||
<QRCode
|
||||
data-testid="qr-code"
|
||||
value={clientDetails?.client_address}
|
||||
size={200}
|
||||
level="H"
|
||||
includeMargin
|
||||
bgColor={isLightMode ? '#FFFFFF' : theme.palette.background.paper}
|
||||
fgColor={isLightMode ? '#000000' : highlightColor}
|
||||
imageSettings={{
|
||||
src: '',
|
||||
excavate: true,
|
||||
width: 32,
|
||||
height: 32,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
mt: 3,
|
||||
color: 'text.secondary',
|
||||
textAlign: 'center',
|
||||
maxWidth: '80%',
|
||||
}}
|
||||
>
|
||||
Scan this QR code with a compatible wallet to receive NYM tokens
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</SimpleModal>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Stack, TextField, Typography, SxProps, FormControlLabel, Checkbox } from '@mui/material';
|
||||
import { Stack, Typography, SxProps, FormControlLabel, Checkbox, Alert } from '@mui/material';
|
||||
import Big from 'big.js';
|
||||
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
|
||||
import { CurrencyDenom, DecCoin, isValidRawCoin } from '@nymproject/types';
|
||||
import { validateAmount } from 'src/utils';
|
||||
import { SimpleModal } from '../Modals/SimpleModal';
|
||||
import { ModalListItem } from '../Modals/ModalListItem';
|
||||
import { TextFieldWithPaste } from '../Clipboard/ClipboardFormFields';
|
||||
import { CurrencyFormFieldWithPaste } from '../CurrencyFormFieldWithPaste';
|
||||
|
||||
const maxUserFees = '10.0';
|
||||
const minUserFees = '0.000001'; // aka 1 unym
|
||||
const MIN_AMOUNT_TO_SEND = '0.000001'; // Adjust this as needed
|
||||
|
||||
// NYM address validation function
|
||||
const validateNymAddress = (address: string): boolean => {
|
||||
if (!address) return false;
|
||||
|
||||
if (!address.startsWith('n1')) return false;
|
||||
|
||||
if (address.length !== 40) return false;
|
||||
|
||||
const validCharsRegex = /^[a-z0-9]+$/;
|
||||
return validCharsRegex.test(address);
|
||||
};
|
||||
|
||||
export const SendInputModal = ({
|
||||
fromAddress,
|
||||
@@ -52,28 +66,118 @@ export const SendInputModal = ({
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
const [memoIsValid, setMemoIsValid] = useState(true);
|
||||
const [feeAmountIsValid, setFeeAmountIsValid] = useState(true);
|
||||
const [addressIsValid, setAddressIsValid] = useState(false);
|
||||
const [errorAmount, setErrorAmount] = useState<string | undefined>();
|
||||
const [errorFee, setErrorFee] = useState<string | undefined>();
|
||||
|
||||
const validate = async (value: DecCoin) => {
|
||||
const isValidAmount = await validateAmount(value.amount, '0');
|
||||
setIsValid(isValidAmount);
|
||||
// Calculate noAccount at the component root level instead of using useEffect
|
||||
const noAccount = !balance || balance === '0' || parseFloat(balance) === 0;
|
||||
|
||||
const validateSendAmount = async (value: DecCoin) => {
|
||||
let newValidatedValue = true;
|
||||
let errorAmountMessage;
|
||||
|
||||
if (noAccount) {
|
||||
newValidatedValue = false;
|
||||
errorAmountMessage = 'You need to acquire NYMs before sending. Try using the Buy section in the app.';
|
||||
setIsValid(newValidatedValue);
|
||||
setErrorAmount(errorAmountMessage);
|
||||
return newValidatedValue;
|
||||
}
|
||||
|
||||
if (!value.amount) {
|
||||
newValidatedValue = false;
|
||||
} else {
|
||||
// Skip validation for partial decimal inputs during typing
|
||||
if (value.amount === '.' || value.amount.endsWith('.')) {
|
||||
newValidatedValue = false;
|
||||
setIsValid(newValidatedValue);
|
||||
setErrorAmount(undefined);
|
||||
return newValidatedValue;
|
||||
}
|
||||
|
||||
if (!(await validateAmount(value.amount, '0'))) {
|
||||
newValidatedValue = false;
|
||||
errorAmountMessage = 'Please enter a valid amount';
|
||||
} else if (Number(value.amount) < Number(MIN_AMOUNT_TO_SEND)) {
|
||||
newValidatedValue = false;
|
||||
errorAmountMessage = `Min. send amount: ${MIN_AMOUNT_TO_SEND} ${denom?.toUpperCase()}`;
|
||||
} else if (balance && value.amount) {
|
||||
try {
|
||||
const amountBig = new Big(value.amount);
|
||||
const balanceBig = new Big(balance);
|
||||
|
||||
if (amountBig.gt(balanceBig)) {
|
||||
newValidatedValue = false;
|
||||
errorAmountMessage = `Make sure you have sufficient funds. Available: ${balance} ${denom?.toUpperCase()}`;
|
||||
}
|
||||
} catch (err) {
|
||||
if (!/^\d*\.?\d*$/.test(value.amount)) {
|
||||
newValidatedValue = false;
|
||||
errorAmountMessage = 'Invalid number format';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setIsValid(newValidatedValue);
|
||||
setErrorAmount(errorAmountMessage);
|
||||
return newValidatedValue;
|
||||
};
|
||||
|
||||
const validateUserFees = (fees: DecCoin) => {
|
||||
let feeValid = true;
|
||||
let errorFeeMessage;
|
||||
|
||||
if (noAccount) {
|
||||
feeValid = false;
|
||||
errorFeeMessage = 'You need to acquire NYMs before setting fees.';
|
||||
setFeeAmountIsValid(feeValid);
|
||||
setErrorFee(errorFeeMessage);
|
||||
return feeValid;
|
||||
}
|
||||
|
||||
// Skip validation for partial decimal inputs during typing
|
||||
if (fees.amount === '.' || fees.amount.endsWith('.')) {
|
||||
setFeeAmountIsValid(false);
|
||||
setErrorFee(undefined);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidRawCoin(fees.amount) || !Number(fees.amount)) {
|
||||
setFeeAmountIsValid(false);
|
||||
return;
|
||||
feeValid = false;
|
||||
errorFeeMessage = 'Please enter a valid fee amount';
|
||||
} else {
|
||||
try {
|
||||
const f = Big(fees.amount);
|
||||
if (f.gt(maxUserFees)) {
|
||||
feeValid = false;
|
||||
errorFeeMessage = `Max fee: ${maxUserFees} ${denom?.toUpperCase()}`;
|
||||
} else if (f.lt(minUserFees)) {
|
||||
feeValid = false;
|
||||
errorFeeMessage = `Min. fee: ${minUserFees} ${denom?.toUpperCase()}`;
|
||||
}
|
||||
} catch (err) {
|
||||
if (!/^\d*\.?\d*$/.test(fees.amount)) {
|
||||
feeValid = false;
|
||||
errorFeeMessage = 'Invalid fee format';
|
||||
}
|
||||
}
|
||||
}
|
||||
const f = Big(fees.amount);
|
||||
if (f.gt(maxUserFees) || f.lt(minUserFees)) {
|
||||
setFeeAmountIsValid(false);
|
||||
return;
|
||||
}
|
||||
setFeeAmountIsValid(true);
|
||||
|
||||
setFeeAmountIsValid(feeValid);
|
||||
setErrorFee(errorFeeMessage);
|
||||
return feeValid;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (amount) validate(amount);
|
||||
}, []);
|
||||
if (amount) validateSendAmount(amount);
|
||||
}, [amount, balance, noAccount]);
|
||||
|
||||
// Effect to validate address whenever it changes
|
||||
useEffect(() => {
|
||||
setAddressIsValid(validateNymAddress(toAddress));
|
||||
}, [toAddress]);
|
||||
|
||||
useEffect(() => {
|
||||
if (memo && !/^(\w|\s)+$/.test(memo)) {
|
||||
@@ -89,7 +193,7 @@ export const SendInputModal = ({
|
||||
} else {
|
||||
setFeeAmountIsValid(true);
|
||||
}
|
||||
}, [userFees]);
|
||||
}, [userFees, noAccount]);
|
||||
|
||||
return (
|
||||
<SimpleModal
|
||||
@@ -98,30 +202,50 @@ export const SendInputModal = ({
|
||||
onClose={onClose}
|
||||
okLabel="Next"
|
||||
onOk={async () => onNext()}
|
||||
okDisabled={!isValid || !memoIsValid || !feeAmountIsValid}
|
||||
okDisabled={!isValid || !memoIsValid || !feeAmountIsValid || !addressIsValid || noAccount}
|
||||
sx={sx}
|
||||
backdropProps={backdropProps}
|
||||
>
|
||||
{noAccount && (
|
||||
<Alert severity="warning" sx={{ mb: 3 }}>
|
||||
To start staking, sending or operating the on the NYM network, you first need to get native NYM tokens.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Stack gap={3}>
|
||||
<ModalListItem label="Your address" value={fromAddress} fontWeight="light" />
|
||||
<TextField
|
||||
|
||||
{/* Recipient address field with paste button */}
|
||||
<TextFieldWithPaste
|
||||
label="Recipient address"
|
||||
fullWidth
|
||||
onChange={(e) => onAddressChange(e.target.value)}
|
||||
value={toAddress}
|
||||
error={toAddress !== '' && !addressIsValid}
|
||||
helperText={
|
||||
toAddress !== '' && !addressIsValid
|
||||
? 'Invalid NYM address. Must start with n1 and be exactly 40 characters long.'
|
||||
: undefined
|
||||
}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
onPasteValue={onAddressChange}
|
||||
/>
|
||||
<CurrencyFormField
|
||||
|
||||
{/* Amount field with paste button */}
|
||||
<CurrencyFormFieldWithPaste
|
||||
label="Amount"
|
||||
fullWidth
|
||||
onChanged={(value) => {
|
||||
onAmountChange(value);
|
||||
validate(value);
|
||||
validateSendAmount(value);
|
||||
}}
|
||||
initialValue={amount?.amount}
|
||||
denom={denom}
|
||||
validationError={errorAmount}
|
||||
/>
|
||||
<TextField
|
||||
|
||||
{/* Memo field with paste button */}
|
||||
<TextFieldWithPaste
|
||||
name="memo"
|
||||
label="Memo"
|
||||
onChange={(e) => onMemoChange(e.target.value)}
|
||||
@@ -133,29 +257,39 @@ export const SendInputModal = ({
|
||||
}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
fullWidth
|
||||
onPasteValue={onMemoChange}
|
||||
/>
|
||||
|
||||
<Typography fontSize="smaller" sx={{ color: 'error.main' }}>
|
||||
{error}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<Stack gap={0.5} sx={{ mt: 1 }}>
|
||||
<ModalListItem label="Account balance" value={balance?.toUpperCase()} divider fontWeight={600} />
|
||||
<ModalListItem label="Account balance" value={balance ? balance.toUpperCase() : '0'} divider fontWeight={600} />
|
||||
<Typography fontSize="smaller" sx={{ color: 'text.primary' }}>
|
||||
Est. fee for this transaction will be shown on the next page
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<FormControlLabel
|
||||
control={<Checkbox onChange={() => setShowMore(!showMore)} checked={showMore} />}
|
||||
label="More options"
|
||||
sx={{ mt: 2 }}
|
||||
/>
|
||||
|
||||
{showMore && (
|
||||
<Stack direction="column" gap={3} mt={2} mb={3}>
|
||||
<CurrencyFormField
|
||||
<Stack mt={2} mb={3}>
|
||||
<CurrencyFormFieldWithPaste
|
||||
label="Fee"
|
||||
onChanged={(v) => onUserFeesChange(v)}
|
||||
onChanged={(v) => {
|
||||
onUserFeesChange(v);
|
||||
validateUserFees(v);
|
||||
}}
|
||||
initialValue={userFees?.amount}
|
||||
fullWidth
|
||||
denom={denom}
|
||||
validationError={errorFee}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Stack, Typography, SxProps } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
import { AppContext } from 'src/context';
|
||||
import { TTransactionDetails } from './types';
|
||||
import { ConfirmationModal } from '../Modals/ConfirmationModal';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Button, Stack, Typography } from '@mui/material';
|
||||
import { checkUpdate } from '@tauri-apps/api/updater';
|
||||
import { check } from '@tauri-apps/plugin-updater';
|
||||
import { AppContext } from '../../context';
|
||||
import { checkVersion } from '../../requests';
|
||||
import { Console } from '../../utils/console';
|
||||
@@ -28,7 +28,7 @@ const AppVersion = () => {
|
||||
try {
|
||||
// despite the name, this will spawn an external native window with
|
||||
// an embedded "download and update the Wallet" flow
|
||||
checkUpdate();
|
||||
check();
|
||||
} catch (e) {
|
||||
Console.error(e);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { Link, LinkProps } from '@nymproject/react/link/Link';
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
|
||||
export const TauriLink: React.FC<LinkProps & any> = (props) => {
|
||||
const { href, onClick, ...restProps } = props;
|
||||
|
||||
const handleClick = async (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
if (onClick) {
|
||||
onClick(event);
|
||||
}
|
||||
|
||||
if (href && (href.startsWith('http://') || href.startsWith('https://'))) {
|
||||
event.preventDefault();
|
||||
console.log('Opening link in browser:', href);
|
||||
await openUrl(href);
|
||||
}
|
||||
};
|
||||
|
||||
return <Link href={href} onClick={handleClick} {...restProps} />;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
export * from './AppBar';
|
||||
export * from './ConfirmPassword';
|
||||
export * from './CopyToClipboard';
|
||||
export * from './Clipboard';
|
||||
export * from './ErrorFallback';
|
||||
export * from './InfoToolTip';
|
||||
export * from './LoadingPage';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Alert, Grid, Typography } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { Alert, Grid, Typography, Skeleton } from '@mui/material';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
import { ClientAddress } from '@nymproject/react/client-address/ClientAddress';
|
||||
import { Network } from 'src/types';
|
||||
import { Balance } from '@nymproject/types';
|
||||
@@ -12,11 +12,13 @@ export const BalanceCard = ({
|
||||
userBalanceError,
|
||||
network,
|
||||
clientAddress,
|
||||
isLoading,
|
||||
}: {
|
||||
userBalance?: Balance;
|
||||
userBalanceError?: string;
|
||||
network?: Network;
|
||||
clientAddress?: string;
|
||||
isLoading?: boolean;
|
||||
}) => (
|
||||
<NymCard
|
||||
title="Balance"
|
||||
@@ -31,25 +33,29 @@ export const BalanceCard = ({
|
||||
{userBalanceError}
|
||||
</Alert>
|
||||
)}
|
||||
{!userBalanceError && (
|
||||
<Typography
|
||||
data-testid="refresh-success"
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: '600',
|
||||
fontSize: 28,
|
||||
}}
|
||||
variant="h5"
|
||||
>
|
||||
{userBalance?.printable_balance}
|
||||
</Typography>
|
||||
{isLoading ? (
|
||||
<Skeleton width={160} height={42} />
|
||||
) : (
|
||||
!userBalanceError && (
|
||||
<Typography
|
||||
data-testid="refresh-success"
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: '600',
|
||||
fontSize: 28,
|
||||
}}
|
||||
variant="h5"
|
||||
>
|
||||
{userBalance?.printable_balance || '—'}
|
||||
</Typography>
|
||||
)
|
||||
)}
|
||||
</Grid>
|
||||
{network && (
|
||||
<Grid item>
|
||||
<Link
|
||||
href={`${urls(network).mixnetExplorer}/account/${clientAddress}`}
|
||||
href={`${urls(network).mixnetExplorer}account/${clientAddress}`}
|
||||
target="_blank"
|
||||
text="Last transactions"
|
||||
fontSize={14}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Refresh } from '@mui/icons-material';
|
||||
import { Grid, IconButton, Typography } from '@mui/material';
|
||||
import { Grid, IconButton, Typography, Skeleton } from '@mui/material';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { NymCard } from 'src/components';
|
||||
import { TokenTransfer } from 'src/components/Balance/cards/TokenTransfer';
|
||||
@@ -15,6 +15,7 @@ export const VestingCard = ({
|
||||
onTransfer,
|
||||
fetchBalance,
|
||||
fetchTokenAllocation,
|
||||
isLoading,
|
||||
}: {
|
||||
unlockedTokens?: string;
|
||||
unlockedRewards?: string;
|
||||
@@ -23,6 +24,7 @@ export const VestingCard = ({
|
||||
fetchTokenAllocation: () => Promise<void>;
|
||||
fetchBalance: () => Promise<void>;
|
||||
onTransfer: () => Promise<void>;
|
||||
isLoading?: boolean;
|
||||
}) => {
|
||||
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
|
||||
|
||||
@@ -54,14 +56,15 @@ export const VestingCard = ({
|
||||
await refreshBalances();
|
||||
enqueueSnackbar('Balances updated', { variant: 'success', preventDuplicate: true });
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Refresh />
|
||||
{isLoading ? <Skeleton variant="circular" width={24} height={24} /> : <Refresh />}
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={7} lg={8}>
|
||||
<VestingSchedule />
|
||||
<VestingSchedule isLoading={isLoading} />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={5} lg={4}>
|
||||
<TokenTransfer
|
||||
@@ -69,6 +72,7 @@ export const VestingCard = ({
|
||||
unlockedTokens={unlockedTokens}
|
||||
unlockedRewards={unlockedRewards}
|
||||
unlockedTransferable={unlockedTransferable}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { clean } from 'semver';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { Button, Divider, Typography, TextField, Grid, Box } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import {
|
||||
simulateUpdateGatewayConfig,
|
||||
simulateVestingUpdateGatewayConfig,
|
||||
updateGatewayConfig,
|
||||
vestingUpdateGatewayConfig,
|
||||
} from 'src/requests';
|
||||
import { useBondingContext } from 'src/context/bonding';
|
||||
import { SimpleModal } from 'src/components/Modals/SimpleModal';
|
||||
import { Console } from 'src/utils/console';
|
||||
import { Alert } from 'src/components/Alert';
|
||||
import { ConfirmTx } from 'src/components/ConfirmTX';
|
||||
import { useGetFee } from 'src/hooks/useGetFee';
|
||||
import { LoadingModal } from 'src/components/Modals/LoadingModal';
|
||||
import { updateGatewayValidationSchema } from 'src/components/Bonding/forms/legacyForms/gatewayValidationSchema';
|
||||
import { BalanceWarning } from 'src/components/FeeWarning';
|
||||
import { AppContext } from 'src/context';
|
||||
import { TBondedGateway } from 'src/requests/gatewayDetails';
|
||||
|
||||
export const GeneralGatewaySettings = ({ bondedNode }: { bondedNode: TBondedGateway }) => {
|
||||
const [openConfirmationModal, setOpenConfirmationModal] = useState<boolean>(false);
|
||||
const { getFee, fee, resetFeeState } = useGetFee();
|
||||
const { refresh } = useBondingContext();
|
||||
const { userBalance } = useContext(AppContext);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting, isDirty, isValid },
|
||||
} = useForm({
|
||||
resolver: yupResolver(updateGatewayValidationSchema),
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
host: bondedNode.host,
|
||||
mixPort: bondedNode.mixPort,
|
||||
httpApiPort: bondedNode.httpApiPort,
|
||||
version: bondedNode.version,
|
||||
location: bondedNode.location,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (data: any) => {
|
||||
resetFeeState();
|
||||
const { host, mixPort, httpApiPort, version, location } = data;
|
||||
|
||||
try {
|
||||
const GatewayConfigParams = {
|
||||
host,
|
||||
mix_port: mixPort,
|
||||
location,
|
||||
version: clean(version) as string,
|
||||
clients_port: httpApiPort,
|
||||
};
|
||||
|
||||
if (bondedNode.proxy) {
|
||||
await vestingUpdateGatewayConfig(GatewayConfigParams, fee?.fee);
|
||||
} else {
|
||||
await updateGatewayConfig(GatewayConfigParams, fee?.fee);
|
||||
}
|
||||
|
||||
setOpenConfirmationModal(true);
|
||||
} catch (error) {
|
||||
Console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container xs item>
|
||||
{fee && (
|
||||
<ConfirmTx
|
||||
open
|
||||
header="Update node settings"
|
||||
fee={fee}
|
||||
onConfirm={handleSubmit((d) => onSubmit(d))}
|
||||
onPrev={resetFeeState}
|
||||
onClose={resetFeeState}
|
||||
>
|
||||
{fee.amount?.amount && userBalance?.balance?.amount.amount && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<BalanceWarning fee={fee.amount.amount} />
|
||||
</Box>
|
||||
)}
|
||||
</ConfirmTx>
|
||||
)}
|
||||
{isSubmitting && <LoadingModal />}
|
||||
<Alert
|
||||
title={
|
||||
<Box sx={{ fontWeight: 600 }}>
|
||||
Changing these values will ONLY change the data about your node on the blockchain. Remember to change your
|
||||
node’s config file with the same values too
|
||||
</Box>
|
||||
}
|
||||
dismissable
|
||||
/>
|
||||
<Grid container>
|
||||
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3}>
|
||||
<Grid item>
|
||||
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Port
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid spacing={3} item container alignItems="center" xs={12} md={6}>
|
||||
<Grid item width={1}>
|
||||
<TextField
|
||||
{...register('mixPort')}
|
||||
name="mixPort"
|
||||
label="Mix Port"
|
||||
fullWidth
|
||||
error={!!errors.mixPort}
|
||||
helperText={errors.mixPort?.message}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item width={1}>
|
||||
<TextField
|
||||
{...register('httpApiPort')}
|
||||
name="httpApiPort"
|
||||
label="Client Port"
|
||||
fullWidth
|
||||
error={!!errors.httpApiPort}
|
||||
helperText={errors.httpApiPort?.message}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3}>
|
||||
<Grid item>
|
||||
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Host
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid spacing={3} item container alignItems="center" xs={12} md={6}>
|
||||
<Grid item width={1}>
|
||||
<TextField
|
||||
{...register('host')}
|
||||
name="host"
|
||||
label="Host"
|
||||
fullWidth
|
||||
error={!!errors.host}
|
||||
helperText={errors.host?.message}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3}>
|
||||
<Grid item>
|
||||
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Version
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid spacing={3} item container alignItems="center" xs={12} md={6}>
|
||||
<Grid item width={1}>
|
||||
<TextField
|
||||
{...register('version')}
|
||||
name="version"
|
||||
label="Version"
|
||||
fullWidth
|
||||
error={!!errors.version}
|
||||
helperText={errors.version?.message}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3}>
|
||||
<Grid item>
|
||||
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Location
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid spacing={3} item container alignItems="center" xs={12} md={6}>
|
||||
<Grid item width={1}>
|
||||
<TextField
|
||||
{...register('location')}
|
||||
name="location"
|
||||
label="Location"
|
||||
fullWidth
|
||||
error={!!errors.location}
|
||||
helperText={errors.location?.message}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Divider flexItem />
|
||||
<Grid container justifyContent="end">
|
||||
<Button
|
||||
size="large"
|
||||
variant="contained"
|
||||
disabled={isSubmitting || !isDirty || !isValid}
|
||||
onClick={handleSubmit((data) =>
|
||||
getFee(bondedNode.proxy ? simulateVestingUpdateGatewayConfig : simulateUpdateGatewayConfig, {
|
||||
host: data.host,
|
||||
mix_port: data.mixPort,
|
||||
clients_port: data.httpApiPort,
|
||||
location: bondedNode.location!,
|
||||
version: data.version,
|
||||
}),
|
||||
)}
|
||||
sx={{ m: 3 }}
|
||||
>
|
||||
Submit changes to the blockchain
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<SimpleModal
|
||||
open={openConfirmationModal}
|
||||
header="Your changes are submitted to the blockchain"
|
||||
subHeader="Remember to change the values
|
||||
on your gateways’s config file too."
|
||||
okLabel="close"
|
||||
hideCloseIcon
|
||||
displayInfoIcon
|
||||
onOk={async () => {
|
||||
setOpenConfirmationModal(false);
|
||||
await refresh();
|
||||
}}
|
||||
buttonFullWidth
|
||||
sx={{
|
||||
width: '450px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
headerStyles={{
|
||||
width: '100%',
|
||||
mb: 1,
|
||||
textAlign: 'center',
|
||||
color: theme.palette.nym.nymWallet.text.blue,
|
||||
fontSize: 16,
|
||||
}}
|
||||
subHeaderStyles={{
|
||||
width: '100%',
|
||||
mb: 1,
|
||||
textAlign: 'center',
|
||||
color: 'main',
|
||||
fontSize: 14,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -1,245 +0,0 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { clean } from 'semver';
|
||||
import { Box, Button, Divider, Grid, Stack, TextField, Typography } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { isMixnode } from 'src/types';
|
||||
import { simulateUpdateMixnodeConfig, simulateVestingUpdateMixnodeConfig, updateMixnodeConfig } from 'src/requests';
|
||||
import { SimpleModal } from 'src/components/Modals/SimpleModal';
|
||||
import { bondedInfoParametersValidationSchema } from 'src/components/Bonding/forms/legacyForms/mixnodeValidationSchema';
|
||||
import { Console } from 'src/utils/console';
|
||||
import { Alert } from 'src/components/Alert';
|
||||
import { vestingUpdateMixnodeConfig } from 'src/requests/vesting';
|
||||
import { ConfirmTx } from 'src/components/ConfirmTX';
|
||||
import { useGetFee } from 'src/hooks/useGetFee';
|
||||
import { LoadingModal } from 'src/components/Modals/LoadingModal';
|
||||
import { BalanceWarning } from 'src/components/FeeWarning';
|
||||
import { AppContext } from 'src/context';
|
||||
import { TBondedMixnode } from 'src/requests/mixnodeDetails';
|
||||
|
||||
export const GeneralMixnodeSettings = ({ bondedNode }: { bondedNode: TBondedMixnode }) => {
|
||||
const [openConfirmationModal, setOpenConfirmationModal] = useState<boolean>(false);
|
||||
const { getFee, fee, resetFeeState } = useGetFee();
|
||||
const { userBalance } = useContext(AppContext);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting, isDirty, isValid },
|
||||
} = useForm({
|
||||
resolver: yupResolver(bondedInfoParametersValidationSchema),
|
||||
mode: 'onChange',
|
||||
defaultValues: isMixnode(bondedNode) ? bondedNode : {},
|
||||
});
|
||||
|
||||
const onSubmit = async (data: {
|
||||
host?: string;
|
||||
version?: string;
|
||||
mixPort?: number;
|
||||
verlocPort?: number;
|
||||
httpApiPort?: number;
|
||||
}) => {
|
||||
resetFeeState();
|
||||
const { host, version, mixPort, verlocPort, httpApiPort } = data;
|
||||
if (host && version && mixPort && verlocPort && httpApiPort) {
|
||||
const MixNodeConfigParams = {
|
||||
host,
|
||||
mix_port: mixPort,
|
||||
verloc_port: verlocPort,
|
||||
http_api_port: httpApiPort,
|
||||
version: clean(version) as string,
|
||||
};
|
||||
try {
|
||||
if (bondedNode.proxy) {
|
||||
await vestingUpdateMixnodeConfig(MixNodeConfigParams);
|
||||
} else {
|
||||
await updateMixnodeConfig(MixNodeConfigParams);
|
||||
}
|
||||
setOpenConfirmationModal(true);
|
||||
} catch (error) {
|
||||
Console.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container xs>
|
||||
{fee && (
|
||||
<ConfirmTx
|
||||
open
|
||||
header="Update node settings"
|
||||
fee={fee}
|
||||
onConfirm={handleSubmit((d) => onSubmit(d))}
|
||||
onPrev={resetFeeState}
|
||||
onClose={resetFeeState}
|
||||
>
|
||||
{fee.amount?.amount && userBalance?.balance?.amount.amount && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<BalanceWarning fee={fee.amount.amount} />
|
||||
</Box>
|
||||
)}
|
||||
</ConfirmTx>
|
||||
)}
|
||||
{isSubmitting && <LoadingModal />}
|
||||
<Alert
|
||||
title={
|
||||
<Stack>
|
||||
<Typography fontWeight={600}>
|
||||
Changing these values will ONLY change the data about your node on the blockchain.
|
||||
</Typography>
|
||||
<Typography>Remember to change your node’s config file with the same values too.</Typography>
|
||||
</Stack>
|
||||
}
|
||||
bgColor={`${theme.palette.nym.nymWallet.text.blue}0D !important`}
|
||||
dismissable
|
||||
/>
|
||||
<Grid container mt={2}>
|
||||
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3}>
|
||||
<Grid item>
|
||||
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Port
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid spacing={3} item container alignItems="center" xs={12} md={6}>
|
||||
<Grid item width={1}>
|
||||
<TextField
|
||||
{...register('mixPort')}
|
||||
name="mixPort"
|
||||
label="Mix Port"
|
||||
fullWidth
|
||||
error={!!errors.mixPort}
|
||||
helperText={errors.mixPort?.message}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item width={1}>
|
||||
<TextField
|
||||
{...register('verlocPort')}
|
||||
name="verlocPort"
|
||||
label="Verloc Port"
|
||||
fullWidth
|
||||
error={!!errors.verlocPort}
|
||||
helperText={errors.verlocPort?.message}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item width={1}>
|
||||
<TextField
|
||||
{...register('httpApiPort')}
|
||||
name="httpApiPort"
|
||||
label="HTTP port"
|
||||
fullWidth
|
||||
error={!!errors.httpApiPort}
|
||||
helperText={errors.httpApiPort?.message}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Divider sx={{ width: '100%' }} />
|
||||
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3}>
|
||||
<Grid item>
|
||||
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Host
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid spacing={3} item container alignItems="center" xs={12} md={6}>
|
||||
<Grid item width={1}>
|
||||
<TextField
|
||||
{...register('host')}
|
||||
name="host"
|
||||
label="Host"
|
||||
fullWidth
|
||||
error={!!errors.host}
|
||||
helperText={errors.host?.message}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Divider sx={{ width: '100%' }} />
|
||||
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3}>
|
||||
<Grid item>
|
||||
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
Version
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid spacing={3} item container alignItems="center" xs={12} md={6}>
|
||||
<Grid item width={1}>
|
||||
<TextField
|
||||
{...register('version')}
|
||||
name="version"
|
||||
label="Version"
|
||||
fullWidth
|
||||
error={!!errors.version}
|
||||
helperText={errors.version?.message}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Divider sx={{ width: '100%' }} />
|
||||
<Grid item container direction="row" justifyContent="space-between" padding={3}>
|
||||
<Grid item />
|
||||
<Grid spacing={3} item container alignItems="center" xs={12} md={6}>
|
||||
<Button
|
||||
size="large"
|
||||
variant="contained"
|
||||
disabled={isSubmitting || !isDirty || !isValid}
|
||||
onClick={handleSubmit((data) =>
|
||||
getFee(bondedNode.proxy ? simulateVestingUpdateMixnodeConfig : simulateUpdateMixnodeConfig, {
|
||||
host: data.host,
|
||||
mix_port: data.mixPort,
|
||||
verloc_port: data.verlocPort,
|
||||
http_api_port: data.httpApiPort,
|
||||
version: data.version,
|
||||
}),
|
||||
)}
|
||||
sx={{ m: 3, mr: 0 }}
|
||||
fullWidth
|
||||
>
|
||||
Submit changes to the blockchain
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<SimpleModal
|
||||
open={openConfirmationModal}
|
||||
header="Your changes are submitted to the blockchain"
|
||||
subHeader="Remember to change the values
|
||||
on your node’s config file too."
|
||||
okLabel="close"
|
||||
hideCloseIcon
|
||||
displayInfoIcon
|
||||
onOk={async () => {
|
||||
await setOpenConfirmationModal(false);
|
||||
}}
|
||||
buttonFullWidth
|
||||
sx={{
|
||||
width: '450px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
headerStyles={{
|
||||
width: '100%',
|
||||
mb: 1,
|
||||
textAlign: 'center',
|
||||
color: theme.palette.nym.nymWallet.text.blue,
|
||||
fontSize: 16,
|
||||
}}
|
||||
subHeaderStyles={{
|
||||
width: '100%',
|
||||
mb: 1,
|
||||
textAlign: 'center',
|
||||
color: 'main',
|
||||
fontSize: 14,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Button, Divider, Grid } from '@mui/material';
|
||||
import { isGateway, isMixnode, isNymNode } from 'src/types';
|
||||
import { isMixnode, isNymNode } from 'src/types';
|
||||
import { TBondedNode } from 'src/context/bonding';
|
||||
import { GeneralMixnodeSettings } from './GeneralMixnodeSettings';
|
||||
import { ParametersSettings } from './ParametersSettings';
|
||||
import { GeneralGatewaySettings } from './GeneralGatewaySettings';
|
||||
import { GeneralNymNodeSettings } from './GeneralNymNodeSettings';
|
||||
|
||||
const makeGeneralNav = (bondedNode: TBondedNode) => {
|
||||
@@ -22,8 +20,6 @@ export const NodeGeneralSettings = ({ bondedNode }: { bondedNode: TBondedNode })
|
||||
const getSettings = () => {
|
||||
switch (navSelection) {
|
||||
case 0: {
|
||||
if (isMixnode(bondedNode)) return <GeneralMixnodeSettings bondedNode={bondedNode} />;
|
||||
if (isGateway(bondedNode)) return <GeneralGatewaySettings bondedNode={bondedNode} />;
|
||||
if (isNymNode(bondedNode)) return <GeneralNymNodeSettings bondedNode={bondedNode} />;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { FC, useContext, useEffect, useState } from 'react';
|
||||
import { Alert, AlertTitle, Box, Button, Paper, Stack, Typography } from '@mui/material';
|
||||
import { Theme, useTheme } from '@mui/material/styles';
|
||||
import { DecCoin, decimalToFloatApproximation, DelegationWithEverything, FeeDetails } from '@nymproject/types';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
import { AppContext, urls } from 'src/context/main';
|
||||
import { DelegationList } from 'src/components/Delegation/DelegationList';
|
||||
import { TPoolOption } from 'src/components';
|
||||
@@ -401,7 +401,16 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paper elevation={0} sx={{ p: 3, mt: 4 }}>
|
||||
{/* Main container - make sure it constrains width properly */}
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 3,
|
||||
mt: 4,
|
||||
maxWidth: '100%',
|
||||
overflowX: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={3}>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
{' '}
|
||||
@@ -446,7 +455,9 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{delegationsComponent(delegations)}
|
||||
|
||||
{/* Add a container to ensure delegations are constrained */}
|
||||
<Box sx={{ width: '100%', overflowX: 'hidden' }}>{delegationsComponent(delegations)}</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
import React from 'react';
|
||||
import { Button, Card, CardContent, TextField } from '@mui/material';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
interface DocEntryProps {
|
||||
function: FunctionDef;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
|
||||
import { urls, AppContext } from '../../context/main';
|
||||
|
||||
export const NodeStats = ({ mixnodeId }: { mixnodeId?: string }) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { decimalToPercentage, InclusionProbabilityResponse, MixnodeStatus } from '@nymproject/types';
|
||||
import { getInclusionProbability, getMixnodeStakeSaturation, getMixnodeStatus } from '../../requests';
|
||||
import { getMixnodeStakeSaturation, getMixnodeStatus } from '../../requests';
|
||||
|
||||
export const useSettingsState = () => {
|
||||
const [status, setStatus] = useState<MixnodeStatus>('not_found');
|
||||
@@ -27,14 +27,6 @@ export const useSettingsState = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const getMixnodeInclusionProbability = async (mixId: number) => {
|
||||
const probability = await getInclusionProbability(mixId);
|
||||
if (probability) {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
setInclusionProbability({ in_active: probability.in_active, in_reserve: probability.in_reserve });
|
||||
}
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
setStatus('not_found');
|
||||
setSaturation('-');
|
||||
@@ -46,7 +38,6 @@ export const useSettingsState = () => {
|
||||
try {
|
||||
await getStatus(mixId);
|
||||
await getStakeSaturation(mixId);
|
||||
await getMixnodeInclusionProbability(mixId);
|
||||
} catch (e) {
|
||||
enqueueSnackbar(e as string, { variant: 'error', preventDuplicate: true });
|
||||
reset();
|
||||
|
||||
@@ -59,7 +59,6 @@ async function getGatewayDetails() {
|
||||
version: gateway.version,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
getMixnodeUptime,
|
||||
getMixnodeStakeSaturation,
|
||||
getMixnodeRewardEstimation,
|
||||
getInclusionProbability,
|
||||
getMixnodeAvgUptime,
|
||||
getMixNodeDescription as getNodeDescriptionRequest,
|
||||
getPendingOperatorRewards,
|
||||
@@ -80,14 +79,6 @@ async function getAdditionalMixnodeDetails(mixId: number, host: string, port: nu
|
||||
},
|
||||
};
|
||||
|
||||
const inclusionReq: TauriReq<typeof getInclusionProbability> = {
|
||||
name: 'getInclusionProbability',
|
||||
request: () => getInclusionProbability(mixId),
|
||||
onFulfilled: (value) => {
|
||||
details.setProbability = value;
|
||||
},
|
||||
};
|
||||
|
||||
const avgUptimeReq: TauriReq<typeof getMixnodeAvgUptime> = {
|
||||
name: 'getMixnodeAvgUptime',
|
||||
request: () => getMixnodeAvgUptime(),
|
||||
@@ -117,7 +108,6 @@ async function getAdditionalMixnodeDetails(mixId: number, host: string, port: nu
|
||||
uptimeReq,
|
||||
stakeSaturationReq,
|
||||
rewardReq,
|
||||
inclusionReq,
|
||||
avgUptimeReq,
|
||||
nodeDescReq,
|
||||
operatorRewardsReq,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
DecCoin,
|
||||
InclusionProbabilityResponse,
|
||||
MixnodeStatusResponse,
|
||||
PendingIntervalEvent,
|
||||
RewardEstimationResponse,
|
||||
@@ -33,9 +32,6 @@ export const checkGatewayOwnership = async () => invokeWrapper<boolean>('owns_ga
|
||||
|
||||
export const checkNymNodeOwnership = async () => invokeWrapper<boolean>('owns_nym_node');
|
||||
|
||||
export const getInclusionProbability = async (mixId: number) =>
|
||||
invokeWrapper<InclusionProbabilityResponse>('mixnode_inclusion_probability', { mixId });
|
||||
|
||||
export const getCurrentInterval = async () => invokeWrapper<Interval>('get_current_interval');
|
||||
|
||||
export const getNumberOfMixnodeDelegators = async (mixId: number) =>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { config } from '../config';
|
||||
import { Console } from '../utils/console';
|
||||
|
||||
|
||||
@@ -1,17 +1,51 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo, useEffect } from 'react';
|
||||
import { CssBaseline, PaletteMode } from '@mui/material';
|
||||
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||
import { getDesignTokens } from './theme';
|
||||
import '@assets/fonts/non-variable/fonts.css';
|
||||
|
||||
let fontsInitialized = false;
|
||||
let interFontLink: HTMLLinkElement | null = null;
|
||||
|
||||
const FontLoader = () => {
|
||||
useEffect(() => {
|
||||
// Skip if already initialized
|
||||
if (fontsInitialized === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
fontsInitialized = true;
|
||||
|
||||
interFontLink = document.createElement('link');
|
||||
interFontLink.rel = 'stylesheet';
|
||||
interFontLink.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap';
|
||||
document.head.appendChild(interFontLink);
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => {
|
||||
// Only clean up if the component is truly being unmounted
|
||||
if (interFontLink && document.head.contains(interFontLink)) {
|
||||
document.head.removeChild(interFontLink);
|
||||
interFontLink = null;
|
||||
fontsInitialized = false;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const NymWalletThemeWithMode: FCWithChildren<{ mode: PaletteMode; children: React.ReactNode }> = ({
|
||||
mode,
|
||||
children,
|
||||
}) => {
|
||||
const theme = React.useMemo(() => createTheme(getDesignTokens(mode)), [mode]);
|
||||
// Create theme with memoization for performance
|
||||
const theme = useMemo(() => createTheme(getDesignTokens(mode)), [mode]);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<FontLoader />
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
@@ -8,13 +8,16 @@ import { NymWalletThemeWithMode } from './NymWalletTheme';
|
||||
/**
|
||||
* Provides the theme for the Network Explorer by reacting to the light/dark mode choice stored in the app context.
|
||||
*/
|
||||
|
||||
export const NymWalletTheme: FCWithChildren = ({ children }) => {
|
||||
const { mode } = useContext(AppContext);
|
||||
return <NymWalletThemeWithMode mode={mode}>{children}</NymWalletThemeWithMode>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Themed component specifically for authentication screens
|
||||
*/
|
||||
export const AuthTheme: FCWithChildren = ({ children }) => {
|
||||
// Uses dark mode by default for auth screens
|
||||
const theme = createTheme(getDesignTokens('dark'));
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
|
||||
@@ -52,6 +52,8 @@ declare module '@mui/material/styles' {
|
||||
warn: string;
|
||||
grey: string;
|
||||
greyStroke: string;
|
||||
elevated: string; // New property for more depth options
|
||||
subtle: string; // New property for subtle backgrounds
|
||||
};
|
||||
text: {
|
||||
main: string;
|
||||
@@ -60,6 +62,7 @@ declare module '@mui/material/styles' {
|
||||
contrast: string;
|
||||
grey: string;
|
||||
blue: string;
|
||||
subdued: string; // New property for better text hierarchy
|
||||
};
|
||||
topNav: {
|
||||
background: string;
|
||||
@@ -76,6 +79,16 @@ declare module '@mui/material/styles' {
|
||||
chart: {
|
||||
grey: string;
|
||||
};
|
||||
// New modern properties
|
||||
gradients: {
|
||||
primary: string;
|
||||
subtle: string;
|
||||
};
|
||||
shadows: {
|
||||
light: string;
|
||||
medium: string;
|
||||
strong: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||