Nym Wallet: deps updates, clipboard/updater/, icon, polishing...
This rolls together desktop wallet hardening, UX polish, and operational fixes we have been carrying in the branch. The goal is safer defaults, less noisy background behaviour. Security - Tighten the Tauri CSP for production and keep connect-src aligned with real needs. - Add a safe URL opener path (allowlisted schemes / validation) so user-influenced links do not become an open redirect surface. - Replace unwrap usage in mixnet account flows with proper errors and propagation. - Add an internal threat-model note so future changes keep the same assumptions explicit. Clipboard and desktop - Add a window-level Tauri clipboard hook for normal inputs, with clear exclusions for currency fields, auth-sensitive paste, and opt-in replace-paste fields. - Wire an Edit menu (cut, copy, paste, select all) where it helps, and keep behaviour consistent with the hook. - Deduplicate clipboard field props and satisfy ESLint on optional paste handlers. Updater and vesting operations - Treat legacy static updater JSON (missing per-platform signatures) as a soft failure with a clear warning, instead of erroring the version check IPC - Cut vesting polling spam when the chain has no vesting account for the address, and map vesting "no account" to a dedicated BackendError for stable handling on the client. - Move high-frequency vesting query logs to debug and keep removed-query stubs at warn. Icons and first-run chrome - Regenerate macOS/Windows icon assets from a padded 1024 master so dock and switcher visual weight matches other apps; add a small script to regenerate from app-icon-source.png. - Default the app to dark mode, paint the HTML shell and webview background in the same dark base colour Housekeeping - Mock app context defaults to dark for consistency with the new baseline. Validation run locally where relevant: Rust check, TypeScript check, ESLint, and icon regeneration script smoke run. - Remove storybook and old webdriver tests too
@@ -0,0 +1,34 @@
|
||||
name: ci-nym-wallet-frontend
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'nym-wallet/**'
|
||||
- '.github/workflows/ci-nym-wallet-frontend.yml'
|
||||
|
||||
jobs:
|
||||
types-lint:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: nym-wallet/.nvmrc
|
||||
cache: yarn
|
||||
cache-dependency-path: yarn.lock
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --network-timeout 100000
|
||||
|
||||
- name: Build TypeScript packages (wallet depends on @nymproject/types, etc.)
|
||||
run: yarn build:types
|
||||
|
||||
- name: Typecheck nym-wallet
|
||||
run: yarn --cwd nym-wallet tsc
|
||||
|
||||
- name: Lint nym-wallet
|
||||
run: yarn --cwd nym-wallet lint
|
||||
|
||||
- name: Unit tests (nym-wallet)
|
||||
run: yarn --cwd nym-wallet test
|
||||
@@ -1,53 +0,0 @@
|
||||
name: ci-nym-wallet-storybook
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'nym-wallet/**'
|
||||
- '.github/workflows/ci-nym-wallet-storybook.yml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install rsync
|
||||
run: sudo apt-get install rsync
|
||||
continue-on-error: true
|
||||
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup yarn
|
||||
run: npm install -g yarn
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
- name: Build dependencies
|
||||
run: yarn && yarn build
|
||||
|
||||
- name: Build storybook
|
||||
run: yarn storybook:build
|
||||
working-directory: ./nym-wallet
|
||||
|
||||
- name: Deploy branch to CI www (storybook)
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-rltgoDzvO --delete"
|
||||
SOURCE: "nym-wallet/storybook-static/"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/wallet-${{ env.GITHUB_REF_SLUG }}
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
|
||||
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
|
||||
- name: Install rust toolchain
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
node-version: 22.13.0
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
node-version: 22.13.0
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Rust toolchain
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
- name: Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
node-version: 22.13.0
|
||||
|
||||
- name: Download EV CodeSignTool from ssl.com
|
||||
working-directory: nym-wallet/src-tauri
|
||||
|
||||
@@ -4,5 +4,19 @@
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.eslint.json"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.test.ts", "**/*.test.tsx"],
|
||||
"env": { "jest": true }
|
||||
}
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/lines-between-class-members": "off",
|
||||
"@typescript-eslint/naming-convention": "off",
|
||||
"@typescript-eslint/no-implied-eval": "off",
|
||||
"@typescript-eslint/no-throw-literal": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/quotes": "off"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
18
|
||||
22.13.0
|
||||
@@ -1,60 +0,0 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'],
|
||||
framework: '@storybook/react',
|
||||
core: {
|
||||
builder: 'webpack5',
|
||||
},
|
||||
typescript: { reactDocgen: false },
|
||||
// webpackFinal: async (config, { configType }) => {
|
||||
// // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION'
|
||||
// // You can change the configuration based on that.
|
||||
// // 'PRODUCTION' is used when building the static version of storybook.
|
||||
webpackFinal: async (config) => {
|
||||
config.module.rules.forEach((rule) => {
|
||||
// look for SVG import rule and replace
|
||||
// NOTE: the rule before modification is /\.(svg|ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/
|
||||
if (rule.test?.toString().includes('svg')) {
|
||||
rule.test = /\.(ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/;
|
||||
}
|
||||
});
|
||||
|
||||
// handle asset loading with this
|
||||
config.module.rules.unshift({
|
||||
test: /\.svg(\?.*)?$/i,
|
||||
issuer: /\.[jt]sx?$/,
|
||||
use: ['@svgr/webpack'],
|
||||
});
|
||||
|
||||
config.resolve.extensions = ['.tsx', '.ts', '.js'];
|
||||
config.resolve.plugins = [new TsconfigPathsPlugin()];
|
||||
|
||||
config.plugins.push(
|
||||
new ForkTsCheckerWebpackPlugin({
|
||||
typescript: {
|
||||
mode: 'write-references',
|
||||
diagnosticOptions: {
|
||||
semantic: true,
|
||||
syntactic: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
if (!config.resolve.alias) {
|
||||
config.resolve.alias = {};
|
||||
}
|
||||
|
||||
config.resolve.alias['@tauri-apps/api'] = `${__dirname}/mocks/tauri`;
|
||||
|
||||
// Return the altered config
|
||||
return config;
|
||||
},
|
||||
features: {
|
||||
emotionAlias: false,
|
||||
},
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* This is a mock for Tauri's API package (@tauri-apps/api/app), to prevent stories from being excluded, because they either use
|
||||
* or import dependencies that use Tauri.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
getVersion: () => undefined,
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* This is a mock for Tauri's API package (@tauri-apps/api/app), to prevent stories from being excluded, because they either use
|
||||
* or import dependencies that use Tauri.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
invoke: () => undefined,
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
/**
|
||||
* This is a mock for Tauri's API package (@tauri-apps/api/app), to prevent stories from being excluded, because they either use
|
||||
* or import dependencies that use Tauri.
|
||||
*/
|
||||
@@ -1,113 +0,0 @@
|
||||
const delegations = [
|
||||
{
|
||||
mix_id: 1234,
|
||||
node_identity: 'FiojKW7oY9WQmLCiYAsCA21tpowZHS6zcUoyYm319p6Z',
|
||||
delegated_on_iso_datetime: new Date(2021, 1, 1).toDateString(),
|
||||
unclaimed_rewards: { amount: '0.05', denom: 'nym' },
|
||||
amount: { amount: '10', denom: 'nym' },
|
||||
owner: '',
|
||||
block_height: BigInt(100),
|
||||
cost_params: {
|
||||
profit_margin_percent: '0.04',
|
||||
interval_operating_cost: {
|
||||
amount: '20',
|
||||
denom: 'nym',
|
||||
},
|
||||
},
|
||||
stake_saturation: '0.2',
|
||||
avg_uptime_percent: 0.5,
|
||||
accumulated_by_delegates: { amount: '0', denom: 'nym' },
|
||||
accumulated_by_operator: { amount: '0', denom: 'nym' },
|
||||
uses_vesting_contract_tokens: false,
|
||||
pending_events: [],
|
||||
mixnode_is_unbonding: false,
|
||||
errors: null,
|
||||
},
|
||||
{
|
||||
mix_id: 5678,
|
||||
node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8',
|
||||
unclaimed_rewards: { amount: '0.1', denom: 'nym' },
|
||||
amount: { amount: '100', denom: 'nym' },
|
||||
delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(),
|
||||
owner: '',
|
||||
block_height: BigInt(4000),
|
||||
stake_saturation: '0.5',
|
||||
avg_uptime_percent: 0.1,
|
||||
cost_params: {
|
||||
profit_margin_percent: '0.04',
|
||||
interval_operating_cost: {
|
||||
amount: '60',
|
||||
denom: 'nym',
|
||||
},
|
||||
},
|
||||
accumulated_by_delegates: { amount: '0', denom: 'nym' },
|
||||
accumulated_by_operator: { amount: '0', denom: 'nym' },
|
||||
uses_vesting_contract_tokens: true,
|
||||
pending_events: [],
|
||||
mixnode_is_unbonding: false,
|
||||
errors: null,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* This is a mock for Tauri's API package (@tauri-apps/api), to prevent stories from being excluded, because they either use
|
||||
* or import dependencies that use Tauri.
|
||||
*/
|
||||
module.exports = {
|
||||
invoke: (operation, args) => {
|
||||
switch (operation) {
|
||||
case 'get_balance': {
|
||||
return {
|
||||
amount: {
|
||||
amount: '100',
|
||||
denom: 'nymt',
|
||||
},
|
||||
printable_balance: '100 NYMT',
|
||||
};
|
||||
}
|
||||
case 'delegate_to_mixnode': {
|
||||
return {
|
||||
logs_json: '[]',
|
||||
data_json: '{}',
|
||||
transaction_hash: '12345',
|
||||
};
|
||||
}
|
||||
case 'simulate_send': {
|
||||
return {
|
||||
amount: {
|
||||
amount: '0.01',
|
||||
denom: 'nym',
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'get_delegation_summary': {
|
||||
return {
|
||||
delegations,
|
||||
total_delegations: {
|
||||
amount: '1000',
|
||||
denom: 'nymt',
|
||||
},
|
||||
total_rewards: {
|
||||
amount: '42',
|
||||
denom: 'nymt',
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'get_pending_delegation_events' : {
|
||||
return [];
|
||||
}
|
||||
case 'migrate_vested_delegations': {
|
||||
delegations[1].uses_vesting_contract_tokens = false;
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
console.error(
|
||||
`Tauri cannot be used in Storybook. The operation requested was "${operation}". You can add mock responses to "nym_wallet/.storybook/mocks/tauri.js" if you need. The default response is "void".`,
|
||||
);
|
||||
return new Promise((resolve, reject) => {
|
||||
reject(new Error(`Tauri operation ${operation} not available in storybook.`));
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* This is a mock for Tauri's API package (@tauri-apps/api/window), to prevent stories from being excluded, because they either use
|
||||
* or import dependencies that use Tauri.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
appWindow: {
|
||||
maximize: () => undefined,
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { NymWalletThemeWithMode } from '../src/theme/NymWalletTheme';
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const withThemeProvider = (Story, context) => (
|
||||
<div style={{ display: 'grid', height: '100%', gridTemplateColumns: '50% 50%' }}>
|
||||
<div>
|
||||
<NymWalletThemeWithMode mode="light">
|
||||
<Box
|
||||
p={4}
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateRows: '80vh 2rem',
|
||||
background: (theme) => theme.palette.background.default,
|
||||
color: 'text.primary',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ overflowY: 'auto' }}>
|
||||
<Story {...context} />
|
||||
</Box>
|
||||
<h4 style={{ textAlign: 'center' }}>Light mode</h4>
|
||||
</Box>
|
||||
</NymWalletThemeWithMode>
|
||||
</div>
|
||||
<div>
|
||||
<NymWalletThemeWithMode mode="dark">
|
||||
<Box
|
||||
p={4}
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateRows: '80vh 2rem',
|
||||
background: (theme) => theme.palette.background.default,
|
||||
color: 'text.primary',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ overflowY: 'auto' }}>
|
||||
<Story {...context} />
|
||||
</Box>
|
||||
<h4 style={{ textAlign: 'center' }}>Dark mode</h4>
|
||||
</Box>
|
||||
</NymWalletThemeWithMode>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const decorators = [withThemeProvider];
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Theme } from '@mui/material/styles';
|
||||
|
||||
export const backDropStyles = (theme: Theme) => {
|
||||
const { mode } = theme.palette;
|
||||
return {
|
||||
style: {
|
||||
left: mode === 'light' ? '0' : '50%',
|
||||
width: '50%',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const modalStyles = (theme: Theme) => {
|
||||
const { mode } = theme.palette;
|
||||
return { left: mode === 'light' ? '25%' : '75%' };
|
||||
};
|
||||
|
||||
export const dialogStyles = (theme: Theme) => {
|
||||
const { mode } = theme.palette;
|
||||
return { left: mode === 'light' ? '-50%' : '50%' };
|
||||
};
|
||||
@@ -12,8 +12,8 @@ The Nym desktop wallet enables you to use the Nym network and take advantage of
|
||||
## Installation prerequisites - Linux / Mac
|
||||
|
||||
- `Yarn`
|
||||
- `NodeJS >= v16.8.0`
|
||||
- `Rust & cargo >= v1.56`
|
||||
- `NodeJS >= v22.13.0`
|
||||
- `Rust & cargo >= v1.85`
|
||||
|
||||
## Installation prerequisites - Windows
|
||||
|
||||
@@ -66,6 +66,17 @@ 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.
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/src'],
|
||||
testMatch: ['**/*.test.ts'],
|
||||
transform: {
|
||||
'^.+\\.tsx?$': [
|
||||
'ts-jest',
|
||||
{
|
||||
tsconfig: {
|
||||
esModuleInterop: true,
|
||||
module: 'commonjs',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^src/(.*)$': '<rootDir>/src/$1',
|
||||
},
|
||||
};
|
||||
@@ -10,13 +10,11 @@
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"prebuild": "yarn --cwd .. build",
|
||||
"prestorybook": "yarn --cwd .. build",
|
||||
"prewebpack:dev": "yarn --cwd .. build",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"storybook:build": "build-storybook",
|
||||
"tauri:build": "yarn tauri build",
|
||||
"tauri:dev": "yarn tauri dev",
|
||||
"tauri:buildx86": "yarn tauri build --target x86_64-apple-darwin",
|
||||
"test": "jest --config jest.config.cjs",
|
||||
"tsc": "tsc --noEmit true",
|
||||
"tsc:watch": "tsc --noEmit true --watch",
|
||||
"webpack:dev": "yarn webpack serve --config webpack.dev.js",
|
||||
@@ -26,21 +24,21 @@
|
||||
"@babel/helper-simple-access": "^7.25.9",
|
||||
"@emotion/react": "^11.7.0",
|
||||
"@emotion/styled": "^11.6.0",
|
||||
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
|
||||
"@hookform/resolvers": "^2.8.0",
|
||||
"@mui/icons-material": "^5.2.0",
|
||||
"@mui/material": "^5.2.2",
|
||||
"@mui/styles": "^5.2.2",
|
||||
"@mui/styles": "^5.18.0",
|
||||
"@mui/utils": "^5.7.0",
|
||||
"@nymproject/mui-theme": "^1.0.0",
|
||||
"@nymproject/node-tester": "^1.3.1",
|
||||
"@nymproject/react": "^1.0.0",
|
||||
"@nymproject/types": "^1.0.0",
|
||||
"@storybook/react": "^6.5.15",
|
||||
"@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/api": "^2.10.1",
|
||||
"@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",
|
||||
"bs58": "^4.0.1",
|
||||
@@ -51,8 +49,8 @@
|
||||
"notistack": "^2.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"qrcode.react": "^1.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-error-boundary": "^3.1.3",
|
||||
"react-hook-form": "^7.14.2",
|
||||
"react-router-dom": "6",
|
||||
@@ -72,31 +70,31 @@
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@nymproject/eslint-config-react-typescript": "^1.0.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
|
||||
"@storybook/react": "^6.5.15",
|
||||
"@svgr/webpack": "^6.1.1",
|
||||
"@tauri-apps/cli": "^2.4.0",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
"@tauri-apps/cli": "^2.10.1",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/big.js": "^6.1.6",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/minimatch": "5.1.2",
|
||||
"@types/node": "^16.7.13",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/qrcode.react": "^1.0.2",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/semver": "^7.3.8",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/zxcvbn": "^4.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
||||
"@typescript-eslint/parser": "^5.13.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||
"@typescript-eslint/parser": "^8.56.1",
|
||||
"babel-loader": "^8.3.0",
|
||||
"babel-plugin-root-import": "^6.6.0",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"css-loader": "^6.7.3",
|
||||
"css-minimizer-webpack-plugin": "^3.0.2",
|
||||
"dotenv-webpack": "^7.0.3",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^16.1.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
@@ -107,13 +105,12 @@
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.29.2",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-storybook": "^0.5.12",
|
||||
"favicons": "^7.0.2",
|
||||
"favicons-webpack-plugin": "^5.0.2",
|
||||
"file-loader": "^6.2.0",
|
||||
"fork-ts-checker-webpack-plugin": "^7.2.1",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"jest": "^27.1.0",
|
||||
"jest": "^30.3.0",
|
||||
"mini-css-extract-plugin": "^2.2.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.8.7",
|
||||
@@ -121,10 +118,10 @@
|
||||
"react-refresh-typescript": "^2.0.2",
|
||||
"style-loader": "^3.3.1",
|
||||
"thread-loader": "^3.0.4",
|
||||
"ts-jest": "^27.0.5",
|
||||
"ts-jest": "^29.4.9",
|
||||
"ts-loader": "^9.4.2",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
||||
"typescript": "^4.6.2",
|
||||
"typescript": "^5.9.3",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^4.8.0",
|
||||
|
||||
@@ -4,6 +4,25 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Nym Wallet</title>
|
||||
<style>
|
||||
/* Match dark theme `background.default` (#242B2D) before React/MUI CssBaseline runs. */
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background-color: #242b2d;
|
||||
}
|
||||
#root {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background-color: #242b2d;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -4,6 +4,16 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Nym Wallet Logs</title>
|
||||
<style>
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: #242b2d;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root-log"></div>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerate macOS/Windows icon bundles from the 1024x1024 master in src-tauri/icons/.
|
||||
# Master file: app-icon-source.png (padded per Apple-style safe zone). Edit that asset, then run:
|
||||
# ./scripts/regenerate-tauri-icons.sh
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
SRC="$ROOT/src-tauri/icons/app-icon-source.png"
|
||||
yarn --cwd "$ROOT" tauri icon "$SRC" -o "$ROOT/src-tauri/icons"
|
||||
rm -rf "$ROOT/src-tauri/icons/android" "$ROOT/src-tauri/icons/ios"
|
||||
rm -f "$ROOT/src-tauri/icons"/Square*.png "$ROOT/src-tauri/icons/StoreLogo.png"
|
||||
python3 - <<PY
|
||||
from PIL import Image
|
||||
from pathlib import Path
|
||||
icons = Path("$ROOT/src-tauri/icons")
|
||||
src = Image.open(icons / "app-icon-source.png").convert("RGBA")
|
||||
src.resize((128, 128), Image.Resampling.LANCZOS).save(icons / "tray_icon.png")
|
||||
PY
|
||||
@@ -1,2 +0,0 @@
|
||||
[capabilities]
|
||||
shell = { open = true }
|
||||
@@ -13,15 +13,15 @@ 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 = "2.1.1", features = [] }
|
||||
tauri-build = { version = "2.5.6", 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"
|
||||
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"] }
|
||||
cfg-if = "1.0.0"
|
||||
colored = "2.0"
|
||||
@@ -33,7 +33,6 @@ 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"] }
|
||||
@@ -41,7 +40,7 @@ serde_json = "1.0"
|
||||
serde_repr = "0.1"
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
tap = "1"
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri = { version = "2.10.3", features = [] }
|
||||
#tendermint-rpc = "0.23.0"
|
||||
time = { version = "0.3.30", features = ["local-offset"] }
|
||||
thiserror = "1.0"
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"updater:allow-download-and-install",
|
||||
"updater:allow-install",
|
||||
"core:event:allow-listen",
|
||||
"shell:allow-open"
|
||||
"shell:allow-open",
|
||||
"process:default"
|
||||
]
|
||||
}
|
||||
@@ -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: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"],"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: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"]}}
|
||||
@@ -37,7 +37,7 @@
|
||||
],
|
||||
"definitions": {
|
||||
"Capability": {
|
||||
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
|
||||
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"identifier",
|
||||
@@ -49,7 +49,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.",
|
||||
"description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
@@ -639,10 +639,10 @@
|
||||
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`",
|
||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`",
|
||||
"type": "string",
|
||||
"const": "core:app:default",
|
||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`"
|
||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`"
|
||||
},
|
||||
{
|
||||
"description": "Enables the app_hide command without any pre-configured scope.",
|
||||
@@ -656,6 +656,12 @@
|
||||
"const": "core:app:allow-app-show",
|
||||
"markdownDescription": "Enables the app_show command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the bundle_type command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-bundle-type",
|
||||
"markdownDescription": "Enables the bundle_type command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the default_window_icon command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -680,18 +686,36 @@
|
||||
"const": "core:app:allow-name",
|
||||
"markdownDescription": "Enables the name command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-register-listener",
|
||||
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove_data_store command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-remove-data-store",
|
||||
"markdownDescription": "Enables the remove_data_store command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-remove-listener",
|
||||
"markdownDescription": "Enables the remove_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_app_theme command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-set-app-theme",
|
||||
"markdownDescription": "Enables the set_app_theme command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_dock_visibility command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-set-dock-visibility",
|
||||
"markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the tauri_version command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -716,6 +740,12 @@
|
||||
"const": "core:app:deny-app-show",
|
||||
"markdownDescription": "Denies the app_show command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the bundle_type command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-bundle-type",
|
||||
"markdownDescription": "Denies the bundle_type command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the default_window_icon command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -740,18 +770,36 @@
|
||||
"const": "core:app:deny-name",
|
||||
"markdownDescription": "Denies the name command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-register-listener",
|
||||
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the remove_data_store command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-remove-data-store",
|
||||
"markdownDescription": "Denies the remove_data_store command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the remove_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-remove-listener",
|
||||
"markdownDescription": "Denies the remove_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_app_theme command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-set-app-theme",
|
||||
"markdownDescription": "Denies the set_app_theme command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_dock_visibility command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-set-dock-visibility",
|
||||
"markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the tauri_version command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -1460,6 +1508,12 @@
|
||||
"const": "core:webview:allow-reparent",
|
||||
"markdownDescription": "Enables the reparent command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_webview_auto_resize command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:webview:allow-set-webview-auto-resize",
|
||||
"markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_webview_background_color command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -1562,6 +1616,12 @@
|
||||
"const": "core:webview:deny-reparent",
|
||||
"markdownDescription": "Denies the reparent command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_webview_auto_resize command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:webview:deny-set-webview-auto-resize",
|
||||
"markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_webview_background_color command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -1910,6 +1970,12 @@
|
||||
"const": "core:window:allow-set-focus",
|
||||
"markdownDescription": "Enables the set_focus command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_focusable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:allow-set-focusable",
|
||||
"markdownDescription": "Enables the set_focusable command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -1982,6 +2048,12 @@
|
||||
"const": "core:window:allow-set-shadow",
|
||||
"markdownDescription": "Enables the set_shadow command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_simple_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:allow-set-simple-fullscreen",
|
||||
"markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_size command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -2354,6 +2426,12 @@
|
||||
"const": "core:window:deny-set-focus",
|
||||
"markdownDescription": "Denies the set_focus command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_focusable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:deny-set-focusable",
|
||||
"markdownDescription": "Denies the set_focusable command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -2426,6 +2504,12 @@
|
||||
"const": "core:window:deny-set-shadow",
|
||||
"markdownDescription": "Denies the set_shadow command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_simple_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:deny-set-simple-fullscreen",
|
||||
"markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_size command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
],
|
||||
"definitions": {
|
||||
"Capability": {
|
||||
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
|
||||
"description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"identifier",
|
||||
@@ -49,7 +49,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programatic access to files selected by the user.",
|
||||
"description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.",
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
@@ -639,10 +639,10 @@
|
||||
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`",
|
||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`",
|
||||
"type": "string",
|
||||
"const": "core:app:default",
|
||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`"
|
||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`"
|
||||
},
|
||||
{
|
||||
"description": "Enables the app_hide command without any pre-configured scope.",
|
||||
@@ -656,6 +656,12 @@
|
||||
"const": "core:app:allow-app-show",
|
||||
"markdownDescription": "Enables the app_show command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the bundle_type command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-bundle-type",
|
||||
"markdownDescription": "Enables the bundle_type command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the default_window_icon command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -680,18 +686,36 @@
|
||||
"const": "core:app:allow-name",
|
||||
"markdownDescription": "Enables the name command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-register-listener",
|
||||
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove_data_store command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-remove-data-store",
|
||||
"markdownDescription": "Enables the remove_data_store command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-remove-listener",
|
||||
"markdownDescription": "Enables the remove_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_app_theme command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-set-app-theme",
|
||||
"markdownDescription": "Enables the set_app_theme command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_dock_visibility command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:allow-set-dock-visibility",
|
||||
"markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the tauri_version command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -716,6 +740,12 @@
|
||||
"const": "core:app:deny-app-show",
|
||||
"markdownDescription": "Denies the app_show command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the bundle_type command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-bundle-type",
|
||||
"markdownDescription": "Denies the bundle_type command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the default_window_icon command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -740,18 +770,36 @@
|
||||
"const": "core:app:deny-name",
|
||||
"markdownDescription": "Denies the name command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-register-listener",
|
||||
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the remove_data_store command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-remove-data-store",
|
||||
"markdownDescription": "Denies the remove_data_store command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the remove_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-remove-listener",
|
||||
"markdownDescription": "Denies the remove_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_app_theme command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-set-app-theme",
|
||||
"markdownDescription": "Denies the set_app_theme command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_dock_visibility command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:app:deny-set-dock-visibility",
|
||||
"markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the tauri_version command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -1460,6 +1508,12 @@
|
||||
"const": "core:webview:allow-reparent",
|
||||
"markdownDescription": "Enables the reparent command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_webview_auto_resize command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:webview:allow-set-webview-auto-resize",
|
||||
"markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_webview_background_color command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -1562,6 +1616,12 @@
|
||||
"const": "core:webview:deny-reparent",
|
||||
"markdownDescription": "Denies the reparent command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_webview_auto_resize command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:webview:deny-set-webview-auto-resize",
|
||||
"markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_webview_background_color command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -1910,6 +1970,12 @@
|
||||
"const": "core:window:allow-set-focus",
|
||||
"markdownDescription": "Enables the set_focus command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_focusable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:allow-set-focusable",
|
||||
"markdownDescription": "Enables the set_focusable command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -1982,6 +2048,12 @@
|
||||
"const": "core:window:allow-set-shadow",
|
||||
"markdownDescription": "Enables the set_shadow command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_simple_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:allow-set-simple-fullscreen",
|
||||
"markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_size command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -2354,6 +2426,12 @@
|
||||
"const": "core:window:deny-set-focus",
|
||||
"markdownDescription": "Denies the set_focus command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_focusable command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:deny-set-focusable",
|
||||
"markdownDescription": "Denies the set_focusable command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -2426,6 +2504,12 @@
|
||||
"const": "core:window:deny-set-shadow",
|
||||
"markdownDescription": "Denies the set_shadow command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_simple_fullscreen command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "core:window:deny-set-simple-fullscreen",
|
||||
"markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_size command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 1008 B After Width: | Height: | Size: 994 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 401 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 401 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ -z "${WAYLAND_DISPLAY:-}" ]; then
|
||||
return 0 2>/dev/null || exit 0
|
||||
fi
|
||||
|
||||
if [ -z "${LD_PRELOAD:-}" ]; then
|
||||
for lib_path in \
|
||||
/usr/lib/libwayland-client.so \
|
||||
/usr/lib64/libwayland-client.so \
|
||||
/usr/lib/x86_64-linux-gnu/libwayland-client.so
|
||||
do
|
||||
if [ -f "$lib_path" ]; then
|
||||
export LD_PRELOAD="$lib_path"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
export GDK_BACKEND="${GDK_BACKEND:-wayland}"
|
||||
export GDK_SCALE="${GDK_SCALE:-1}"
|
||||
export GDK_DPI_SCALE="${GDK_DPI_SCALE:-0.8}"
|
||||
@@ -14,7 +14,6 @@ use thiserror::Error;
|
||||
pub enum BackendError {
|
||||
#[error(transparent)]
|
||||
TypesError {
|
||||
#[from]
|
||||
source: TypesError,
|
||||
},
|
||||
#[error(transparent)]
|
||||
@@ -115,6 +114,8 @@ pub enum BackendError {
|
||||
WalletUnexpectedMnemonicAccount,
|
||||
#[error("Failed to derive address from mnemonic")]
|
||||
FailedToDeriveAddress,
|
||||
#[error("Built-in HD derivation path constant failed to parse (internal error)")]
|
||||
InvalidInternalDerivationPath,
|
||||
#[error(transparent)]
|
||||
ValueParseError(#[from] ParseIntError),
|
||||
#[error("The provided coin has an unknown denomination - {0}")]
|
||||
@@ -156,6 +157,10 @@ pub enum BackendError {
|
||||
#[error("there aren't any vesting delegations to migrate")]
|
||||
NoVestingDelegations,
|
||||
|
||||
/// Vesting contract [`nym_vesting_contract_common::VestingContractError::NoAccountForAddress`].
|
||||
#[error("Vesting contract has no account for this address")]
|
||||
VestingContractAccountNotFound,
|
||||
|
||||
#[error("this command has been temporarily disabled")]
|
||||
Disabled,
|
||||
//
|
||||
@@ -172,8 +177,50 @@ impl Serialize for BackendError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Cosmwasm returns vesting [`nym_vesting_contract_common::VestingContractError::NoAccountForAddress`]
|
||||
/// as `VESTING (...): Account does not exist - ...` in the ABCI log.
|
||||
fn nyxd_error_is_vesting_contract_no_account(err: &NyxdError) -> bool {
|
||||
fn text_matches_strict(text: &str) -> bool {
|
||||
text.contains("VESTING") && text.contains("Account does not exist")
|
||||
}
|
||||
// Prefer strict match; fall back for ABCI text that omits the `VESTING` prefix. Exclude Nyxd
|
||||
// `NonExistentAccountError` (`... does not exist on the chain`).
|
||||
fn abci_query_vesting_no_account(text: &str) -> bool {
|
||||
text_matches_strict(text)
|
||||
|| (text.contains("Account does not exist")
|
||||
&& !text.contains("does not exist on the chain"))
|
||||
}
|
||||
match err {
|
||||
NyxdError::AbciError {
|
||||
log,
|
||||
pretty_log,
|
||||
..
|
||||
} => {
|
||||
pretty_log
|
||||
.as_ref()
|
||||
.is_some_and(|s| abci_query_vesting_no_account(s))
|
||||
|| abci_query_vesting_no_account(log)
|
||||
}
|
||||
_ => text_matches_strict(&err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TypesError> for BackendError {
|
||||
fn from(e: TypesError) -> Self {
|
||||
if let TypesError::NyxdError { ref source } = e {
|
||||
if nyxd_error_is_vesting_contract_no_account(source) {
|
||||
return Self::VestingContractAccountNotFound;
|
||||
}
|
||||
}
|
||||
Self::TypesError { source: e }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NyxdError> for BackendError {
|
||||
fn from(source: NyxdError) -> Self {
|
||||
if nyxd_error_is_vesting_contract_no_account(&source) {
|
||||
return Self::VestingContractAccountNotFound;
|
||||
}
|
||||
match source {
|
||||
NyxdError::AbciError {
|
||||
code: _,
|
||||
|
||||
@@ -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::Emitter;
|
||||
use tauri::{Emitter, Manager};
|
||||
use time::{format_description, OffsetDateTime};
|
||||
|
||||
fn formatted_time() -> String {
|
||||
@@ -24,6 +24,7 @@ fn formatted_time() -> String {
|
||||
}
|
||||
|
||||
pub fn setup_logging(app_handle: tauri::AppHandle) -> Result<(), log::SetLoggerError> {
|
||||
let log_window_app = app_handle.clone();
|
||||
let colors = ColoredLevelConfig::new()
|
||||
.trace(Color::Magenta)
|
||||
.debug(Color::Blue)
|
||||
@@ -61,7 +62,10 @@ pub fn setup_logging(app_handle: tauri::AppHandle) -> Result<(), log::SetLoggerE
|
||||
message: record.args().to_string(),
|
||||
level: record.level().into(),
|
||||
};
|
||||
app_handle.emit("log://log", msg).unwrap();
|
||||
// Tauri 2: target the log webview explicitly so the dedicated window receives events.
|
||||
if let Some(log_win) = log_window_app.get_webview_window("log") {
|
||||
let _ = log_win.emit("log://log", msg);
|
||||
}
|
||||
}));
|
||||
|
||||
base_config
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
)]
|
||||
|
||||
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_process::init as init_process;
|
||||
use tauri_plugin_shell::init as init_shell;
|
||||
use tauri_plugin_updater::Builder as UpdaterBuilder;
|
||||
|
||||
@@ -23,6 +23,7 @@ use crate::state::WalletState;
|
||||
mod config;
|
||||
mod error;
|
||||
mod log;
|
||||
mod webview_theme;
|
||||
mod menu;
|
||||
mod network_config;
|
||||
mod operations;
|
||||
@@ -34,11 +35,13 @@ mod wallet_storage;
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn main() {
|
||||
dotenvy::dotenv().ok();
|
||||
configure_linux_wayland_defaults();
|
||||
|
||||
let context = tauri::generate_context!();
|
||||
tauri::Builder::default()
|
||||
.plugin(init_shell())
|
||||
.plugin(init_opener())
|
||||
.plugin(init_process())
|
||||
.plugin(UpdaterBuilder::new().build())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.manage(WalletState::default())
|
||||
@@ -217,25 +220,7 @@ fn main() {
|
||||
app::react::set_react_state,
|
||||
app::react::get_react_state,
|
||||
])
|
||||
.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()
|
||||
}
|
||||
})
|
||||
.menu(|app| menu::build_app_menu(app))
|
||||
.on_menu_event(|app, event| {
|
||||
if event.id() == SHOW_LOG_WINDOW {
|
||||
let _r = help::log::help_log_toggle_window(app.app_handle().clone());
|
||||
@@ -245,3 +230,22 @@ fn main() {
|
||||
.run(context)
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
fn configure_linux_wayland_defaults() {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if std::env::var_os("WAYLAND_DISPLAY").is_some() {
|
||||
if std::env::var_os("GDK_BACKEND").is_none() {
|
||||
unsafe { std::env::set_var("GDK_BACKEND", "wayland") };
|
||||
}
|
||||
|
||||
if std::env::var_os("GDK_SCALE").is_none() {
|
||||
unsafe { std::env::set_var("GDK_SCALE", "1") };
|
||||
}
|
||||
|
||||
if std::env::var_os("GDK_DPI_SCALE").is_none() {
|
||||
unsafe { std::env::set_var("GDK_DPI_SCALE", "0.8") };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,25 @@
|
||||
use tauri::menu::Menu;
|
||||
use tauri::menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder};
|
||||
use tauri::menu::{Menu, MenuBuilder, MenuItemBuilder, SubmenuBuilder};
|
||||
use tauri::{AppHandle, Runtime};
|
||||
|
||||
pub const SHOW_LOG_WINDOW: &str = "show_log_window";
|
||||
|
||||
pub trait AddDefaultSubmenus {
|
||||
#[allow(dead_code)]
|
||||
fn add_default_app_submenus(self) -> Self;
|
||||
}
|
||||
pub fn build_app_menu<R: Runtime>(app: &AppHandle<R>) -> tauri::Result<Menu<R>> {
|
||||
let edit_submenu = SubmenuBuilder::new(app, "Edit")
|
||||
.cut()
|
||||
.copy()
|
||||
.paste()
|
||||
.select_all()
|
||||
.build()?;
|
||||
|
||||
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 app_handle = self.app_handle();
|
||||
let mut menu_builder = MenuBuilder::new(app).item(&edit_submenu);
|
||||
|
||||
let help_text = MenuItemBuilder::with_id(SHOW_LOG_WINDOW, "Show logs")
|
||||
.build(app_handle)
|
||||
.expect("Failed to create menu item");
|
||||
if std::env::var("NYM_WALLET_ENABLE_LOG").is_ok() {
|
||||
let help_text = MenuItemBuilder::with_id(SHOW_LOG_WINDOW, "Show logs").build(app)?;
|
||||
|
||||
let submenu = SubmenuBuilder::new(app_handle, "Help")
|
||||
.items(&[&help_text])
|
||||
.build()
|
||||
.expect("Failed to create help submenu");
|
||||
let help_submenu = SubmenuBuilder::new(app, "Help").items(&[&help_text]).build()?;
|
||||
|
||||
let menu_builder = MenuBuilder::new(app_handle);
|
||||
|
||||
match menu_builder.item(&submenu).build() {
|
||||
Ok(new_menu) => new_menu,
|
||||
Err(_) => self,
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
menu_builder = menu_builder.item(&help_submenu);
|
||||
}
|
||||
|
||||
menu_builder.build()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
use url::Url;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_url(url: String, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
println!("Opening URL: {url}");
|
||||
let parsed = Url::parse(&url).map_err(|e| format!("Invalid URL: {e}"))?;
|
||||
match parsed.scheme() {
|
||||
"https" | "http" => {}
|
||||
other => {
|
||||
return Err(format!("URL scheme not allowed: {other}"));
|
||||
}
|
||||
}
|
||||
|
||||
match app_handle.opener().open_url(&url, None::<&str>) {
|
||||
Ok(_) => Ok(()),
|
||||
|
||||
@@ -15,10 +15,29 @@ pub async fn check_version(handle: tauri::AppHandle) -> Result<AppVersion, Backe
|
||||
})?;
|
||||
|
||||
// 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
|
||||
})?;
|
||||
let update_info = match updater.check().await {
|
||||
Ok(info) => info,
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
// Hosted static JSON must include per-platform `signature` (base64) alongside `url`.
|
||||
// Legacy manifests with only `url` fail serde in tauri-plugin-updater 2.x.
|
||||
if msg.contains("missing field") && msg.contains("signature") {
|
||||
let current_version = handle.package_info().version.to_string();
|
||||
log::warn!(
|
||||
"Updater check skipped: manifest at configured endpoint is not Tauri 2-compatible \
|
||||
(missing or invalid `signature` field). Users will not be notified of updates \
|
||||
until the hosted updater.json is republished. Error: {msg}"
|
||||
);
|
||||
return Ok(AppVersion {
|
||||
current_version: current_version.clone(),
|
||||
latest_version: current_version,
|
||||
is_update_available: false,
|
||||
});
|
||||
}
|
||||
log::error!("An error occurred while checking for app update {e}");
|
||||
return Err(BackendError::CheckAppVersionError);
|
||||
}
|
||||
};
|
||||
|
||||
// Process the result
|
||||
if let Some(update) = update_info {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use tauri::Manager;
|
||||
|
||||
use crate::error::BackendError;
|
||||
use crate::webview_theme::NYM_WALLET_WEBVIEW_BG;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_main_window(app_handle: tauri::AppHandle) -> Result<(), BackendError> {
|
||||
@@ -32,6 +33,7 @@ async fn create_window(
|
||||
tauri::WebviewUrl::App(new_window_url.into()),
|
||||
)
|
||||
.title("Nym Wallet")
|
||||
.background_color(NYM_WALLET_WEBVIEW_BG)
|
||||
.build()
|
||||
{
|
||||
Ok(window) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::error::BackendError;
|
||||
use crate::webview_theme::NYM_WALLET_WEBVIEW_BG;
|
||||
use tauri::Manager;
|
||||
|
||||
#[tauri::command]
|
||||
@@ -18,6 +19,7 @@ pub fn help_log_toggle_window(app_handle: tauri::AppHandle) -> Result<(), Backen
|
||||
tauri::WebviewUrl::App("log.html".into()),
|
||||
)
|
||||
.title("Nym Wallet Logs")
|
||||
.background_color(NYM_WALLET_WEBVIEW_BG)
|
||||
.build()
|
||||
{
|
||||
Ok(window) => {
|
||||
|
||||
@@ -18,6 +18,12 @@ use std::collections::HashMap;
|
||||
use strum::IntoEnumIterator;
|
||||
use url::Url;
|
||||
|
||||
fn cosmos_derivation_path() -> Result<DerivationPath, BackendError> {
|
||||
COSMOS_DERIVATION_PATH
|
||||
.parse()
|
||||
.map_err(|_| BackendError::InvalidInternalDerivationPath)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn connect_with_mnemonic(
|
||||
mnemonic: Mnemonic,
|
||||
@@ -48,8 +54,9 @@ pub async fn get_balance(state: tauri::State<'_, WalletState>) -> Result<Balance
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_new_mnemonic() -> Mnemonic {
|
||||
random_mnemonic()
|
||||
pub fn create_new_mnemonic() -> Result<Mnemonic, BackendError> {
|
||||
let mut rng = rand::thread_rng();
|
||||
Mnemonic::generate_in_with(&mut rng, Language::English, 24).map_err(Into::into)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -82,11 +89,6 @@ pub async fn logout(state: tauri::State<'_, WalletState>) -> Result<(), BackendE
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn random_mnemonic() -> Mnemonic {
|
||||
let mut rng = rand::thread_rng();
|
||||
Mnemonic::generate_in_with(&mut rng, Language::English, 24).unwrap()
|
||||
}
|
||||
|
||||
async fn _connect_with_mnemonic(
|
||||
mnemonic: Mnemonic,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
@@ -129,12 +131,26 @@ async fn _connect_with_mnemonic(
|
||||
|
||||
let default_nyxd_urls: HashMap<WalletNetwork, Url> = untested_nyxd_urls
|
||||
.iter()
|
||||
.map(|(network, urls)| (*network, urls.iter().next().unwrap().clone()))
|
||||
.collect();
|
||||
.map(|(network, urls)| {
|
||||
let url = urls
|
||||
.iter()
|
||||
.next()
|
||||
.cloned()
|
||||
.ok_or(BackendError::WalletNoDefaultValidator)?;
|
||||
Ok((*network, url))
|
||||
})
|
||||
.collect::<Result<HashMap<_, _>, BackendError>>()?;
|
||||
let default_api_urls: HashMap<WalletNetwork, Url> = untested_api_urls
|
||||
.iter()
|
||||
.map(|(network, urls)| (*network, urls.iter().next().unwrap().clone()))
|
||||
.collect();
|
||||
.map(|(network, urls)| {
|
||||
let url = urls
|
||||
.iter()
|
||||
.next()
|
||||
.cloned()
|
||||
.ok_or(BackendError::WalletNoDefaultValidator)?;
|
||||
Ok((*network, url))
|
||||
})
|
||||
.collect::<Result<HashMap<_, _>, BackendError>>()?;
|
||||
|
||||
let nyxd_urls = pick_good_nyxd_urls(&default_nyxd_urls, &nyxd_urls).await?;
|
||||
let api_urls = pick_good_api_urls(&default_api_urls, &api_urls).await?;
|
||||
@@ -339,7 +355,7 @@ pub fn create_password(mnemonic: Mnemonic, password: UserPassword) -> Result<(),
|
||||
}
|
||||
log::info!("Creating password");
|
||||
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let hd_path: DerivationPath = cosmos_derivation_path()?;
|
||||
// Currently we only support a single, default, login id in the wallet
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
wallet_storage::store_login_with_multiple_accounts(mnemonic, hd_path, login_id, &password)
|
||||
@@ -451,7 +467,7 @@ pub async fn add_account_for_password(
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<AccountEntry, BackendError> {
|
||||
log::info!("Adding account for the current password: {account_id}");
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let hd_path: DerivationPath = cosmos_derivation_path()?;
|
||||
// Currently we only support a single, default, login id in the wallet
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
let account_id = wallet_storage::AccountId::new(account_id.to_string());
|
||||
@@ -501,27 +517,24 @@ async fn set_state_with_all_accounts(
|
||||
|
||||
let all_account_ids: Vec<WalletAccountIds> = all_accounts
|
||||
.iter()
|
||||
.map(|account| {
|
||||
.map(|account| -> Result<WalletAccountIds, BackendError> {
|
||||
let mnemonic = account.mnemonic();
|
||||
let addresses: HashMap<WalletNetwork, cosmrs::AccountId> = WalletNetwork::iter()
|
||||
.map(|network| {
|
||||
let config_network: NymNetworkDetails = network.into();
|
||||
(
|
||||
network,
|
||||
derive_address(
|
||||
mnemonic.clone(),
|
||||
&config_network.chain_details.bech32_account_prefix,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
let addr = derive_address(
|
||||
mnemonic.clone(),
|
||||
&config_network.chain_details.bech32_account_prefix,
|
||||
)?;
|
||||
Ok((network, addr))
|
||||
})
|
||||
.collect();
|
||||
WalletAccountIds {
|
||||
.collect::<Result<HashMap<_, _>, BackendError>>()?;
|
||||
Ok(WalletAccountIds {
|
||||
id: account.id().clone(),
|
||||
addresses,
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
.collect::<Result<Vec<_>, BackendError>>()?;
|
||||
|
||||
let mut w_state = state.write().await;
|
||||
w_state.set_all_accounts(all_account_ids);
|
||||
|
||||
@@ -16,7 +16,7 @@ pub(crate) async fn locked_coins(
|
||||
block_time: Option<u64>,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<DecCoin, BackendError> {
|
||||
log::info!(">>> Query locked coins");
|
||||
log::debug!(">>> Query locked coins");
|
||||
let guard = state.read().await;
|
||||
let client = guard.current_client()?;
|
||||
|
||||
@@ -28,7 +28,7 @@ pub(crate) async fn locked_coins(
|
||||
)
|
||||
.await?;
|
||||
let display = guard.attempt_convert_to_display_dec_coin(res)?;
|
||||
log::info!("<<< locked coins = {display}");
|
||||
log::debug!("<<< locked coins = {display}");
|
||||
Ok(display)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ pub(crate) async fn spendable_coins(
|
||||
block_time: Option<u64>,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<DecCoin, BackendError> {
|
||||
log::info!(">>> Query spendable coins");
|
||||
log::debug!(">>> Query spendable coins");
|
||||
let guard = state.read().await;
|
||||
let client = guard.current_client()?;
|
||||
|
||||
@@ -50,7 +50,7 @@ pub(crate) async fn spendable_coins(
|
||||
.await?;
|
||||
|
||||
let display = guard.attempt_convert_to_display_dec_coin(res)?;
|
||||
log::info!("<<< spendable coins = {display}");
|
||||
log::debug!("<<< spendable coins = {display}");
|
||||
Ok(display)
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ pub(crate) async fn spendable_coins(
|
||||
pub(crate) async fn spendable_vested_coins(
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<DecCoin, BackendError> {
|
||||
log::info!(">>> Query spendable vested coins");
|
||||
log::debug!(">>> Query spendable vested coins");
|
||||
let guard = state.read().await;
|
||||
let client = guard.current_client()?;
|
||||
|
||||
@@ -68,7 +68,7 @@ pub(crate) async fn spendable_vested_coins(
|
||||
.await?;
|
||||
|
||||
let display = guard.attempt_convert_to_display_dec_coin(res)?;
|
||||
log::info!("<<< spendable vested coins = {display}");
|
||||
log::debug!("<<< spendable vested coins = {display}");
|
||||
Ok(display)
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ pub(crate) async fn spendable_vested_coins(
|
||||
pub(crate) async fn spendable_reward_coins(
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<DecCoin, BackendError> {
|
||||
log::info!(">>> Query spendable reward coins");
|
||||
log::debug!(">>> Query spendable reward coins");
|
||||
let guard = state.read().await;
|
||||
let client = guard.current_client()?;
|
||||
|
||||
@@ -86,7 +86,7 @@ pub(crate) async fn spendable_reward_coins(
|
||||
.await?;
|
||||
|
||||
let display = guard.attempt_convert_to_display_dec_coin(res)?;
|
||||
log::info!("<<< spendable reward coins = {display}");
|
||||
log::debug!("<<< spendable reward coins = {display}");
|
||||
Ok(display)
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ pub(crate) async fn vested_coins(
|
||||
block_time: Option<u64>,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<DecCoin, BackendError> {
|
||||
log::info!(">>> Query vested coins");
|
||||
log::debug!(">>> Query vested coins");
|
||||
let guard = state.read().await;
|
||||
|
||||
let res = guard
|
||||
@@ -109,7 +109,7 @@ pub(crate) async fn vested_coins(
|
||||
.await?;
|
||||
|
||||
let display = guard.attempt_convert_to_display_dec_coin(res)?;
|
||||
log::info!("<<< vested coins = {display}");
|
||||
log::debug!("<<< vested coins = {display}");
|
||||
Ok(display)
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ pub(crate) async fn vesting_coins(
|
||||
block_time: Option<u64>,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<DecCoin, BackendError> {
|
||||
log::info!(">>> Query vesting coins");
|
||||
log::debug!(">>> Query vesting coins");
|
||||
let guard = state.read().await;
|
||||
|
||||
let res = guard
|
||||
@@ -132,7 +132,7 @@ pub(crate) async fn vesting_coins(
|
||||
.await?;
|
||||
|
||||
let display = guard.attempt_convert_to_display_dec_coin(res)?;
|
||||
log::info!("<<< vesting coins = {display}");
|
||||
log::debug!("<<< vesting coins = {display}");
|
||||
Ok(display)
|
||||
}
|
||||
|
||||
@@ -141,12 +141,12 @@ pub(crate) async fn vesting_start_time(
|
||||
vesting_account_address: &str,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<u64, BackendError> {
|
||||
log::info!(">>> Query vesting start time");
|
||||
log::debug!(">>> Query vesting start time");
|
||||
let res = nyxd_client!(state)
|
||||
.vesting_start_time(vesting_account_address)
|
||||
.await?
|
||||
.seconds();
|
||||
log::info!("<<< vesting start time = {res}");
|
||||
log::debug!("<<< vesting start time = {res}");
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -155,12 +155,12 @@ pub(crate) async fn vesting_end_time(
|
||||
vesting_account_address: &str,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<u64, BackendError> {
|
||||
log::info!(">>> Query vesting end time");
|
||||
log::debug!(">>> Query vesting end time");
|
||||
let res = nyxd_client!(state)
|
||||
.vesting_end_time(vesting_account_address)
|
||||
.await?
|
||||
.seconds();
|
||||
log::info!("<<< vesting end time = {res}");
|
||||
log::debug!("<<< vesting end time = {res}");
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ pub(crate) async fn original_vesting(
|
||||
vesting_account_address: &str,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<OriginalVestingResponse, BackendError> {
|
||||
log::info!(">>> Query original vesting");
|
||||
log::debug!(">>> Query original vesting");
|
||||
let guard = state.read().await;
|
||||
let reg = guard.registered_coins()?;
|
||||
|
||||
@@ -180,7 +180,7 @@ pub(crate) async fn original_vesting(
|
||||
.await?;
|
||||
|
||||
let res = OriginalVestingResponse::from_vesting_contract(res, reg)?;
|
||||
log::info!("<<< {res:?}");
|
||||
log::debug!("<<< {res:?}");
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ pub(crate) async fn original_vesting(
|
||||
pub(crate) async fn get_historical_vesting_staking_reward(
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<DecCoin, BackendError> {
|
||||
log::info!(">>> Query historical vesting staking reward coins");
|
||||
log::debug!(">>> Query historical vesting staking reward coins");
|
||||
let guard = state.read().await;
|
||||
let client = guard.current_client()?;
|
||||
|
||||
@@ -197,7 +197,7 @@ pub(crate) async fn get_historical_vesting_staking_reward(
|
||||
.get_historical_vesting_staking_reward(client.nyxd.address().as_ref())
|
||||
.await?;
|
||||
let display = guard.attempt_convert_to_display_dec_coin(res)?;
|
||||
log::info!("<<< historical vesting staking reward coins = {display}");
|
||||
log::debug!("<<< historical vesting staking reward coins = {display}");
|
||||
Ok(display)
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ pub(crate) async fn get_historical_vesting_staking_reward(
|
||||
pub(crate) async fn get_spendable_vested_coins(
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<DecCoin, BackendError> {
|
||||
log::info!(">>> Query spendable vested coins");
|
||||
log::debug!(">>> Query spendable vested coins");
|
||||
let guard = state.read().await;
|
||||
let client = guard.current_client()?;
|
||||
|
||||
@@ -214,7 +214,7 @@ pub(crate) async fn get_spendable_vested_coins(
|
||||
.get_spendable_vested_coins(client.nyxd.address().as_ref())
|
||||
.await?;
|
||||
let display = guard.attempt_convert_to_display_dec_coin(res)?;
|
||||
log::info!("<<< spendable vested coins = {display}");
|
||||
log::debug!("<<< spendable vested coins = {display}");
|
||||
Ok(display)
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ pub(crate) async fn get_spendable_vested_coins(
|
||||
pub(crate) async fn get_spendable_reward_coins(
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<DecCoin, BackendError> {
|
||||
log::info!(">>> Query spendable reward coins");
|
||||
log::debug!(">>> Query spendable reward coins");
|
||||
let guard = state.read().await;
|
||||
let client = guard.current_client()?;
|
||||
|
||||
@@ -231,7 +231,7 @@ pub(crate) async fn get_spendable_reward_coins(
|
||||
.get_spendable_reward_coins(client.nyxd.address().as_ref())
|
||||
.await?;
|
||||
let display = guard.attempt_convert_to_display_dec_coin(res)?;
|
||||
log::info!("<<< spendable reward coins = {display}");
|
||||
log::debug!("<<< spendable reward coins = {display}");
|
||||
Ok(display)
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ pub(crate) async fn get_spendable_reward_coins(
|
||||
pub(crate) async fn get_delegated_coins(
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<DecCoin, BackendError> {
|
||||
log::info!(">>> Query delegated coins");
|
||||
log::debug!(">>> Query delegated coins");
|
||||
let guard = state.read().await;
|
||||
let client = guard.current_client()?;
|
||||
|
||||
@@ -248,7 +248,7 @@ pub(crate) async fn get_delegated_coins(
|
||||
.get_delegated_coins(client.nyxd.address().as_ref())
|
||||
.await?;
|
||||
let display = guard.attempt_convert_to_display_dec_coin(res)?;
|
||||
log::info!("<<< delegated coins = {display}");
|
||||
log::debug!("<<< delegated coins = {display}");
|
||||
Ok(display)
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ pub(crate) async fn get_delegated_coins(
|
||||
pub(crate) async fn get_pledged_coins(
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<DecCoin, BackendError> {
|
||||
log::info!(">>> Query pledged coins");
|
||||
log::debug!(">>> Query pledged coins");
|
||||
let guard = state.read().await;
|
||||
let client = guard.current_client()?;
|
||||
|
||||
@@ -265,7 +265,7 @@ pub(crate) async fn get_pledged_coins(
|
||||
.get_pledged_coins(client.nyxd.address().as_ref())
|
||||
.await?;
|
||||
let display = guard.attempt_convert_to_display_dec_coin(res)?;
|
||||
log::info!("<<< pledged coins = {display}");
|
||||
log::debug!("<<< pledged coins = {display}");
|
||||
Ok(display)
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ pub(crate) async fn get_pledged_coins(
|
||||
pub(crate) async fn get_staked_coins(
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<DecCoin, BackendError> {
|
||||
log::info!(">>> Query staked coins");
|
||||
log::debug!(">>> Query staked coins");
|
||||
let guard = state.read().await;
|
||||
let client = guard.current_client()?;
|
||||
|
||||
@@ -282,7 +282,7 @@ pub(crate) async fn get_staked_coins(
|
||||
.get_staked_coins(client.nyxd.address().as_ref())
|
||||
.await?;
|
||||
let display = guard.attempt_convert_to_display_dec_coin(res)?;
|
||||
log::info!("<<< staked coins = {display}");
|
||||
log::debug!("<<< staked coins = {display}");
|
||||
Ok(display)
|
||||
}
|
||||
|
||||
@@ -290,7 +290,7 @@ pub(crate) async fn get_staked_coins(
|
||||
pub(crate) async fn get_withdrawn_coins(
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<DecCoin, BackendError> {
|
||||
log::info!(">>> Query withdrawn coins");
|
||||
log::debug!(">>> Query withdrawn coins");
|
||||
let guard = state.read().await;
|
||||
let client = guard.current_client()?;
|
||||
|
||||
@@ -299,7 +299,7 @@ pub(crate) async fn get_withdrawn_coins(
|
||||
.get_withdrawn_coins(client.nyxd.address().as_ref())
|
||||
.await?;
|
||||
let display = guard.attempt_convert_to_display_dec_coin(res)?;
|
||||
log::info!("<<< pledged coins = {display}");
|
||||
log::debug!("<<< withdrawn coins = {display}");
|
||||
Ok(display)
|
||||
}
|
||||
|
||||
@@ -309,7 +309,7 @@ pub(crate) async fn delegated_free(
|
||||
_block_time: Option<u64>,
|
||||
_state: tauri::State<'_, WalletState>,
|
||||
) -> Result<DecCoin, BackendError> {
|
||||
log::info!(">>> Query delegated free -> THIS QUERY HAS BEEN REMOVED FROM THE CONTRACT");
|
||||
log::warn!(">>> Query delegated free -> THIS QUERY HAS BEEN REMOVED FROM THE CONTRACT");
|
||||
Err(BackendError::RemovedCommand {
|
||||
name: "vesting::queries::delegated_free".to_string(),
|
||||
alternative: "vesting::queries::get_delegated_coins".to_string(),
|
||||
@@ -323,7 +323,7 @@ pub(crate) async fn delegated_vesting(
|
||||
_vesting_account_address: &str,
|
||||
_state: tauri::State<'_, WalletState>,
|
||||
) -> Result<DecCoin, BackendError> {
|
||||
log::info!(">>> Query delegated vesting -> THIS QUERY HAS BEEN REMOVED FROM THE CONTRACT");
|
||||
log::warn!(">>> Query delegated vesting -> THIS QUERY HAS BEEN REMOVED FROM THE CONTRACT");
|
||||
Err(BackendError::RemovedCommand {
|
||||
name: "vesting::queries::delegated_vesting".to_string(),
|
||||
alternative: "vesting::queries::get_delegated_coins".to_string(),
|
||||
@@ -335,7 +335,7 @@ pub(crate) async fn vesting_get_mixnode_pledge(
|
||||
address: &str,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<Option<PledgeData>, BackendError> {
|
||||
log::info!(">>> Query vesting get mixnode pledge");
|
||||
log::debug!(">>> Query vesting get mixnode pledge");
|
||||
let guard = state.read().await;
|
||||
let reg = guard.registered_coins()?;
|
||||
|
||||
@@ -347,7 +347,7 @@ pub(crate) async fn vesting_get_mixnode_pledge(
|
||||
.map(|pledge| PledgeData::from_vesting_contract(pledge, reg))
|
||||
.transpose()?;
|
||||
|
||||
log::info!("<<< {res:?}");
|
||||
log::debug!("<<< {res:?}");
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -356,7 +356,7 @@ pub(crate) async fn vesting_get_gateway_pledge(
|
||||
address: &str,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<Option<PledgeData>, BackendError> {
|
||||
log::info!(">>> Query vesting get gateway pledge");
|
||||
log::debug!(">>> Query vesting get gateway pledge");
|
||||
let guard = state.read().await;
|
||||
let reg = guard.registered_coins()?;
|
||||
|
||||
@@ -368,7 +368,7 @@ pub(crate) async fn vesting_get_gateway_pledge(
|
||||
.map(|pledge| PledgeData::from_vesting_contract(pledge, reg))
|
||||
.transpose()?;
|
||||
|
||||
log::info!("<<< {res:?}");
|
||||
log::debug!("<<< {res:?}");
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -377,11 +377,11 @@ pub(crate) async fn get_current_vesting_period(
|
||||
address: &str,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<Period, BackendError> {
|
||||
log::info!(">>> Query current vesting period");
|
||||
log::debug!(">>> Query current vesting period");
|
||||
let res = nyxd_client!(state)
|
||||
.get_current_vesting_period(address)
|
||||
.await?;
|
||||
log::info!("<<< {res:?}");
|
||||
log::debug!("<<< {res:?}");
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -390,13 +390,13 @@ pub(crate) async fn get_account_info(
|
||||
address: &str,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<VestingAccountInfo, BackendError> {
|
||||
log::info!(">>> Query account info");
|
||||
log::debug!(">>> Query account info");
|
||||
let guard = state.read().await;
|
||||
let res = guard.registered_coins()?;
|
||||
|
||||
let vesting_account = guard.current_client()?.nyxd.get_account(address).await?;
|
||||
let res = VestingAccountInfo::from_vesting_contract(vesting_account, res)?;
|
||||
|
||||
log::info!("<<< {res:?}");
|
||||
log::debug!("<<< {res:?}");
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Shared webview chrome (matches wallet dark `background.default` #242B2D).
|
||||
|
||||
use tauri::window::Color;
|
||||
|
||||
pub const NYM_WALLET_WEBVIEW_BG: Color = Color(36, 43, 45, 255);
|
||||
@@ -26,6 +26,11 @@
|
||||
"entitlements": null
|
||||
},
|
||||
"linux": {
|
||||
"appimage": {
|
||||
"files": {
|
||||
"/apprun-hooks/99-nym-wayland.sh": "scripts/appimage-wayland-hook.sh"
|
||||
}
|
||||
},
|
||||
"deb": {
|
||||
"depends": []
|
||||
}
|
||||
@@ -55,15 +60,30 @@
|
||||
"capabilities": [
|
||||
"main-capability"
|
||||
],
|
||||
"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"
|
||||
"csp": {
|
||||
"default-src": "'self' customprotocol: asset:",
|
||||
"script-src": "'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'",
|
||||
"style-src": "'unsafe-inline' 'self'",
|
||||
"img-src": "'self' asset: http://asset.localhost https://asset.localhost blob: data:",
|
||||
"font-src": "'self' data:",
|
||||
"connect-src": "ipc: http://ipc.localhost http://127.0.0.1:* http://localhost:* ws://127.0.0.1:* ws://localhost:* wss://127.0.0.1:* wss://localhost:* http: https: ws: wss:",
|
||||
"media-src": "'self' blob: data:",
|
||||
"worker-src": "'self' blob:",
|
||||
"frame-src": "'none'",
|
||||
"object-src": "'none'",
|
||||
"base-uri": "'self'"
|
||||
}
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"title": "Nym Wallet",
|
||||
"width": 1268,
|
||||
"height": 768,
|
||||
"minWidth": 1024,
|
||||
"minHeight": 640,
|
||||
"resizable": true,
|
||||
"useHttpsScheme": true
|
||||
"useHttpsScheme": true,
|
||||
"backgroundColor": "#242b2d"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { fetchNymPriceDeduped } from './networkOverview';
|
||||
|
||||
const sampleTokenomics = {
|
||||
quotes: {
|
||||
USD: {
|
||||
price: 0.0331,
|
||||
market_cap: 26_000_000,
|
||||
volume_24h: 1_200_000,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('fetchNymPriceDeduped', () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('coalesces concurrent requests for the same URL', async () => {
|
||||
let callCount = 0;
|
||||
global.fetch = jest.fn(() => {
|
||||
callCount += 1;
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(sampleTokenomics),
|
||||
} as Response);
|
||||
});
|
||||
|
||||
const url = 'https://api.example.test/v1/nym-price';
|
||||
const p1 = fetchNymPriceDeduped(url);
|
||||
const p2 = fetchNymPriceDeduped(url);
|
||||
const [a, b] = await Promise.all([p1, p2]);
|
||||
|
||||
expect(a).toStrictEqual(sampleTokenomics);
|
||||
expect(b).toStrictEqual(sampleTokenomics);
|
||||
expect(callCount).toBe(1);
|
||||
});
|
||||
|
||||
it('does not coalesce different URLs', async () => {
|
||||
let callCount = 0;
|
||||
global.fetch = jest.fn(() => {
|
||||
callCount += 1;
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(sampleTokenomics),
|
||||
} as Response);
|
||||
});
|
||||
|
||||
await Promise.all([fetchNymPriceDeduped('https://a.test/p'), fetchNymPriceDeduped('https://b.test/p')]);
|
||||
expect(callCount).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
import { addSeconds } from 'date-fns';
|
||||
import type { Network } from 'src/types';
|
||||
|
||||
export type NetworkOverviewEndpoints = {
|
||||
mixnodeStats: string;
|
||||
epochCurrent: string;
|
||||
epochRewards: string;
|
||||
nymPrice: string;
|
||||
observatoryNodesBase: string;
|
||||
};
|
||||
|
||||
export const getNetworkOverviewEndpoints = (network?: Network): NetworkOverviewEndpoints => {
|
||||
if (network === 'SANDBOX' || network === 'QA') {
|
||||
return {
|
||||
mixnodeStats: 'https://sandbox-node-status-api.nymte.ch/v2/mixnodes/stats',
|
||||
epochCurrent: 'https://sandbox-nym-api1.nymtech.net/api/v1/epoch/current',
|
||||
epochRewards: 'https://sandbox-nym-api1.nymtech.net/api/v1/epoch/reward_params',
|
||||
nymPrice: 'https://api.nym.spectredao.net/api/v1/nym-price',
|
||||
observatoryNodesBase: 'https://sandbox-node-status-api.nymte.ch/explorer/v3/nym-nodes',
|
||||
};
|
||||
}
|
||||
return {
|
||||
mixnodeStats: 'https://mainnet-node-status-api.nymtech.cc/v2/mixnodes/stats',
|
||||
epochCurrent: 'https://validator.nymtech.net/api/v1/epoch/current',
|
||||
epochRewards: 'https://validator.nymtech.net/api/v1/epoch/reward_params',
|
||||
nymPrice: 'https://api.nym.spectredao.net/api/v1/nym-price',
|
||||
observatoryNodesBase: 'https://mainnet-node-status-api.nymtech.cc/explorer/v3/nym-nodes',
|
||||
};
|
||||
};
|
||||
|
||||
export interface PacketsAndStakingPoint {
|
||||
date_utc: string;
|
||||
total_packets_received: number;
|
||||
total_packets_sent: number;
|
||||
total_packets_dropped: number;
|
||||
total_stake: number;
|
||||
}
|
||||
|
||||
export interface CurrentEpochApiData {
|
||||
id: number;
|
||||
current_epoch_id: number;
|
||||
current_epoch_start: string;
|
||||
epoch_length: { secs: number; nanos: number };
|
||||
epochs_in_interval: number;
|
||||
total_elapsed_epochs: number;
|
||||
}
|
||||
|
||||
export type CurrentEpochWithEnd = CurrentEpochApiData & { current_epoch_end: string };
|
||||
|
||||
export interface EpochRewardsData {
|
||||
interval: {
|
||||
reward_pool: string;
|
||||
staking_supply: string;
|
||||
staking_supply_scale_factor: string;
|
||||
epoch_reward_budget: string;
|
||||
stake_saturation_point: string;
|
||||
active_set_work_factor: string;
|
||||
interval_pool_emission: string;
|
||||
sybil_resistance: string;
|
||||
};
|
||||
rewarded_set: {
|
||||
entry_gateways: number;
|
||||
exit_gateways: number;
|
||||
mixnodes: number;
|
||||
standby: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface NymTokenomics {
|
||||
quotes: {
|
||||
USD: {
|
||||
price: number;
|
||||
market_cap: number;
|
||||
volume_24h: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
type ObservatoryPage = {
|
||||
page: number;
|
||||
size: number;
|
||||
total: number;
|
||||
items: Array<{ rewarding_details?: { unique_delegations?: number } }>;
|
||||
};
|
||||
|
||||
const jsonHeaders = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
};
|
||||
|
||||
export async function fetchPacketsAndStaking(url: string): Promise<PacketsAndStakingPoint[]> {
|
||||
const response = await fetch(url, { headers: jsonHeaders });
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch mixnode stats');
|
||||
}
|
||||
const data: PacketsAndStakingPoint[] = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function fetchCurrentEpoch(url: string): Promise<CurrentEpochWithEnd> {
|
||||
const response = await fetch(url, { headers: jsonHeaders, cache: 'no-store' });
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch current epoch');
|
||||
}
|
||||
const data: CurrentEpochApiData = await response.json();
|
||||
const current_epoch_end = addSeconds(new Date(data.current_epoch_start), data.epoch_length.secs).toISOString();
|
||||
return { ...data, current_epoch_end };
|
||||
}
|
||||
|
||||
export async function fetchEpochRewards(url: string): Promise<EpochRewardsData> {
|
||||
const response = await fetch(url, { headers: jsonHeaders, cache: 'no-store' });
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch epoch rewards');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function fetchNymPrice(url: string): Promise<NymTokenomics> {
|
||||
const response = await fetch(url, { headers: jsonHeaders });
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch NYM price');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
const nymPriceInflight = new Map<string, Promise<NymTokenomics>>();
|
||||
|
||||
/** Coalesces concurrent requests for the same price URL (e.g. Balance card + Network overview). */
|
||||
export function fetchNymPriceDeduped(url: string): Promise<NymTokenomics> {
|
||||
const existing = nymPriceInflight.get(url);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const pending = fetchNymPrice(url).finally(() => {
|
||||
nymPriceInflight.delete(url);
|
||||
});
|
||||
nymPriceInflight.set(url, pending);
|
||||
return pending;
|
||||
}
|
||||
|
||||
/** Paginates observatory nodes and sums unique_delegations (same idea as explorer StakersNumberCard). */
|
||||
export async function fetchTotalDelegationsCount(baseUrl: string): Promise<number> {
|
||||
const all: ObservatoryPage['items'] = [];
|
||||
let page = 0;
|
||||
const PAGE_SIZE = 200;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
/* Paginated API: each request needs the previous page index. */
|
||||
// eslint-disable-next-line no-await-in-loop -- sequential pagination
|
||||
const response = await fetch(`${baseUrl}?page=${page}&size=${PAGE_SIZE}`, { headers: jsonHeaders });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch observatory nodes (page ${page})`);
|
||||
}
|
||||
// eslint-disable-next-line no-await-in-loop -- follows fetch above
|
||||
const data: ObservatoryPage = await response.json();
|
||||
all.push(...data.items);
|
||||
const totalPages = Math.ceil(data.total / PAGE_SIZE);
|
||||
if (page >= totalPages - 1 || data.items.length < PAGE_SIZE) {
|
||||
hasMore = false;
|
||||
} else {
|
||||
page += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return all.reduce((sum, node) => sum + (node.rewarding_details?.unique_delegations ?? 0), 0);
|
||||
}
|
||||
|
||||
export function formatCompactNumber(num: number): string {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
maximumFractionDigits: 2,
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
}).format(num);
|
||||
}
|
||||
@@ -7,6 +7,12 @@ import { ErrorFallback } from './components';
|
||||
import { NymWalletTheme } from './theme';
|
||||
import { maximizeWindow } from './utils';
|
||||
import { config } from './config';
|
||||
import { useTauriTextEditingClipboard } from './hooks/useTauriTextEditingClipboard';
|
||||
|
||||
const ClipboardBridge: FCWithChildren = ({ children }) => {
|
||||
useTauriTextEditingClipboard();
|
||||
return children;
|
||||
};
|
||||
|
||||
export const AppCommon = ({ children }: { children: React.ReactNode }) => {
|
||||
useEffect(() => {
|
||||
@@ -26,7 +32,9 @@ export const AppCommon = ({ children }: { children: React.ReactNode }) => {
|
||||
}}
|
||||
>
|
||||
<AppProvider>
|
||||
<NymWalletTheme>{children}</NymWalletTheme>
|
||||
<NymWalletTheme>
|
||||
<ClipboardBridge>{children}</ClipboardBridge>
|
||||
</NymWalletTheme>
|
||||
</AppProvider>
|
||||
</SnackbarProvider>
|
||||
</Router>
|
||||
|
||||
@@ -2,11 +2,12 @@ import React from 'react';
|
||||
import { Button } from '@mui/material';
|
||||
import { AccountEntry } from '@nymproject/types';
|
||||
import { AccountAvatar } from './AccountAvatar';
|
||||
import { headerControlPillSx } from '../headerControlPillSx';
|
||||
|
||||
export const AccountOverview = ({ account, onClick }: { account: AccountEntry; onClick: () => void }) => (
|
||||
<Button
|
||||
startIcon={<AccountAvatar name={account.id} small />}
|
||||
sx={{ color: 'text.primary', fontSize: 14 }}
|
||||
sx={headerControlPillSx}
|
||||
color="inherit"
|
||||
onClick={onClick}
|
||||
>
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { MockAccountsProvider } from 'src/context/mocks/accounts';
|
||||
import { Accounts } from '../Accounts';
|
||||
|
||||
export default {
|
||||
title: 'Wallet / Multi Account',
|
||||
component: Accounts,
|
||||
} as ComponentMeta<typeof Accounts>;
|
||||
|
||||
export const Default: ComponentStory<typeof Accounts> = () => (
|
||||
<Box display="flex" alignContent="center">
|
||||
<MockAccountsProvider>
|
||||
<Accounts />
|
||||
</MockAccountsProvider>
|
||||
</Box>
|
||||
);
|
||||
@@ -8,7 +8,7 @@ export const ActionsMenu: FCWithChildren<{
|
||||
onOpen: () => void;
|
||||
onClose: () => void;
|
||||
}> = ({ children, open, onOpen, onClose }) => {
|
||||
const anchorEl: any = useRef<HTMLElement>();
|
||||
const anchorEl = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { AppBar as MuiAppBar, Grid, IconButton, Toolbar } from '@mui/material';
|
||||
import { AppBar as MuiAppBar, Box, IconButton, Stack, Toolbar } from '@mui/material';
|
||||
import { alpha } from '@mui/material/styles';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Logout, SettingsOutlined as SettingsIcon } from '@mui/icons-material';
|
||||
import { CONTENT_RAIL_MAX_WIDTH_WIDE } from '../layouts/contentRail';
|
||||
import { AppContext } from '../context/main';
|
||||
import { NetworkSelector } from './NetworkSelector';
|
||||
import { MultiAccounts } from './Accounts';
|
||||
@@ -11,24 +13,48 @@ export const AppBar = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<MuiAppBar position="sticky" sx={{ boxShadow: 'none', bgcolor: 'transparent', backgroundImage: 'none', mt: 3 }}>
|
||||
<Toolbar disableGutters>
|
||||
<Grid container justifyContent="space-between" alignItems="center" flexWrap="nowrap">
|
||||
<Grid item container alignItems="center" spacing={1}>
|
||||
<Grid item>
|
||||
<MuiAppBar
|
||||
position="sticky"
|
||||
sx={{
|
||||
boxShadow: 'none',
|
||||
bgcolor: 'transparent',
|
||||
backgroundImage: 'none',
|
||||
pt: { xs: 2, md: 3 },
|
||||
}}
|
||||
>
|
||||
<Toolbar
|
||||
disableGutters
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
minHeight: { xs: 48, sm: 56 },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
maxWidth: CONTENT_RAIL_MAX_WIDTH_WIDE,
|
||||
mx: 'auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
borderBottom: (theme) => `1px solid ${alpha(theme.palette.divider, 0.55)}`,
|
||||
pb: 1.25,
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={1.5} flexWrap="wrap" sx={{ py: 0.5, rowGap: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<MultiAccounts />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<NetworkSelector />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item container justifyContent="flex-end" md={12} lg={5} spacing={2}>
|
||||
<Grid item>
|
||||
</Box>
|
||||
<Stack direction="row" spacing={0.5} alignItems="center">
|
||||
<IconButton size="small" onClick={() => navigate('/settings')} sx={{ color: 'text.primary' }}>
|
||||
<SettingsIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={async () => {
|
||||
@@ -39,9 +65,9 @@ export const AppBar = () => {
|
||||
>
|
||||
<Logout fontSize="small" />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</MuiAppBar>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import { Box, CircularProgress, Fade, Stack, Typography } from '@mui/material';
|
||||
import { alpha, useTheme } from '@mui/material/styles';
|
||||
import { NymWordmark } from '@nymproject/react/logo/NymWordmark';
|
||||
|
||||
export type AppSessionLoadingOverlayProps = {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* In-session full-viewport overlay (blur + card) for account switch and sign-out.
|
||||
* Keeps the app theme unlike {@link LoadingPage} which uses the auth splash look.
|
||||
*/
|
||||
export const AppSessionLoadingOverlay = ({ title, subtitle }: AppSessionLoadingOverlayProps) => {
|
||||
const theme = useTheme();
|
||||
const fill = theme.palette.mode === 'dark' ? '#FFFFFF' : theme.palette.text.primary;
|
||||
|
||||
return (
|
||||
<Fade in timeout={220}>
|
||||
<Box
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-busy="true"
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 2000,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
px: 2,
|
||||
py: 4,
|
||||
bgcolor: (t) => alpha(t.palette.background.default, t.palette.mode === 'dark' ? 0.78 : 0.86),
|
||||
backdropFilter: 'blur(14px)',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
maxWidth: 400,
|
||||
borderRadius: 3,
|
||||
border: (t) => `1px solid ${t.palette.divider}`,
|
||||
bgcolor: (t) => (t.palette.mode === 'dark' ? alpha(t.palette.background.paper, 0.94) : 'background.paper'),
|
||||
boxShadow: (t) => t.palette.nym.nymWallet.shadows.strong,
|
||||
px: { xs: 3, sm: 4 },
|
||||
py: { xs: 3.5, sm: 4 },
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2.5} alignItems="center" textAlign="center">
|
||||
<NymWordmark width={64} fill={fill} />
|
||||
<Stack spacing={0.75} alignItems="center">
|
||||
<Typography variant="h6" component="p" sx={{ fontWeight: 700, lineHeight: 1.25 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
{subtitle ? (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.45, maxWidth: 320 }}>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Stack>
|
||||
<CircularProgress size={40} thickness={4} color="primary" aria-label="Loading" />
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
</Fade>
|
||||
);
|
||||
};
|
||||
@@ -11,40 +11,66 @@ export const Bond = ({
|
||||
|
||||
disabled: boolean;
|
||||
}) => (
|
||||
<NymCard title="Bonding" borderless>
|
||||
<NymCard hideHeader borderless dataTestid="bond-run-node">
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'space-between',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
gap: 3,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2">
|
||||
Bond a nym node. Learn how to set up and run a Nym node{' '}
|
||||
<Link href="https://nym.com/docs/operators/nodes/nym-node" target="_blank">
|
||||
here
|
||||
</Link>
|
||||
</Typography>
|
||||
<Box
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Typography
|
||||
variant="overline"
|
||||
sx={{
|
||||
display: 'block',
|
||||
color: 'text.secondary',
|
||||
letterSpacing: '0.12em',
|
||||
fontWeight: 600,
|
||||
mb: 0.75,
|
||||
}}
|
||||
>
|
||||
Operator
|
||||
</Typography>
|
||||
<Typography component="h2" variant="h5" sx={{ fontWeight: 700, lineHeight: 1.25 }}>
|
||||
Run a node
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'space-between',
|
||||
gap: 2,
|
||||
color: 'text.secondary',
|
||||
maxWidth: 640,
|
||||
mx: 'auto',
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size="large"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="button"
|
||||
disableElevation
|
||||
onClick={onBond}
|
||||
disabled={disabled}
|
||||
>
|
||||
Bond
|
||||
</Button>
|
||||
</Box>
|
||||
Bonding locks NYM as pledge so you can run a nym node on the network. You will need enough liquid NYM to cover
|
||||
the minimum pledge and fees. Read the{' '}
|
||||
<Link href="https://nym.com/docs/operators/nodes/nym-node" target="_blank">
|
||||
node setup and bonding guide
|
||||
</Link>{' '}
|
||||
before you continue.
|
||||
</Typography>
|
||||
<Button
|
||||
size="large"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="button"
|
||||
disableElevation
|
||||
onClick={onBond}
|
||||
disabled={disabled}
|
||||
sx={{
|
||||
alignSelf: 'stretch',
|
||||
maxWidth: 360,
|
||||
width: '100%',
|
||||
mx: 'auto',
|
||||
}}
|
||||
>
|
||||
Bond
|
||||
</Button>
|
||||
</Box>
|
||||
</NymCard>
|
||||
);
|
||||
|
||||
@@ -95,6 +95,8 @@ export const NodeSettings = ({
|
||||
return (
|
||||
<SimpleModal
|
||||
open
|
||||
dense
|
||||
accent="primary"
|
||||
hideCloseIcon
|
||||
sx={{ p: 0 }}
|
||||
header={
|
||||
@@ -129,7 +131,11 @@ export const NodeSettings = ({
|
||||
<FormHelperText>Your new profit margin will be applied in the next interval</FormHelperText>
|
||||
</Box>
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<ModalListItem label="Est. fee for this operation will be caculated in the next page" value="" />
|
||||
<ModalListItem
|
||||
label="Next step"
|
||||
value="Fee for this operation is calculated on the next page."
|
||||
layout="stack"
|
||||
/>
|
||||
</Box>
|
||||
<Button variant="contained" fullWidth size="large" onClick={handleValidate} disabled={error}>
|
||||
Next
|
||||
|
||||
@@ -51,6 +51,8 @@ export const UnbondModal = ({ node, onConfirm, onClose, onError }: Props) => {
|
||||
return (
|
||||
<SimpleModal
|
||||
open
|
||||
dense
|
||||
accent="primary"
|
||||
header="Unbond"
|
||||
subHeader="Unbond and remove your node from the mixnet"
|
||||
okLabel="Unbond"
|
||||
|
||||
@@ -104,8 +104,13 @@ export const UpdateBondAmountModal = ({
|
||||
onPrev={resetFeeState}
|
||||
onConfirm={handleConfirm}
|
||||
>
|
||||
<ModalListItem label="New bond details" value={newBondToDisplay()} divider />
|
||||
<ModalListItem label="Change bond details" value={`${currentBond.amount} ${currentBond.denom}`} divider />
|
||||
<ModalListItem label="New bond details" value={newBondToDisplay()} divider layout="stack" />
|
||||
<ModalListItem
|
||||
label="Previous bond"
|
||||
value={`${currentBond.amount} ${currentBond.denom}`}
|
||||
divider
|
||||
layout="stack"
|
||||
/>
|
||||
{userBalance.balance?.amount.amount && fee?.amount?.amount && (
|
||||
<Box sx={{ my: 2 }}>
|
||||
<BalanceWarning fee={fee?.amount?.amount} tx={newBond?.amount} />
|
||||
@@ -117,6 +122,8 @@ export const UpdateBondAmountModal = ({
|
||||
return (
|
||||
<SimpleModal
|
||||
open
|
||||
dense
|
||||
accent="primary"
|
||||
header="Change bond amount"
|
||||
subHeader="Add or reduce amount of tokens on your node"
|
||||
okLabel="Next"
|
||||
@@ -156,7 +163,12 @@ export const UpdateBondAmountModal = ({
|
||||
) : (
|
||||
<ModalListItem label="Node saturation" value={`${stakeSaturation}%`} divider />
|
||||
)}
|
||||
<ModalListItem label="Est. fee for this operation will be calculated in the next page" value="" divider />
|
||||
<ModalListItem
|
||||
label="Next step"
|
||||
value="Fee for this operation is calculated on the next page."
|
||||
divider
|
||||
layout="stack"
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</SimpleModal>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Tutorial } from './Tutorial';
|
||||
|
||||
export default {
|
||||
title: 'Buy/Tutorial',
|
||||
component: Tutorial,
|
||||
};
|
||||
|
||||
export const TutorialPage = () => <Tutorial />;
|
||||
@@ -1,5 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography, Grid, Link, Card, CardContent, Stack } from '@mui/material';
|
||||
import { Box, Typography, Grid, Card, CardContent, Stack, Button } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { safeOpenUrl } from 'src/utils/safeOpenUrl';
|
||||
import BitfinexIcon from 'src/svg-icons/bitfinex.svg';
|
||||
import KrakenIcon from 'src/svg-icons/kraken.svg';
|
||||
import BybitIcon from 'src/svg-icons/bybit.svg';
|
||||
@@ -12,11 +15,13 @@ const ExchangeCard = ({
|
||||
tokenType,
|
||||
url,
|
||||
IconComponent,
|
||||
onOpenExchange,
|
||||
}: {
|
||||
name: string;
|
||||
tokenType: string;
|
||||
url: string;
|
||||
IconComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
|
||||
onOpenExchange: (name: string, url: string) => void;
|
||||
}) => (
|
||||
<Card
|
||||
variant="outlined"
|
||||
@@ -51,21 +56,26 @@ const ExchangeCard = ({
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
{tokenType}
|
||||
</Typography>
|
||||
<Link
|
||||
href={url}
|
||||
target="_blank"
|
||||
variant="body2"
|
||||
<Button
|
||||
variant="text"
|
||||
data-testid="link-get-nym"
|
||||
onClick={() => onOpenExchange(name, url)}
|
||||
sx={{
|
||||
alignSelf: 'flex-start',
|
||||
p: 0,
|
||||
minWidth: 0,
|
||||
textTransform: 'none',
|
||||
textDecoration: 'underline',
|
||||
fontWeight: 500,
|
||||
fontSize: '0.875rem',
|
||||
'&:hover': {
|
||||
textDecoration: 'none',
|
||||
background: 'transparent',
|
||||
},
|
||||
}}
|
||||
>
|
||||
GET NYM
|
||||
</Link>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
@@ -73,6 +83,22 @@ const ExchangeCard = ({
|
||||
);
|
||||
|
||||
export const Tutorial = () => {
|
||||
const theme = useTheme();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const openExchange = async (name: string, url: string) => {
|
||||
try {
|
||||
enqueueSnackbar(`Opening ${name} in your default browser - always verify the URL in the address bar.`, {
|
||||
variant: 'info',
|
||||
});
|
||||
await safeOpenUrl(url);
|
||||
} catch (e) {
|
||||
enqueueSnackbar('Could not open the link. Copy the URL from the exchange website instead.', {
|
||||
variant: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const exchanges = [
|
||||
{
|
||||
name: 'Bitfinex',
|
||||
@@ -107,7 +133,15 @@ export const Tutorial = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<NymCard borderless title="Where you can get NYM tokens" sx={{ mt: 4 }}>
|
||||
<NymCard
|
||||
borderless
|
||||
title="Where you can get NYM tokens"
|
||||
sx={{
|
||||
backgroundColor: 'background.paper',
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
boxShadow: theme.palette.nym.nymWallet.shadows.light,
|
||||
}}
|
||||
>
|
||||
<Typography mb={3} fontSize={14} sx={{ color: 'text.secondary' }}>
|
||||
You can get NYM tokens from these exchanges
|
||||
</Typography>
|
||||
@@ -115,7 +149,7 @@ export const Tutorial = () => {
|
||||
<Grid container spacing={3}>
|
||||
{exchanges.map((exchange) => (
|
||||
<Grid item xs={12} md={6} lg={4} key={exchange.name}>
|
||||
<ExchangeCard {...exchange} />
|
||||
<ExchangeCard {...exchange} onOpenExchange={openExchange} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
@@ -71,6 +71,7 @@ export const ClipboardActions = ({
|
||||
if (!fieldRef) return;
|
||||
|
||||
const keydownHandler = async (e: KeyboardEvent) => {
|
||||
if (e.defaultPrevented) return;
|
||||
// Only handle if the associated field is focused
|
||||
const { activeElement } = document;
|
||||
if (fieldRef.current && activeElement === fieldRef.current) {
|
||||
|
||||
@@ -6,6 +6,11 @@ import { UseFormRegister, UseFormSetValue, FieldValues, Path, FieldErrors } from
|
||||
import { writeText, readText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
import { PasteFromClipboard } from './ClipboardActions';
|
||||
|
||||
/**
|
||||
* Keyboard clipboard helpers for fields that opt in via this module.
|
||||
* App-wide Tauri paste/copy for normal inputs lives in `useTauriTextEditingClipboard` (see
|
||||
* `data-nym-paste-replace`, `data-nym-currency-field`, `data-nym-auth-paste-field` exclusions).
|
||||
*/
|
||||
export const useCopyAllSupport = (
|
||||
inputRef: React.MutableRefObject<HTMLInputElement | HTMLTextAreaElement | null>,
|
||||
onPasteValue?: (value: string) => void,
|
||||
@@ -15,6 +20,7 @@ export const useCopyAllSupport = (
|
||||
|
||||
const handleKeyDown = async (e: Event) => {
|
||||
const keyEvent = e as KeyboardEvent;
|
||||
if (keyEvent.defaultPrevented) return;
|
||||
|
||||
if (document.activeElement !== inputRef.current) return;
|
||||
|
||||
@@ -71,46 +77,47 @@ export const useCopyAllSupport = (
|
||||
}, [inputRef.current, onPasteValue]);
|
||||
};
|
||||
|
||||
export const TextFieldWithPaste = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
TextFieldProps & {
|
||||
onPasteValue?: (value: string) => void;
|
||||
}
|
||||
>(({ onPasteValue, ...props }, ref) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
export type TextFieldWithPasteProps = TextFieldProps & {
|
||||
// eslint-disable-next-line react/require-default-props -- optional on `forwardRef` + intersection props; see `onPasteValue = undefined` in render
|
||||
onPasteValue?: (value: string) => void;
|
||||
};
|
||||
|
||||
useCopyAllSupport(inputRef, onPasteValue);
|
||||
export const TextFieldWithPaste = React.forwardRef<HTMLDivElement, TextFieldWithPasteProps>(
|
||||
({ onPasteValue = undefined, inputProps: userInputProps, InputProps: userInputPropsMui, ...props }, ref) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handlePaste = (pastedText: string) => {
|
||||
onPasteValue?.(pastedText);
|
||||
useCopyAllSupport(inputRef, onPasteValue);
|
||||
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
};
|
||||
const handlePaste = (pastedText: string) => {
|
||||
onPasteValue?.(pastedText);
|
||||
|
||||
return (
|
||||
<TextField
|
||||
{...props}
|
||||
ref={ref}
|
||||
inputRef={inputRef}
|
||||
InputProps={{
|
||||
...props.InputProps,
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const fieldProps = {
|
||||
...props,
|
||||
ref,
|
||||
inputRef,
|
||||
inputProps: {
|
||||
...userInputProps,
|
||||
'data-nym-paste-replace': 'true',
|
||||
},
|
||||
InputProps: {
|
||||
...userInputPropsMui,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{onPasteValue && <PasteFromClipboard onPaste={handlePaste} fieldRef={inputRef} />}
|
||||
{props.InputProps?.endAdornment}
|
||||
{userInputPropsMui?.endAdornment}
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// Add defaultProps to fix the "require-default-props" warning
|
||||
TextFieldWithPaste.defaultProps = {
|
||||
onPasteValue: undefined,
|
||||
};
|
||||
return <TextField {...(fieldProps as TextFieldProps)} />;
|
||||
},
|
||||
);
|
||||
|
||||
export const CurrencyFormFieldWithPaste = ({
|
||||
label,
|
||||
@@ -167,6 +174,7 @@ export const CurrencyFormFieldWithPaste = ({
|
||||
|
||||
const handleKeyDown = async (e: Event) => {
|
||||
const keyEvent = e as KeyboardEvent;
|
||||
if (keyEvent.defaultPrevented) return;
|
||||
if (document.activeElement !== inputRef.current) return;
|
||||
|
||||
// Handle Cmd+A (Select All)
|
||||
@@ -236,7 +244,7 @@ export const CurrencyFormFieldWithPaste = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box position="relative" width="100%" ref={fieldRef}>
|
||||
<Box position="relative" width="100%" ref={fieldRef} data-nym-currency-field>
|
||||
<CurrencyFormField
|
||||
label={label}
|
||||
fullWidth={fullWidth}
|
||||
@@ -274,6 +282,8 @@ export const HookFormTextFieldWithPaste = <TFieldValues extends FieldValues>({
|
||||
register,
|
||||
setValue,
|
||||
errors,
|
||||
inputProps: userInputProps,
|
||||
InputProps: userInputPropsMui,
|
||||
...props
|
||||
}: {
|
||||
name: Path<TFieldValues>;
|
||||
@@ -295,25 +305,29 @@ export const HookFormTextFieldWithPaste = <TFieldValues extends FieldValues>({
|
||||
// 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}
|
||||
/>
|
||||
);
|
||||
const fieldProps = {
|
||||
...register(name),
|
||||
...props,
|
||||
label,
|
||||
error: Boolean(errors[name]),
|
||||
helperText: errors[name]?.message?.toString(),
|
||||
inputRef,
|
||||
inputProps: {
|
||||
...userInputProps,
|
||||
'data-nym-paste-replace': 'true',
|
||||
},
|
||||
InputProps: {
|
||||
...userInputPropsMui,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<PasteFromClipboard onPaste={handlePaste} fieldRef={inputRef} />
|
||||
{userInputPropsMui?.endAdornment}
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
return <TextField {...(fieldProps as TextFieldProps)} />;
|
||||
};
|
||||
|
||||
export const HookFormCurrencyFieldWithPaste = <TFieldValues extends FieldValues>({
|
||||
@@ -361,6 +375,7 @@ export const HookFormCurrencyFieldWithPaste = <TFieldValues extends FieldValues>
|
||||
|
||||
const handleKeyDown = async (e: Event) => {
|
||||
const keyEvent = e as KeyboardEvent;
|
||||
if (keyEvent.defaultPrevented) return;
|
||||
if (document.activeElement !== inputRef.current) return;
|
||||
|
||||
// Handle Cmd+A (Select All)
|
||||
@@ -435,7 +450,7 @@ export const HookFormCurrencyFieldWithPaste = <TFieldValues extends FieldValues>
|
||||
};
|
||||
|
||||
return (
|
||||
<Box position="relative" width="100%" ref={fieldRef}>
|
||||
<Box position="relative" width="100%" ref={fieldRef} data-nym-currency-field>
|
||||
<CurrencyFormField
|
||||
label={label}
|
||||
fullWidth
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { ConfirmTx } from './ConfirmTX';
|
||||
import { ModalListItem } from './Modals/ModalListItem';
|
||||
|
||||
export default {
|
||||
title: 'Wallet / Confirm Transaction',
|
||||
component: ConfirmTx,
|
||||
} as ComponentMeta<typeof ConfirmTx>;
|
||||
|
||||
const Template: ComponentStory<typeof ConfirmTx> = (args) => (
|
||||
<ConfirmTx {...args}>
|
||||
<ModalListItem label="Transaction type" value="Bond" divider />
|
||||
<ModalListItem label="Current bond" value="100 NYM" divider />
|
||||
<ModalListItem label="Additional bond" value="50 NYM" divider />
|
||||
</ConfirmTx>
|
||||
);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
open: true,
|
||||
header: 'Confirm transaction',
|
||||
subheader: 'Confirm and proceed or cancel transaction',
|
||||
fee: { amount: { amount: '0.001', denom: 'nym' }, fee: { Auto: null } },
|
||||
onClose: () => {},
|
||||
onConfirm: async () => {},
|
||||
onPrev: () => {},
|
||||
isStorybook: true,
|
||||
};
|
||||
@@ -1,19 +1,9 @@
|
||||
import React from 'react';
|
||||
import { FeeDetails } from '@nymproject/types';
|
||||
import { Box } from '@mui/material';
|
||||
import { useTheme, Theme } from '@mui/material/styles';
|
||||
import { SimpleModal } from './Modals/SimpleModal';
|
||||
import { ModalFee } from './Modals/ModalFee';
|
||||
import { ModalDivider } from './Modals/ModalDivider';
|
||||
import { backDropStyles, modalStyles } from '../../.storybook/storiesStyles';
|
||||
|
||||
const storybookStyles = (theme: Theme, isStorybook?: boolean, backdropProps?: object) =>
|
||||
isStorybook
|
||||
? {
|
||||
backdropProps: { ...backDropStyles(theme), ...backdropProps },
|
||||
sx: modalStyles(theme),
|
||||
}
|
||||
: {};
|
||||
|
||||
export const ConfirmTx: FCWithChildren<{
|
||||
open: boolean;
|
||||
@@ -23,26 +13,21 @@ export const ConfirmTx: FCWithChildren<{
|
||||
onConfirm: () => Promise<void>;
|
||||
onClose?: () => void;
|
||||
onPrev: () => void;
|
||||
isStorybook?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}> = ({ open, fee, onConfirm, onClose, header, subheader, onPrev, children, isStorybook }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<SimpleModal
|
||||
open={open}
|
||||
header={header}
|
||||
subHeader={subheader}
|
||||
okLabel="Confirm"
|
||||
onOk={onConfirm}
|
||||
onClose={onClose}
|
||||
onBack={onPrev}
|
||||
{...storybookStyles(theme, isStorybook)}
|
||||
>
|
||||
<Box sx={{ mt: 3 }}>
|
||||
{children}
|
||||
<ModalFee fee={fee} isLoading={false} />
|
||||
<ModalDivider />
|
||||
</Box>
|
||||
</SimpleModal>
|
||||
);
|
||||
};
|
||||
}> = ({ open, fee, onConfirm, onClose, header, subheader, onPrev, children }) => (
|
||||
<SimpleModal
|
||||
open={open}
|
||||
header={header}
|
||||
subHeader={subheader}
|
||||
okLabel="Confirm"
|
||||
onOk={onConfirm}
|
||||
onClose={onClose}
|
||||
onBack={onPrev}
|
||||
>
|
||||
<Box sx={{ mt: 3 }}>
|
||||
{children}
|
||||
<ModalFee fee={fee} isLoading={false} />
|
||||
<ModalDivider />
|
||||
</Box>
|
||||
</SimpleModal>
|
||||
);
|
||||
|
||||
@@ -95,6 +95,7 @@ export const CurrencyFormFieldWithPaste = ({
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = async (e: KeyboardEvent) => {
|
||||
if (e.defaultPrevented) return;
|
||||
if (inputRef.current && document.activeElement === inputRef.current) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'v') {
|
||||
e.preventDefault();
|
||||
@@ -120,7 +121,7 @@ export const CurrencyFormFieldWithPaste = ({
|
||||
}, [denom, onChanged]);
|
||||
|
||||
return (
|
||||
<Box position="relative" width="100%" ref={fieldRef}>
|
||||
<Box position="relative" width="100%" ref={fieldRef} data-nym-currency-field>
|
||||
<CurrencyFormField
|
||||
label={label}
|
||||
fullWidth={fullWidth}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import React from 'react';
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Button, Paper, Typography } from '@mui/material';
|
||||
import { backDropStyles, modalStyles } from '../../../.storybook/storiesStyles';
|
||||
|
||||
import { OverSaturatedBlockerModal } from './DelegateBlocker';
|
||||
|
||||
export default {
|
||||
title: 'Delegation/Components/Delegation Over Saturated Warning Modal',
|
||||
component: OverSaturatedBlockerModal,
|
||||
} as ComponentMeta<typeof OverSaturatedBlockerModal>;
|
||||
|
||||
export const Default = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const handleClick = () => setOpen(true);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<>
|
||||
<Paper elevation={0} sx={{ px: 4, pt: 2, pb: 4 }}>
|
||||
<h2>Lorem ipsum</h2>
|
||||
<Button variant="contained" onClick={handleClick} sx={{ mb: 3 }}>
|
||||
Show modal
|
||||
</Button>
|
||||
<Typography>
|
||||
Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis
|
||||
sunt velit elit do minim mollit non duis reprehenderit. Eiusmod dolore adipisicing ex nostrud consectetur
|
||||
culpa exercitation do. Ad elit esse ipsum aliqua labore irure laborum qui culpa.
|
||||
</Typography>
|
||||
</Paper>
|
||||
<OverSaturatedBlockerModal
|
||||
open={open}
|
||||
header="Node saturation: 114%"
|
||||
onClose={() => setOpen(false)}
|
||||
backdropProps={backDropStyles(theme)}
|
||||
sx={modalStyles(theme)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -219,7 +219,7 @@ export const DelegateModal: FCWithChildren<{
|
||||
<BalanceWarning fee={fee?.amount?.amount} tx={amount} />
|
||||
</Box>
|
||||
)}
|
||||
<ModalListItem label="Node identity key" value={identityKey} divider />
|
||||
<ModalListItem label="Node identity key" value={identityKey} divider layout="stack" />
|
||||
<ModalListItem label="Amount" value={`${amount} ${denom.toUpperCase()}`} divider />
|
||||
</ConfirmTx>
|
||||
);
|
||||
@@ -249,10 +249,12 @@ export const DelegateModal: FCWithChildren<{
|
||||
header={header || 'Delegate'}
|
||||
okLabel={buttonText || 'Delegate stake'}
|
||||
okDisabled={!isValidated}
|
||||
dense
|
||||
accent="primary"
|
||||
sx={sx}
|
||||
backdropProps={backdropProps}
|
||||
>
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Box sx={{ mt: 2.5 }}>
|
||||
<TextFieldWithPaste
|
||||
label="Node identity key"
|
||||
fullWidth
|
||||
@@ -264,7 +266,7 @@ export const DelegateModal: FCWithChildren<{
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</Box>
|
||||
<Box display="flex" gap={2} alignItems="center" sx={{ mt: 3 }}>
|
||||
<Box display="flex" gap={2} alignItems="center" sx={{ mt: 2.5 }}>
|
||||
{hasVestingContract && <TokenPoolSelector disabled={false} onSelect={(pool) => setTokenPool(pool)} />}
|
||||
<CurrencyFormFieldWithPaste
|
||||
label="Amount"
|
||||
@@ -275,7 +277,7 @@ export const DelegateModal: FCWithChildren<{
|
||||
validationError={errorAmount}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Box sx={{ mt: 2.5 }}>
|
||||
<ModalListItem label="Account balance" value={accountBalance?.toUpperCase()} divider fontWeight={600} />
|
||||
</Box>
|
||||
|
||||
@@ -299,7 +301,11 @@ export const DelegateModal: FCWithChildren<{
|
||||
hidden
|
||||
divider
|
||||
/>
|
||||
<ModalListItem label="Est. fee for this transaction will be calculated in the next page" />
|
||||
<ModalListItem
|
||||
label="Next step"
|
||||
value="Fee for this transaction is calculated on the next page."
|
||||
layout="stack"
|
||||
/>
|
||||
</SimpleModal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from 'react';
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { DelegationActions } from './DelegationActions';
|
||||
|
||||
export default {
|
||||
title: 'Delegation/Components/Delegation List Item Actions',
|
||||
component: DelegationActions,
|
||||
} as ComponentMeta<typeof DelegationActions>;
|
||||
|
||||
export const Default = () => <DelegationActions />;
|
||||
|
||||
export const RedeemingDisabled = () => <DelegationActions disableRedeemingRewards />;
|
||||
|
||||
export const PendingDelegation = () => <DelegationActions isPending={{ actionType: 'delegate', blockHeight: 1000 }} />;
|
||||
|
||||
export const PendingUndelegation = () => (
|
||||
<DelegationActions isPending={{ actionType: 'undelegate', blockHeight: 1000 }} />
|
||||
);
|
||||
@@ -1,135 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, Chip, IconButton, TableCell, TableRow, Tooltip, Typography } from '@mui/material';
|
||||
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';
|
||||
import { toPercentIntegerString } from 'src/utils';
|
||||
import { format } from 'date-fns';
|
||||
import { Undelegate } from 'src/svg-icons';
|
||||
import { DelegationListItemActions, DelegationsActionsMenu } from './DelegationActions';
|
||||
|
||||
const getStakeSaturation = (item: DelegationWithEverything) =>
|
||||
!item.stake_saturation ? '-' : `${decimalToPercentage(item.stake_saturation)}%`;
|
||||
|
||||
const getRewardValue = (item: DelegationWithEverything) => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { unclaimed_rewards } = item;
|
||||
return !unclaimed_rewards ? '-' : `${unclaimed_rewards.amount} ${unclaimed_rewards.denom}`;
|
||||
};
|
||||
|
||||
export const DelegationItem = ({
|
||||
item,
|
||||
explorerUrl,
|
||||
nodeIsUnbonded,
|
||||
onItemActionClick,
|
||||
}: {
|
||||
item: DelegationWithEverything;
|
||||
explorerUrl: string;
|
||||
nodeIsUnbonded: boolean;
|
||||
onItemActionClick?: (item: DelegationWithEverything, action: DelegationListItemActions) => void;
|
||||
}) => {
|
||||
const { setDelegationItemErrors } = useDelegationContext();
|
||||
|
||||
const operatingCost = isDelegation(item) && item.cost_params?.interval_operating_cost;
|
||||
|
||||
const tooltipText = () => {
|
||||
if (nodeIsUnbonded) {
|
||||
return 'This node has unbonded and it does not exist anymore. You need to undelegate from it to get your stake and outstanding rewards (if any) back.';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip arrow title={tooltipText()}>
|
||||
<TableRow key={item.node_identity} sx={{ color: !item.node_identity ? 'error.main' : 'inherit' }}>
|
||||
<TableCell sx={{ color: 'inherit', pr: 1 }} padding="normal">
|
||||
{nodeIsUnbonded ? (
|
||||
'-'
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{item.errors && (
|
||||
<Tooltip title="Open to view a list of errors that occurred">
|
||||
<IconButton
|
||||
sx={{ mr: 1 }}
|
||||
size="small"
|
||||
onClick={() => setDelegationItemErrors({ nodeId: item.node_identity, errors: item.errors! })}
|
||||
>
|
||||
<WarningAmberOutlined color="warning" fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Link
|
||||
target="_blank"
|
||||
href={`${explorerUrl}/nodes/${item.mix_id}`}
|
||||
text={`${item.node_identity.slice(0, 6)}...${item.node_identity.slice(-6)}`}
|
||||
color="text.primary"
|
||||
noIcon
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell sx={{ color: 'inherit' }}>
|
||||
{isDelegation(item) && (!item.avg_uptime_percent ? '-' : `${item.avg_uptime_percent}%`)}
|
||||
</TableCell>
|
||||
<TableCell sx={{ color: 'inherit' }}>
|
||||
{isDelegation(item) &&
|
||||
(!item.cost_params?.profit_margin_percent
|
||||
? '-'
|
||||
: `${toPercentIntegerString(item.cost_params.profit_margin_percent)}%`)}
|
||||
</TableCell>
|
||||
<TableCell sx={{ color: 'inherit' }}>
|
||||
<Typography style={{ textTransform: 'uppercase', fontSize: 'inherit' }}>
|
||||
{operatingCost ? `${operatingCost.amount} ${operatingCost.denom}` : '-'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ color: 'inherit' }}>{getStakeSaturation(item)}</TableCell>
|
||||
<TableCell sx={{ color: 'inherit' }}>
|
||||
{item.delegated_on_iso_datetime && format(new Date(item.delegated_on_iso_datetime), 'dd/MM/yyyy')}
|
||||
</TableCell>
|
||||
<TableCell sx={{ color: 'inherit' }}>
|
||||
<Typography style={{ textTransform: 'uppercase', fontSize: 'inherit' }}>
|
||||
{isDelegation(item) ? `${item.amount.amount} ${item.amount.denom}` : '-'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ textTransform: 'uppercase', color: 'inherit' }}>{getRewardValue(item)}</TableCell>
|
||||
<TableCell>
|
||||
{item.uses_vesting_contract_tokens && (
|
||||
<Tooltip title="Delegation uses locked tokens">
|
||||
<LockOutlined sx={{ color: 'grey.800' }} fontSize="small" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center" sx={{ color: 'inherit' }}>
|
||||
{!item.pending_events.length && !nodeIsUnbonded && (
|
||||
<DelegationsActionsMenu
|
||||
onActionClick={(action) => (onItemActionClick ? onItemActionClick(item, action) : undefined)}
|
||||
disableRedeemingRewards={!item.unclaimed_rewards || item.unclaimed_rewards.amount === '0'}
|
||||
disableDelegateMore={item.mixnode_is_unbonding}
|
||||
/>
|
||||
)}
|
||||
{!item.pending_events.length && nodeIsUnbonded && (
|
||||
<IconButton sx={{ color: (t) => t.palette.nym.nymWallet.text.main }}>
|
||||
<Undelegate onClick={() => (onItemActionClick ? onItemActionClick(item, 'undelegate') : undefined)} />
|
||||
</IconButton>
|
||||
)}
|
||||
{item.pending_events.length > 0 && (
|
||||
<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" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -1,311 +0,0 @@
|
||||
import React from 'react';
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { DelegationWithEverything } from '@nymproject/types';
|
||||
import { DelegationList } from './DelegationList';
|
||||
|
||||
export default {
|
||||
title: 'Delegation/Components/Delegation List',
|
||||
component: DelegationList,
|
||||
} as ComponentMeta<typeof DelegationList>;
|
||||
|
||||
const explorerUrl = 'https://sandbox-explorer.nymtech.net/network-components/mixnodes';
|
||||
|
||||
export const items: DelegationWithEverything[] = [
|
||||
{
|
||||
mix_id: 1,
|
||||
node_identity: 'FiojKW7oY9WQmLCiYAsCA21tpowZHS6zcUoyYm319p6Z',
|
||||
delegated_on_iso_datetime: new Date(2021, 1, 1).toDateString(),
|
||||
unclaimed_rewards: { amount: '0.05', denom: 'nym' },
|
||||
amount: { amount: '10', denom: 'nym' },
|
||||
cost_params: {
|
||||
profit_margin_percent: '0.1122323949234',
|
||||
interval_operating_cost: {
|
||||
amount: '40',
|
||||
denom: 'nym',
|
||||
},
|
||||
},
|
||||
accumulated_by_delegates: { amount: '50', denom: 'nym' },
|
||||
accumulated_by_operator: { amount: '100', denom: 'nym' },
|
||||
owner: '',
|
||||
block_height: BigInt(100),
|
||||
stake_saturation: '0.25',
|
||||
avg_uptime_percent: 0.5,
|
||||
uses_vesting_contract_tokens: false,
|
||||
pending_events: [],
|
||||
mixnode_is_unbonding: true,
|
||||
errors: null,
|
||||
},
|
||||
{
|
||||
mix_id: 2,
|
||||
node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8',
|
||||
amount: { amount: '1010', denom: 'nym' },
|
||||
delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(),
|
||||
unclaimed_rewards: { amount: '0.05', denom: 'nym' },
|
||||
cost_params: {
|
||||
profit_margin_percent: '0.1122323949234',
|
||||
interval_operating_cost: {
|
||||
amount: '40',
|
||||
denom: 'nym',
|
||||
},
|
||||
},
|
||||
accumulated_by_delegates: { amount: '50', denom: 'nym' },
|
||||
accumulated_by_operator: { amount: '200', denom: 'nym' },
|
||||
owner: '',
|
||||
block_height: BigInt(4000),
|
||||
stake_saturation: '0.43',
|
||||
avg_uptime_percent: 0.22,
|
||||
uses_vesting_contract_tokens: true,
|
||||
pending_events: [],
|
||||
mixnode_is_unbonding: true,
|
||||
errors: null,
|
||||
},
|
||||
{
|
||||
mix_id: 3,
|
||||
node_identity: '',
|
||||
amount: { amount: '300', denom: 'nym' },
|
||||
delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(),
|
||||
unclaimed_rewards: { amount: '0.05', denom: 'nym' },
|
||||
cost_params: {
|
||||
profit_margin_percent: '0.1122323949234',
|
||||
interval_operating_cost: {
|
||||
amount: '50',
|
||||
denom: 'nym',
|
||||
},
|
||||
},
|
||||
accumulated_by_delegates: { amount: '50', denom: 'nym' },
|
||||
accumulated_by_operator: { amount: '300', denom: 'nym' },
|
||||
owner: '',
|
||||
block_height: BigInt(4000),
|
||||
stake_saturation: '0.5',
|
||||
avg_uptime_percent: 0.1,
|
||||
uses_vesting_contract_tokens: true,
|
||||
pending_events: [],
|
||||
mixnode_is_unbonding: true,
|
||||
errors: null,
|
||||
},
|
||||
{
|
||||
mix_id: 4,
|
||||
node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8',
|
||||
amount: { amount: '201', denom: 'nym' },
|
||||
delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(),
|
||||
unclaimed_rewards: { amount: '0.05', denom: 'nym' },
|
||||
cost_params: {
|
||||
profit_margin_percent: '0.1122323949234',
|
||||
interval_operating_cost: {
|
||||
amount: '60',
|
||||
denom: 'nym',
|
||||
},
|
||||
},
|
||||
accumulated_by_delegates: { amount: '50', denom: 'nym' },
|
||||
accumulated_by_operator: { amount: '202', denom: 'nym' },
|
||||
owner: '',
|
||||
block_height: BigInt(4000),
|
||||
stake_saturation: '0.5',
|
||||
avg_uptime_percent: 0.1,
|
||||
uses_vesting_contract_tokens: true,
|
||||
pending_events: [],
|
||||
mixnode_is_unbonding: true,
|
||||
errors: null,
|
||||
},
|
||||
{
|
||||
mix_id: 5,
|
||||
node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8',
|
||||
amount: { amount: '100', denom: 'nym' },
|
||||
delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(),
|
||||
unclaimed_rewards: { amount: '0.05', denom: 'nym' },
|
||||
cost_params: {
|
||||
profit_margin_percent: '0.1122323949234',
|
||||
interval_operating_cost: {
|
||||
amount: '80',
|
||||
denom: 'nym',
|
||||
},
|
||||
},
|
||||
accumulated_by_delegates: { amount: '50', denom: 'nym' },
|
||||
accumulated_by_operator: { amount: '100', denom: 'nym' },
|
||||
owner: '',
|
||||
block_height: BigInt(4000),
|
||||
stake_saturation: '0.5',
|
||||
avg_uptime_percent: 0.1,
|
||||
uses_vesting_contract_tokens: true,
|
||||
pending_events: [],
|
||||
mixnode_is_unbonding: true,
|
||||
errors: null,
|
||||
},
|
||||
{
|
||||
mix_id: 6,
|
||||
node_identity: '',
|
||||
amount: { amount: '202', denom: 'nym' },
|
||||
delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(),
|
||||
unclaimed_rewards: { amount: '0.05', denom: 'nym' },
|
||||
cost_params: {
|
||||
profit_margin_percent: '0.8',
|
||||
interval_operating_cost: {
|
||||
amount: '40',
|
||||
denom: 'nym',
|
||||
},
|
||||
},
|
||||
accumulated_by_delegates: { amount: '50', denom: 'nym' },
|
||||
accumulated_by_operator: { amount: '100', denom: 'nym' },
|
||||
owner: '',
|
||||
block_height: BigInt(4000),
|
||||
stake_saturation: '0.5',
|
||||
avg_uptime_percent: 0.1,
|
||||
uses_vesting_contract_tokens: true,
|
||||
pending_events: [],
|
||||
mixnode_is_unbonding: true,
|
||||
errors: null,
|
||||
},
|
||||
{
|
||||
mix_id: 7,
|
||||
node_identity: 'FiojKW7oY9WQmLCiYAsCA21tpowZHS6zcUoyYm319p6Z',
|
||||
delegated_on_iso_datetime: new Date(2021, 1, 1).toDateString(),
|
||||
unclaimed_rewards: { amount: '0.05', denom: 'nym' },
|
||||
amount: { amount: '202', denom: 'nym' },
|
||||
cost_params: {
|
||||
profit_margin_percent: '0.59',
|
||||
interval_operating_cost: {
|
||||
amount: '40',
|
||||
denom: 'nym',
|
||||
},
|
||||
},
|
||||
accumulated_by_delegates: { amount: '50', denom: 'nym' },
|
||||
accumulated_by_operator: { amount: '100', denom: 'nym' },
|
||||
owner: '',
|
||||
block_height: BigInt(100),
|
||||
stake_saturation: '0.5',
|
||||
avg_uptime_percent: 0.5,
|
||||
uses_vesting_contract_tokens: false,
|
||||
pending_events: [],
|
||||
mixnode_is_unbonding: true,
|
||||
errors: null,
|
||||
},
|
||||
{
|
||||
mix_id: 8,
|
||||
node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8',
|
||||
amount: { amount: '100', denom: 'nym' },
|
||||
delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(),
|
||||
unclaimed_rewards: { amount: '0.05', denom: 'nym' },
|
||||
cost_params: {
|
||||
profit_margin_percent: '0.1122323949234',
|
||||
interval_operating_cost: {
|
||||
amount: '40',
|
||||
denom: 'nym',
|
||||
},
|
||||
},
|
||||
accumulated_by_delegates: { amount: '50', denom: 'nym' },
|
||||
accumulated_by_operator: { amount: '100', denom: 'nym' },
|
||||
owner: '',
|
||||
block_height: BigInt(4000),
|
||||
stake_saturation: '0.9',
|
||||
avg_uptime_percent: 0.1,
|
||||
uses_vesting_contract_tokens: true,
|
||||
pending_events: [],
|
||||
mixnode_is_unbonding: true,
|
||||
errors: null,
|
||||
},
|
||||
{
|
||||
mix_id: 9,
|
||||
node_identity: '',
|
||||
amount: { amount: '1000', denom: 'nym' },
|
||||
delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(),
|
||||
unclaimed_rewards: { amount: '0.05', denom: 'nym' },
|
||||
cost_params: {
|
||||
profit_margin_percent: '0.4',
|
||||
interval_operating_cost: {
|
||||
amount: '40',
|
||||
denom: 'nym',
|
||||
},
|
||||
},
|
||||
accumulated_by_delegates: { amount: '50', denom: 'nym' },
|
||||
accumulated_by_operator: { amount: '100', denom: 'nym' },
|
||||
owner: '',
|
||||
block_height: BigInt(4000),
|
||||
stake_saturation: '0.9',
|
||||
avg_uptime_percent: 0.1,
|
||||
uses_vesting_contract_tokens: true,
|
||||
pending_events: [],
|
||||
mixnode_is_unbonding: true,
|
||||
errors: null,
|
||||
},
|
||||
{
|
||||
mix_id: 10,
|
||||
node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8',
|
||||
amount: { amount: '100', denom: 'nym' },
|
||||
delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(),
|
||||
unclaimed_rewards: { amount: '0.05', denom: 'nym' },
|
||||
cost_params: {
|
||||
profit_margin_percent: '0.1122323949234',
|
||||
interval_operating_cost: {
|
||||
amount: '40',
|
||||
denom: 'nym',
|
||||
},
|
||||
},
|
||||
accumulated_by_delegates: { amount: '50', denom: 'nym' },
|
||||
accumulated_by_operator: { amount: '100', denom: 'nym' },
|
||||
owner: '',
|
||||
block_height: BigInt(4000),
|
||||
stake_saturation: '0.5',
|
||||
avg_uptime_percent: 0.1,
|
||||
uses_vesting_contract_tokens: true,
|
||||
pending_events: [],
|
||||
mixnode_is_unbonding: true,
|
||||
errors: null,
|
||||
},
|
||||
{
|
||||
mix_id: 11,
|
||||
node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8',
|
||||
amount: { amount: '100', denom: 'nym' },
|
||||
delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(),
|
||||
unclaimed_rewards: { amount: '0.05', denom: 'nym' },
|
||||
cost_params: {
|
||||
profit_margin_percent: '0.1122323949234',
|
||||
interval_operating_cost: {
|
||||
amount: '40',
|
||||
denom: 'nym',
|
||||
},
|
||||
},
|
||||
accumulated_by_delegates: { amount: '50', denom: 'nym' },
|
||||
accumulated_by_operator: { amount: '100', denom: 'nym' },
|
||||
owner: '',
|
||||
block_height: BigInt(4000),
|
||||
stake_saturation: '0.56',
|
||||
avg_uptime_percent: 0.9,
|
||||
uses_vesting_contract_tokens: true,
|
||||
pending_events: [],
|
||||
mixnode_is_unbonding: true,
|
||||
errors: null,
|
||||
},
|
||||
{
|
||||
mix_id: 12,
|
||||
node_identity: '',
|
||||
amount: { amount: '100', denom: 'nym' },
|
||||
delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(),
|
||||
unclaimed_rewards: { amount: '0.05', denom: 'nym' },
|
||||
cost_params: {
|
||||
profit_margin_percent: '0.1122323949234',
|
||||
interval_operating_cost: {
|
||||
amount: '40',
|
||||
denom: 'nym',
|
||||
},
|
||||
},
|
||||
accumulated_by_delegates: { amount: '50', denom: 'nym' },
|
||||
accumulated_by_operator: { amount: '100', denom: 'nym' },
|
||||
owner: '',
|
||||
block_height: BigInt(4000),
|
||||
stake_saturation: '0.5',
|
||||
avg_uptime_percent: 0.1,
|
||||
uses_vesting_contract_tokens: true,
|
||||
pending_events: [],
|
||||
mixnode_is_unbonding: true,
|
||||
errors: null,
|
||||
},
|
||||
];
|
||||
|
||||
export const WithData = () => <DelegationList items={items} explorerUrl={explorerUrl} />;
|
||||
|
||||
export const Empty = () => <DelegationList items={[]} explorerUrl={explorerUrl} />;
|
||||
|
||||
export const OneItem = () => <DelegationList items={[items[0]]} explorerUrl={explorerUrl} />;
|
||||
|
||||
export const Loading = () => <DelegationList items={[]} isLoading explorerUrl={explorerUrl} />;
|
||||
@@ -4,24 +4,36 @@ import {
|
||||
AlertTitle,
|
||||
Box,
|
||||
Button,
|
||||
Collapse,
|
||||
FormControl,
|
||||
IconButton,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableSortLabel,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { visuallyHidden } from '@mui/utils';
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
import { DelegationWithEverything } from '@nymproject/types';
|
||||
import { alpha } from '@mui/material/styles';
|
||||
import { KeyboardArrowDown, KeyboardArrowUp, LockOutlined, WarningAmberOutlined } from '@mui/icons-material';
|
||||
import { decimalToFloatApproximation, decimalToPercentage, 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';
|
||||
import { LoadingModal } from '../Modals/LoadingModal';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
import { format } from 'date-fns';
|
||||
import { Undelegate } from 'src/svg-icons';
|
||||
import { toPercentIntegerString } from 'src/utils';
|
||||
import { InfoTooltip } from '../InfoToolTip';
|
||||
import { DelegationListItemActions, DelegationsActionsMenu } from './DelegationActions';
|
||||
import { PendingDelegationCard } from './PendingDelegationCard';
|
||||
import { isDelegation, isPendingDelegation, TDelegations, useDelegationContext } from '../../context/delegations';
|
||||
import { ErrorModal } from '../Modals/ErrorModal';
|
||||
|
||||
@@ -29,126 +41,37 @@ 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;
|
||||
orderBy: string;
|
||||
}
|
||||
|
||||
interface HeadCell {
|
||||
id: string;
|
||||
label: string | React.ReactNode;
|
||||
sortable: boolean;
|
||||
disablePadding?: boolean;
|
||||
align: 'left' | 'center' | 'right';
|
||||
width?: string;
|
||||
}
|
||||
|
||||
const headCells: HeadCell[] = [
|
||||
{ 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', 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 SORT_FIELD_OPTIONS: { id: SortingKeys; label: string }[] = [
|
||||
{ id: 'delegated_on_iso_datetime', label: 'Delegated on' },
|
||||
{ id: 'node_identity', label: 'Node ID' },
|
||||
{ id: 'avg_uptime_percent', label: 'Routing score' },
|
||||
{ id: 'profit_margin_percent', label: 'Profit margin' },
|
||||
{ id: 'operating_cost', label: 'Operating cost' },
|
||||
{ id: 'stake_saturation', label: 'Stake saturation' },
|
||||
{ id: 'amount', label: 'Delegation' },
|
||||
{ id: 'unclaimed_rewards', label: 'Reward' },
|
||||
];
|
||||
|
||||
const EnhancedTableHead: FCWithChildren<EnhancedTableProps> = ({ order, orderBy, onRequestSort }) => {
|
||||
const createSortHandler = (property: string) => (event: React.MouseEvent<unknown>) => {
|
||||
onRequestSort(event, property);
|
||||
};
|
||||
|
||||
return (
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{headCells.map((headCell) => (
|
||||
<TableCell
|
||||
key={headCell.id}
|
||||
align={headCell.align}
|
||||
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}
|
||||
direction={orderBy === headCell.id ? order : 'asc'}
|
||||
onClick={createSortHandler(headCell.id)}
|
||||
IconComponent={ArrowDropDownIcon}
|
||||
>
|
||||
{headCell.label}
|
||||
{orderBy === headCell.id ? (
|
||||
<Box component="span" sx={visuallyHidden}>
|
||||
{order === 'desc' ? 'sorted descending' : 'sorted ascending'}
|
||||
</Box>
|
||||
) : null}
|
||||
</TableSortLabel>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
const hasPruningError = (item: any): boolean => {
|
||||
if (!isDelegation(item) || !item.errors) return false;
|
||||
|
||||
@@ -158,43 +81,69 @@ const hasPruningError = (item: any): boolean => {
|
||||
);
|
||||
};
|
||||
|
||||
const getStakeSaturation = (item: DelegationWithEverything) =>
|
||||
!item.stake_saturation ? '-' : `${decimalToPercentage(item.stake_saturation)}%`;
|
||||
|
||||
const getRewardValue = (item: DelegationWithEverything) => {
|
||||
const { unclaimed_rewards } = item;
|
||||
return !unclaimed_rewards ? '-' : `${unclaimed_rewards.amount} ${unclaimed_rewards.denom}`;
|
||||
};
|
||||
|
||||
const saturationNumeric = (item: DelegationWithEverything): number | undefined => {
|
||||
if (!item.stake_saturation) return undefined;
|
||||
return decimalToFloatApproximation(item.stake_saturation);
|
||||
};
|
||||
|
||||
export const DelegationList: FCWithChildren<{
|
||||
isLoading?: boolean;
|
||||
items: TDelegations;
|
||||
onItemActionClick?: (item: DelegationWithEverything, action: DelegationListItemActions) => void;
|
||||
explorerUrl: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
}> = ({ isLoading, items, onItemActionClick, explorerUrl }) => {
|
||||
nextEpoch?: string | Error;
|
||||
}> = ({ items, onItemActionClick, explorerUrl, nextEpoch }) => {
|
||||
const [order, setOrder] = React.useState<Order>('asc');
|
||||
const [orderBy, setOrderBy] = React.useState<SortingKeys>('delegated_on_iso_datetime');
|
||||
const [identityFilter, setIdentityFilter] = React.useState('');
|
||||
const [expandedKey, setExpandedKey] = React.useState<string | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { delegationItemErrors, setDelegationItemErrors } = useDelegationContext();
|
||||
const {
|
||||
delegationItemErrors,
|
||||
setDelegationItemErrors,
|
||||
totalDelegations,
|
||||
totalRewards,
|
||||
totalDelegationsAndRewards,
|
||||
isLoading: delegationsSummaryLoading,
|
||||
} = useDelegationContext();
|
||||
|
||||
const handleRequestSort = (_: React.MouseEvent<unknown>, property: any) => {
|
||||
const isAsc = orderBy === property && order === 'asc';
|
||||
setOrder(isAsc ? 'desc' : 'asc');
|
||||
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 activeDelegations = React.useMemo(
|
||||
() => filteredItems.filter((item): item is DelegationWithEverything => isDelegation(item)),
|
||||
[filteredItems],
|
||||
);
|
||||
|
||||
const pendingItems = React.useMemo(() => filteredItems.filter((item) => isPendingDelegation(item)), [filteredItems]);
|
||||
|
||||
const searchNeedle = identityFilter.trim().toLowerCase();
|
||||
|
||||
const displayedDelegations = React.useMemo(() => {
|
||||
if (!searchNeedle) return activeDelegations;
|
||||
return activeDelegations.filter((d) => d.node_identity.toLowerCase().includes(searchNeedle));
|
||||
}, [activeDelegations, searchNeedle]);
|
||||
|
||||
const activeCount = activeDelegations.length;
|
||||
|
||||
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.';
|
||||
@@ -202,9 +151,22 @@ export const DelegationList: FCWithChildren<{
|
||||
return message;
|
||||
};
|
||||
|
||||
const pendingKey = (item: any, suffix: string) =>
|
||||
`pending-${item.event?.mix_id}-${item.event?.address ?? ''}-${item.event?.kind ?? ''}-${
|
||||
item.node_identity ?? ''
|
||||
}-${suffix}`;
|
||||
|
||||
const nextEpochLine =
|
||||
nextEpoch instanceof Error || !nextEpoch ? null : (
|
||||
<Typography fontSize={14} color="text.secondary" sx={{ lineHeight: 1.5 }}>
|
||||
Next epoch starts at <strong>{nextEpoch}</strong>
|
||||
</Typography>
|
||||
);
|
||||
|
||||
const emptyTableMessage = searchNeedle ? 'No delegations match your search.' : 'No delegations to show.';
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Display pruning error alert at the top if needed */}
|
||||
{hasPruningErrors && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
@@ -223,78 +185,412 @@ export const DelegationList: FCWithChildren<{
|
||||
</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()}`;
|
||||
<Stack spacing={2} sx={{ width: '100%' }}>
|
||||
<Stack spacing={2}>
|
||||
<Box sx={{ maxWidth: 800 }}>
|
||||
<Typography variant="h6" component="h2">
|
||||
Delegations
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
||||
{activeCount} active
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
<Stack spacing={1.25}>
|
||||
<Stack
|
||||
direction={{ xs: 'column', md: 'row' }}
|
||||
spacing={2}
|
||||
alignItems={{ xs: 'stretch', md: 'flex-end' }}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Search identity"
|
||||
value={identityFilter}
|
||||
onChange={(e) => setIdentityFilter(e.target.value)}
|
||||
sx={{
|
||||
minWidth: { xs: '100%', md: 220 },
|
||||
flex: { md: '1 1 200px' },
|
||||
'& .MuiOutlinedInput-root': { borderRadius: 2 },
|
||||
}}
|
||||
/>
|
||||
<FormControl size="small" sx={{ minWidth: 200, '& .MuiOutlinedInput-root': { borderRadius: 2 } }}>
|
||||
<InputLabel id="delegation-sort-field-label">Sort by</InputLabel>
|
||||
<Select
|
||||
labelId="delegation-sort-field-label"
|
||||
label="Sort by"
|
||||
value={orderBy}
|
||||
onChange={(e) => setOrderBy(e.target.value as SortingKeys)}
|
||||
>
|
||||
{SORT_FIELD_OPTIONS.map((opt) => (
|
||||
<MenuItem key={opt.id} value={opt.id}>
|
||||
{opt.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl size="small" sx={{ minWidth: 160, '& .MuiOutlinedInput-root': { borderRadius: 2 } }}>
|
||||
<InputLabel id="delegation-sort-order-label">Order</InputLabel>
|
||||
<Select
|
||||
labelId="delegation-sort-order-label"
|
||||
label="Order"
|
||||
value={order}
|
||||
onChange={(e) => setOrder(e.target.value as Order)}
|
||||
>
|
||||
<MenuItem value="asc">Ascending</MenuItem>
|
||||
<MenuItem value="desc">Descending</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
{nextEpochLine ? <Box sx={{ pt: 0.25 }}>{nextEpochLine}</Box> : null}
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
return <PendingDelegationItem key={pendingKey} item={item} explorerUrl={explorerUrl} />;
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} alignItems="stretch">
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
minHeight: 92,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 2,
|
||||
border: (t) => `1px solid ${t.palette.divider}`,
|
||||
bgcolor: (t) =>
|
||||
t.palette.mode === 'dark' ? 'nym.nymWallet.nav.background' : 'nym.nymWallet.background.subtle',
|
||||
p: 2.5,
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" gap={0.5}>
|
||||
<InfoTooltip title="The total amount you have delegated to node(s) in the network. The amount also includes the rewards you have accrued since last time you claimed your rewards" />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Total delegations
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Typography fontWeight={600} fontSize={16} sx={{ mt: 0.5, textTransform: 'uppercase' }}>
|
||||
{delegationsSummaryLoading ? <Skeleton width={140} height={22} /> : totalDelegationsAndRewards ?? '-'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
minHeight: 92,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 2,
|
||||
border: (t) => `1px solid ${t.palette.divider}`,
|
||||
bgcolor: (t) =>
|
||||
t.palette.mode === 'dark' ? 'nym.nymWallet.nav.background' : 'nym.nymWallet.background.subtle',
|
||||
p: 2.5,
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" gap={0.5}>
|
||||
<InfoTooltip title="The initial amount you delegated to the node(s)" />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Original delegations
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Typography fontWeight={600} fontSize={16} sx={{ mt: 0.5, textTransform: 'uppercase' }}>
|
||||
{delegationsSummaryLoading ? <Skeleton width={120} height={22} /> : totalDelegations ?? '-'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
minHeight: 92,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 2,
|
||||
border: (t) => `1px solid ${t.palette.divider}`,
|
||||
bgcolor: (t) =>
|
||||
t.palette.mode === 'dark' ? 'nym.nymWallet.nav.background' : 'nym.nymWallet.background.subtle',
|
||||
p: 2.5,
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" gap={0.5}>
|
||||
<InfoTooltip title="The rewards you have accrued since the last time you claimed your rewards. Rewards are automatically compounded. You can claim your rewards at any time." />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Total rewards
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Typography fontWeight={600} fontSize={16} sx={{ mt: 0.5, textTransform: 'uppercase' }}>
|
||||
{delegationsSummaryLoading ? <Skeleton width={120} height={22} /> : totalRewards ?? '-'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
{pendingItems.length > 0 && (
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="subtitle2" color="text.secondary">
|
||||
Pending
|
||||
</Typography>
|
||||
<Stack spacing={2}>
|
||||
{pendingItems.map((item: any, index: number) => {
|
||||
if (
|
||||
item.event &&
|
||||
item.event.kind === 'Delegate' &&
|
||||
(!item.node_identity || item.node_identity === '')
|
||||
) {
|
||||
return (
|
||||
<PendingDelegationCard
|
||||
key={pendingKey(item, `d-${index}`)}
|
||||
item={{
|
||||
...item,
|
||||
node_identity: `Mix Identity Key ${item.event.mix_id}`,
|
||||
}}
|
||||
explorerUrl={explorerUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PendingDelegationCard key={pendingKey(item, `p-${index}`)} item={item} explorerUrl={explorerUrl} />
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<TableContainer
|
||||
sx={{
|
||||
width: '100%',
|
||||
overflowX: 'auto',
|
||||
borderRadius: 3,
|
||||
border: (t) => `1px solid ${t.palette.divider}`,
|
||||
bgcolor: (t) =>
|
||||
t.palette.mode === 'dark' ? 'nym.nymWallet.nav.background' : 'nym.nymWallet.background.subtle',
|
||||
}}
|
||||
>
|
||||
<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 stickyHeader size="small" sx={{ tableLayout: 'fixed' }}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ fontWeight: 600, py: 1.25, width: '40%' }}>Node</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600, py: 1.25, width: '16%' }}>
|
||||
Amount
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600, py: 1.25, width: '14%' }}>
|
||||
Saturation
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600, py: 1.25, width: '18%' }}>
|
||||
Reward
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600, py: 1.25, width: 120, minWidth: 112 }}>
|
||||
Actions
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{displayedDelegations.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} sx={{ py: 1.25 }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ py: 1 }}>
|
||||
{emptyTableMessage}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
displayedDelegations.map((item) => {
|
||||
const rowKey = `${item.mix_id}-${item.node_identity}`;
|
||||
const isOpen = expandedKey === rowKey;
|
||||
const nodeIsUnbonded = Boolean(!item.node_identity);
|
||||
const satNum = saturationNumeric(item);
|
||||
let satColor: 'text.secondary' | 'error.main' | 'success.main' = 'text.secondary';
|
||||
if (satNum !== undefined) {
|
||||
satColor = satNum > 1 ? 'error.main' : 'success.main';
|
||||
}
|
||||
|
||||
if (isDelegation(item)) {
|
||||
if (!item.node_identity || item.node_identity === '-' || item.node_identity === '...') {
|
||||
return null;
|
||||
}
|
||||
const operatingCost = item.cost_params?.interval_operating_cost;
|
||||
const uptime = item.avg_uptime_percent;
|
||||
const routingDisplay = uptime != null && String(uptime) !== '-' ? `${uptime}%` : '-';
|
||||
const marginDisplay = item.cost_params?.profit_margin_percent
|
||||
? `${toPercentIntegerString(item.cost_params.profit_margin_percent)}%`
|
||||
: '-';
|
||||
const costDisplay = operatingCost ? `${operatingCost.amount} ${operatingCost.denom}` : '-';
|
||||
const delegatedDisplay = item.delegated_on_iso_datetime
|
||||
? format(new Date(item.delegated_on_iso_datetime), 'dd/MM/yyyy')
|
||||
: '-';
|
||||
|
||||
return (
|
||||
<DelegationItem
|
||||
key={`delegation-${item.mix_id}`}
|
||||
item={item}
|
||||
explorerUrl={explorerUrl}
|
||||
nodeIsUnbonded={Boolean(!item.node_identity)}
|
||||
onItemActionClick={onItemActionClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const unbondedTooltip =
|
||||
'This node has unbonded and it does not exist anymore. You need to undelegate from it to get your stake and outstanding rewards (if any) back.';
|
||||
|
||||
return null;
|
||||
return (
|
||||
<React.Fragment key={rowKey}>
|
||||
<TableRow hover sx={{ '& > *': { borderBottom: 'unset' } }}>
|
||||
<TableCell sx={{ py: 1.25, verticalAlign: 'middle' }}>
|
||||
<Stack direction="row" alignItems="center" spacing={0.5}>
|
||||
<IconButton
|
||||
aria-label="expand row"
|
||||
size="small"
|
||||
onClick={() => setExpandedKey(isOpen ? null : rowKey)}
|
||||
>
|
||||
{isOpen ? <KeyboardArrowUp /> : <KeyboardArrowDown />}
|
||||
</IconButton>
|
||||
<Stack direction="row" alignItems="center" gap={0.5} flexWrap="wrap" sx={{ minWidth: 0 }}>
|
||||
{item.errors && (
|
||||
<Tooltip title="Open to view a list of errors that occurred">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() =>
|
||||
setDelegationItemErrors({ nodeId: item.node_identity, errors: item.errors! })
|
||||
}
|
||||
>
|
||||
<WarningAmberOutlined color="warning" fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{item.uses_vesting_contract_tokens && (
|
||||
<Tooltip title="Delegation uses locked tokens">
|
||||
<LockOutlined sx={{ color: 'text.secondary', fontSize: 18 }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{nodeIsUnbonded ? (
|
||||
<Tooltip title={unbondedTooltip} arrow>
|
||||
<Typography color="text.secondary">-</Typography>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Link
|
||||
target="_blank"
|
||||
href={`${explorerUrl}/nodes/${item.mix_id}`}
|
||||
text={`${item.node_identity.slice(0, 6)}...${item.node_identity.slice(-6)}`}
|
||||
color="text.primary"
|
||||
noIcon
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ py: 1.25, whiteSpace: 'nowrap', verticalAlign: 'middle' }}>
|
||||
{item.amount.amount} {item.amount.denom}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{ py: 1.25, color: satColor, whiteSpace: 'nowrap', verticalAlign: 'middle' }}
|
||||
>
|
||||
{getStakeSaturation(item)}
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ py: 1.25, whiteSpace: 'nowrap', verticalAlign: 'middle' }}>
|
||||
{getRewardValue(item)}
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ py: 1.25, whiteSpace: 'nowrap', verticalAlign: 'middle' }}>
|
||||
{!item.pending_events.length && !nodeIsUnbonded && (
|
||||
<DelegationsActionsMenu
|
||||
onActionClick={(action) =>
|
||||
onItemActionClick ? onItemActionClick(item, action) : undefined
|
||||
}
|
||||
disableRedeemingRewards={!item.unclaimed_rewards || item.unclaimed_rewards.amount === '0'}
|
||||
disableDelegateMore={item.mixnode_is_unbonding}
|
||||
/>
|
||||
)}
|
||||
{!item.pending_events.length && nodeIsUnbonded && (
|
||||
<IconButton sx={{ color: (t) => t.palette.nym.nymWallet.text.main }} size="small">
|
||||
<Undelegate
|
||||
onClick={() => (onItemActionClick ? onItemActionClick(item, 'undelegate') : undefined)}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
{item.pending_events.length > 0 && (
|
||||
<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' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Pending events
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={5}>
|
||||
<Collapse in={isOpen} timeout="auto" unmountOnExit>
|
||||
<Box
|
||||
sx={{
|
||||
py: 2,
|
||||
px: 2,
|
||||
borderRadius: 3,
|
||||
overflow: 'hidden',
|
||||
border: (t) =>
|
||||
`1px solid ${alpha(t.palette.divider, t.palette.mode === 'dark' ? 0.35 : 0.5)}`,
|
||||
bgcolor: (t) =>
|
||||
t.palette.mode === 'dark'
|
||||
? alpha(t.palette.common.white, 0.04)
|
||||
: alpha(t.palette.common.black, 0.03),
|
||||
}}
|
||||
>
|
||||
<Table size="small" sx={{ tableLayout: 'fixed' }}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ fontWeight: 600, color: 'text.secondary', py: 1, width: '25%' }}>
|
||||
Routing
|
||||
</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, color: 'text.secondary', py: 1, width: '25%' }}>
|
||||
Margin
|
||||
</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, color: 'text.secondary', py: 1, width: '25%' }}>
|
||||
NYM cost
|
||||
</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, color: 'text.secondary', py: 1, width: '25%' }}>
|
||||
Delegated on
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell sx={{ py: 1.25, verticalAlign: 'top', borderBottom: 'none' }}>
|
||||
<Typography variant="body2" color="text.primary">
|
||||
{routingDisplay}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 1.25, verticalAlign: 'top', borderBottom: 'none' }}>
|
||||
<Typography variant="body2" color="text.primary">
|
||||
{marginDisplay}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 1.25, verticalAlign: 'top', borderBottom: 'none' }}>
|
||||
<Typography variant="body2" color="text.primary">
|
||||
{costDisplay}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 1.25, verticalAlign: 'top', borderBottom: 'none' }}>
|
||||
<Typography variant="body2" color="text.primary">
|
||||
{delegatedDisplay}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
import React from 'react';
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { Paper, Button } from '@mui/material';
|
||||
import { useTheme, Theme } from '@mui/material/styles';
|
||||
import { Delegations } from './Delegations';
|
||||
import { items } from './DelegationList.stories';
|
||||
import { DelegationModal } from './DelegationModal';
|
||||
import { backDropStyles, modalStyles } from '../../../.storybook/storiesStyles';
|
||||
|
||||
const explorerUrl = 'https://sandbox-explorer.nymtech.net';
|
||||
|
||||
const storybookStyles = (theme: Theme) => ({
|
||||
backdropProps: backDropStyles(theme),
|
||||
sx: modalStyles(theme),
|
||||
});
|
||||
|
||||
export default {
|
||||
title: 'Delegation/Components/Delegation Modals',
|
||||
component: Delegations,
|
||||
} as ComponentMeta<typeof Delegations>;
|
||||
|
||||
const transaction = {
|
||||
url: 'https://sandbox-blocks.nymtech.net/transactions/11ED7B9E21534A9421834F52FED5103DC6E982949C06335F5E12EFC71DAF0CFB',
|
||||
hash: '11ED7B9E21534A9421834F52FED5103DC6E982949C06335F5E12EFC71DAF0CFB',
|
||||
};
|
||||
// Another transaction for Dark Theme to avoid duplicate key errors in rendering
|
||||
const transactionForDarkTheme = {
|
||||
url: 'https://sandbox-blocks.nymtech.net/transactions/11ED7B9E21534A9421834F52FED5103DC6E982949C06335F5E12EFC71DAF0CFO',
|
||||
hash: '11ED7B9E21534A9421834F52FED5103DC6E982949C06335F5E12EFC71DAF0CF0',
|
||||
};
|
||||
|
||||
const Content: FCWithChildren<{ children: React.ReactElement<any, any>; handleClick: () => void }> = ({
|
||||
children,
|
||||
handleClick,
|
||||
}) => (
|
||||
<>
|
||||
<Paper elevation={0} sx={{ px: 4, pt: 2, pb: 4 }}>
|
||||
<h2>Your Delegations</h2>
|
||||
<Button variant="contained" onClick={handleClick} sx={{ mb: 3 }}>
|
||||
Show modal
|
||||
</Button>
|
||||
<Delegations items={items} explorerUrl={explorerUrl} />
|
||||
</Paper>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
||||
export const Loading = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const handleClick = () => setOpen(true);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Content handleClick={handleClick}>
|
||||
<DelegationModal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
status="loading"
|
||||
action="delegate"
|
||||
{...storybookStyles(theme)}
|
||||
/>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export const DelegateSuccess = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const handleClick = () => setOpen(true);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Content handleClick={handleClick}>
|
||||
<DelegationModal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
status="success"
|
||||
action="delegate"
|
||||
message="You delegated 5 NYM"
|
||||
transactions={theme.palette.mode === 'light' ? [transaction] : [transactionForDarkTheme]}
|
||||
{...storybookStyles(theme)}
|
||||
/>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export const UndelegateSuccess = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const handleClick = () => setOpen(true);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Content handleClick={handleClick}>
|
||||
<DelegationModal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
status="success"
|
||||
action="undelegate"
|
||||
message="You undelegated 5 NYM"
|
||||
transactions={theme.palette.mode === 'light' ? [transaction] : [transactionForDarkTheme]}
|
||||
{...storybookStyles(theme)}
|
||||
/>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export const RedeemSuccess = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const handleClick = () => setOpen(true);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Content handleClick={handleClick}>
|
||||
<DelegationModal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
status="success"
|
||||
action="redeem"
|
||||
message="42 NYM"
|
||||
transactions={
|
||||
theme.palette.mode === 'light'
|
||||
? [transaction, transaction]
|
||||
: [transactionForDarkTheme, transactionForDarkTheme]
|
||||
}
|
||||
{...storybookStyles(theme)}
|
||||
/>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export const RedeemWithVestedSuccess = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const handleClick = () => setOpen(true);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Content handleClick={handleClick}>
|
||||
<DelegationModal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
status="success"
|
||||
action="redeem"
|
||||
message="42 NYM"
|
||||
transactions={
|
||||
theme.palette.mode === 'light'
|
||||
? [transaction, transaction]
|
||||
: [transactionForDarkTheme, transactionForDarkTheme]
|
||||
}
|
||||
{...storybookStyles(theme)}
|
||||
/>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export const RedeemAllSuccess = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const handleClick = () => setOpen(true);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Content handleClick={handleClick}>
|
||||
<DelegationModal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
status="success"
|
||||
action="redeem-all"
|
||||
message="42 NYM"
|
||||
transactions={
|
||||
theme.palette.mode === 'light'
|
||||
? [transaction, transaction]
|
||||
: [transactionForDarkTheme, transactionForDarkTheme]
|
||||
}
|
||||
{...storybookStyles(theme)}
|
||||
/>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export const Error = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const handleClick = () => setOpen(true);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Content handleClick={handleClick}>
|
||||
<DelegationModal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
status="error"
|
||||
action="redeem-all"
|
||||
message="Minim esse veniam Lorem id velit Lorem eu eu est. Excepteur labore sunt do proident proident sint aliquip consequat Lorem sint non nulla ad excepteur."
|
||||
transactions={theme.palette.mode === 'light' ? [transaction] : [transactionForDarkTheme]}
|
||||
{...storybookStyles(theme)}
|
||||
/>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import React from 'react';
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { Paper } from '@mui/material';
|
||||
import { Delegations } from './Delegations';
|
||||
import { items } from './DelegationList.stories';
|
||||
|
||||
const explorerUrl = 'https://sandbox-explorer.nymtech.net';
|
||||
|
||||
export default {
|
||||
title: 'Delegation/Components/Delegations',
|
||||
component: Delegations,
|
||||
} as ComponentMeta<typeof Delegations>;
|
||||
|
||||
export const Default = () => (
|
||||
<Paper elevation={0} sx={{ px: 4, pt: 2, pb: 4 }}>
|
||||
<h2>Your Delegations</h2>
|
||||
<Delegations items={items} explorerUrl={explorerUrl} />
|
||||
</Paper>
|
||||
);
|
||||
|
||||
export const Empty = () => (
|
||||
<Paper elevation={0} sx={{ px: 4, pt: 2, pb: 4 }}>
|
||||
<h2>Your Delegations</h2>
|
||||
<Delegations items={[]} explorerUrl={explorerUrl} />
|
||||
</Paper>
|
||||
);
|
||||
@@ -6,18 +6,12 @@ import { DelegationList } from './DelegationList';
|
||||
import { DelegationListItemActions } from './DelegationActions';
|
||||
|
||||
export const Delegations: FCWithChildren<{
|
||||
isLoading?: boolean;
|
||||
items: DelegationWithEverything[];
|
||||
explorerUrl: string;
|
||||
onDelegationItemActionClick?: (item: DelegationWithEverything, action: DelegationListItemActions) => void;
|
||||
}> = ({ isLoading, items, explorerUrl, onDelegationItemActionClick }) => (
|
||||
}> = ({ items, explorerUrl, onDelegationItemActionClick }) => (
|
||||
<>
|
||||
<DelegationList
|
||||
isLoading={isLoading}
|
||||
items={items}
|
||||
explorerUrl={explorerUrl}
|
||||
onItemActionClick={onDelegationItemActionClick}
|
||||
/>
|
||||
<DelegationList items={items} explorerUrl={explorerUrl} onItemActionClick={onDelegationItemActionClick} />
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Link href={`${explorerUrl}/network-components/mixnodes/`} target="_blank" rel="noreferrer">
|
||||
Check the{' '}
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Button, Paper, Typography } from '@mui/material';
|
||||
import { useTheme, Theme } from '@mui/material/styles';
|
||||
import { DelegateModal } from './DelegateModal';
|
||||
import { UndelegateModal } from './UndelegateModal';
|
||||
import { backDropStyles, modalStyles } from '../../../.storybook/storiesStyles';
|
||||
|
||||
const storybookStyles = (theme: Theme) => ({
|
||||
backdropProps: backDropStyles(theme),
|
||||
sx: modalStyles(theme),
|
||||
});
|
||||
|
||||
export default {
|
||||
title: 'Delegation/Components/Action Modals',
|
||||
};
|
||||
|
||||
const Background: FCWithChildren<{ onOpen: () => void }> = ({ onOpen }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Paper elevation={0} sx={{ px: 4, pt: 2, pb: 4 }}>
|
||||
<h2>Lorem ipsum</h2>
|
||||
<Button variant="contained" onClick={onOpen}>
|
||||
Show modal
|
||||
</Button>
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis sunt
|
||||
velit elit do minim mollit non duis reprehenderit. Eiusmod dolore adipisicing ex nostrud consectetur culpa
|
||||
exercitation do. Ad elit esse ipsum aliqua labore irure laborum qui culpa.
|
||||
</Typography>
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
Occaecat commodo excepteur anim ut officia dolor laboris dolore id occaecat enim qui eiusmod occaecat aliquip ad
|
||||
tempor. Labore amet laborum magna amet consequat dolor cupidatat in consequat sunt aliquip magna laboris tempor
|
||||
culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna elit ut
|
||||
mollit.
|
||||
</Typography>
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
Labore voluptate elit amet ipsum qui officia duis in et occaecat culpa ex do non labore mollit. Cillum cupidatat
|
||||
duis ea dolore laboris laboris sunt duis anim consectetur cupidatat nulla ad minim sunt ea. Aliqua amet commodo
|
||||
est irure sint magna sunt. Pariatur dolore commodo labore quis incididunt proident duis voluptate exercitation
|
||||
in duis. Occaecat aliqua laboris reprehenderit nostrud est aute pariatur fugiat anim. Dolore sunt cillum ea
|
||||
aliquip consectetur laborum ipsum qui veniam Lorem consectetur adipisicing velit magna aute. Amet tempor quis
|
||||
excepteur minim culpa velit Lorem enim ad.
|
||||
</Typography>
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
Mollit laborum exercitation excepteur laboris adipisicing ipsum veniam cillum mollit voluptate do. Amet et anim
|
||||
Lorem mollit minim duis cupidatat non. Consectetur sit deserunt nisi nisi non excepteur dolor eiusmod aute aute
|
||||
irure anim dolore ipsum et veniam.
|
||||
</Typography>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export const Delegate = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<>
|
||||
<Background onOpen={() => setOpen(true)} />
|
||||
<DelegateModal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onOk={async () => setOpen(false)}
|
||||
denom="nym"
|
||||
estimatedReward={50.423}
|
||||
accountBalance="425.2345053"
|
||||
nodeUptimePercentage={99.28394}
|
||||
profitMarginPercentage="11.12334234"
|
||||
rewardInterval="monthlyish"
|
||||
hasVestingContract={false}
|
||||
{...storybookStyles(theme)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const DelegateBelowMinimum = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<>
|
||||
<Background onOpen={() => setOpen(true)} />
|
||||
<DelegateModal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onOk={async () => setOpen(false)}
|
||||
denom="nym"
|
||||
estimatedReward={425.2345053}
|
||||
nodeUptimePercentage={99.28394}
|
||||
profitMarginPercentage="11.12334234"
|
||||
rewardInterval="monthlyish"
|
||||
initialAmount="0.1"
|
||||
hasVestingContract={false}
|
||||
{...storybookStyles(theme)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const DelegateMore = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<>
|
||||
<Background onOpen={() => setOpen(true)} />
|
||||
<DelegateModal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onOk={async () => setOpen(false)}
|
||||
header="Delegate more"
|
||||
buttonText="Delegate more"
|
||||
denom="nym"
|
||||
estimatedReward={50.423}
|
||||
accountBalance="425.2345053"
|
||||
nodeUptimePercentage={99.28394}
|
||||
profitMarginPercentage="11.12334234"
|
||||
rewardInterval="monthlyish"
|
||||
hasVestingContract={false}
|
||||
{...storybookStyles(theme)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Undelegate = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<>
|
||||
<Background onOpen={() => setOpen(true)} />
|
||||
<UndelegateModal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onOk={async () => setOpen(false)}
|
||||
currency="nym"
|
||||
amount={150}
|
||||
mixId={1234}
|
||||
identityKey="AA6RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujyxx"
|
||||
usesVestingContractTokens={false}
|
||||
{...storybookStyles(theme)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import { Box, Chip, Paper, Stack, Tooltip, Typography } from '@mui/material';
|
||||
import { WrappedDelegationEvent } from '@nymproject/types';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
|
||||
export const PendingDelegationCard = ({ item, explorerUrl }: { item: WrappedDelegationEvent; explorerUrl: string }) => (
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: 2,
|
||||
borderRadius: 3,
|
||||
bgcolor: (t) => (t.palette.mode === 'dark' ? 'nym.nymWallet.nav.background' : 'nym.nymWallet.background.subtle'),
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={1.5} direction={{ xs: 'column', sm: 'row' }} alignItems={{ sm: 'center' }} flexWrap="wrap">
|
||||
<Link
|
||||
target="_blank"
|
||||
href={`${explorerUrl}/nodes/${item.event.mix_id}`}
|
||||
text={`${item.node_identity.slice(0, 6)}...${item.node_identity.slice(-6)}`}
|
||||
color="text.primary"
|
||||
noIcon
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{item.event.amount?.amount} {item.event.amount?.denom?.toUpperCase() ?? 'NYM'}
|
||||
</Typography>
|
||||
<Box sx={{ flex: 1 }} />
|
||||
<Tooltip
|
||||
title={
|
||||
<Box sx={{ textAlign: 'left' }}>
|
||||
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.
|
||||
</Box>
|
||||
}
|
||||
arrow
|
||||
PopperProps={{
|
||||
sx: {
|
||||
'& .MuiTooltip-tooltip': { textAlign: 'left' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Chip label="Pending" size="small" color="primary" variant="outlined" />
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
@@ -1,49 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, Chip, TableCell, TableRow, Tooltip } from '@mui/material';
|
||||
import { WrappedDelegationEvent } from '@nymproject/types';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
|
||||
export const PendingDelegationItem = ({ item, explorerUrl }: { item: WrappedDelegationEvent; explorerUrl: string }) => (
|
||||
<TableRow key={item.node_identity}>
|
||||
<TableCell>
|
||||
<Link
|
||||
target="_blank"
|
||||
href={`${explorerUrl}/nodes/${item.event.mix_id}`}
|
||||
text={`${item.node_identity.slice(0, 6)}...${item.node_identity.slice(-6)}`}
|
||||
color="text.primary"
|
||||
noIcon
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell>
|
||||
<Box sx={{ textAlign: 'left' }}>{item.event.amount?.amount} NYM</Box>
|
||||
</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<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>
|
||||
);
|
||||
@@ -1,20 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { Box } from '@mui/material';
|
||||
import { BalanceWarning } from './FeeWarning';
|
||||
|
||||
export default {
|
||||
title: 'Wallet / Balance warning',
|
||||
component: BalanceWarning,
|
||||
} as ComponentMeta<typeof BalanceWarning>;
|
||||
|
||||
const Template: ComponentStory<typeof BalanceWarning> = (args) => (
|
||||
<Box mt={2} height={800}>
|
||||
<BalanceWarning {...args} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const WithWarning = Template.bind({});
|
||||
WithWarning.args = {
|
||||
fee: '200',
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { FC, useEffect, useRef, useState } from 'react';
|
||||
import type { UnlistenFn } from '@tauri-apps/api/event';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { getCurrentWebview } from '@tauri-apps/api/webview';
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
@@ -90,32 +90,43 @@ interface RecordPayload {
|
||||
|
||||
export const LogViewer: FC = () => {
|
||||
const theme = useTheme();
|
||||
const unlisten = useRef<UnlistenFn>();
|
||||
const unlisten = useRef<UnlistenFn | null>(null);
|
||||
const [messages, setMessages] = useState<RecordPayload[]>([]);
|
||||
const [messageCount, setMessageCount] = useState(0);
|
||||
const tableRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
listen('log://log', (event) => {
|
||||
const payload = event.payload as RecordPayload;
|
||||
const payloadWithTimestamp = {
|
||||
...payload,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
let cancelled = false;
|
||||
|
||||
setMessages((prev) => [payloadWithTimestamp, ...prev]);
|
||||
setMessageCount((prev) => prev + 1);
|
||||
const setupListener = async () => {
|
||||
const unlistenFn = await getCurrentWebview().listen<RecordPayload>('log://log', (event) => {
|
||||
const { payload } = event;
|
||||
const payloadWithTimestamp = {
|
||||
...payload,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
if (tableRef.current) {
|
||||
tableRef.current.scrollTop = 0;
|
||||
setMessages((prev) => [payloadWithTimestamp, ...prev]);
|
||||
setMessageCount((prev) => prev + 1);
|
||||
|
||||
if (tableRef.current) {
|
||||
tableRef.current.scrollTop = 0;
|
||||
}
|
||||
});
|
||||
if (cancelled) {
|
||||
unlistenFn();
|
||||
} else {
|
||||
unlisten.current = unlistenFn;
|
||||
}
|
||||
}).then((fn) => {
|
||||
unlisten.current = fn;
|
||||
});
|
||||
};
|
||||
|
||||
setupListener();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (unlisten.current) {
|
||||
unlisten.current();
|
||||
unlisten.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -43,7 +43,12 @@ export const EnhancedPasswordInput: React.FC<EnhancedPasswordInputProps> = ({
|
||||
const input = inputRef.current.querySelector('input');
|
||||
if (!input) return undefined;
|
||||
|
||||
input.setAttribute('autocomplete', 'new-password');
|
||||
input.setAttribute('data-lpignore', 'true');
|
||||
input.setAttribute('data-1p-ignore', 'true');
|
||||
|
||||
const handleKeyDown = async (e: KeyboardEvent) => {
|
||||
if (e.defaultPrevented) return;
|
||||
if (document.activeElement !== input) return;
|
||||
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'a') {
|
||||
@@ -101,7 +106,7 @@ export const EnhancedPasswordInput: React.FC<EnhancedPasswordInputProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Box position="relative" ref={inputRef}>
|
||||
<Box position="relative" ref={inputRef} data-nym-auth-paste-field>
|
||||
<PasswordInput password={password} onUpdatePassword={onUpdatePassword} {...otherProps} />
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@@ -46,6 +46,7 @@ export const EnhancedMnemonicInput: React.FC<EnhancedMnemonicInputProps> = ({
|
||||
// Fix the event type issue by casting Event to KeyboardEvent
|
||||
const handleKeyDown = async (e: Event) => {
|
||||
const keyEvent = e as KeyboardEvent;
|
||||
if (keyEvent.defaultPrevented) return;
|
||||
if (document.activeElement !== input) return;
|
||||
|
||||
if ((keyEvent.metaKey || keyEvent.ctrlKey) && keyEvent.key === 'a') {
|
||||
@@ -108,7 +109,7 @@ export const EnhancedMnemonicInput: React.FC<EnhancedMnemonicInputProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Box position="relative" ref={inputRef}>
|
||||
<Box position="relative" ref={inputRef} data-nym-auth-paste-field>
|
||||
<OriginalMnemonicInput mnemonic={mnemonic} onUpdateMnemonic={onUpdateMnemonic} {...otherProps} />
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@@ -29,19 +29,29 @@ export const Mnemonic = ({
|
||||
</Box>
|
||||
<TextField
|
||||
label="Mnemonic"
|
||||
type="input"
|
||||
value={mnemonic}
|
||||
multiline
|
||||
autoFocus={false}
|
||||
fullWidth
|
||||
inputProps={{
|
||||
readOnly: true,
|
||||
'aria-readonly': true,
|
||||
style: {
|
||||
height: '160px',
|
||||
minHeight: '160px',
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
}}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
helperText="Read-only - copy or write it down. It cannot be edited here."
|
||||
FormHelperTextProps={{ sx: { textAlign: 'center' } }}
|
||||
sx={{
|
||||
'input::-webkit-textfield-decoration-container': {
|
||||
'& .MuiInputBase-input': {
|
||||
cursor: 'text',
|
||||
caretColor: 'transparent',
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
'textarea::-webkit-textfield-decoration-container': {
|
||||
alignItems: 'start',
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ErrorOutline } from '@mui/icons-material';
|
||||
import { useTheme, Theme } from '@mui/material/styles';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { Button } from '@mui/material';
|
||||
import { ConfirmationModal } from './ConfirmationModal';
|
||||
import { backDropStyles, dialogStyles } from '../../../.storybook/storiesStyles';
|
||||
|
||||
const storybookStyles = (theme: Theme) => ({
|
||||
backdropProps: backDropStyles(theme),
|
||||
sx: dialogStyles(theme),
|
||||
});
|
||||
|
||||
export default {
|
||||
title: 'Modals/ConfirmationModal',
|
||||
component: ConfirmationModal,
|
||||
} as ComponentMeta<typeof ConfirmationModal>;
|
||||
|
||||
const Template: ComponentStory<typeof ConfirmationModal> = (args) => {
|
||||
const [open, setOpen] = useState(true);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<>
|
||||
<Button variant="outlined" onClick={() => setOpen(true)}>
|
||||
Open confirmation dialog
|
||||
</Button>
|
||||
<ConfirmationModal
|
||||
{...args}
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onConfirm={() => setOpen(false)}
|
||||
{...storybookStyles(theme)}
|
||||
>
|
||||
Dialog content.
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const withError: ComponentStory<typeof ConfirmationModal> = () => {
|
||||
const [open, setOpen] = useState(true);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<>
|
||||
<Button variant="outlined" onClick={() => setOpen(true)}>
|
||||
Open confirmation dialog
|
||||
</Button>
|
||||
<ConfirmationModal
|
||||
title="An error occured"
|
||||
confirmButton="Done"
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onConfirm={() => setOpen(false)}
|
||||
{...storybookStyles(theme)}
|
||||
>
|
||||
<ErrorOutline color="error" />
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
title: 'Confirmation Modal',
|
||||
subTitle: '',
|
||||
fullWidth: true,
|
||||
confirmButton: 'Confirm',
|
||||
maxWidth: 'xs',
|
||||
disabled: false,
|
||||
};
|
||||
@@ -11,22 +11,45 @@ export const ModalListItem: FCWithChildren<{
|
||||
light?: boolean;
|
||||
value?: React.ReactNode;
|
||||
sxValue?: SxProps;
|
||||
}> = ({ label, value, hidden, fontWeight, fontSize, divider, sxValue }) => (
|
||||
/** row: label and value on one line; stack: label above value (better for long strings) */
|
||||
layout?: 'row' | 'stack';
|
||||
}> = ({ label, value, hidden, fontWeight, fontSize, divider, sxValue, layout = 'row' }) => (
|
||||
<Box sx={{ display: hidden ? 'none' : 'block' }}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Typography fontSize="smaller" fontWeight={fontWeight} sx={{ color: 'text.primary', fontSize: 14 }}>
|
||||
{label}
|
||||
</Typography>
|
||||
{value && (
|
||||
{layout === 'stack' ? (
|
||||
<Stack spacing={0.5} alignItems="flex-start">
|
||||
<Typography
|
||||
fontSize="smaller"
|
||||
fontWeight={fontWeight}
|
||||
sx={{ color: 'text.primary', fontSize: fontSize || 14, ...sxValue }}
|
||||
fontWeight={fontWeight ?? 600}
|
||||
sx={{ color: 'text.secondary', fontSize: 12, textTransform: 'uppercase', letterSpacing: 0.6 }}
|
||||
>
|
||||
{value}
|
||||
{label}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
{value ? (
|
||||
<Typography
|
||||
fontSize="smaller"
|
||||
fontWeight={fontWeight}
|
||||
sx={{ color: 'text.primary', fontSize: fontSize || 14, width: '100%', wordBreak: 'break-word', ...sxValue }}
|
||||
>
|
||||
{value}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" gap={1}>
|
||||
<Typography fontSize="smaller" fontWeight={fontWeight} sx={{ color: 'text.primary', fontSize: 14 }}>
|
||||
{label}
|
||||
</Typography>
|
||||
{value ? (
|
||||
<Typography
|
||||
fontSize="smaller"
|
||||
fontWeight={fontWeight}
|
||||
sx={{ color: 'text.primary', fontSize: fontSize || 14, textAlign: 'right', ...sxValue }}
|
||||
>
|
||||
{value}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Stack>
|
||||
)}
|
||||
{divider && <ModalDivider />}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
import React from 'react';
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
import { Button, Paper, Typography } from '@mui/material';
|
||||
import { useTheme, Theme } from '@mui/material/styles';
|
||||
import { SimpleModal } from './SimpleModal';
|
||||
import { ModalDivider } from './ModalDivider';
|
||||
import { backDropStyles, modalStyles } from '../../../.storybook/storiesStyles';
|
||||
|
||||
const storybookStyles = (theme: Theme) => ({
|
||||
backdropProps: backDropStyles(theme),
|
||||
sx: modalStyles(theme),
|
||||
});
|
||||
|
||||
export default {
|
||||
title: 'Modals/Simple Modal',
|
||||
component: SimpleModal,
|
||||
} as ComponentMeta<typeof SimpleModal>;
|
||||
|
||||
const BasePage: FCWithChildren<{ children: React.ReactElement<any, any>; handleClick: () => void }> = ({
|
||||
children,
|
||||
handleClick,
|
||||
}) => (
|
||||
<>
|
||||
<Paper elevation={0} sx={{ px: 4, pt: 2, pb: 4 }}>
|
||||
<h2>Lorem ipsum</h2>
|
||||
<Button variant="contained" onClick={handleClick} sx={{ mb: 3 }}>
|
||||
Show modal
|
||||
</Button>
|
||||
<Typography>
|
||||
Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis sunt
|
||||
velit elit do minim mollit non duis reprehenderit. Eiusmod dolore adipisicing ex nostrud consectetur culpa
|
||||
exercitation do. Ad elit esse ipsum aliqua labore irure laborum qui culpa.
|
||||
</Typography>
|
||||
<Typography>
|
||||
Occaecat commodo excepteur anim ut officia dolor laboris dolore id occaecat enim qui eiusmod occaecat aliquip ad
|
||||
tempor. Labore amet laborum magna amet consequat dolor cupidatat in consequat sunt aliquip magna laboris tempor
|
||||
culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna elit ut
|
||||
mollit.
|
||||
</Typography>
|
||||
<Typography>
|
||||
Labore voluptate elit amet ipsum qui officia duis in et occaecat culpa ex do non labore mollit. Cillum cupidatat
|
||||
duis ea dolore laboris laboris sunt duis anim consectetur cupidatat nulla ad minim sunt ea. Aliqua amet commodo
|
||||
est irure sint magna sunt. Pariatur dolore commodo labore quis incididunt proident duis voluptate exercitation
|
||||
in duis. Occaecat aliqua laboris reprehenderit nostrud est aute pariatur fugiat anim. Dolore sunt cillum ea
|
||||
aliquip consectetur laborum ipsum qui veniam Lorem consectetur adipisicing velit magna aute. Amet tempor quis
|
||||
excepteur minim culpa velit Lorem enim ad.
|
||||
</Typography>
|
||||
<Typography>
|
||||
Mollit laborum exercitation excepteur laboris adipisicing ipsum veniam cillum mollit voluptate do. Amet et anim
|
||||
Lorem mollit minim duis cupidatat non. Consectetur sit deserunt nisi nisi non excepteur dolor eiusmod aute aute
|
||||
irure anim dolore ipsum et veniam.
|
||||
</Typography>
|
||||
</Paper>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
||||
export const Default = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const handleClick = () => setOpen(true);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<BasePage handleClick={handleClick}>
|
||||
<SimpleModal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onOk={async () => setOpen(false)}
|
||||
header="This is a modal"
|
||||
subHeader="This is a sub header"
|
||||
okLabel="Click to continue"
|
||||
{...storybookStyles(theme)}
|
||||
>
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
Lorem mollit minim duis cupidatat non. Consectetur sit deserunt
|
||||
</Typography>
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis.
|
||||
</Typography>
|
||||
<ModalDivider />
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
Occaecat commodo excepteur anim ut officia dolor laboris dolore id occaecat enim qui eius
|
||||
</Typography>
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
Tempor culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna
|
||||
elit ut mollit.
|
||||
</Typography>
|
||||
</SimpleModal>
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
export const NoSubheader = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const handleClick = () => setOpen(true);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<BasePage handleClick={handleClick}>
|
||||
<SimpleModal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onOk={async () => setOpen(false)}
|
||||
header="This is a modal"
|
||||
okLabel="Kaplow!"
|
||||
{...storybookStyles(theme)}
|
||||
>
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
Tempor culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna
|
||||
elit ut mollit.
|
||||
</Typography>
|
||||
<ModalDivider />
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis.
|
||||
</Typography>
|
||||
</SimpleModal>
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
export const hideCloseIcon = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const handleClick = () => setOpen(true);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<BasePage handleClick={handleClick}>
|
||||
<SimpleModal
|
||||
open={open}
|
||||
hideCloseIcon
|
||||
onClose={() => setOpen(false)}
|
||||
onOk={async () => setOpen(false)}
|
||||
header="This is a modal"
|
||||
okLabel="Kaplow!"
|
||||
{...storybookStyles(theme)}
|
||||
>
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
Tempor culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna
|
||||
elit ut mollit.
|
||||
</Typography>
|
||||
<ModalDivider />
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis.
|
||||
</Typography>
|
||||
</SimpleModal>
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
export const hideCloseIconAndDisplayErrorIcon = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const handleClick = () => setOpen(true);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<BasePage handleClick={handleClick}>
|
||||
<SimpleModal
|
||||
open={open}
|
||||
hideCloseIcon
|
||||
displayErrorIcon
|
||||
onClose={() => setOpen(false)}
|
||||
onOk={async () => setOpen(false)}
|
||||
header="This modal announces an error !"
|
||||
okLabel="Kaplow!"
|
||||
backdropProps={backDropStyles(theme)}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
...modalStyles(theme),
|
||||
}}
|
||||
headerStyles={{
|
||||
width: '100%',
|
||||
mb: 3,
|
||||
textAlign: 'center',
|
||||
color: 'error.main',
|
||||
}}
|
||||
subHeaderStyles={{ textAlign: 'center', color: 'text.primary', fontSize: 14, fontWeight: 400 }}
|
||||
>
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
Tempor culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna
|
||||
elit ut mollit.
|
||||
</Typography>
|
||||
<ModalDivider />
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis.
|
||||
</Typography>
|
||||
</SimpleModal>
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
export const withBackButton = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const handleClick = () => setOpen(true);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<BasePage handleClick={handleClick}>
|
||||
<SimpleModal
|
||||
open={open}
|
||||
hideCloseIcon
|
||||
onClose={() => setOpen(false)}
|
||||
onOk={async () => setOpen(false)}
|
||||
header="This is a modal"
|
||||
okLabel="Primary action"
|
||||
onBack={() => setOpen(false)}
|
||||
{...storybookStyles(theme)}
|
||||
>
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
Tempor culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna
|
||||
elit ut mollit.
|
||||
</Typography>
|
||||
<ModalDivider />
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis.
|
||||
</Typography>
|
||||
</SimpleModal>
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
export const withBackButtonAndCustomLabel = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const handleClick = () => setOpen(true);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<BasePage handleClick={handleClick}>
|
||||
<SimpleModal
|
||||
open={open}
|
||||
hideCloseIcon
|
||||
onClose={() => setOpen(false)}
|
||||
onOk={async () => setOpen(false)}
|
||||
header="This is a modal"
|
||||
okLabel="Primary action"
|
||||
onBack={() => setOpen(false)}
|
||||
backLabel="Cancel"
|
||||
backButtonFullWidth
|
||||
{...storybookStyles(theme)}
|
||||
>
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
Tempor culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna
|
||||
elit ut mollit.
|
||||
</Typography>
|
||||
<ModalDivider />
|
||||
<Typography sx={{ color: theme.palette.text.primary }}>
|
||||
Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis.
|
||||
</Typography>
|
||||
</SimpleModal>
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Box, Button, Modal, Stack, SxProps, Typography } from '@mui/material';
|
||||
import { alpha } from '@mui/material/styles';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import ErrorOutline from '@mui/icons-material/ErrorOutline';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
@@ -11,9 +12,15 @@ export const SimpleModal: FCWithChildren<{
|
||||
hideCloseIcon?: boolean;
|
||||
displayErrorIcon?: boolean;
|
||||
displayInfoIcon?: boolean;
|
||||
/** Center the header title; close control stays top-right. */
|
||||
headerCentered?: boolean;
|
||||
headerStyles?: SxProps;
|
||||
subHeaderStyles?: SxProps;
|
||||
buttonFullWidth?: boolean;
|
||||
/** Tighter padding and typography */
|
||||
dense?: boolean;
|
||||
/** Primary left accent bar */
|
||||
accent?: 'none' | 'primary';
|
||||
onClose?: () => void;
|
||||
onOk?: () => Promise<void>;
|
||||
onBack?: () => void;
|
||||
@@ -31,9 +38,12 @@ export const SimpleModal: FCWithChildren<{
|
||||
hideCloseIcon,
|
||||
displayErrorIcon,
|
||||
displayInfoIcon,
|
||||
headerCentered,
|
||||
headerStyles,
|
||||
subHeaderStyles,
|
||||
buttonFullWidth,
|
||||
dense,
|
||||
accent = 'none',
|
||||
onClose,
|
||||
okDisabled,
|
||||
onOk,
|
||||
@@ -48,37 +58,103 @@ export const SimpleModal: FCWithChildren<{
|
||||
backdropProps,
|
||||
}) => (
|
||||
<Modal open={open} onClose={onClose} BackdropProps={backdropProps}>
|
||||
<Box sx={{ border: (t) => `1px solid ${t.palette.nym.nymWallet.modal.border}`, ...modalStyle, ...sx }}>
|
||||
{displayErrorIcon && <ErrorOutline color="error" sx={{ mb: 3 }} />}
|
||||
<Box
|
||||
sx={{
|
||||
border: (t) => `1px solid ${t.palette.nym.nymWallet.modal.border}`,
|
||||
...modalStyle,
|
||||
...(dense ? { p: 3, borderRadius: '12px' } : {}),
|
||||
...(accent === 'primary'
|
||||
? {
|
||||
borderLeft: (t) => `4px solid ${t.palette.primary.main}`,
|
||||
}
|
||||
: {}),
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{displayErrorIcon && <ErrorOutline color="error" sx={{ mb: dense ? 2 : 3 }} />}
|
||||
{displayInfoIcon && <InfoOutlinedIcon sx={{ mb: 2, color: (theme) => theme.palette.nym.nymWallet.text.blue }} />}
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent={headerCentered ? 'center' : 'space-between'}
|
||||
alignItems="center"
|
||||
sx={headerCentered ? { position: 'relative', width: '100%' } : undefined}
|
||||
>
|
||||
{typeof header === 'string' ? (
|
||||
<Typography fontSize={20} fontWeight={600} sx={{ color: 'text.primary', ...headerStyles }}>
|
||||
<Typography
|
||||
fontSize={dense ? 18 : 20}
|
||||
fontWeight={600}
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
...(headerCentered ? { flex: 1, textAlign: 'center', pr: 4 } : {}),
|
||||
...headerStyles,
|
||||
}}
|
||||
>
|
||||
{header}
|
||||
</Typography>
|
||||
) : (
|
||||
header
|
||||
)}
|
||||
{!hideCloseIcon && <CloseIcon onClick={onClose} cursor="pointer" />}
|
||||
{!hideCloseIcon && (
|
||||
<CloseIcon
|
||||
onClick={onClose}
|
||||
cursor="pointer"
|
||||
sx={
|
||||
headerCentered
|
||||
? {
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Typography
|
||||
mt={subHeader ? 0.5 : 0}
|
||||
mb={3}
|
||||
fontSize={12}
|
||||
color={(theme) => theme.palette.text.secondary}
|
||||
sx={{ color: (theme) => theme.palette.nym.nymWallet.text.muted, ...subHeaderStyles }}
|
||||
>
|
||||
{subHeader}
|
||||
</Typography>
|
||||
{subHeader ? (
|
||||
<Typography
|
||||
mt={0.5}
|
||||
mb={dense ? 2 : 3}
|
||||
fontSize={14}
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
lineHeight: 1.45,
|
||||
...(headerCentered ? { textAlign: 'center' } : {}),
|
||||
...subHeaderStyles,
|
||||
}}
|
||||
>
|
||||
{subHeader}
|
||||
</Typography>
|
||||
) : null}
|
||||
|
||||
{children}
|
||||
|
||||
{(onOk || onBack) && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 2, width: buttonFullWidth ? '100%' : null }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
mt: dense ? 1.5 : 2,
|
||||
width: buttonFullWidth ? '100%' : null,
|
||||
}}
|
||||
>
|
||||
{onBack && <StyledBackButton onBack={onBack} label={backLabel} fullWidth={backButtonFullWidth} />}
|
||||
{onOk && (
|
||||
<Button variant="contained" fullWidth size="large" onClick={onOk} disabled={okDisabled}>
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
size="large"
|
||||
onClick={onOk}
|
||||
disabled={okDisabled}
|
||||
sx={(theme) => ({
|
||||
color: theme.palette.primary.contrastText,
|
||||
'&.Mui-disabled': {
|
||||
color: alpha(theme.palette.primary.contrastText, 0.55),
|
||||
},
|
||||
})}
|
||||
>
|
||||
{okLabel}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -1,88 +1,89 @@
|
||||
import React, { useState, useContext } from 'react';
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { List, ListItem, ListItemIcon, ListItemText } from '@mui/material';
|
||||
import {
|
||||
AccountBalanceWalletOutlined,
|
||||
ArrowBack,
|
||||
ArrowForward,
|
||||
Description,
|
||||
Settings,
|
||||
Toll,
|
||||
} from '@mui/icons-material';
|
||||
import { Divider, List, ListItemButton, ListItemIcon, ListItemText, Stack, Typography } from '@mui/material';
|
||||
import type { Theme } from '@mui/material/styles';
|
||||
import { alpha } from '@mui/material/styles';
|
||||
import { AccountBalanceWalletOutlined, Description, Settings, VpnKeyOutlined } from '@mui/icons-material';
|
||||
import { safeOpenUrl } from 'src/utils/safeOpenUrl';
|
||||
import { AppContext } from '../context/main';
|
||||
import { Delegate, Bonding } from '../svg-icons';
|
||||
|
||||
const activeNavPrimaryColor = (theme: Theme) =>
|
||||
theme.palette.mode === 'dark' ? theme.palette.common.white : theme.palette.grey[900];
|
||||
|
||||
export const Nav = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { isAdminAddress, handleShowSendModal, handleShowReceiveModal } = useContext(AppContext);
|
||||
const { isAdminAddress } = useContext(AppContext);
|
||||
|
||||
const [routesSchema] = useState([
|
||||
{
|
||||
label: 'Balance',
|
||||
route: '/balance',
|
||||
Icon: AccountBalanceWalletOutlined,
|
||||
onClick: () => navigate('/balance'),
|
||||
},
|
||||
{
|
||||
label: 'Send',
|
||||
Icon: ArrowForward,
|
||||
onClick: handleShowSendModal,
|
||||
},
|
||||
{
|
||||
label: 'Receive',
|
||||
Icon: ArrowBack,
|
||||
onClick: handleShowReceiveModal,
|
||||
},
|
||||
{
|
||||
label: 'Delegation',
|
||||
route: '/delegation',
|
||||
Icon: Delegate,
|
||||
onClick: () => navigate('/delegation'),
|
||||
},
|
||||
{
|
||||
label: 'Bonding',
|
||||
route: '/bonding',
|
||||
Icon: Bonding,
|
||||
onClick: () => navigate('/bonding'),
|
||||
},
|
||||
{
|
||||
label: 'Docs',
|
||||
route: '/admin',
|
||||
Icon: Description,
|
||||
mode: 'dev',
|
||||
onClick: () => navigate('/docs'),
|
||||
},
|
||||
{
|
||||
label: 'Admin',
|
||||
route: '/admin',
|
||||
Icon: Settings,
|
||||
mode: 'admin',
|
||||
onClick: () => navigate('/admin'),
|
||||
},
|
||||
{
|
||||
label: 'Buy',
|
||||
route: '/buy',
|
||||
Icon: Toll,
|
||||
onClick: () => navigate('/buy'),
|
||||
},
|
||||
]);
|
||||
const routesSchema = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: 'Balance',
|
||||
description: 'Portfolio',
|
||||
route: '/balance',
|
||||
Icon: AccountBalanceWalletOutlined,
|
||||
onClick: () => navigate('/balance'),
|
||||
},
|
||||
{
|
||||
label: 'Delegation',
|
||||
description: 'Stake and manage rewards',
|
||||
route: '/delegation',
|
||||
Icon: Delegate,
|
||||
onClick: () => navigate('/delegation'),
|
||||
},
|
||||
{
|
||||
label: 'Bonding',
|
||||
description: 'Run operator workflows',
|
||||
route: '/bonding',
|
||||
Icon: Bonding,
|
||||
onClick: () => navigate('/bonding'),
|
||||
},
|
||||
{
|
||||
label: 'Docs',
|
||||
description: 'Internal wallet notes',
|
||||
route: '/docs',
|
||||
Icon: Description,
|
||||
mode: 'dev',
|
||||
onClick: () => navigate('/docs'),
|
||||
},
|
||||
{
|
||||
label: 'Admin',
|
||||
description: 'Network management',
|
||||
route: '/admin',
|
||||
Icon: Settings,
|
||||
mode: 'admin',
|
||||
onClick: () => navigate('/admin'),
|
||||
},
|
||||
],
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const ecosystemLinks = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: 'NymVPN',
|
||||
description: 'Plans and subscribe',
|
||||
href: 'https://nym.com/pricing',
|
||||
Icon: VpnKeyOutlined,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
marginLeft: 12,
|
||||
marginRight: 12,
|
||||
}}
|
||||
>
|
||||
<Stack spacing={1.5}>
|
||||
<Typography variant="caption" sx={{ color: 'nym.text.muted', textTransform: 'uppercase', letterSpacing: 1 }}>
|
||||
Navigation
|
||||
</Typography>
|
||||
<List
|
||||
disablePadding
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
{routesSchema
|
||||
@@ -99,47 +100,137 @@ export const Nav = () => {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.map(({ Icon, onClick, label, route }) => (
|
||||
<ListItem
|
||||
disableGutters
|
||||
key={label}
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
py: 2,
|
||||
paddingLeft: 3.5,
|
||||
borderRadius: 1,
|
||||
'&:hover': { backgroundColor: (theme) => theme.palette.nym.nymWallet.hover.background },
|
||||
}}
|
||||
>
|
||||
<ListItemIcon
|
||||
.map(({ Icon, onClick, label, route, description }) => {
|
||||
const isActive = route ? location.pathname.startsWith(route) : false;
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
key={label}
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
height: '20px',
|
||||
minWidth: 30,
|
||||
color: location.pathname === route ? 'primary.main' : 'text.primary',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
sx={{
|
||||
fontSize: 20,
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
sx={{
|
||||
height: '20px',
|
||||
margin: 0,
|
||||
color: location.pathname === route ? 'primary.main' : 'text.primary',
|
||||
'& .MuiListItemText-primary': {
|
||||
fontSize: 14,
|
||||
fontWeight: (theme) => (theme.palette.mode === 'light' ? 600 : 500),
|
||||
px: 2,
|
||||
py: 2,
|
||||
borderRadius: 3,
|
||||
alignItems: 'flex-start',
|
||||
border: (theme) => {
|
||||
if (isActive) {
|
||||
return `1px solid ${theme.palette.primary.main}`;
|
||||
}
|
||||
if (theme.palette.mode === 'dark') {
|
||||
return '1px solid rgba(255,255,255,0.06)';
|
||||
}
|
||||
return `1px solid ${alpha(theme.palette.common.black, 0.08)}`;
|
||||
},
|
||||
backgroundColor: (theme) => {
|
||||
if (isActive) {
|
||||
return `${theme.palette.primary.main}12`;
|
||||
}
|
||||
|
||||
return theme.palette.mode === 'dark'
|
||||
? 'rgba(255,255,255,0.02)'
|
||||
: alpha(theme.palette.common.black, 0.02);
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: (theme) =>
|
||||
isActive ? `${theme.palette.primary.main}18` : theme.palette.nym.nymWallet.hover.background,
|
||||
},
|
||||
}}
|
||||
primary={label}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: 40,
|
||||
mt: 0.5,
|
||||
color: isActive ? 'primary.main' : 'text.primary',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
sx={{
|
||||
fontSize: 20,
|
||||
color: 'inherit',
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
sx={{
|
||||
margin: 0,
|
||||
'& .MuiListItemText-primary': {
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.2,
|
||||
color: (theme) => (isActive ? activeNavPrimaryColor(theme) : theme.palette.text.primary),
|
||||
},
|
||||
'& .MuiListItemText-secondary': {
|
||||
mt: 0.5,
|
||||
fontSize: 12,
|
||||
lineHeight: 1.35,
|
||||
color: 'text.secondary',
|
||||
},
|
||||
}}
|
||||
primary={label}
|
||||
secondary={description}
|
||||
/>
|
||||
</ListItemButton>
|
||||
);
|
||||
})}
|
||||
<Divider sx={{ my: 0.5, borderColor: 'divider' }} />
|
||||
<Typography variant="caption" sx={{ color: 'nym.text.muted', textTransform: 'uppercase', letterSpacing: 1 }}>
|
||||
Ecosystem
|
||||
</Typography>
|
||||
{ecosystemLinks.map(({ Icon, label, description, href }) => (
|
||||
<ListItemButton
|
||||
key={label}
|
||||
onClick={() => {
|
||||
safeOpenUrl(href).catch(() => {
|
||||
/* opener unavailable or user cancelled */
|
||||
});
|
||||
}}
|
||||
sx={{
|
||||
px: 2,
|
||||
py: 2,
|
||||
borderRadius: 3,
|
||||
alignItems: 'flex-start',
|
||||
border: (theme) =>
|
||||
theme.palette.mode === 'dark'
|
||||
? '1px solid rgba(255,255,255,0.06)'
|
||||
: `1px solid ${alpha(theme.palette.common.black, 0.08)}`,
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.02)' : alpha(theme.palette.common.black, 0.02),
|
||||
'&:hover': {
|
||||
backgroundColor: (theme) => theme.palette.nym.nymWallet.hover.background,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: 40,
|
||||
mt: 0.5,
|
||||
color: 'primary.main',
|
||||
}}
|
||||
>
|
||||
<Icon sx={{ fontSize: 20, color: 'inherit' }} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
sx={{
|
||||
margin: 0,
|
||||
'& .MuiListItemText-primary': {
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.2,
|
||||
color: 'text.primary',
|
||||
},
|
||||
'& .MuiListItemText-secondary': {
|
||||
mt: 0.5,
|
||||
fontSize: 12,
|
||||
lineHeight: 1.35,
|
||||
color: 'text.secondary',
|
||||
},
|
||||
}}
|
||||
primary={label}
|
||||
secondary={description}
|
||||
/>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { Box } from '@mui/material';
|
||||
import { MockMainContextProvider } from '../context/mocks/main';
|
||||
import { NetworkSelector } from './NetworkSelector';
|
||||
|
||||
export default {
|
||||
title: 'Wallet / Network Selector',
|
||||
component: NetworkSelector,
|
||||
} as ComponentMeta<typeof NetworkSelector>;
|
||||
|
||||
const Template: ComponentStory<typeof NetworkSelector> = () => (
|
||||
<Box mt={2} height={800}>
|
||||
<MockMainContextProvider>
|
||||
<NetworkSelector />
|
||||
</MockMainContextProvider>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
@@ -4,6 +4,7 @@ import { ArrowDropDown, Check } from '@mui/icons-material';
|
||||
import { Network } from 'src/types';
|
||||
import { AppContext } from '../context/main';
|
||||
import { config } from '../config';
|
||||
import { headerControlPillSx } from './headerControlPillSx';
|
||||
|
||||
const networks: { networkName: Network; name: string }[] = [
|
||||
{ networkName: 'MAINNET', name: 'Nym Mainnet' },
|
||||
@@ -59,10 +60,10 @@ export const NetworkSelector = () => {
|
||||
<Button
|
||||
variant="text"
|
||||
color="inherit"
|
||||
sx={{ color: 'text.primary', fontSize: 14 }}
|
||||
sx={headerControlPillSx}
|
||||
onClick={handleClick}
|
||||
disableElevation
|
||||
endIcon={<ArrowDropDown sx={{ color: (theme) => `1px solid ${theme.palette.text.primary}` }} />}
|
||||
endIcon={<ArrowDropDown sx={{ color: 'text.primary' }} />}
|
||||
>
|
||||
{networks.find((n) => n.networkName === network)?.name}
|
||||
</Button>
|
||||
|
||||
@@ -11,34 +11,53 @@ const CardContentNoPadding = styled(CardContent)(() => ({
|
||||
}));
|
||||
|
||||
export const NymCard: FCWithChildren<{
|
||||
title: string | React.ReactElement;
|
||||
subheader?: string | React.ReactChild;
|
||||
title?: string | React.ReactElement;
|
||||
subheader?: React.ReactNode;
|
||||
Action?: React.ReactNode;
|
||||
Icon?: React.ReactNode;
|
||||
noPadding?: boolean;
|
||||
borderless?: boolean;
|
||||
/** Omit the card header row (use for fully custom headers inside children). */
|
||||
hideHeader?: boolean;
|
||||
dataTestid?: string;
|
||||
sx?: SxProps;
|
||||
sxTitle?: SxProps;
|
||||
children?: React.ReactNode;
|
||||
}> = ({ title, subheader, Action, Icon, noPadding, borderless, children, dataTestid, sx, sxTitle }) => (
|
||||
<Card variant="outlined" sx={{ overflow: 'auto', ...(borderless && { border: 'none', dropShadow: 'none' }), ...sx }}>
|
||||
<CardHeader
|
||||
sx={{
|
||||
p: 3,
|
||||
color: (theme: Theme) => theme.palette.text.primary,
|
||||
'& .MuiCardHeader-title h5': { fontSize: '1.25rem' },
|
||||
}}
|
||||
title={<Title title={title} Icon={Icon} sx={sxTitle} />}
|
||||
subheader={subheader}
|
||||
data-testid={dataTestid || title}
|
||||
subheaderTypographyProps={{ variant: 'subtitle1' }}
|
||||
action={Action}
|
||||
/>
|
||||
}> = ({ title, subheader, Action, Icon, noPadding, borderless, hideHeader, children, dataTestid, sx, sxTitle }) => (
|
||||
<Card
|
||||
variant="outlined"
|
||||
data-testid={hideHeader ? dataTestid : undefined}
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
borderRadius: 4,
|
||||
borderColor: 'divider',
|
||||
backgroundImage: 'none',
|
||||
...(borderless && { border: 'none', boxShadow: 'none' }),
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{!hideHeader && title !== undefined && (
|
||||
<CardHeader
|
||||
sx={{
|
||||
p: 3,
|
||||
color: (theme: Theme) => theme.palette.text.primary,
|
||||
'& .MuiCardHeader-title h5': { fontSize: '1.25rem' },
|
||||
'& .MuiCardHeader-action': {
|
||||
alignSelf: 'center',
|
||||
m: 0,
|
||||
},
|
||||
}}
|
||||
title={<Title title={title} Icon={Icon} sx={sxTitle} />}
|
||||
subheader={subheader}
|
||||
data-testid={dataTestid || (typeof title === 'string' ? title : 'nym-card')}
|
||||
subheaderTypographyProps={{ variant: 'subtitle1' }}
|
||||
action={Action}
|
||||
/>
|
||||
)}
|
||||
{noPadding ? (
|
||||
<CardContentNoPadding>{children}</CardContentNoPadding>
|
||||
) : (
|
||||
<CardContent sx={{ p: 3, paddingTop: 0 }}>{children}</CardContent>
|
||||
<CardContent sx={{ p: 3, paddingTop: hideHeader ? 3 : 0 }}>{children}</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,54 +1,48 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { AppContext } from 'src/context';
|
||||
import { Box, Stack, SxProps, Typography, alpha, useTheme } from '@mui/material';
|
||||
import QRCode from 'qrcode.react';
|
||||
import { ClientAddress } from '@nymproject/react/client-address/ClientAddress';
|
||||
import { Box, Stack, Typography, alpha, useTheme } from '@mui/material';
|
||||
import QrCode2Icon from '@mui/icons-material/QrCode2';
|
||||
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
|
||||
import { QrCodeReact } from 'src/utils/qrCodeReact';
|
||||
import { SimpleModal } from '../Modals/SimpleModal';
|
||||
|
||||
export const ReceiveModal = ({
|
||||
onClose,
|
||||
sx,
|
||||
backdropProps,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
sx?: SxProps;
|
||||
backdropProps?: object;
|
||||
}) => {
|
||||
export const ReceiveModal = ({ onClose }: { onClose: () => void }) => {
|
||||
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;
|
||||
const address = clientDetails?.client_address?.trim() ?? '';
|
||||
|
||||
return (
|
||||
<SimpleModal
|
||||
header="Receive"
|
||||
header="Receive NYM"
|
||||
subHeader="Share your address or scan the QR code to receive NYM from another wallet."
|
||||
headerCentered
|
||||
open
|
||||
onClose={onClose}
|
||||
okLabel=""
|
||||
sx={{
|
||||
...sx,
|
||||
'& .MuiPaper-root': {
|
||||
overflow: 'hidden',
|
||||
borderRadius: '20px',
|
||||
boxShadow: '0 12px 48px rgba(0, 0, 0, 0.15)',
|
||||
boxShadow: theme.palette.nym.nymWallet.shadows.strong,
|
||||
maxWidth: '480px',
|
||||
},
|
||||
}}
|
||||
backdropProps={backdropProps}
|
||||
subHeaderStyles={{ mb: 0, px: 3, pt: 2 }}
|
||||
subHeaderStyles={{ mb: 0, px: 3, pt: 0.5 }}
|
||||
>
|
||||
<Stack
|
||||
gap={4}
|
||||
gap={3}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
px: 3,
|
||||
pb: 3,
|
||||
pt: 1,
|
||||
pt: 0,
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
@@ -56,6 +50,7 @@ export const ReceiveModal = ({
|
||||
color: 'text.secondary',
|
||||
fontWeight: 600,
|
||||
display: 'block',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Your address
|
||||
@@ -64,23 +59,43 @@ export const ReceiveModal = ({
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: alpha(theme.palette.primary.main, 0.04),
|
||||
bgcolor: alpha(theme.palette.primary.main, 0.06),
|
||||
borderRadius: '12px',
|
||||
border: `1px solid ${alpha(theme.palette.primary.main, 0.1)}`,
|
||||
border: `1px solid ${alpha(theme.palette.primary.main, 0.18)}`,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{clientDetails?.client_address && (
|
||||
{address ? (
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: '0.9rem',
|
||||
fontFamily: 'monospace',
|
||||
letterSpacing: '0.5px',
|
||||
wordBreak: 'break-all',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'space-between',
|
||||
gap: 1.5,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<ClientAddress address={clientDetails?.client_address} withCopy showEntireAddress />
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
fontSize: '0.9rem',
|
||||
fontFamily: 'monospace',
|
||||
letterSpacing: '0.5px',
|
||||
wordBreak: 'break-all',
|
||||
color: 'text.primary',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{address}
|
||||
</Typography>
|
||||
<CopyToClipboard value={address} sx={{ flexShrink: 0, mt: 0.25 }} />
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center' }}>
|
||||
No client address available. Sign in again if this persists.
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -92,12 +107,22 @@ export const ReceiveModal = ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
bgcolor: isLightMode ? alpha(highlightColor, 0.06) : alpha(darkBgColor, 0.7),
|
||||
bgcolor: isLightMode ? alpha(highlightColor, 0.06) : alpha(theme.palette.background.paper, 0.5),
|
||||
borderRadius: '16px',
|
||||
py: 4,
|
||||
py: 3,
|
||||
px: 2,
|
||||
border: `1px solid ${alpha(highlightColor, isLightMode ? 0.2 : 0.12)}`,
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" gap={1} sx={{ mb: 1.5 }}>
|
||||
<QrCode2Icon sx={{ color: 'primary.main', fontSize: 22 }} />
|
||||
<Typography variant="subtitle2" fontWeight={600} color="text.primary">
|
||||
QR code
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mb: 2, textAlign: 'center', maxWidth: 320 }}>
|
||||
Share this address only with people you trust.
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
@@ -114,7 +139,7 @@ export const ReceiveModal = ({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '16px',
|
||||
background: `radial-gradient(circle, ${alpha(highlightColor, 0.15)} 0%, transparent 70%)`,
|
||||
background: `radial-gradient(circle, ${alpha(highlightColor, 0.18)} 0%, transparent 70%)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -124,9 +149,9 @@ export const ReceiveModal = ({
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
p: 3,
|
||||
bgcolor: isLightMode ? 'white' : alpha(darkBgColor, 0.7),
|
||||
bgcolor: isLightMode ? 'white' : alpha(darkBgColor, 0.85),
|
||||
borderRadius: '16px',
|
||||
border: `2px solid ${isLightMode ? highlightColor : theme.palette.nym.nymWallet.modal.border}`,
|
||||
border: `2px solid ${isLightMode ? highlightColor : alpha(highlightColor, 0.35)}`,
|
||||
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': {
|
||||
@@ -135,22 +160,21 @@ export const ReceiveModal = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
{clientDetails && (
|
||||
<QRCode
|
||||
{address ? (
|
||||
<QrCodeReact
|
||||
renderAs="svg"
|
||||
data-testid="qr-code"
|
||||
value={clientDetails?.client_address}
|
||||
value={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,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
QR unavailable without an address
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -158,10 +182,11 @@ export const ReceiveModal = ({
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
mt: 3,
|
||||
mt: 2.5,
|
||||
color: 'text.secondary',
|
||||
textAlign: 'center',
|
||||
maxWidth: '80%',
|
||||
maxWidth: '90%',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
Scan this QR code with a compatible wallet to receive NYM tokens
|
||||
|
||||
@@ -2,10 +2,10 @@ import React, { useContext } from 'react';
|
||||
import { AppContext } from 'src/context';
|
||||
import { ReceiveModal } from './ReceiveModal';
|
||||
|
||||
export const Receive = ({ hasStorybookStyles }: { hasStorybookStyles?: {} }) => {
|
||||
const { showReceiveModal, handleShowReceiveModal } = useContext(AppContext);
|
||||
export const Receive = () => {
|
||||
const { showReceiveModal, handleCloseReceiveModal } = useContext(AppContext);
|
||||
|
||||
if (showReceiveModal) return <ReceiveModal onClose={handleShowReceiveModal} {...hasStorybookStyles} />;
|
||||
if (showReceiveModal) return <ReceiveModal onClose={handleCloseReceiveModal} />;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
import React from 'react';
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
import { Button, Paper } from '@mui/material';
|
||||
import { useTheme, Theme } from '@mui/material/styles';
|
||||
import { RedeemModal } from './RedeemModal';
|
||||
import { backDropStyles, modalStyles } from '../../../.storybook/storiesStyles';
|
||||
|
||||
const storybookStyles = (theme: Theme) => ({
|
||||
backdropProps: backDropStyles(theme),
|
||||
sx: modalStyles(theme),
|
||||
});
|
||||
|
||||
export default {
|
||||
title: 'Rewards/Components/Redeem Modals',
|
||||
component: RedeemModal,
|
||||
} as ComponentMeta<typeof RedeemModal>;
|
||||
|
||||
const Content: FCWithChildren<{
|
||||
setOpen: (value: boolean) => void;
|
||||
}> = ({ setOpen }) => (
|
||||
<Paper elevation={0} sx={{ px: 4, pt: 2, pb: 4 }}>
|
||||
<h2>Lorem ipsum</h2>
|
||||
<Button variant="contained" onClick={() => setOpen(true)}>
|
||||
Show modal
|
||||
</Button>
|
||||
<p>
|
||||
Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis sunt
|
||||
velit elit do minim mollit non duis reprehenderit. Eiusmod dolore adipisicing ex nostrud consectetur culpa
|
||||
exercitation do. Ad elit esse ipsum aliqua labore irure laborum qui culpa.
|
||||
</p>
|
||||
<p>
|
||||
Occaecat commodo excepteur anim ut officia dolor laboris dolore id occaecat enim qui eiusmod occaecat aliquip ad
|
||||
tempor. Labore amet laborum magna amet consequat dolor cupidatat in consequat sunt aliquip magna laboris tempor
|
||||
culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna elit ut
|
||||
mollit.
|
||||
</p>
|
||||
<p>
|
||||
Labore voluptate elit amet ipsum qui officia duis in et occaecat culpa ex do non labore mollit. Cillum cupidatat
|
||||
duis ea dolore laboris laboris sunt duis anim consectetur cupidatat nulla ad minim sunt ea. Aliqua amet commodo
|
||||
est irure sint magna sunt. Pariatur dolore commodo labore quis incididunt proident duis voluptate exercitation in
|
||||
duis. Occaecat aliqua laboris reprehenderit nostrud est aute pariatur fugiat anim. Dolore sunt cillum ea aliquip
|
||||
consectetur laborum ipsum qui veniam Lorem consectetur adipisicing velit magna aute. Amet tempor quis excepteur
|
||||
minim culpa velit Lorem enim ad.
|
||||
</p>
|
||||
<p>
|
||||
Mollit laborum exercitation excepteur laboris adipisicing ipsum veniam cillum mollit voluptate do. Amet et anim
|
||||
Lorem mollit minim duis cupidatat non. Consectetur sit deserunt nisi nisi non excepteur dolor eiusmod aute aute
|
||||
irure anim dolore ipsum et veniam.
|
||||
</p>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
export const RedeemAllRewards = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<>
|
||||
<Content setOpen={setOpen} />
|
||||
<RedeemModal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onOk={async () => setOpen(false)}
|
||||
message="Redeem all rewards"
|
||||
denom="nym"
|
||||
mixId={1234}
|
||||
identityKey="D88RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujaaa"
|
||||
amount={425.65843}
|
||||
{...storybookStyles(theme)}
|
||||
usesVestingTokens={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const RedeemRewardForMixnode = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<>
|
||||
<Content setOpen={setOpen} />
|
||||
<RedeemModal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onOk={async () => setOpen(false)}
|
||||
message="Claim rewards"
|
||||
denom="nym"
|
||||
mixId={1234}
|
||||
identityKey="D88RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujaaa"
|
||||
amount={425.65843}
|
||||
{...storybookStyles(theme)}
|
||||
usesVestingTokens={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const FeeIsMoreThanAllRewards = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<>
|
||||
<Content setOpen={setOpen} />
|
||||
<RedeemModal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onOk={() => setOpen(false)}
|
||||
message="Redeem all rewards"
|
||||
denom="nym"
|
||||
mixId={1234}
|
||||
identityKey="D88RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujaaa"
|
||||
amount={0.001}
|
||||
{...storybookStyles(theme)}
|
||||
usesVestingTokens={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const FeeIsMoreThanMixnodeReward = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<>
|
||||
<Content setOpen={setOpen} />
|
||||
<RedeemModal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onOk={async () => setOpen(false)}
|
||||
mixId={1234}
|
||||
identityKey="D88RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujaaa"
|
||||
message="Claim rewards"
|
||||
denom="nym"
|
||||
amount={0.001}
|
||||
{...storybookStyles(theme)}
|
||||
usesVestingTokens={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||