Send Max UX, shared address helper, CI and desktop packaging

- Wallet CI, Tauri, webpack, routes
- Send and Sahred UI
- Wallet app build and readme
This commit is contained in:
Tommy Verrall
2026-04-17 14:41:20 +02:00
parent a4c4345257
commit 3dc94cc85a
25 changed files with 283 additions and 735 deletions
@@ -33,5 +33,9 @@ jobs:
- name: Lint nym-wallet
run: yarn --cwd nym-wallet lint
- name: Yarn audit (workspace lockfile; informational)
run: yarn audit --level critical
continue-on-error: true
- name: Unit tests (nym-wallet)
run: yarn --cwd nym-wallet test
+13
View File
@@ -74,3 +74,16 @@ jobs:
with:
command: clippy
args: --manifest-path nym-wallet/Cargo.toml --workspace --all-features --all-targets -- -D warnings
- name: Install cargo-audit
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-audit --locked
- name: Cargo audit (nym-wallet workspace)
uses: actions-rs/cargo@v1
with:
command: audit
working-directory: nym-wallet
continue-on-error: true
@@ -72,6 +72,40 @@ jobs:
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: Inspect AppImage (hook + bundled graphics libs)
shell: bash
run: |
set -euo pipefail
APPIMAGE_REL=$(find target/release/bundle -name '*.AppImage' | head -n 1)
if [ -z "${APPIMAGE_REL}" ]; then
echo "No AppImage under target/release/bundle"
exit 1
fi
APPIMAGE_ABS="${GITHUB_WORKSPACE}/nym-wallet/${APPIMAGE_REL}"
chmod +x "${APPIMAGE_ABS}"
EXTRACT_DIR=$(mktemp -d)
cd "${EXTRACT_DIR}"
"${APPIMAGE_ABS}" --appimage-extract
HOOK=$(find squashfs-root -name '99-nym-wayland.sh' 2>/dev/null | head -n 1)
if [ -z "${HOOK}" ]; then
echo "apprun-hooks/99-nym-wayland.sh not found; listing squashfs-root:"
find squashfs-root -type f 2>/dev/null | head -80 || true
exit 1
fi
echo "Found Wayland hook at ${HOOK}"
find squashfs-root/usr/lib -maxdepth 6 \
\( -name 'libwayland-client.so*' -o -name 'libEGL.so*' -o -name 'libgbm.so*' \) \
2>/dev/null | sort > "${GITHUB_WORKSPACE}/nym-wallet/appimage-bundled-graphics-libs.txt"
wc -l "${GITHUB_WORKSPACE}/nym-wallet/appimage-bundled-graphics-libs.txt"
head -50 "${GITHUB_WORKSPACE}/nym-wallet/appimage-bundled-graphics-libs.txt" || true
- name: Upload AppImage graphics lib inventory
uses: actions/upload-artifact@v6
with:
name: nym-wallet-appimage-lib-inventory
path: nym-wallet/appimage-bundled-graphics-libs.txt
retention-days: 30
- name: Create AppImage tarball if needed
run: |
+1
View File
@@ -27,6 +27,7 @@ v6-topology.json
/explorer/public/downloads/mixmining.json
/explorer/public/downloads/topology.json
/nym-wallet/dist/*
/nym-wallet/appimage-bundled-graphics-libs.txt
/clients/validator/examples/nym-driver-example/current-contract.txt
validator-api/v4.json
validator-api/v6.json
-32
View File
@@ -46,7 +46,6 @@ dependencies = [
"tauri-plugin-clipboard-manager",
"tauri-plugin-opener",
"tauri-plugin-process",
"tauri-plugin-shell",
"tauri-plugin-updater",
"tempfile",
"thiserror 1.0.69",
@@ -7130,16 +7129,6 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shared_child"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09fa9338aed9a1df411814a5b2252f7cd206c55ae9bf2fa763f8de84603aa60c"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "shlex"
version = "1.3.0"
@@ -7773,27 +7762,6 @@ dependencies = [
"tauri-plugin",
]
[[package]]
name = "tauri-plugin-shell"
version = "2.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b"
dependencies = [
"encoding_rs",
"log",
"open",
"os_pipe",
"regex",
"schemars",
"serde",
"serde_json",
"shared_child",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
"tokio",
]
[[package]]
name = "tauri-plugin-updater"
version = "2.10.1"
+18 -13
View File
@@ -12,8 +12,24 @@ The Nym desktop wallet enables you to use the Nym network and take advantage of
## Installation prerequisites - Linux / Mac
- `Yarn`
- `NodeJS >= v22.13.0`
- `Rust & cargo >= v1.85`
- `NodeJS >= v16.8.0`
- `Rust & cargo >= v1.56`
## Linux: WebKit and EGL troubleshooting
Some rolling distributions (for example Arch-based) or Wayland compositors can hit WebKitGTK / EGL errors at startup (for example `EGL_BAD_PARAMETER`, `EGL_BAD_ALLOC`, or `Could not create default EGL display`).
**AppImage (Wayland):** The bundle installs an AppRun hook that preloads the system `libwayland-client` when possible, sets `GDK_BACKEND`, `GDK_SCALE`, `GDK_DPI_SCALE`, and defaults `WEBKIT_DISABLE_DMABUF_RENDERER=1`. Override if needed: `WEBKIT_DISABLE_DMABUF_RENDERER=0`, or set your own `GDK_*` / `LD_PRELOAD` before launching.
**`.deb`, installed binary, or `target/release` binary:** Use the same variables in a wrapper script or in a `.desktop` file, for example:
`Exec=env WEBKIT_DISABLE_DMABUF_RENDERER=1 GDK_BACKEND=wayland GDK_SCALE=1 GDK_DPI_SCALE=0.8 /path/to/NymWallet`
If problems persist on Wayland, try preloading the system client library (path may vary by distro):
`LD_PRELOAD=/usr/lib/libwayland-client.so` (or `/usr/lib64/...`).
**Diagnostic (slow):** `LIBGL_ALWAYS_SOFTWARE=1` forces software GL to confirm a GPU / EGL stack mismatch.
## Installation prerequisites - Windows
@@ -66,17 +82,6 @@ yarn build
```
The output will compile different types of binaries dependent on your hardware / OS system. Once the binaries are built, they can be located as follows:
## Linux AppImage notes
The wallet AppImage now ships with a Wayland-focused launch hook for modern Linux desktops. On Wayland sessions it:
- prefers the system `libwayland-client.so` when one is available
- defaults `GDK_BACKEND=wayland`
- defaults `GDK_SCALE=1`
- defaults `GDK_DPI_SCALE=0.8`
If you need to override this behavior for troubleshooting, set your own environment variables before launching the AppImage.
## Admin mode
The admin screens can be shown by setting the environment variable `ADMIN_ADDRESS`. You'll need to know the admin account address for the network you are using.
-1
View File
@@ -42,7 +42,6 @@
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@tauri-apps/plugin-opener": "^2.5.3",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-shell": "^2.3.5",
"@tauri-apps/plugin-updater": "^2.10.1",
"@tauri-apps/tauri-forage": "^1.0.0-beta.2",
"big.js": "^6.2.1",
-1
View File
@@ -19,7 +19,6 @@ tauri-build = { version = "2.5.6", features = [] }
async-trait = "0.1.68"
tauri-plugin-updater = "2.10.1"
tauri-plugin-clipboard-manager = "2.3.2"
tauri-plugin-shell = "2.3.5"
tauri-plugin-process = "2.3.1"
tauri-plugin-opener = "2.5.3"
bip39 = { version = "2.0.0", features = ["zeroize", "rand"] }
@@ -34,7 +34,6 @@
"updater:allow-download-and-install",
"updater:allow-install",
"core:event:allow-listen",
"shell:allow-open",
"process:default"
]
}
File diff suppressed because one or more lines are too long
@@ -1 +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:window:allow-maximize","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","shell:allow-open","process:default"],"platforms":["linux","macOS","windows"]}}
{"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:window:allow-maximize","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","process:default"],"platforms":["linux","macOS","windows"]}}
@@ -302,216 +302,6 @@
}
}
},
{
"if": {
"properties": {
"identifier": {
"anyOf": [
{
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
"type": "string",
"const": "shell:default",
"markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`"
},
{
"description": "Enables the execute command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-execute",
"markdownDescription": "Enables the execute command without any pre-configured scope."
},
{
"description": "Enables the kill command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-kill",
"markdownDescription": "Enables the kill command without any pre-configured scope."
},
{
"description": "Enables the open command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-open",
"markdownDescription": "Enables the open command without any pre-configured scope."
},
{
"description": "Enables the spawn command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-spawn",
"markdownDescription": "Enables the spawn command without any pre-configured scope."
},
{
"description": "Enables the stdin_write command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-stdin-write",
"markdownDescription": "Enables the stdin_write command without any pre-configured scope."
},
{
"description": "Denies the execute command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-execute",
"markdownDescription": "Denies the execute command without any pre-configured scope."
},
{
"description": "Denies the kill command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-kill",
"markdownDescription": "Denies the kill command without any pre-configured scope."
},
{
"description": "Denies the open command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-open",
"markdownDescription": "Denies the open command without any pre-configured scope."
},
{
"description": "Denies the spawn command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-spawn",
"markdownDescription": "Denies the spawn command without any pre-configured scope."
},
{
"description": "Denies the stdin_write command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-stdin-write",
"markdownDescription": "Denies the stdin_write command without any pre-configured scope."
}
]
}
}
},
"then": {
"properties": {
"allow": {
"items": {
"title": "ShellScopeEntry",
"description": "Shell scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"cmd",
"name"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"cmd": {
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"name",
"sidecar"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
},
"sidecar": {
"description": "If this command is a sidecar command.",
"type": "boolean"
}
},
"additionalProperties": false
}
]
}
},
"deny": {
"items": {
"title": "ShellScopeEntry",
"description": "Shell scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"cmd",
"name"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"cmd": {
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"name",
"sidecar"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
},
"sidecar": {
"description": "If this command is a sidecar command.",
"type": "boolean"
}
},
"additionalProperties": false
}
]
}
}
}
},
"properties": {
"identifier": {
"description": "Identifier of the permission or permission set.",
"allOf": [
{
"$ref": "#/definitions/Identifier"
}
]
}
}
},
{
"properties": {
"identifier": {
@@ -2678,72 +2468,6 @@
"const": "process:deny-restart",
"markdownDescription": "Denies the restart command without any pre-configured scope."
},
{
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
"type": "string",
"const": "shell:default",
"markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`"
},
{
"description": "Enables the execute command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-execute",
"markdownDescription": "Enables the execute command without any pre-configured scope."
},
{
"description": "Enables the kill command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-kill",
"markdownDescription": "Enables the kill command without any pre-configured scope."
},
{
"description": "Enables the open command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-open",
"markdownDescription": "Enables the open command without any pre-configured scope."
},
{
"description": "Enables the spawn command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-spawn",
"markdownDescription": "Enables the spawn command without any pre-configured scope."
},
{
"description": "Enables the stdin_write command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-stdin-write",
"markdownDescription": "Enables the stdin_write command without any pre-configured scope."
},
{
"description": "Denies the execute command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-execute",
"markdownDescription": "Denies the execute command without any pre-configured scope."
},
{
"description": "Denies the kill command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-kill",
"markdownDescription": "Denies the kill command without any pre-configured scope."
},
{
"description": "Denies the open command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-open",
"markdownDescription": "Denies the open command without any pre-configured scope."
},
{
"description": "Denies the spawn command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-spawn",
"markdownDescription": "Denies the spawn command without any pre-configured scope."
},
{
"description": "Denies the stdin_write command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-stdin-write",
"markdownDescription": "Denies the stdin_write command without any pre-configured scope."
},
{
"description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`",
"type": "string",
@@ -2910,50 +2634,6 @@
"type": "string"
}
]
},
"ShellScopeEntryAllowedArg": {
"description": "A command argument allowed to be executed by the webview API.",
"anyOf": [
{
"description": "A non-configurable argument that is passed to the command in the order it was specified.",
"type": "string"
},
{
"description": "A variable that is set while calling the command from the webview API.",
"type": "object",
"required": [
"validator"
],
"properties": {
"raw": {
"description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.",
"default": false,
"type": "boolean"
},
"validator": {
"description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: <https://docs.rs/regex/latest/regex/#syntax>",
"type": "string"
}
},
"additionalProperties": false
}
]
},
"ShellScopeEntryAllowedArgs": {
"description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.",
"anyOf": [
{
"description": "Use a simple boolean to allow all or disable all arguments to this command configuration.",
"type": "boolean"
},
{
"description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.",
"type": "array",
"items": {
"$ref": "#/definitions/ShellScopeEntryAllowedArg"
}
}
]
}
}
}
@@ -302,216 +302,6 @@
}
}
},
{
"if": {
"properties": {
"identifier": {
"anyOf": [
{
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
"type": "string",
"const": "shell:default",
"markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`"
},
{
"description": "Enables the execute command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-execute",
"markdownDescription": "Enables the execute command without any pre-configured scope."
},
{
"description": "Enables the kill command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-kill",
"markdownDescription": "Enables the kill command without any pre-configured scope."
},
{
"description": "Enables the open command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-open",
"markdownDescription": "Enables the open command without any pre-configured scope."
},
{
"description": "Enables the spawn command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-spawn",
"markdownDescription": "Enables the spawn command without any pre-configured scope."
},
{
"description": "Enables the stdin_write command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-stdin-write",
"markdownDescription": "Enables the stdin_write command without any pre-configured scope."
},
{
"description": "Denies the execute command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-execute",
"markdownDescription": "Denies the execute command without any pre-configured scope."
},
{
"description": "Denies the kill command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-kill",
"markdownDescription": "Denies the kill command without any pre-configured scope."
},
{
"description": "Denies the open command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-open",
"markdownDescription": "Denies the open command without any pre-configured scope."
},
{
"description": "Denies the spawn command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-spawn",
"markdownDescription": "Denies the spawn command without any pre-configured scope."
},
{
"description": "Denies the stdin_write command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-stdin-write",
"markdownDescription": "Denies the stdin_write command without any pre-configured scope."
}
]
}
}
},
"then": {
"properties": {
"allow": {
"items": {
"title": "ShellScopeEntry",
"description": "Shell scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"cmd",
"name"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"cmd": {
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"name",
"sidecar"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
},
"sidecar": {
"description": "If this command is a sidecar command.",
"type": "boolean"
}
},
"additionalProperties": false
}
]
}
},
"deny": {
"items": {
"title": "ShellScopeEntry",
"description": "Shell scope entry.",
"anyOf": [
{
"type": "object",
"required": [
"cmd",
"name"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"cmd": {
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"type": "string"
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"name",
"sidecar"
],
"properties": {
"args": {
"description": "The allowed arguments for the command execution.",
"allOf": [
{
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
}
]
},
"name": {
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
"type": "string"
},
"sidecar": {
"description": "If this command is a sidecar command.",
"type": "boolean"
}
},
"additionalProperties": false
}
]
}
}
}
},
"properties": {
"identifier": {
"description": "Identifier of the permission or permission set.",
"allOf": [
{
"$ref": "#/definitions/Identifier"
}
]
}
}
},
{
"properties": {
"identifier": {
@@ -2678,72 +2468,6 @@
"const": "process:deny-restart",
"markdownDescription": "Denies the restart command without any pre-configured scope."
},
{
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
"type": "string",
"const": "shell:default",
"markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`"
},
{
"description": "Enables the execute command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-execute",
"markdownDescription": "Enables the execute command without any pre-configured scope."
},
{
"description": "Enables the kill command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-kill",
"markdownDescription": "Enables the kill command without any pre-configured scope."
},
{
"description": "Enables the open command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-open",
"markdownDescription": "Enables the open command without any pre-configured scope."
},
{
"description": "Enables the spawn command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-spawn",
"markdownDescription": "Enables the spawn command without any pre-configured scope."
},
{
"description": "Enables the stdin_write command without any pre-configured scope.",
"type": "string",
"const": "shell:allow-stdin-write",
"markdownDescription": "Enables the stdin_write command without any pre-configured scope."
},
{
"description": "Denies the execute command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-execute",
"markdownDescription": "Denies the execute command without any pre-configured scope."
},
{
"description": "Denies the kill command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-kill",
"markdownDescription": "Denies the kill command without any pre-configured scope."
},
{
"description": "Denies the open command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-open",
"markdownDescription": "Denies the open command without any pre-configured scope."
},
{
"description": "Denies the spawn command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-spawn",
"markdownDescription": "Denies the spawn command without any pre-configured scope."
},
{
"description": "Denies the stdin_write command without any pre-configured scope.",
"type": "string",
"const": "shell:deny-stdin-write",
"markdownDescription": "Denies the stdin_write command without any pre-configured scope."
},
{
"description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`",
"type": "string",
@@ -2910,50 +2634,6 @@
"type": "string"
}
]
},
"ShellScopeEntryAllowedArg": {
"description": "A command argument allowed to be executed by the webview API.",
"anyOf": [
{
"description": "A non-configurable argument that is passed to the command in the order it was specified.",
"type": "string"
},
{
"description": "A variable that is set while calling the command from the webview API.",
"type": "object",
"required": [
"validator"
],
"properties": {
"raw": {
"description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.",
"default": false,
"type": "boolean"
},
"validator": {
"description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: <https://docs.rs/regex/latest/regex/#syntax>",
"type": "string"
}
},
"additionalProperties": false
}
]
},
"ShellScopeEntryAllowedArgs": {
"description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.",
"anyOf": [
{
"description": "Use a simple boolean to allow all or disable all arguments to this command configuration.",
"type": "boolean"
},
{
"description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.",
"type": "array",
"items": {
"$ref": "#/definitions/ShellScopeEntryAllowedArg"
}
}
]
}
}
}
@@ -20,3 +20,6 @@ fi
export GDK_BACKEND="${GDK_BACKEND:-wayland}"
export GDK_SCALE="${GDK_SCALE:-1}"
export GDK_DPI_SCALE="${GDK_DPI_SCALE:-0.8}"
# Reduces WebKit DMA-BUF / EGL failures on some rolling Mesa + Wayland stacks. Set WEBKIT_DISABLE_DMABUF_RENDERER=0 to opt out.
export WEBKIT_DISABLE_DMABUF_RENDERER="${WEBKIT_DISABLE_DMABUF_RENDERER:-1}"
-2
View File
@@ -7,7 +7,6 @@ use nym_mixnet_contract_common::{Gateway, MixNode};
use tauri::Manager;
use tauri_plugin_opener::init as init_opener;
use tauri_plugin_process::init as init_process;
use tauri_plugin_shell::init as init_shell;
use tauri_plugin_updater::Builder as UpdaterBuilder;
use crate::menu::SHOW_LOG_WINDOW;
@@ -39,7 +38,6 @@ fn main() {
let context = tauri::generate_context!();
tauri::Builder::default()
.plugin(init_shell())
.plugin(init_opener())
.plugin(init_process())
.plugin(UpdaterBuilder::new().build())
@@ -1,5 +1,5 @@
import React, { useRef, useEffect } from 'react';
import { Box } from '@mui/material';
import { Box, InputAdornment } from '@mui/material';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
import { CurrencyDenom, DecCoin } from '@nymproject/types';
import { PasteFromClipboard } from './Clipboard/ClipboardActions';
@@ -13,6 +13,8 @@ export const CurrencyFormFieldWithPaste = ({
required,
autoFocus,
validationError,
endAdornment,
showPaste = true,
}: {
label: string;
fullWidth?: boolean;
@@ -22,6 +24,10 @@ export const CurrencyFormFieldWithPaste = ({
required?: boolean;
autoFocus?: boolean;
validationError?: string;
/** Rendered inside the outlined input (e.g. Max). */
endAdornment?: React.ReactNode;
/** When false, no paste control; native keyboard paste still works on the input. */
showPaste?: boolean;
}) => {
const fieldRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
@@ -58,6 +64,10 @@ export const CurrencyFormFieldWithPaste = ({
};
useEffect(() => {
if (!showPaste) {
return undefined;
}
const pasteEventHandler = (e: ClipboardEvent) => {
e.preventDefault();
@@ -91,9 +101,13 @@ export const CurrencyFormFieldWithPaste = ({
inputRef.current.removeEventListener('paste', pasteEventHandler as EventListener);
}
};
}, [denom, onChanged]);
}, [showPaste, denom, onChanged]);
useEffect(() => {
if (!showPaste) {
return undefined;
}
const handleKeyDown = async (e: KeyboardEvent) => {
if (e.defaultPrevented) return;
if (inputRef.current && document.activeElement === inputRef.current) {
@@ -118,7 +132,7 @@ export const CurrencyFormFieldWithPaste = ({
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [denom, onChanged]);
}, [showPaste, denom, onChanged]);
return (
<Box position="relative" width="100%" ref={fieldRef} data-nym-currency-field>
@@ -131,18 +145,27 @@ export const CurrencyFormFieldWithPaste = ({
required={required}
autoFocus={autoFocus}
validationError={validationError}
mergedInputProps={
endAdornment
? {
endAdornment: <InputAdornment position="end">{endAdornment}</InputAdornment>,
}
: undefined
}
/>
<Box
sx={{
position: 'absolute',
right: '14px',
top: '50%',
transform: 'translateY(-50%)',
zIndex: 1,
}}
>
<PasteFromClipboard onPaste={processPastedText} fieldRef={inputRef} />
</Box>
{showPaste && (
<Box
sx={{
position: 'absolute',
right: endAdornment ? 88 : 14,
top: '50%',
transform: 'translateY(-50%)',
zIndex: 1,
}}
>
<PasteFromClipboard onPaste={processPastedText} fieldRef={inputRef} />
</Box>
)}
</Box>
);
};
@@ -1,10 +1,21 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Box, Stack, Typography, SxProps, FormControlLabel, Checkbox, Alert } from '@mui/material';
import {
Box,
Button,
Stack,
Typography,
SxProps,
FormControlLabel,
Checkbox,
Alert,
CircularProgress,
} from '@mui/material';
import { alpha, useTheme } from '@mui/material/styles';
import Big from 'big.js';
import { CurrencyDenom, DecCoin, isValidRawCoin } from '@nymproject/types';
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
import { validateAmount } from 'src/utils';
import { validateNymAddress } from 'src/utils/validateNymAddress';
import { SimpleModal } from '../Modals/SimpleModal';
import { ModalListItem } from '../Modals/ModalListItem';
import { TextFieldWithPaste } from '../Clipboard/ClipboardFormFields';
@@ -28,18 +39,6 @@ const recipientHelperText = (
return undefined;
};
// 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,
toAddress,
@@ -59,6 +58,9 @@ export const SendInputModal = ({
onMemoChange,
showMore,
setShowMore,
amountFieldKey,
onMaxAmount,
maxAmountLoading,
}: {
fromAddress?: string;
toAddress: string;
@@ -78,6 +80,10 @@ export const SendInputModal = ({
memo?: string;
onUserFeesChange: (value: DecCoin) => void;
onMemoChange: (value: string) => void;
/** Bump to remount the amount field after programmatic Max (CurrencyFormField is defaultValue-based). */
amountFieldKey: number;
onMaxAmount: () => void | Promise<void>;
maxAmountLoading: boolean;
}) => {
const [isValid, setIsValid] = useState(false);
const [memoIsValid, setMemoIsValid] = useState(true);
@@ -339,10 +345,12 @@ export const SendInputModal = ({
}}
/>
{/* Amount field with paste button */}
{/* Amount: Max in endAdornment; no paste chip (recipient/memo still have paste). */}
<CurrencyFormFieldWithPaste
key={`send-amount-${amountFieldKey}`}
label="Amount"
fullWidth
showPaste={false}
onChanged={(value) => {
setAmountTouched(true);
onAmountChange(value);
@@ -353,6 +361,29 @@ export const SendInputModal = ({
initialValue={amount?.amount}
denom={denom}
validationError={errorAmount}
endAdornment={
<Button
variant="outlined"
size="small"
disabled={noAccount || !addressIsValid || maxAmountLoading}
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
await onMaxAmount();
}}
sx={{
minWidth: 52,
py: 0.25,
px: 0.75,
textTransform: 'none',
fontWeight: 600,
lineHeight: 1.2,
}}
aria-label="Fill maximum sendable amount after fees"
>
{maxAmountLoading ? <CircularProgress size={16} color="inherit" /> : 'Max'}
</Button>
}
/>
{/* Memo field with paste button */}
@@ -383,7 +414,8 @@ export const SendInputModal = ({
fontWeight={600}
/>
<Typography fontSize="smaller" sx={{ color: 'text.primary' }}>
Est. fee for this transaction will be shown on the next page
Fees use the current network estimate (or your custom fee if set below). Max fills your balance minus that
fee and a small reserve. The next step shows the full breakdown before you confirm.
</Typography>
</Stack>
@@ -1,10 +1,12 @@
import React, { useContext, useEffect, useState } from 'react';
import Big from 'big.js';
import { DecCoin } from '@nymproject/types';
import { AppContext, urls } from 'src/context';
import { useGetFee } from 'src/hooks/useGetFee';
import { send } from 'src/requests';
import { Console } from 'src/utils/console';
import { simulateSend } from 'src/requests/simulate';
import { validateNymAddress } from 'src/utils/validateNymAddress';
import { LoadingModal } from '../Modals/LoadingModal';
import { SendDetailsModal } from './SendDetailsModal';
import { SendErrorModal } from './SendErrorModal';
@@ -12,6 +14,10 @@ import { SendInputModal } from './SendInputModal';
import { SendSuccessModal } from './SendSuccessModal';
import { TTransactionDetails } from './types';
/** Extra NYM left in the account after Max to absorb minor gas / rounding drift. */
const MAX_SEND_FEE_RESERVE = '0.01';
const MIN_SEND_AMOUNT = '0.000001';
export const SendModal = ({ onClose }: { onClose: () => void }) => {
const [toAddress, setToAddress] = useState<string>('');
const [amount, setAmount] = useState<DecCoin>();
@@ -24,6 +30,8 @@ export const SendModal = ({ onClose }: { onClose: () => void }) => {
const [memo, setMemo] = useState<string>('');
const [txDetails, setTxDetails] = useState<TTransactionDetails>();
const [showMoreOptions, setShowMoreOptions] = useState(false);
const [amountFieldKey, setAmountFieldKey] = useState(0);
const [maxAmountLoading, setMaxAmountLoading] = useState(false);
const { clientDetails, userBalance, network } = useContext(AppContext);
const { fee, getFee, feeError, setFeeManually } = useGetFee();
@@ -44,6 +52,88 @@ export const SendModal = ({ onClose }: { onClose: () => void }) => {
// removes any zero-width spaces and trailing white space
const sanitizeAddress = (address: string) => address.replace(/[\u200B-\u200D\uFEFF]/g, '').trim();
const handleMaxAmount = async () => {
setError(undefined);
if (!userBalance.balance?.amount) {
setError('Balance unavailable.');
return;
}
if (!validateNymAddress(toAddress)) {
setError('Enter a valid recipient address to estimate network fees for Max.');
return;
}
const balDec = userBalance.balance.amount;
setMaxAmountLoading(true);
try {
let feeDisplay: string;
if (showMoreOptions && userFees?.amount && String(userFees.amount).trim() !== '') {
if (!Number(userFees.amount)) {
setError('Set a valid custom fee or turn off More options.');
return;
}
feeDisplay = userFees.amount;
} else {
const probe = await simulateSend({
address: toAddress,
amount: balDec,
memo: memo || '',
});
if (!probe.amount?.amount) {
setError('Could not estimate network fee. Try again or set a custom fee under More options.');
return;
}
feeDisplay = probe.amount.amount;
}
const balanceBig = new Big(balDec.amount);
const feeBig = new Big(feeDisplay);
const reserveBig = new Big(MAX_SEND_FEE_RESERVE);
let maxBig = balanceBig.minus(feeBig).minus(reserveBig);
if (maxBig.lte(0)) {
setError(
'Balance is too low to send after fees and reserve. Add funds, lower the custom fee, or try again later.',
);
return;
}
if (!(showMoreOptions && userFees?.amount && String(userFees.amount).trim() !== '')) {
const refined = await simulateSend({
address: toAddress,
amount: { amount: maxBig.toString(), denom: balDec.denom },
memo: memo || '',
});
if (refined.amount?.amount) {
maxBig = balanceBig.minus(new Big(refined.amount.amount)).minus(reserveBig);
}
}
if (maxBig.lte(0) || maxBig.lt(new Big(MIN_SEND_AMOUNT))) {
setError(
`Max sendable amount is below the minimum (${MIN_SEND_AMOUNT} ${(
clientDetails?.display_mix_denom || 'nym'
).toUpperCase()}).`,
);
return;
}
const rounded = maxBig.round(6, 0);
let amountStr = rounded.toFixed(6).replace(/\.?0+$/, '');
if (amountStr === '' || amountStr === '.') {
amountStr = MIN_SEND_AMOUNT;
}
setAmount({ amount: amountStr, denom: balDec.denom });
setAmountFieldKey((k) => k + 1);
} catch (e) {
setError(String(e));
} finally {
setMaxAmountLoading(false);
}
};
const handleOnNext = async () => {
if (amount) {
setIsLoading(true);
@@ -129,6 +219,9 @@ export const SendModal = ({ onClose }: { onClose: () => void }) => {
onUserFeesChange={(value) => setUserFees(value)}
onMemoChange={(value) => setMemo(value)}
setShowMore={setShowMoreOptions}
amountFieldKey={amountFieldKey}
onMaxAmount={handleMaxAmount}
maxAmountLoading={maxAmountLoading}
/>
);
};
+5
View File
@@ -1,4 +1,9 @@
/** Opt-in for production builds: set `NYM_WALLET_INTERNAL_DOCS=true` when running webpack (rare debugging). */
const internalDocsEnvOverride = process.env.NYM_WALLET_INTERNAL_DOCS === 'true';
export const config = {
IS_DEV_MODE: process.env.NODE_ENV === 'development',
LOG_TAURI_OPERATIONS: process.env.NODE_ENV === 'development',
/** Arbitrary `invoke` playground; off in production unless `NYM_WALLET_INTERNAL_DOCS` is set at build time. */
INTERNAL_DOCS_ENABLED: process.env.NODE_ENV !== 'production' || internalDocsEnvOverride,
};
+2 -1
View File
@@ -2,12 +2,13 @@ import React, { useContext } from 'react';
import { NymCard } from '../../components';
import { ApiList } from './ApiList';
import { config } from '../../config';
import { AppContext } from '../../context/main';
export const InternalDocs = () => {
const { isAdminAddress } = useContext(AppContext);
if (!isAdminAddress) {
if (!config.INTERNAL_DOCS_ENABLED || !isAdminAddress) {
return null;
}
+2 -1
View File
@@ -4,6 +4,7 @@ import { ApplicationLayout } from '../layouts';
import { Terminal } from '../pages/terminal';
import { Send } from '../components/Send';
import { Receive } from '../components/Receive';
import { config } from '../config';
import {
Balance,
InternalDocs,
@@ -27,7 +28,7 @@ export const AppRoutes = () => (
<Route path="/bonding" element={<BondingPage />} />
<Route path="/bonding/node-settings" element={<NodeSettingsPage />} />
<Route path="/delegation" element={<DelegationPage />} />
<Route path="/docs" element={<InternalDocs />} />
{config.INTERNAL_DOCS_ENABLED && <Route path="/docs" element={<InternalDocs />} />}
<Route path="/admin" element={<Admin />} />
<Route path="/buy" element={<BuyPage />} />
</Routes>
@@ -0,0 +1,7 @@
/** NYM bech32 sending address: `n1` prefix, 40 chars total, lowercase alphanumeric body. */
export const validateNymAddress = (address: string): boolean => {
if (!address) return false;
if (!address.startsWith('n1')) return false;
if (address.length !== 40) return false;
return /^[a-z0-9]+$/.test(address);
};
+6
View File
@@ -1,4 +1,5 @@
const path = require('path');
const webpack = require('webpack');
const { mergeWithRules } = require('webpack-merge');
const { webpackCommon } = require('@nymproject/webpack');
@@ -67,5 +68,10 @@ module.exports = mergeWithRules({
experiments: {
asyncWebAssembly: true,
},
plugins: [
new webpack.EnvironmentPlugin({
NYM_WALLET_INTERNAL_DOCS: '',
}),
],
},
);
@@ -1,6 +1,6 @@
import * as React from 'react';
import { ChangeEvent } from 'react';
import { TextField } from '@mui/material';
import { TextField, TextFieldProps } from '@mui/material';
import { SxProps } from '@mui/system';
import { CurrencyDenom, DecCoin } from '@nymproject/types';
@@ -21,6 +21,8 @@ export const CurrencyFormField: FCWithChildren<{
onChanged?: (newValue: DecCoin) => void;
onValidate?: (newValue: string | undefined, isValid: boolean, error?: string) => void;
sx?: SxProps;
/** Merged after built-in InputProps (e.g. `endAdornment`). */
mergedInputProps?: TextFieldProps['InputProps'];
}> = ({
autoFocus,
required,
@@ -34,6 +36,7 @@ export const CurrencyFormField: FCWithChildren<{
onValidate,
sx,
denom = 'nym',
mergedInputProps,
}) => {
const [value, setValue] = React.useState<string | undefined>(initialValue);
const [validationError, setValidationError] = React.useState<string | undefined>(validationErrorProp);
@@ -131,6 +134,7 @@ export const CurrencyFormField: FCWithChildren<{
min: MIN_VALUE,
max: MAX_VALUE,
},
...mergedInputProps,
}}
aria-readonly={readOnly}
error={validationError !== undefined}
+4 -11
View File
@@ -3729,10 +3729,10 @@
resolved "https://registry.yarnpkg.com/@nymproject/contract-clients/-/contract-clients-1.4.1.tgz#ae2644387b518eb13e8825fa8021d4c81ffe7852"
integrity sha512-HuJZ4Hv+Rl6ZZEtCHKgurNLJapM+QQRJlGkevFH2a4UdqUqF9omUkUi3AVes4679dPoSFgvA7plyVSDBdbgV6w==
"@nymproject/mix-fetch@>=1.4.1-rc1 || ^1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@nymproject/mix-fetch/-/mix-fetch-1.4.1.tgz#6a923b40e09c4a571fb947297a3afd41cd2d5ea6"
integrity sha512-FN5UeCkje6fauCt2pd8kFGFYXj2kaNewEJVKRLWzI2/suxD+J2bmg/YvXBGLWMWglXNA3+YFHA/1Vjh6OGtgig==
"@nymproject/mix-fetch@>=1.4.2-rc.0 || ^1":
version "1.4.4"
resolved "https://registry.yarnpkg.com/@nymproject/mix-fetch/-/mix-fetch-1.4.4.tgz#f10fbde17255c16f9de8c3dd6e8c8f49c4d12c88"
integrity sha512-sdyXXJG7sYv2OFOEf6FYm7HglKfMvJmJjrQC3Rbusy5rH5C2ajg2KyuBbZReLgIIbkkx3mK8sc5WRUOKTcKt2Q==
"@nymproject/node-tester@^1.3.1":
version "1.3.1"
@@ -6489,13 +6489,6 @@
dependencies:
"@tauri-apps/api" "^2.8.0"
"@tauri-apps/plugin-shell@^2.3.5":
version "2.3.5"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz#ec7b5c2aedaae9e4641cd3ccf1106e443e193919"
integrity sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==
dependencies:
"@tauri-apps/api" "^2.10.1"
"@tauri-apps/plugin-updater@^2.10.1":
version "2.10.1"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-updater/-/plugin-updater-2.10.1.tgz#ea0efd766890394b6c719b9fc21de7da0029c69c"