Compare commits

..

2 Commits

Author SHA1 Message Date
Tommy Verrall dfde3bb2e4 push up some more changes ahead of the wallet release too
- change versions
2025-06-24 16:08:44 +02:00
Tommy Verrall 4ef59bb95b fix import errors on develop 2025-06-24 16:03:19 +02:00
52 changed files with 2466 additions and 37 deletions
@@ -0,0 +1,75 @@
name: ci-nym-wallet-storybook
on:
pull_request:
paths:
- 'nym-wallet/**'
- '.github/workflows/ci-nym-wallet-storybook.yml'
jobs:
build:
runs-on: custom-linux
steps:
- uses: actions/checkout@v4
- 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 stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: 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/"
- name: Matrix - Node Install
run: npm install
working-directory: .github/workflows/support-files
- name: Matrix - Send Notification
env:
NYM_NOTIFICATION_KIND: nym-wallet
NYM_PROJECT_NAME: "nym-wallet"
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
NYM_CI_WWW_LOCATION: "wallet-${{ env.GITHUB_REF_SLUG }}"
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
GIT_BRANCH: "${GITHUB_REF##*/}"
IS_SUCCESS: "${{ job.status == 'success' }}"
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}"
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
uses: docker://keybaseio/client:stable-node
with:
args: .github/workflows/support-files/notifications/entry_point.sh
@@ -5,6 +5,8 @@
>
> ➡️➡️➡️➡️➡️ **View output:**
>
> `storybook`: https://{{ env.NYM_CI_WWW_LOCATION }}.{{ env.NYM_CI_WWW_BASE }}
>
> `branch` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/tree/{{ env.GIT_BRANCH_NAME }}
>
> `commit` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/commit/{{ env.GITHUB_SHA }}
+60
View File
@@ -0,0 +1,60 @@
/* 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,
},
};
+8
View File
@@ -0,0 +1,8 @@
/**
* 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,
};
@@ -0,0 +1,8 @@
/**
* 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,
}
@@ -0,0 +1,4 @@
/**
* 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.
*/
+113
View File
@@ -0,0 +1,113 @@
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.`));
});
},
};
@@ -0,0 +1,10 @@
/**
* 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,
}
}
+55
View File
@@ -0,0 +1,55 @@
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];
+21
View File
@@ -0,0 +1,21 @@
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%' };
};
View File
+7 -1
View File
@@ -10,7 +10,10 @@
"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",
@@ -32,6 +35,7 @@
"@nymproject/node-tester": "^1.2.3",
"@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",
@@ -68,6 +72,7 @@
"@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",
@@ -100,7 +105,8 @@
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.29.2",
"eslint-plugin-react-hooks": "^4.3.0",
"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",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "NymWallet"
version = "1.2.18"
version = "1.2.19"
description = "Nym Native Wallet"
authors = ["Nym Technologies SA"]
license = ""
+1 -1
View File
@@ -40,7 +40,7 @@
},
"productName": "NymWallet",
"mainBinaryName": "NymWallet",
"version": "1.2.18",
"version": "1.2.19",
"identifier": "net.nymtech.wallet",
"plugins": {
"updater": {
@@ -0,0 +1,18 @@
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>
);
@@ -0,0 +1,10 @@
import React from 'react';
import { Tutorial } from './Tutorial';
export default {
title: 'Buy/Tutorial',
component: Tutorial,
};
export const TutorialPage = () => <Tutorial />;
+26 -22
View File
@@ -1,32 +1,32 @@
import React from 'react';
import { Box, Typography, Grid, Link, Card, CardContent, Stack } from '@mui/material';
import { NymCard } from '..';
import BitfinexIcon from 'src/svg-icons/bitfinex.svg';
import KrakenIcon from 'src/svg-icons/kraken.svg';
import BybitIcon from 'src/svg-icons/bybit.svg';
import GateIcon from 'src/svg-icons/gate22.svg';
import HTXIcon from 'src/svg-icons/htx.svg';
import { NymCard } from '..';
const ExchangeCard = ({
name,
tokenType,
url,
IconComponent,
const ExchangeCard = ({
name,
tokenType,
url,
IconComponent
}: {
name: string;
tokenType: string;
url: string;
IconComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
}) => (
<Card
variant="outlined"
sx={{
<Card
variant="outlined"
sx={{
height: '100%',
transition: 'all 0.2s ease-in-out',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: 2,
},
}
}}
>
<CardContent sx={{ p: 3 }}>
@@ -51,17 +51,17 @@ const ExchangeCard = ({
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{tokenType}
</Typography>
<Link
href={url}
target="_blank"
<Link
href={url}
target="_blank"
variant="body2"
data-testid="link-get-nym"
sx={{
sx={{
textDecoration: 'underline',
fontWeight: 500,
'&:hover': {
textDecoration: 'none',
},
}
}}
>
GET NYM
@@ -78,40 +78,44 @@ export const Tutorial = () => {
name: 'Bitfinex',
tokenType: 'Native NYM, ERC-20',
url: 'https://www.bitfinex.com/',
IconComponent: BitfinexIcon,
IconComponent: BitfinexIcon
},
{
name: 'Kraken',
tokenType: 'Native NYM',
url: 'https://www.kraken.com/',
IconComponent: KrakenIcon,
IconComponent: KrakenIcon
},
{
name: 'Bybit',
tokenType: 'ERC-20',
url: 'https://www.bybit.com/en/',
IconComponent: BybitIcon,
IconComponent: BybitIcon
},
{
name: 'Gate.io',
tokenType: 'ERC-20',
url: 'https://www.gate.io/',
IconComponent: GateIcon,
IconComponent: GateIcon
},
{
name: 'HTX',
tokenType: 'ERC-20',
url: 'https://www.htx.com/',
IconComponent: HTXIcon,
IconComponent: HTXIcon
},
];
return (
<NymCard borderless title="Where you can get NYM tokens" sx={{ mt: 4 }}>
<NymCard
borderless
title="Where you can get NYM tokens"
sx={{ mt: 4 }}
>
<Typography mb={3} fontSize={14} sx={{ color: 'text.secondary' }}>
You can get NYM tokens from these exchanges
</Typography>
<Grid container spacing={3}>
{exchanges.map((exchange) => (
<Grid item xs={12} md={6} lg={4} key={exchange.name}>
@@ -0,0 +1,29 @@
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,
};
+12 -2
View File
@@ -5,6 +5,15 @@ 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;
@@ -14,9 +23,9 @@ export const ConfirmTx: FCWithChildren<{
onConfirm: () => Promise<void>;
onClose?: () => void;
onPrev: () => void;
isStorybook?: boolean;
children?: React.ReactNode;
}> = ({ open, fee, onConfirm, onClose, header, subheader, onPrev, children }) => {
}> = ({ open, fee, onConfirm, onClose, header, subheader, onPrev, children, isStorybook }) => {
const theme = useTheme();
return (
<SimpleModal
@@ -27,6 +36,7 @@ export const ConfirmTx: FCWithChildren<{
onOk={onConfirm}
onClose={onClose}
onBack={onPrev}
{...storybookStyles(theme, isStorybook)}
>
<Box sx={{ mt: 3 }}>
{children}
@@ -0,0 +1,40 @@
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)}
/>
</>
);
};
@@ -0,0 +1,19 @@
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 }} />
);
@@ -0,0 +1,311 @@
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} />;
@@ -0,0 +1,190 @@
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>
);
};
@@ -0,0 +1,27 @@
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>
);
@@ -0,0 +1,144 @@
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,20 @@
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',
};
@@ -0,0 +1,70 @@
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,
};
@@ -0,0 +1,260 @@
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>
);
};
@@ -0,0 +1,20 @@
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({});
+2 -2
View File
@@ -2,10 +2,10 @@ import React, { useContext } from 'react';
import { AppContext } from 'src/context';
import { ReceiveModal } from './ReceiveModal';
export const Receive = () => {
export const Receive = ({ hasStorybookStyles }: { hasStorybookStyles?: {} }) => {
const { showReceiveModal, handleShowReceiveModal } = useContext(AppContext);
if (showReceiveModal) return <ReceiveModal onClose={handleShowReceiveModal} />;
if (showReceiveModal) return <ReceiveModal onClose={handleShowReceiveModal} {...hasStorybookStyles} />;
return null;
};
@@ -0,0 +1,139 @@
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}
/>
</>
);
};
@@ -0,0 +1,28 @@
import React from 'react';
import { ComponentMeta } from '@storybook/react';
import { Paper } from '@mui/material';
import { RewardsSummary } from './RewardsSummary';
export default {
title: 'Rewards/Components/Rewards Summary',
component: RewardsSummary,
} as ComponentMeta<typeof RewardsSummary>;
export const Default = () => (
<Paper elevation={0} sx={{ px: 4, py: 2 }}>
<RewardsSummary totalDelegation="860.123 NYM" totalRewards="4.86723 NYM" />
</Paper>
);
export const Empty = () => (
<Paper elevation={0} sx={{ px: 4, py: 2 }}>
<RewardsSummary />
</Paper>
);
export const Loading = () => (
<Paper elevation={0} sx={{ px: 4, py: 2 }}>
<RewardsSummary isLoading />
</Paper>
);
@@ -0,0 +1,19 @@
import React from 'react';
import { Typography } from '@mui/material';
import { SelectionChance } from '@nymproject/types';
const colorMap: { [key in SelectionChance]: string } = {
Low: 'error.main',
Good: 'warning.main',
High: 'success.main',
};
const textMap: { [key in SelectionChance]: string } = {
Low: 'Low',
Good: 'Good',
High: 'High',
};
export const InclusionProbability = ({ probability }: { probability: SelectionChance }) => (
<Typography sx={{ color: colorMap[probability] }}>{textMap[probability]}</Typography>
);
@@ -0,0 +1,91 @@
import React, { useCallback } from 'react';
import { yupResolver } from '@hookform/resolvers/yup';
import { Button, Grid, TextField, Typography } from '@mui/material';
import { useForm } from 'react-hook-form';
import { DefaultInputValues } from 'src/pages/bonding/node-settings/apy-playground';
import { inputValidationSchema } from './inputsValidationSchema';
export type InputFields = {
label: string;
name: 'profitMargin' | 'uptime' | 'bond' | 'delegations' | 'operatorCost' | 'uptime';
isPercentage?: boolean;
}[];
export type CalculateArgs = {
bond: string;
delegations: string;
uptime: string;
profitMargin: string;
operatorCost: string;
};
const inputFields: InputFields = [
{ label: 'Profit margin', name: 'profitMargin', isPercentage: true },
{ label: 'Operator cost', name: 'operatorCost' },
{ label: 'Bond', name: 'bond' },
{ label: 'Delegations', name: 'delegations' },
{ label: 'Uptime', name: 'uptime', isPercentage: true },
];
export const Inputs = ({
onCalculate,
defaultValues,
}: {
onCalculate: (args: CalculateArgs) => Promise<void>;
defaultValues: DefaultInputValues;
}) => {
const handleCalculate = useCallback(
async (args: CalculateArgs) => {
onCalculate({
bond: args.bond,
delegations: args.delegations,
uptime: args.uptime,
profitMargin: args.profitMargin,
operatorCost: args.operatorCost,
});
},
[onCalculate],
);
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: yupResolver(inputValidationSchema),
defaultValues,
});
return (
<Grid container spacing={3}>
{inputFields.map((field) => (
<Grid item xs={12} lg={2} key={field.name}>
<TextField
{...register(field.name)}
fullWidth
label={field.label}
name={field.name}
error={Boolean(errors[field.name])}
helperText={errors[field.name]?.message}
InputProps={{
endAdornment: <Typography sx={{ color: 'grey.600' }}>{field.isPercentage ? '%' : 'NYM'}</Typography>,
}}
InputLabelProps={{ shrink: true }}
/>
</Grid>
))}{' '}
<Grid item xs={12} lg={2}>
<Button
variant="contained"
disableElevation
onClick={handleSubmit(handleCalculate)}
size="large"
fullWidth
disabled={Boolean(Object.keys(errors).length)}
>
Calculate
</Button>
</Grid>
</Grid>
);
};
@@ -0,0 +1,32 @@
import React from 'react';
import { Card, CardContent, Divider, Stack, Typography } from '@mui/material';
import { SelectionChance } from '@nymproject/types';
import { InclusionProbability } from './InclusionProbability';
const computeSelectionProbability = (saturation: number): SelectionChance => {
if (saturation < 5) return 'Low';
if (saturation > 5 && saturation < 15) return 'Good';
return 'High';
};
export const NodeDetails = ({ saturation }: { saturation?: string }) => {
if (!saturation) return null;
return (
<Card variant="outlined" sx={{ p: 1 }}>
<CardContent>
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight="medium">Stake saturation</Typography>
<Typography>{saturation || '- '}%</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight="medium">Selection probability</Typography>
<InclusionProbability probability={computeSelectionProbability(parseInt(saturation, 10))} />
</Stack>
</CardContent>
</Card>
);
};
@@ -0,0 +1,75 @@
import React from 'react';
import {
Card,
CardContent,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
} from '@mui/material';
export type Results = {
operator: {
daily: string;
monthly: string;
yearly: string;
};
delegator: {
daily: string;
monthly: string;
yearly: string;
};
total: {
daily: string;
monthly: string;
yearly: string;
};
};
const tableHeader = [
{ title: 'Estimated rewards', bold: true },
{ title: 'Per day' },
{ title: 'Per month' },
{ title: 'Per year' },
];
export const ResultsTable = ({ results }: { results: Results }) => {
const tableRows = [
{ title: 'Total node reward', ...results.total },
{ title: 'Operator rewards', ...results.operator },
{ title: 'Delegator rewards', ...results.delegator },
];
return (
<Card variant="outlined" sx={{ p: 1 }}>
<CardContent>
<TableContainer>
<Table>
<TableHead>
<TableRow>
{tableHeader.map((header) => (
<TableCell>
<Typography fontWeight={header.bold ? 'bold' : 'regular'}>{header.title}</Typography>
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{tableRows.map((row) => (
<TableRow>
<TableCell>{row.title}</TableCell>
<TableCell>{row.daily}</TableCell>
<TableCell>{row.monthly}</TableCell>
<TableCell>{row.yearly}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</CardContent>
</Card>
);
};
@@ -0,0 +1,43 @@
import * as Yup from 'yup';
import { isGreaterThan, isLessThan } from 'src/utils';
export const inputValidationSchema = Yup.object().shape({
profitMargin: Yup.string()
.required('profit margin is a required field')
.test('Is valid profit margin value', (value, ctx) => {
const stringValueToNumber = Math.round(Number(value));
if (isGreaterThan(stringValueToNumber, -1) && isLessThan(stringValueToNumber, 101)) return true;
return ctx.createError({ message: 'Profit margin must be a number from 0 and 100' });
}),
uptime: Yup.string()
.required()
.test('Is valid uptime value', (value, ctx) => {
const stringValueToNumber = Math.round(Number(value));
if (stringValueToNumber && isGreaterThan(stringValueToNumber, 0) && isLessThan(stringValueToNumber, 101))
return true;
return ctx.createError({ message: 'Uptime must be a number between 0 and 100' });
}),
bond: Yup.string()
.required()
.test('Is valid bond value', (value, ctx) => {
if (Number(value)) return true;
return ctx.createError({ message: 'Bond must be a valid number' });
}),
delegations: Yup.string()
.required()
.test('Is valid delegation value', (value, ctx) => {
if (Number(value)) return true;
return ctx.createError({ message: 'Delegations must be a valid number' });
}),
operatorCost: Yup.string()
.required('operator cost is a required field')
.test('Is valid operator cost value', (value, ctx) => {
const stringValueToNumber = Math.round(Number(value));
if (isLessThan(stringValueToNumber, 0))
return ctx.createError({ message: 'Operator cost must be a valid number' });
return true;
}),
});
@@ -0,0 +1,86 @@
import React from 'react';
import { ComponentMeta } from '@storybook/react';
import { useTheme, Theme } from '@mui/material/styles';
import { MockMainContextProvider } from 'src/context/mocks/main';
import { SendDetailsModal } from './SendDetailsModal';
import { SendSuccessModal } from './SendSuccessModal';
import { SendErrorModal } from './SendErrorModal';
import { SendInputModal } from './SendInputModal';
import { Send } from '.';
import { backDropStyles, modalStyles, dialogStyles } from '../../../.storybook/storiesStyles';
const storybookStylesModal = (theme: Theme) => ({
backdropProps: backDropStyles(theme),
sx: modalStyles(theme),
});
const storybookStylesDialog = (theme: Theme) => ({
backdropProps: backDropStyles(theme),
sx: dialogStyles(theme),
});
export default {
title: 'Send/Components',
component: SendDetailsModal,
} as ComponentMeta<typeof SendDetailsModal>;
export const SendInput = () => {
const theme = useTheme();
return (
<SendInputModal
toAddress=""
fromAddress="nymt1w8qp7zsxggvtxhpqpt6e329j42wtv07dm5ts8u"
denom="nym"
onNext={() => {}}
onClose={() => {}}
onAddressChange={() => {}}
onAmountChange={() => {}}
onUserFeesChange={() => {}}
onMemoChange={() => {}}
setShowMore={() => {}}
{...storybookStylesModal(theme)}
/>
);
};
export const SendDetails = () => {
const theme = useTheme();
return (
<SendDetailsModal
fromAddress="nymt1w8qp7zsxggvtxhpqpt6e329j42wtv07dm5ts8u"
toAddress="nymt1w8qp7zsxggvtxhpqpt6e329j42wtv07dm5ts8u"
fee={{ amount: { amount: '0.01', denom: 'nym' }, fee: { Auto: null } }}
denom="nym"
amount={{ amount: '100', denom: 'nym' }}
onPrev={() => {}}
onSend={() => {}}
onClose={() => {}}
{...storybookStylesModal(theme)}
/>
);
};
export const SendSuccess = () => {
const theme = useTheme();
return (
<SendSuccessModal
txDetails={{ amount: '100 NYM', txUrl: 'dummtUrl.com' }}
onClose={() => {}}
{...storybookStylesDialog(theme)}
/>
);
};
export const SendError = () => {
const theme = useTheme();
return <SendErrorModal onClose={() => {}} {...storybookStylesModal(theme)} />;
};
export const SendFlow = () => {
const theme = useTheme();
return (
<MockMainContextProvider>
<Send hasStorybookStyles={{ backdropProps: { ...backDropStyles(theme) }, sx: modalStyles(theme) }} />
</MockMainContextProvider>
);
};
+3 -1
View File
@@ -12,7 +12,7 @@ import { SendInputModal } from './SendInputModal';
import { SendSuccessModal } from './SendSuccessModal';
import { TTransactionDetails } from './types';
export const SendModal = ({ onClose }: { onClose: () => void }) => {
export const SendModal = ({ onClose, hasStorybookStyles }: { onClose: () => void; hasStorybookStyles?: {} }) => {
const [toAddress, setToAddress] = useState<string>('');
const [amount, setAmount] = useState<DecCoin>();
const [modal, setModal] = useState<'send' | 'send details'>('send');
@@ -108,6 +108,7 @@ export const SendModal = ({ onClose }: { onClose: () => void }) => {
onSend={handleSend}
denom={clientDetails?.display_mix_denom || 'nym'}
memo={memo}
{...hasStorybookStyles}
/>
);
@@ -129,6 +130,7 @@ export const SendModal = ({ onClose }: { onClose: () => void }) => {
onUserFeesChange={(value) => setUserFees(value)}
onMemoChange={(value) => setMemo(value)}
setShowMore={setShowMoreOptions}
{...hasStorybookStyles}
/>
);
};
+2 -2
View File
@@ -2,10 +2,10 @@ import React, { useContext } from 'react';
import { AppContext } from 'src/context';
import { SendModal } from './SendModal';
export const Send = () => {
export const Send = ({ hasStorybookStyles }: { hasStorybookStyles?: {} }) => {
const { showSendModal, handleShowSendModal } = useContext(AppContext);
if (showSendModal) return <SendModal onClose={handleShowSendModal} />;
if (showSendModal) return <SendModal onClose={handleShowSendModal} hasStorybookStyles={hasStorybookStyles} />;
return null;
};
@@ -0,0 +1,24 @@
import * as React from 'react';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { Box } from '@mui/material';
import { TokenPoolSelector } from './TokenPoolSelector';
import { MockMainContextProvider } from '../context/mocks/main';
export default {
title: 'Wallet / Token pool',
component: TokenPoolSelector,
} as ComponentMeta<typeof TokenPoolSelector>;
const Template: ComponentStory<typeof TokenPoolSelector> = (args) => (
<Box mt={2} height={800}>
<MockMainContextProvider>
<TokenPoolSelector {...args} />
</MockMainContextProvider>
</Box>
);
export const Default = Template.bind({});
Default.args = {
disabled: false,
onSelect: () => {},
};
+19 -1
View File
@@ -118,7 +118,25 @@ export const MockDelegationContextProvider: FCWithChildren = ({ children }) => {
};
};
const updateDelegation = async (newDelegation: DelegationWithEverything): Promise<TDelegationTransaction> => {
const updateDelegation = async (
newDelegation: DelegationWithEverything,
ignorePendingForStorybook?: boolean,
): Promise<TDelegationTransaction> => {
if (ignorePendingForStorybook) {
mockDelegations = mockDelegations.map((d) => {
if (d.node_identity === newDelegation.node_identity) {
return { ...newDelegation };
}
return d;
});
await recalculate();
triggerStateUpdate();
return {
transactionUrl:
'https://sandbox-blocks.nymtech.net/transactions/55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354',
};
}
await mockSleep(SLEEP_MS);
mockDelegations = mockDelegations.map((d) => {
if (d.node_identity === newDelegation.node_identity) {
@@ -0,0 +1,61 @@
import * as React from 'react';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { Stack, TextField } from '@mui/material';
import { PasswordStrength } from './password-strength';
export default {
title: 'Wallet / Password Strength',
component: PasswordStrength,
} as ComponentMeta<typeof PasswordStrength>;
const Template: ComponentStory<typeof PasswordStrength> = ({ password, withWarnings, handleIsSafePassword }) => {
const [value, setValue] = React.useState(password);
return (
<Stack alignContent="center">
<TextField value={value} onChange={(e) => setValue(e.target.value)} sx={{ mb: 0.5 }} />
{!!password.length && (
<PasswordStrength handleIsSafePassword={handleIsSafePassword} withWarnings={withWarnings} password={password} />
)}
</Stack>
);
};
export const VeryStrong = Template.bind({});
VeryStrong.args = { password: 'fedgklnrf34£', withWarnings: true, handleIsSafePassword: () => undefined };
export const Strong = Template.bind({});
Strong.args = { password: '"56%abc123?@', withWarnings: true, handleIsSafePassword: () => undefined };
export const Average = Template.bind({});
Average.args = { password: '"abc123?', withWarnings: true, handleIsSafePassword: () => undefined };
export const Weak = Template.bind({});
Weak.args = { password: 'abc123?', withWarnings: true, handleIsSafePassword: () => undefined };
export const VeryWeak = Template.bind({});
VeryWeak.args = {
password: 'abc123',
withWarnings: true,
handleIsSafePassword: () => undefined,
};
export const WithName = Template.bind({});
WithName.args = {
password: 'fred',
withWarnings: true,
handleIsSafePassword: () => undefined,
};
export const WithSequence = Template.bind({});
WithSequence.args = {
password: '121212',
withWarnings: true,
handleIsSafePassword: () => undefined,
};
export const Default = Template.bind({});
Default.args = {
password: 'abc123',
withWarnings: true,
handleIsSafePassword: () => undefined,
};
@@ -0,0 +1,13 @@
import * as React from 'react';
import { BondingPage } from './Bonding';
import { MockBondingContextProvider } from '../../context/mocks/bonding';
export default {
title: 'Bonding/Flows/Mock',
};
export const Default = () => (
<MockBondingContextProvider>
<BondingPage />
</MockBondingContextProvider>
);
@@ -0,0 +1,140 @@
import React, { useEffect, useState } from 'react';
import { Box, Card, CardContent, CardHeader, Grid, Typography } from '@mui/material';
import { ResultsTable } from 'src/components/RewardsPlayground/ResultsTable';
import { getDelegationSummary } from 'src/requests';
import { NodeDetails } from 'src/components/RewardsPlayground/NodeDetail';
import { CalculateArgs, Inputs } from 'src/components/RewardsPlayground/Inputs';
import { useSnackbar } from 'notistack';
import { LoadingModal } from 'src/components/Modals/LoadingModal';
import { Console } from 'src/utils/console';
import { TBondedMixnode } from 'src/requests/mixnodeDetails';
import { computeEstimate, computeStakeSaturation, handleCalculatePeriodRewards } from './utils';
export type DefaultInputValues = {
profitMargin: string;
uptime: string;
bond: string;
delegations: string;
operatorCost: string;
};
export const ApyPlayground = ({ bondedNode }: { bondedNode: TBondedMixnode }) => {
const { enqueueSnackbar } = useSnackbar();
const [results, setResults] = useState({
total: { daily: '-', monthly: '-', yearly: '-' },
operator: { daily: '-', monthly: '-', yearly: '-' },
delegator: { daily: '-', monthly: '-', yearly: '-' },
});
const [defaultInputValues, setDefaultInputValues] = useState<DefaultInputValues>();
const [stakeSaturation, setStakeSaturation] = useState<string>();
const [isLoading, setIsLoading] = useState(true);
const initialise = async (node: TBondedMixnode) => {
try {
const delegations = await getDelegationSummary();
const { estimation } = await computeEstimate({
mixId: node.mixId,
uptime: node.uptime.toString(),
profitMargin: node.profitMargin,
operatorCost: node.operatorCost.amount,
totalDelegation: delegations.total_delegations.amount,
pledgeAmount: node.bond.amount,
});
setResults(
handleCalculatePeriodRewards({
estimatedOperatorReward: estimation.operator,
estimatedDelegatorsReward: estimation.delegates,
}),
);
setStakeSaturation(node.stakeSaturation);
setDefaultInputValues({
profitMargin: node.profitMargin,
uptime: (node.uptime || 0).toString(),
bond: node.bond.amount || '',
delegations: delegations.total_delegations.amount,
operatorCost: node.operatorCost.amount,
});
setIsLoading(false);
} catch (e) {
enqueueSnackbar(e as string, { variant: 'error' });
}
};
useEffect(() => {
if (bondedNode) {
initialise(bondedNode);
}
}, []);
if (isLoading) return <LoadingModal />;
const handleCalculateEstimate = async ({ bond, delegations, uptime, profitMargin, operatorCost }: CalculateArgs) => {
try {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { estimation, reward_params } = await computeEstimate({
mixId: bondedNode.mixId,
uptime,
profitMargin,
operatorCost,
totalDelegation: delegations,
pledgeAmount: bond,
});
const estimationResult = handleCalculatePeriodRewards({
estimatedOperatorReward: estimation.operator,
estimatedDelegatorsReward: estimation.delegates,
});
const computedStakeSaturation = computeStakeSaturation(
bond,
delegations,
reward_params.interval.stake_saturation_point,
);
setStakeSaturation(computedStakeSaturation);
setResults(estimationResult);
} catch (e) {
Console.log(e);
}
};
return (
<Box sx={{ p: 3 }}>
<Typography fontWeight="medium" sx={{ mb: 1 }}>
Playground
</Typography>
<Typography variant="body2" sx={{ color: 'grey.600', mb: 2 }}>
This is your parameters playground - change the parameters below to see the node specific estimations in the
table
</Typography>
{defaultInputValues && (
<Card variant="outlined" sx={{ p: 1, mb: 3 }}>
<CardHeader
title={
<Typography variant="body2" fontWeight="medium">
Estimation calculator
</Typography>
}
/>
<CardContent>
<Inputs onCalculate={handleCalculateEstimate} defaultValues={defaultInputValues} />
</CardContent>
</Card>
)}
<Grid container spacing={3}>
<Grid item xs={12} md={8}>
<ResultsTable results={results} />
</Grid>
<Grid item xs={12} md={4}>
<NodeDetails saturation={stakeSaturation} />
</Grid>
</Grid>
</Box>
);
};
@@ -0,0 +1,66 @@
import { decimalToPercentage, percentToDecimal } from '@nymproject/types';
import { computeMixnodeRewardEstimation } from 'src/requests';
const SCALE_FACTOR = 1_000_000;
export const computeStakeSaturation = (bond: string, delegations: string, stakeSaturationPoint: string) => {
const res = ((+bond + +delegations) * SCALE_FACTOR) / +stakeSaturationPoint;
return decimalToPercentage(res.toFixed(18).toString());
};
export const computeEstimate = async ({
mixId,
uptime,
pledgeAmount,
totalDelegation,
profitMargin,
operatorCost,
}: {
mixId: number;
uptime: string;
pledgeAmount: string;
totalDelegation: string;
profitMargin: string;
operatorCost: string;
}) => {
const computedEstimate = await computeMixnodeRewardEstimation({
mixId,
performance: percentToDecimal(uptime),
pledgeAmount: Math.round(+pledgeAmount * SCALE_FACTOR),
totalDelegation: Math.round(+totalDelegation * SCALE_FACTOR),
profitMarginPercent: percentToDecimal(profitMargin),
intervalOperatingCost: { denom: 'unym', amount: Math.round(+operatorCost * SCALE_FACTOR).toString() },
});
return computedEstimate;
};
export const handleCalculatePeriodRewards = ({
estimatedOperatorReward,
estimatedDelegatorsReward,
}: {
estimatedOperatorReward: string;
estimatedDelegatorsReward: string;
}) => {
const dailyOperatorReward = (+estimatedOperatorReward / SCALE_FACTOR) * 24; // epoch_reward * 1 epoch_per_hour * 24 hours
const dailyDelegatorReward = (+estimatedDelegatorsReward / SCALE_FACTOR) * 24;
const dailyTotal = dailyOperatorReward + dailyDelegatorReward;
return {
total: {
daily: dailyTotal.toFixed(3).toString(),
monthly: (dailyTotal * 30).toFixed(3).toString(),
yearly: (dailyTotal * 365).toFixed(3).toString(),
},
operator: {
daily: dailyOperatorReward.toFixed(3).toString(),
monthly: (dailyOperatorReward * 30).toFixed(3).toString(),
yearly: (dailyOperatorReward * 365).toFixed(3).toString(),
},
delegator: {
daily: dailyDelegatorReward.toFixed(3).toString(),
monthly: (dailyDelegatorReward * 30).toFixed(3).toString(),
yearly: (dailyDelegatorReward * 365).toFixed(3).toString(),
},
};
};
@@ -0,0 +1,10 @@
import React from 'react';
import { Tutorial } from '../../components/Buy/Tutorial';
export default {
title: 'Buy/Page',
component: Tutorial,
};
export const BuyPage = () => <Tutorial />;
@@ -0,0 +1,19 @@
import * as React from 'react';
import { DelegationPage } from './index';
import { MockDelegationContextProvider } from '../../context/mocks/delegations';
import { MockRewardsContextProvider } from '../../context/mocks/rewards';
import { MockMainContextProvider } from '../../context/mocks/main';
export default {
title: 'Delegation/Flows/Mock',
};
export const Default = () => (
<MockMainContextProvider>
<MockDelegationContextProvider>
<MockRewardsContextProvider>
<DelegationPage isStorybook />
</MockRewardsContextProvider>
</MockDelegationContextProvider>
</MockMainContextProvider>
);
+13 -3
View File
@@ -19,9 +19,18 @@ import { UndelegateModal } from '../../components/Delegation/UndelegateModal';
import { DelegationListItemActions } from '../../components/Delegation/DelegationActions';
import { RedeemModal } from '../../components/Rewards/RedeemModal';
import { DelegationModal, DelegationModalProps } from '../../components/Delegation/DelegationModal';
import { backDropStyles, modalStyles } from '../../../.storybook/storiesStyles';
import { VestingWarningModal } from '../../components/VestingWarningModal';
export const Delegation: FC = () => {
const storybookStyles = (theme: Theme, isStorybook?: boolean, backdropProps?: object) =>
isStorybook
? {
backdropProps: { ...backDropStyles(theme), ...backdropProps },
sx: modalStyles(theme),
}
: {};
export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
const [showNewDelegationModal, setShowNewDelegationModal] = useState<boolean>(false);
const [showDelegateMoreModal, setShowDelegateMoreModal] = useState<boolean>(false);
const [showUndelegateModal, setShowUndelegateModal] = useState<boolean>(false);
@@ -463,6 +472,7 @@ export const Delegation: FC = () => {
accountBalance={balance?.printable_balance}
rewardInterval="weekly"
hasVestingContract={Boolean(originalVesting)}
{...storybookStyles(theme, isStorybook)}
/>
)}
@@ -538,10 +548,10 @@ export const Delegation: FC = () => {
);
};
export const DelegationPage: FC = () => (
export const DelegationPage: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => (
<DelegationContextProvider>
<RewardsContextProvider>
<Delegation />
<Delegation isStorybook={isStorybook} />
</RewardsContextProvider>
</DelegationContextProvider>
);
@@ -0,0 +1,7 @@
import { Meta } from '@storybook/addon-docs';
<Meta title="Introduction" />
# Nym Wallet Storybook
This is the Storybook for the Nym Wallet.
@@ -0,0 +1,10 @@
import * as React from 'react';
import { ComponentMeta } from '@storybook/react';
import { Playground } from '@nymproject/react/playground/Playground';
export default {
title: 'Playground',
component: Playground,
} as ComponentMeta<typeof Playground>;
export const AllControls = () => <Playground />;
+3 -1
View File
@@ -5,11 +5,13 @@
"noEmit": true
},
"include": [
".storybook/*.js",
"webpack.*.js",
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx"
"src/**/*.tsx",
"src/**/*.stories.*",
],
"exclude": [
"node_modules",