Compare commits

...

27 Commits

Author SHA1 Message Date
fmtabbara e68a2c2ae3 create reuseable ActionMenu component 2022-07-21 11:31:03 +01:00
fmtabbara e945c94afc rebuild BondedNodeCard using existing shared components 2022-07-20 22:01:14 +01:00
fmtabbara 641b8179ba fix displayed denom 2022-07-20 12:22:36 +01:00
fmtabbara df4385ab71 update coin types in new bonding page 2022-07-18 11:35:59 +01:00
pierre 777166b93d feat(wallet-bonding): fetch mixnode status 2022-07-18 11:10:08 +01:00
pierre ccd889fc38 chore(wallet-bonding): add todo 2022-07-18 11:09:58 +01:00
pierre d9291df347 fix(wallet-bonding): bonding context mock 2022-07-18 11:09:33 +01:00
pierre 5fcce2de48 fix(wallet-bonding): bonding context mock 2022-07-18 11:09:13 +01:00
pierre abf9ccb823 refactor(wallet-bonding): switch to simpledialog component to keep modals consistency 2022-07-18 11:09:11 +01:00
pierre 352527a098 feat(wallet-bonding): unbond with gasFee and request 2022-07-18 11:08:45 +01:00
pierre 724888e790 feat(wallet-bonding): unbond with gasFee and request 2022-07-18 11:08:38 +01:00
pierre 4b552db19f refactor(wallet-bonding): bonding flow with new gasFee estimation 2022-07-18 11:08:38 +01:00
pierre 75aa2579a0 feat(wallet-bonding): node menu ui 2022-07-18 11:08:38 +01:00
pierre 2493abcdff various ui adjustments 2022-07-18 11:08:23 +01:00
pierre 508f8324f9 feat(wallet): use confirmation modal component 2022-07-18 11:08:14 +01:00
pierre 468d0f38e9 feat(wallet-bonding): bond more flow (done) 2022-07-18 11:07:07 +01:00
pierre 3382642d70 feat(wallet-bonding): node settings flow 2022-07-18 11:07:07 +01:00
pierre 8d3f1a3c38 feat(wallet-bonding): new dialog component 2022-07-18 11:07:07 +01:00
pierre c5e695f8b5 refactor(wallet-bonding): code structure 2022-07-18 11:07:07 +01:00
pierre 80cfe83f9d refactor(wallet-bonding): code structure 2022-07-18 11:06:58 +01:00
pierre 76a22035be feat(wallet-bonding): node settings wip 2022-07-18 11:06:51 +01:00
pierre 79bc2ab493 feat(wallet-bonding): add node table component 2022-07-18 11:06:39 +01:00
pierre 8c2c0f8033 fix(wallet-bonding): post merge 2022-07-18 11:06:39 +01:00
pierre 5deb0875e2 feat(wallet-bonding): bonding page, new bond form wip 2022-07-18 11:06:39 +01:00
pierre d1ee6faca8 feat(wallet-bonding): bonding page, new bond form wip 2022-07-18 11:06:24 +01:00
pierre 756bad977f feat(wallet-bonding): add context mock 2022-07-18 11:04:12 +01:00
Mark Sinclair b6448691ce feat(wallet-bonding): create context wip 2022-07-18 11:04:02 +01:00
64 changed files with 3159 additions and 83 deletions
+44
View File
@@ -0,0 +1,44 @@
import React, { useRef } from 'react';
import { MoreVertSharp } from '@mui/icons-material';
import { IconButton, ListItemIcon, ListItemText, Menu, MenuItem } from '@mui/material';
export const ActionsMenu: React.FC<{ open: boolean; onOpen: () => void; onClose: () => void }> = ({
children,
open,
onOpen,
onClose,
}) => {
const anchorEl: any = useRef<HTMLElement>();
return (
<>
<IconButton ref={anchorEl} onClick={onOpen}>
<MoreVertSharp />
</IconButton>
<Menu anchorEl={anchorEl.current} open={open} onClose={onClose}>
{children}
</Menu>
</>
);
};
export const ActionsMenuItem = ({
title,
description,
onClick,
Icon,
disabled,
}: {
title: string;
description?: string;
onClick?: () => void;
Icon?: React.ReactNode;
disabled?: boolean;
}) => {
return (
<MenuItem sx={{ p: 2 }} onClick={onClick} disabled={disabled}>
<ListItemIcon sx={{ color: 'text.primary' }}>{Icon}</ListItemIcon>
<ListItemText sx={{ color: 'text.primary' }} primary={title} secondary={description} />
</MenuItem>
);
};
@@ -0,0 +1 @@
// TODO
@@ -36,7 +36,7 @@ export const CopyToClipboard = ({ text = '', iconButton }: { text?: string; icon
color: 'text.primary',
}}
>
{!copied ? <ContentCopy fontSize="small" /> : <Check color="success" />}
{!copied ? <ContentCopy sx={{ fontSize: 14 }} /> : <Check color="success" sx={{ fontSize: 14 }} />}
</IconButton>
</Tooltip>
);
@@ -219,6 +219,7 @@ export const DelegateModal: React.FC<{
initialValue={amount}
autoFocus={Boolean(initialIdentityKey)}
onChanged={handleAmountChanged}
denom={currency}
/>
</Box>
<Typography
@@ -1,19 +1,8 @@
import React from 'react';
import {
Box,
Button,
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Stack,
Tooltip,
Typography,
} from '@mui/material';
import { MoreVertSharp } from '@mui/icons-material';
import React, { useState } from 'react';
import { Box, Button, ListItemIcon, ListItemText, MenuItem, Stack, Tooltip, Typography } from '@mui/material';
import { DelegationEventKind } from '@nymproject/types';
import { Delegate, Undelegate } from '../../svg-icons';
import { ActionsMenu, ActionsMenuItem } from '../ActionsMenu';
import { DelegateListItemPending } from './types';
export type DelegationListItemActions = 'delegate' | 'undelegate' | 'redeem' | 'compound';
@@ -100,17 +89,14 @@ export const DelegationsActionsMenu: React.FC<{
disableRedeemingRewards?: boolean;
disableCompoundRewards?: boolean;
}> = ({ disableRedeemingRewards, disableCompoundRewards, onActionClick, isPending }) => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const [isOpen, setIsOpen] = useState(false);
const handleClose = () => setAnchorEl(null);
const handleOpenMenu = () => setIsOpen(true);
const handleOnClose = () => setIsOpen(false);
const handleActionSelect = (action: DelegationListItemActions) => {
handleClose();
onActionClick?.(action);
handleOnClose();
};
if (isPending) {
@@ -126,37 +112,28 @@ export const DelegationsActionsMenu: React.FC<{
}
return (
<>
<IconButton onClick={handleClick}>
<MoreVertSharp />
</IconButton>
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
<DelegationActionsMenuItem
title="Delegate more"
Icon={<Delegate />}
onClick={() => handleActionSelect?.('delegate')}
/>
<DelegationActionsMenuItem
title="Undelegate"
Icon={<Undelegate />}
onClick={() => handleActionSelect?.('undelegate')}
disabled={false}
/>
<DelegationActionsMenuItem
title="Redeem"
description="Trasfer your rewards to your balance"
Icon={<Typography sx={{ pl: 1 }}>R</Typography>}
onClick={() => handleActionSelect?.('redeem')}
disabled={disableRedeemingRewards}
/>
<DelegationActionsMenuItem
title="Compound"
description="Add your rewards to this delegation"
Icon={<Typography sx={{ pl: 1 }}>C</Typography>}
onClick={() => handleActionSelect?.('compound')}
disabled={disableCompoundRewards}
/>
</Menu>
</>
<ActionsMenu open={isOpen} onOpen={handleOpenMenu} onClose={handleOnClose}>
<ActionsMenuItem title="Delegate more" Icon={<Delegate />} onClick={() => handleActionSelect('delegate')} />
<ActionsMenuItem
title="Undelegate"
Icon={<Undelegate />}
onClick={() => handleActionSelect('undelegate')}
disabled={false}
/>
<ActionsMenuItem
title="Redeem"
description="Trasfer your rewards to your balance"
Icon={<Typography sx={{ pl: 1 }}>R</Typography>}
onClick={() => handleActionSelect('redeem')}
disabled={disableRedeemingRewards}
/>
<ActionsMenuItem
title="Compound"
description="Add your rewards to this delegation"
Icon={<Typography sx={{ pl: 1 }}>C</Typography>}
onClick={() => handleActionSelect('compound')}
disabled={disableCompoundRewards}
/>
</ActionsMenu>
);
};
+13
View File
@@ -0,0 +1,13 @@
import React from 'react';
import { Stack, Typography } from '@mui/material';
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
import { splice } from 'src/utils';
export const IdentityKey = ({ identityKey }: { identityKey: string }) => (
<Stack direction="row">
<Typography variant="body2" component="span" fontWeight={400} sx={{ mr: 1, color: 'text.primary' }}>
{splice(6, identityKey)}
</Typography>
<CopyToClipboard value={identityKey} sx={{ fontSize: 18 }} />
</Stack>
);
+7 -1
View File
@@ -3,7 +3,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
import { List, ListItem, ListItemIcon, ListItemText } from '@mui/material';
import { AccountBalanceWalletOutlined, ArrowBack, ArrowForward, Description, Settings } from '@mui/icons-material';
import { AppContext } from '../context/main';
import { Bond, Delegate, Unbond } from '../svg-icons';
import { Bond, Delegate, Unbond, Bonding } from '../svg-icons';
export const Nav = () => {
const location = useLocation();
@@ -29,6 +29,12 @@ export const Nav = () => {
Icon: ArrowBack,
onClick: () => navigate('/receive'),
},
{
label: 'Bonding',
route: '/bonding',
Icon: Bond,
onClick: () => navigate('/bonding'),
},
{
label: 'Bond',
route: '/bond',
@@ -30,6 +30,7 @@ export const SendInput = () => {
<SendInputModal
toAddress=""
fromAddress="nymt1w8qp7zsxggvtxhpqpt6e329j42wtv07dm5ts8u"
denom="NYM"
onNext={() => {}}
onClose={() => {}}
onAddressChange={() => {}}
@@ -13,6 +13,7 @@ export const SendInputModal = ({
amount,
balance,
error,
denom,
onNext,
onClose,
onAmountChange,
@@ -25,6 +26,7 @@ export const SendInputModal = ({
amount?: DecCoin;
balance?: string;
error?: string;
denom: string;
onNext: () => void;
onClose: () => void;
onAmountChange: (value: DecCoin) => void;
@@ -69,6 +71,7 @@ export const SendInputModal = ({
validate(value);
}}
initialValue={amount?.amount}
denom={denom}
/>
<Typography fontSize="smaller" sx={{ color: 'error.main' }}>
{error}
@@ -89,6 +89,7 @@ export const SendModal = ({ onClose, hasStorybookStyles }: { onClose: () => void
error={error}
onAmountChange={(value) => setAmount(value)}
onAddressChange={(value) => setToAddress(value)}
denom={denom}
{...hasStorybookStyles}
/>
);
+396
View File
@@ -0,0 +1,396 @@
import { FeeDetails, DecCoin, MixnodeStatus, TransactionExecuteResult } from '@nymproject/types';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import type { Network } from 'src/types';
import { TBondGatewayArgs, TBondMixNodeArgs } from 'src/types';
import {
bondGateway as bondGatewayRequest,
bondMixNode as bondMixNodeRequest,
claimOperatorRewards,
compoundOperatorRewards,
simulateBondGateway,
simulateBondMixnode,
simulateUnbondGateway,
simulateUnbondMixnode,
simulateVestingBondGateway,
simulateVestingBondMixnode,
simulateVestingUnbondGateway,
simulateVestingUnbondMixnode,
simulateUpdateMixnode,
simulateVestingUpdateMixnode,
unbondGateway as unbondGatewayRequest,
unbondMixNode as unbondMixnodeRequest,
vestingBondGateway,
vestingBondMixNode,
vestingUnbondGateway,
vestingUnbondMixnode,
updateMixnode as updateMixnodeRequest,
vestingUpdateMixnode as updateMixnodeVestingRequest,
getGatewayBondDetails,
getMixnodeBondDetails,
getMixnodeStatus,
} from '../requests';
import { useGetFee } from '../hooks/useGetFee';
import { useCheckOwnership } from '../hooks/useCheckOwnership';
import { AppContext } from './main';
const bounded: BondedMixnode = {
identityKey: 'B2Xx4haarLWMajX8w259oHjtRZsC7nHwagbWrJNiA3QC',
bond: { denom: 'nym', amount: '1234' },
delegators: 123,
ip: '1.2.34.5',
nodeRewards: { denom: 'nym', amount: '123' },
operatorRewards: { denom: 'nym', amount: '12' },
profitMargin: 10,
stake: { denom: 'nym', amount: '99' },
stakeSaturation: 99,
status: 'active',
};
/* const bounded: BondedMixnode | BondedGateway = {
bond: { denom: 'nym', amount: '1234' },
identityKey: 'B2Xx4haarLWMajX8w259oHjtRZsC7nHwagbWrJNiA3QC',
ip: '1.2.34.5',
location: 'France',
}; */
// TODO add relevant data
export interface BondedMixnode {
identityKey: string;
ip: string;
stake: DecCoin;
bond: DecCoin;
stakeSaturation: number;
profitMargin: number;
nodeRewards: DecCoin;
operatorRewards: DecCoin;
delegators: number;
status: MixnodeStatus;
}
// TODO add relevant data
export interface BondedGateway {
identityKey: string;
ip: string;
bond: DecCoin;
location?: string; // TODO not yet available, only available in Network Explorer API
}
export type TokenPool = 'locked' | 'balance';
export type FeeOperation =
| 'bondMixnode'
| 'bondMixnodeWithVesting'
| 'bondGateway'
| 'bondGatewayWithVesting'
| 'unbondMixnode'
| 'unbondGateway'
| 'updateMixnode'
| 'bondMore'
| 'compoundRewards'
| 'redeemRewards';
export type TBondingContext = {
loading: boolean;
error?: string;
bondedMixnode?: BondedMixnode | null;
bondedGateway?: BondedGateway | null;
refresh: () => Promise<void>;
bondMixnode: (
data: Omit<TBondMixNodeArgs, 'fee'>,
tokenPool: TokenPool,
) => Promise<TransactionExecuteResult | undefined>;
bondGateway: (
data: Omit<TBondGatewayArgs, 'fee'>,
tokenPool: TokenPool,
) => Promise<TransactionExecuteResult | undefined>;
bondMore: (signature: string, additionalBond: DecCoin) => Promise<TransactionExecuteResult | undefined>;
unbondMixnode: () => Promise<TransactionExecuteResult | undefined>;
unbondGateway: () => Promise<TransactionExecuteResult | undefined>;
redeemRewards: () => Promise<TransactionExecuteResult[] | undefined>;
compoundRewards: () => Promise<TransactionExecuteResult[] | undefined>;
updateMixnode: (pm: number) => Promise<TransactionExecuteResult | undefined>;
fee?: FeeDetails;
getFee: <T>(feeOperation: FeeOperation, args: T) => Promise<FeeDetails | undefined>;
feeDetails?: FeeDetails;
feeLoading: boolean;
feeError?: string;
resetFeeState: () => void;
};
export const BondingContext = createContext<TBondingContext>({
loading: true,
feeLoading: false,
refresh: async () => undefined,
bondMixnode: async () => {
throw new Error('Not implemented');
},
bondGateway: async () => {
throw new Error('Not implemented');
},
unbondMixnode: async () => {
throw new Error('Not implemented');
},
unbondGateway: async () => {
throw new Error('Not implemented');
},
redeemRewards: async () => {
throw new Error('Not implemented');
},
compoundRewards: async () => {
throw new Error('Not implemented');
},
getFee(): Promise<FeeDetails> {
throw new Error('Not implemented');
},
resetFeeState(): void {},
updateMixnode: async () => {
throw new Error('Not implemented');
},
bondMore(): Promise<TransactionExecuteResult | undefined> {
throw new Error('Not implemented');
},
});
export const BondingContextProvider = ({
network,
children,
}: {
network?: Network;
children?: React.ReactNode;
}): JSX.Element => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string>();
const [bondedMixnode, setBondedMixnode] = useState<BondedMixnode | null>(null);
const [bondedGateway, setBondedGateway] = useState<BondedGateway | null>(null);
const { userBalance } = useContext(AppContext);
const { ownership, checkOwnership } = useCheckOwnership();
const { fee, resetFeeState, feeError, isFeeLoading } = useGetFee();
const isVesting = Boolean(ownership.vestingPledge);
console.log(ownership);
useEffect(() => {
if (feeError) {
setError(`An error occurred while retrieving fee: ${feeError}`);
}
}, [feeError]);
const resetState = () => {
setLoading(true);
setError(undefined);
setBondedGateway(null);
setBondedMixnode(null);
};
const refresh = useCallback(async () => {
let data, status;
setLoading(true);
if (ownership.hasOwnership && ownership.nodeType === 'mixnode') {
try {
data = await getMixnodeBondDetails();
status = data ? await getMixnodeStatus(data?.mix_node.identity_key) : undefined;
} catch (e: any) {
setError(`While fetching current bond state, an error occurred: ${e}`);
}
// TODO convert the returned data from the request to `BondedMixnode` type
setBondedMixnode(bounded);
}
if (ownership.hasOwnership && ownership.nodeType === 'gateway') {
try {
data = await getGatewayBondDetails();
status = data ? await getMixnodeStatus(data?.gateway.identity_key) : undefined;
} catch (e: any) {
setError(`While fetching current bond state, an error occurred: ${e}`);
}
// TODO convert the returned data from the request to `BondedGateway` type
setBondedGateway(bounded);
}
setLoading(false);
}, [network, ownership]);
useEffect(() => {
resetState();
refresh();
}, [network, ownership]);
const bondMixnode = async (data: Omit<TBondMixNodeArgs, 'fee'>, tokenPool: TokenPool) => {
let tx: TransactionExecuteResult | undefined;
const payload = {
...data,
fee: fee?.fee,
};
setLoading(true);
try {
if (tokenPool === 'balance') {
tx = await bondMixNodeRequest(payload);
await userBalance.fetchBalance();
}
if (tokenPool === 'locked') {
tx = await vestingBondMixNode(payload);
await userBalance.fetchTokenAllocation();
}
return tx;
} catch (e: any) {
setError(`an error occurred: ${e}`);
} finally {
setLoading(false);
}
return undefined;
};
const bondGateway = async (data: Omit<TBondGatewayArgs, 'fee'>, tokenPool: TokenPool) => {
let tx: TransactionExecuteResult | undefined;
const payload = {
...data,
fee: fee?.fee,
};
setLoading(true);
try {
if (tokenPool === 'balance') {
tx = await bondGatewayRequest(payload);
await userBalance.fetchBalance();
}
if (tokenPool === 'locked') {
tx = await vestingBondGateway(payload);
await userBalance.fetchTokenAllocation();
}
return tx;
} catch (e: any) {
setError(`an error occurred: ${e}`);
} finally {
setLoading(false);
}
return undefined;
};
const unbondMixnode = async () => {
let tx;
setLoading(true);
try {
if (isVesting) tx = await vestingUnbondMixnode(fee?.fee);
if (!isVesting) tx = await unbondMixnodeRequest(fee?.fee);
} catch (e: any) {
setError(`an error occurred: ${e}`);
} finally {
await checkOwnership();
setLoading(false);
}
return tx;
};
const unbondGateway = async () => {
let tx;
setLoading(true);
try {
if (isVesting) tx = await vestingUnbondGateway(fee?.fee);
if (!isVesting) tx = await unbondGatewayRequest(fee?.fee);
} catch (e: any) {
setError(`an error occurred: ${e}`);
} finally {
await checkOwnership();
setLoading(false);
}
return tx;
};
const updateMixnode = async (pm: number) => {
let tx;
setLoading(true);
try {
// TODO use estimated fee, need requests update
if (isVesting) tx = await updateMixnodeRequest(pm);
if (!isVesting) tx = await updateMixnodeVestingRequest(pm);
} catch (e: any) {
setError(`an error occurred: ${e}`);
} finally {
setLoading(false);
}
return tx;
};
const redeemRewards = async () => {
let tx;
setLoading(true);
try {
tx = await claimOperatorRewards(); // TODO use estimated fee, update `claimOperatorRewards`
} catch (e: any) {
setError(`an error occurred: ${e}`);
} finally {
setLoading(false);
}
return tx;
};
const compoundRewards = async () => {
let tx;
setLoading(true);
try {
tx = await compoundOperatorRewards(); // TODO use estimated fee, update `compoundOperatorRewards`
} catch (e: any) {
setError(`an error occurred: ${e}`);
} finally {
setLoading(false);
}
return tx;
};
const bondMore = async (_signature: string, _additionalBond: DecCoin) =>
// TODO to implement
undefined;
const feeOps = useMemo(
() => ({
bondMixnode: simulateBondMixnode,
bondMixnodeWithVesting: simulateVestingBondMixnode,
bondGateway: simulateBondGateway,
bondGatewayWithVesting: simulateVestingBondGateway,
unbondMixnode: isVesting ? simulateVestingUnbondMixnode : simulateUnbondMixnode,
unbondGateway: isVesting ? simulateVestingUnbondGateway : simulateUnbondGateway,
updateMixnode: isVesting ? simulateVestingUpdateMixnode : simulateUpdateMixnode,
bondMore: () => undefined as unknown as Promise<FeeDetails>, // TODO fee request to implement
compoundRewards: () => undefined as unknown as Promise<FeeDetails>, // TODO fee request to implement
redeemRewards: () => undefined as unknown as Promise<FeeDetails>, // TODO fee request to implement
}),
[isVesting],
);
const getFee = async (feeOperation: FeeOperation, args: any) => {
let details;
try {
details = feeOps[feeOperation](args);
} catch (e: any) {
setError(`An error occurred while retrieving fee: ${e}`);
}
return details;
};
const memoizedValue = useMemo(
() => ({
loading,
error,
bondMixnode,
bondedMixnode,
bondedGateway,
bondGateway,
unbondMixnode,
unbondGateway,
updateMixnode,
refresh,
redeemRewards,
compoundRewards,
feeLoading: isFeeLoading,
feeError,
getFee,
fee,
resetFeeState,
bondMore,
}),
[loading, error, bondedMixnode, bondedGateway, isFeeLoading, feeError, fee, resetFeeState, isVesting],
);
return <BondingContext.Provider value={memoizedValue}>{children}</BondingContext.Provider>;
};
export const useBondingContext = () => useContext<TBondingContext>(BondingContext);
+1
View File
@@ -1,3 +1,4 @@
export * from './main';
export * from './auth';
export * from './accounts';
export * from './bonding';
+187
View File
@@ -0,0 +1,187 @@
import { FeeDetails, DecCoin, TransactionExecuteResult } from '@nymproject/types';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import type { Network } from 'src/types';
import { BondedGateway, BondedMixnode, BondingContext, FeeOperation } from '../bonding';
import { mockSleep } from './utils';
const SLEEP_MS = 1000;
const bondedMixnodeMock: BondedMixnode = {
identityKey: '7mjM2fYbtN6kxMwp1TrmQ4VwPks3URR5pBgWPWhzT98F',
ip: '112.43.234.56',
stake: { denom: 'nym', amount: '1234' },
bond: { denom: 'nym', amount: '1234' },
stakeSaturation: 95,
profitMargin: 15,
nodeRewards: { denom: 'nym', amount: '1234' },
operatorRewards: { denom: 'nym', amount: '1234' },
delegators: 5423,
status: 'active',
};
const bondedGatewayMock: BondedGateway = {
identityKey: 'WayM2fYbtN6kxMwp1TrmQ4VwPks3URR5pBgWPWhzT98F',
ip: '112.43.234.57',
bond: { denom: 'nym', amount: '1234' },
};
const TxResultMock: TransactionExecuteResult = {
logs_json: '',
data_json: '',
transaction_hash: '55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354',
gas_info: {
gas_wanted: { gas_units: BigInt(1) },
gas_used: { gas_units: BigInt(1) },
},
fee: { amount: '1', denom: 'nym' },
};
const feeMock: FeeDetails = {
amount: { denom: 'nym', amount: '1' },
fee: { Auto: 1 },
};
export const MockBondingContextProvider = ({
network,
children,
}: {
network?: Network;
children?: React.ReactNode;
}): JSX.Element => {
const [loading, setLoading] = useState(true);
const [feeLoading, setFeeLoading] = useState(false);
const [fee, setFee] = useState<FeeDetails | undefined>();
const [error, setError] = useState<string>();
const [bondedData, setBondedData] = useState<BondedMixnode | BondedGateway | null>(null);
const [bondedMixnode, setBondedMixnode] = useState<BondedMixnode | null>(null);
const [bondedGateway, setBondedGateway] = useState<BondedGateway | null>(null);
const [trigger, setTrigger] = useState<Date>(new Date());
const triggerStateUpdate = () => setTrigger(new Date());
const resetState = () => {
setLoading(true);
setError(undefined);
setBondedGateway(null);
setBondedMixnode(null);
};
// fake tauri request
const fetchBondingData: () => Promise<BondedMixnode | BondedGateway | null> = async () => {
await mockSleep(SLEEP_MS);
return bondedData;
};
const refresh = useCallback(async () => {
const bounded = await fetchBondingData();
if (bounded && 'stake' in bounded) {
setBondedMixnode(bounded);
}
if (bounded && !('stake' in bounded)) {
setBondedGateway(bounded);
}
setLoading(false);
}, [network]);
useEffect(() => {
resetState();
refresh();
}, [network, bondedData]);
const bondMixnode = async (): Promise<TransactionExecuteResult> => {
setLoading(true);
await mockSleep(SLEEP_MS);
setBondedData(bondedMixnodeMock);
setLoading(false);
return TxResultMock;
};
const bondGateway = async (): Promise<TransactionExecuteResult> => {
setLoading(true);
await mockSleep(SLEEP_MS);
setBondedData(bondedGatewayMock);
setLoading(false);
return TxResultMock;
};
const unbondMixnode = async (): Promise<TransactionExecuteResult> => {
setLoading(true);
await mockSleep(SLEEP_MS);
setBondedData(null);
setLoading(false);
return TxResultMock;
};
const unbondGateway = async (): Promise<TransactionExecuteResult> => {
setLoading(true);
await mockSleep(SLEEP_MS);
setBondedData(null);
setLoading(false);
return TxResultMock;
};
const redeemRewards = async (): Promise<TransactionExecuteResult[] | undefined> => {
setLoading(true);
await mockSleep(SLEEP_MS);
triggerStateUpdate();
setLoading(false);
return [TxResultMock];
};
const compoundRewards = async (): Promise<TransactionExecuteResult[] | undefined> => {
setLoading(true);
await mockSleep(SLEEP_MS);
triggerStateUpdate();
setLoading(false);
return [TxResultMock];
};
const updateMixnode = async (): Promise<TransactionExecuteResult> => {
setLoading(true);
await mockSleep(SLEEP_MS);
triggerStateUpdate();
setLoading(false);
return TxResultMock;
};
const bondMore = async (_signature: string, _additionalBond: DecCoin) => {
setLoading(true);
await mockSleep(SLEEP_MS);
triggerStateUpdate();
setLoading(false);
return TxResultMock;
};
const getFee = async (_feeOperation: FeeOperation, _args: any) => {
setFeeLoading(true);
await mockSleep(SLEEP_MS);
setFeeLoading(false);
setFee(feeMock);
return feeMock;
};
const resetFeeState = () => {};
const memoizedValue = useMemo(
() => ({
loading,
error,
bondMixnode,
bondGateway,
unbondMixnode,
unbondGateway,
refresh,
redeemRewards,
compoundRewards,
fee,
feeLoading,
getFee,
resetFeeState,
updateMixnode,
bondMore,
}),
[loading, error, bondedMixnode, bondedGateway, trigger, fee],
);
return <BondingContext.Provider value={memoizedValue}>{children}</BondingContext.Provider>;
};
@@ -0,0 +1,13 @@
import * as React from 'react';
import { BondingPage } from './index';
import { MockBondingContextProvider } from '../../context/mocks/bonding';
export default {
title: 'Bonding/Flows/Mock',
};
export const Default = () => (
<MockBondingContextProvider>
<BondingPage />
</MockBondingContextProvider>
);
@@ -0,0 +1,101 @@
import React, { useContext } from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { Box, Divider, Stack, Typography } from '@mui/material';
import { AmountData, NodeType } from '../types';
import { AppContext } from '../../../context';
import amountSchema from './amountSchema';
import { TokenPoolSelector } from '../../../components';
import { TextFieldInput, CurrencyInput } from '../components';
import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from '../../../utils';
import { SimpleModal } from '../../../components/Modals/SimpleModal';
export interface Props {
nodeType: NodeType;
open: boolean;
onClose?: () => void;
onSubmit: (data: AmountData) => Promise<void>;
}
const AmountModal = ({ open, onClose, onSubmit, nodeType }: Props) => {
const {
control,
setValue,
setError,
handleSubmit,
formState: { errors },
} = useForm<AmountData>({
resolver: yupResolver(amountSchema),
defaultValues: {
tokenPool: 'balance',
profitMargin: 10,
},
});
const { userBalance, denom } = useContext(AppContext);
const onSubmitForm = async (data: AmountData) => {
if (data.tokenPool === 'balance' && !(await checkHasEnoughFunds(data.amount.amount || ''))) {
return setError('amount.amount', { message: 'Not enough funds in wallet' });
}
if (data.tokenPool === 'locked' && !(await checkHasEnoughLockedTokens(data.amount.amount || ''))) {
return setError('amount.amount', { message: 'Not enough locked tokens' });
}
return onSubmit(data);
};
return (
<SimpleModal
open={open}
onClose={onClose}
onOk={handleSubmit(onSubmitForm)}
header="Bond"
subHeader="Step 2/2"
okLabel="Next"
>
<Box sx={{ mt: 1 }}>
<form>
{nodeType === 'mixnode' && (
<TextFieldInput
name="profitMargin"
control={control}
defaultValue=""
label="Profit Margin"
placeholder="Profit Margin"
error={Boolean(errors.profitMargin)}
helperText={errors.profitMargin ? errors.profitMargin.message : 'Default is 10%'}
required
muiTextFieldProps={{ fullWidth: true }}
sx={{ mb: 2.5 }}
/>
)}
<Stack direction="row" spacing={2}>
{userBalance.originalVesting && (
<TokenPoolSelector onSelect={(pool) => setValue('tokenPool', pool)} disabled={false} />
)}
<CurrencyInput
control={control}
required
fullWidth
label="Amount"
name="amount"
currencyDenom={denom}
errorMessage={errors.amount?.amount?.message}
/>
</Stack>
</form>
<Stack direction="row" justifyContent="space-between" mt={3}>
<Typography fontWeight={600}>Account balance</Typography>
<Typography fontWeight={600} textTransform="uppercase">
{userBalance.balance?.printable_balance || 0}
</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Typography fontWeight={400}>Est. fee for this transaction will be calculated in the next page</Typography>
</Box>
</SimpleModal>
);
};
export default AmountModal;
@@ -0,0 +1,249 @@
import React, { useContext, useEffect, useReducer } from 'react';
import { Box, Button, Typography } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { TransactionExecuteResult } from '@nymproject/types';
import { ErrorOutline } from '@mui/icons-material';
import { ConfirmationModal, NymCard } from '../../../components';
import NodeIdentityModal from './NodeIdentityModal';
import {
ACTIONTYPE,
BondState,
BondStatus,
FormStep,
GatewayAmount,
GatewayData,
MixnodeAmount,
MixnodeData,
NodeData,
} from '../types';
import AmountModal from './AmountModal';
import { AppContext, urls, useBondingContext } from '../../../context';
import SummaryModal from './SummaryModal';
const initialState: BondState = {
showModal: false,
formStep: 1,
bondStatus: 'init',
};
function reducer(state: BondState, action: ACTIONTYPE) {
let step;
switch (action.type) {
case 'change_bond_type':
return { ...state, type: action.payload };
case 'set_node_data':
return { ...state, nodeData: action.payload };
case 'set_amount_data':
return { ...state, amountData: action.payload };
case 'set_step':
return { ...state, formStep: action.payload };
case 'set_tx':
return { ...state, tx: action.payload };
case 'set_bond_status':
return { ...state, bondStatus: action.payload };
case 'set_error':
return { ...state, error: action.payload, bondStatus: 'error' as BondStatus };
case 'next_step':
step = state.formStep + 1;
return { ...state, formStep: step <= 4 ? (step as FormStep) : 4 };
case 'prev_step':
step = state.formStep - 1;
return { ...state, formStep: step >= 1 ? (step as FormStep) : 1 };
case 'show_modal':
return { ...state, showModal: true };
case 'close_modal':
return { ...state, showModal: false };
case 'reset':
return initialState;
default:
throw new Error();
}
}
const BondingCard = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const { formStep, showModal } = state;
const { clientDetails, network } = useContext(AppContext);
const { error, bondMixnode: bondMixnodeRequest, bondGateway: bondGatewayRequest } = useBondingContext();
useEffect(() => {
dispatch({ type: 'reset' });
}, [clientDetails]);
useEffect(() => {
if (error) {
dispatch({ type: 'set_error', payload: error });
}
}, [error]);
const bondMixnode = async () => {
const { signature, identityKey, sphinxKey, host, version, mixPort, verlocPort, httpApiPort } =
state.nodeData as NodeData<MixnodeData>;
const { profitMargin, amount, tokenPool } = state.amountData as MixnodeAmount;
const payload = {
ownerSignature: signature,
mixnode: {
identity_key: identityKey,
sphinx_key: sphinxKey,
host,
version,
mix_port: mixPort,
profit_margin_percent: profitMargin,
verloc_port: verlocPort,
http_api_port: httpApiPort,
},
pledge: amount,
};
if (tokenPool !== 'locked' && tokenPool !== 'balance') {
throw new Error(`token pool [${tokenPool}] not supported`);
}
const tx = await bondMixnodeRequest(payload, tokenPool);
if (tx) {
dispatch({ type: 'set_bond_status', payload: 'success' });
} else {
dispatch({ type: 'set_bond_status', payload: 'error' });
}
return tx;
};
const bondGateway = async () => {
const { signature, identityKey, sphinxKey, host, version, location, mixPort, clientsPort } =
state.nodeData as NodeData<GatewayData>;
const { amount, tokenPool } = state.amountData as GatewayAmount;
const payload = {
ownerSignature: signature,
gateway: {
identity_key: identityKey,
sphinx_key: sphinxKey,
host,
version,
mix_port: mixPort,
location,
clients_port: clientsPort,
},
pledge: amount,
};
if (tokenPool !== 'locked' && tokenPool !== 'balance') {
throw new Error(`token pool [${tokenPool}] not supported`);
}
const tx = await bondGatewayRequest(payload, tokenPool);
if (tx) {
dispatch({ type: 'set_bond_status', payload: 'success' });
} else {
dispatch({ type: 'set_bond_status', payload: 'error' });
}
return tx;
};
const onSubmit = async () => {
const { nodeData } = state;
let tx: TransactionExecuteResult | undefined;
// TODO show a special UI for loading state
dispatch({ type: 'set_bond_status', payload: 'loading' });
if ((nodeData as NodeData).nodeType === 'mixnode') {
tx = await bondMixnode();
} else {
tx = await bondGateway();
}
dispatch({ type: 'set_tx', payload: tx });
if (state.bondStatus === 'success') {
dispatch({ type: 'next_step' });
}
};
const onConfirm = () => {
dispatch({ type: 'close_modal' });
dispatch({ type: 'reset' });
};
return (
<NymCard title="Bonding">
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
pt: 0,
}}
>
<Typography>Bond a node or a gateway</Typography>
<Button
disabled={false}
variant="contained"
color="primary"
type="button"
disableElevation
onClick={() => dispatch({ type: 'show_modal' })}
sx={{ py: 1.5, px: 3 }}
>
Bond
</Button>
</Box>
{formStep === 1 && showModal && (
<NodeIdentityModal
open={formStep === 1 && showModal}
onClose={() => dispatch({ type: 'reset' })}
onSubmit={async (data) => {
dispatch({ type: 'set_node_data', payload: data });
dispatch({ type: 'next_step' });
}}
/>
)}
{formStep === 2 && showModal && (
<AmountModal
open={formStep === 2 && showModal}
onClose={() => dispatch({ type: 'reset' })}
onSubmit={async (data) => {
dispatch({ type: 'set_amount_data', payload: data });
dispatch({ type: 'next_step' });
}}
nodeType={state.nodeData?.nodeType || 'mixnode'}
/>
)}
{formStep === 3 && showModal && (
<SummaryModal
open={formStep === 3 && showModal}
onClose={() => dispatch({ type: 'reset' })}
onCancel={() => dispatch({ type: 'prev_step' })}
onSubmit={onSubmit}
node={state.nodeData as NodeData}
amount={state.amountData as MixnodeAmount | GatewayAmount}
onError={(msg: string) => {
dispatch({ type: 'set_error', payload: msg });
}}
/>
)}
{state.bondStatus === 'success' && formStep === 4 && showModal && (
<ConfirmationModal
open={formStep === 4 && showModal}
onConfirm={onConfirm}
onClose={onConfirm}
title="Bonding successful"
confirmButton="Done"
maxWidth="xs"
fullWidth
>
<Link href={`${urls(network).blockExplorer}/transaction/${state.tx?.transaction_hash}`} noIcon>
View on blockchain
</Link>
</ConfirmationModal>
)}
{state.bondStatus === 'error' && (
<ConfirmationModal
open={showModal}
onClose={() => dispatch({ type: 'reset' })}
onConfirm={() => dispatch({ type: 'reset' })}
title="Unbonding failed"
confirmButton="Done"
maxWidth="xs"
>
<Typography variant="caption">Error: {state.error}</Typography>
<ErrorOutline color="error" />
</ConfirmationModal>
)}
</NymCard>
);
};
export default BondingCard;
@@ -0,0 +1,221 @@
import React from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { Stack } from '@mui/material';
import { yupResolver } from '@hookform/resolvers/yup';
import { FieldErrors } from 'react-hook-form/dist/types/errors';
import { GatewayData, MixnodeData, NodeData, NodeType } from '../types';
import { RadioInput, TextFieldInput, CheckboxInput } from '../components';
import nodeSchema from './nodeSchema';
import { SimpleModal } from '../../../components/Modals/SimpleModal';
export interface Props {
open: boolean;
onClose?: () => void;
onSubmit: (data: NodeData) => Promise<void>;
}
const radioOptions: { label: string; value: NodeType }[] = [
{
label: 'Mixnode',
value: 'mixnode',
},
{
label: 'Gateway',
value: 'gateway',
},
];
const NodeIdentityModal = ({ open, onClose, onSubmit }: Props) => {
const {
control,
getValues,
handleSubmit,
formState: { errors },
} = useForm<NodeData>({
defaultValues: {
nodeType: 'mixnode',
advancedOpt: false,
mixPort: 1789,
verlocPort: 1790,
httpApiPort: 8000,
clientsPort: 9000,
},
resolver: yupResolver(nodeSchema),
});
const nodeType = useWatch({ name: 'nodeType', control });
const advancedOpt = useWatch({ name: 'advancedOpt', control });
const onSubmitForm = (data: NodeData) => {
onSubmit(data);
};
return (
<SimpleModal
open={open}
onClose={onClose}
onOk={handleSubmit(onSubmitForm)}
header="Bond"
subHeader="Step 1/2"
okLabel="Next"
>
<form>
<RadioInput
name="nodeType"
label="Select node type"
options={radioOptions}
control={control}
defaultValue={getValues('nodeType')}
muiRadioGroupProps={{ row: true }}
/>
<TextFieldInput
name="identityKey"
control={control}
defaultValue=""
label="Identity Key"
placeholder="Identity Key"
error={Boolean(errors.identityKey)}
helperText={errors.identityKey?.message}
required
muiTextFieldProps={{ fullWidth: true }}
sx={{ mb: 2.5, mt: 1 }}
/>
<TextFieldInput
name="sphinxKey"
control={control}
defaultValue=""
label="Sphinx Key"
placeholder="Sphinx Key"
error={Boolean(errors.sphinxKey)}
helperText={errors.sphinxKey?.message}
required
muiTextFieldProps={{ fullWidth: true }}
sx={{ mb: 2.5 }}
/>
<TextFieldInput
name="signature"
control={control}
defaultValue=""
label="Signature"
placeholder="Signature"
error={Boolean(errors.signature)}
helperText={errors.signature?.message}
required
muiTextFieldProps={{ fullWidth: true }}
sx={{ mb: 2.5 }}
/>
{nodeType === 'gateway' && (
<TextFieldInput
name="location"
control={control}
defaultValue=""
label="Location"
placeholder="Location"
error={Boolean((errors as FieldErrors<NodeData<GatewayData>>).location)}
helperText={(errors as FieldErrors<NodeData<GatewayData>>).location?.message}
required
muiTextFieldProps={{ fullWidth: true }}
sx={{ mb: 2.5 }}
/>
)}
<Stack direction="row" spacing={2}>
<TextFieldInput
name="host"
control={control}
defaultValue=""
label="Host"
placeholder="Host"
error={Boolean(errors.host)}
helperText={errors.host?.message}
required
muiTextFieldProps={{ fullWidth: true }}
sx={{ mb: 2.5 }}
/>
<TextFieldInput
name="version"
control={control}
defaultValue=""
label="Version"
placeholder="Version"
error={Boolean(errors.version)}
helperText={errors.version?.message}
required
muiTextFieldProps={{ fullWidth: true }}
sx={{ mb: 2.5 }}
/>
</Stack>
<CheckboxInput
name="advancedOpt"
label="Use advanced options"
control={control}
defaultValue={false}
sx={{ mb: 2.5 }}
/>
{advancedOpt && (
<Stack direction="row" spacing={1.5}>
<TextFieldInput
name="mixPort"
control={control}
label="Mix Port"
placeholder="Mix Port"
error={Boolean(errors.mixPort)}
helperText={errors.mixPort?.message && 'A valid port value is required'}
required
registerOptions={{ valueAsNumber: true }}
sx={{ mb: 2.5 }}
/>
{nodeType === 'mixnode' ? (
<>
<TextFieldInput
name="verlocPort"
control={control}
label="Verloc Port"
placeholder="Verloc Port"
error={Boolean((errors as FieldErrors<NodeData<MixnodeData>>).verlocPort)}
helperText={
(errors as FieldErrors<NodeData<MixnodeData>>).verlocPort?.message &&
'A valid port value is required'
}
required
registerOptions={{ valueAsNumber: true }}
sx={{ mb: 2.5 }}
/>
<TextFieldInput
name="httpApiPort"
control={control}
label="HTTP API Port"
placeholder="HTTP API Port"
error={Boolean((errors as FieldErrors<NodeData<MixnodeData>>).httpApiPort)}
helperText={
(errors as FieldErrors<NodeData<MixnodeData>>).httpApiPort?.message &&
'A valid port value is required'
}
required
registerOptions={{ valueAsNumber: true }}
sx={{ mb: 2.5 }}
/>
</>
) : (
<TextFieldInput
name="clientsPort"
control={control}
label="client WS API Port"
placeholder="client WS API Port"
error={Boolean((errors as FieldErrors<NodeData<GatewayData>>).clientsPort)}
helperText={
(errors as FieldErrors<NodeData<GatewayData>>).clientsPort?.message &&
'A valid port value is required'
}
required
registerOptions={{ valueAsNumber: true }}
sx={{ mb: 2.5 }}
/>
)}
</Stack>
)}
</form>
</SimpleModal>
);
};
export default NodeIdentityModal;
@@ -0,0 +1,105 @@
import React, { useEffect } from 'react';
import { Divider, Stack, Typography } from '@mui/material';
import { GatewayAmount, GatewayData, MixnodeAmount, MixnodeData, NodeData } from '../types';
import { SimpleModal } from '../../../components/Modals/SimpleModal';
import { useBondingContext } from '../../../context';
export interface Props {
open: boolean;
onClose: () => void;
onCancel: () => void;
onSubmit: () => Promise<void>;
onError: (message: string) => void;
node: NodeData;
amount: MixnodeAmount | GatewayAmount;
}
const SummaryModal = ({ open, onClose, onSubmit, node, amount, onCancel, onError }: Props) => {
const { fee, getFee, resetFeeState, feeError, feeLoading } = useBondingContext();
useEffect(() => {
if (feeError) onError(feeError);
}, [feeError]);
const fetchFee = async () => {
const { signature, host, version, mixPort, identityKey, sphinxKey } = node;
try {
if (node.nodeType === 'mixnode') {
await getFee(amount.tokenPool === 'locked' ? 'bondMixnodeWithVesting' : 'bondMixnode', {
ownerSignature: signature,
mixnode: {
identity_key: identityKey,
sphinx_key: sphinxKey,
host,
version,
mix_port: mixPort,
profit_margin_percent: (amount as MixnodeAmount).profitMargin,
verloc_port: (node as NodeData<MixnodeData>).verlocPort,
http_api_port: (node as NodeData<MixnodeData>).httpApiPort,
},
pledge: amount.amount,
});
} else {
await getFee(amount.tokenPool === 'locked' ? 'bondGatewayWithVesting' : 'bondGateway', {
ownerSignature: signature,
gateway: {
identity_key: identityKey,
sphinx_key: sphinxKey,
host,
version,
mix_port: mixPort,
location: (node as NodeData<GatewayData>).location,
clients_port: (node as NodeData<GatewayData>).clientsPort,
},
pledge: amount.amount,
});
}
} catch (e) {
onError(e as string);
}
};
useEffect(() => {
fetchFee();
}, [node, amount]);
const onConfirm = async () => onSubmit();
return (
<SimpleModal
open={open}
onClose={() => {
resetFeeState();
onClose();
}}
onBack={() => {
resetFeeState();
onCancel();
}}
onOk={onConfirm}
header="Bond details"
okLabel="Confirm"
>
<Stack direction="row" justifyContent="space-between">
<Typography>Identity Key</Typography>
<Typography>{node.identityKey}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between">
<Typography>Amount</Typography>
<Typography>{`${amount.amount.amount} ${amount.amount.denom}`}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between">
<Typography>Fee for this operation</Typography>
{feeLoading ? (
<Typography>loading</Typography>
) : (
<Typography>{fee ? `${fee.amount?.amount} ${fee.amount?.denom}` : ''}</Typography>
)}
</Stack>
</SimpleModal>
);
};
export default SummaryModal;
@@ -0,0 +1,19 @@
import { number, object, string } from 'yup';
import { validateAmount } from '../../../utils';
const amountSchema = object().shape({
amount: object().shape({
amount: string()
.required('An amount is required')
.test('valid-amount', 'Pledge error', async function isValidAmount(this, value) {
const isValid = await validateAmount(value || '', '100');
if (!isValid) {
return this.createError({ message: 'A valid amount is required (min 100)' });
}
return true;
}),
}),
profitMargin: number().required('Profit Percentage is required').min(0).max(100),
});
export default amountSchema;
@@ -0,0 +1,3 @@
import BondingCard from './BondingCard';
export default BondingCard;
@@ -0,0 +1,57 @@
import { boolean, lazy, mixed, number, object, string } from 'yup';
import { isValidHostname, validateKey, validateLocation, validateRawPort, validateVersion } from '../../../utils';
import { NodeType } from '../types';
const nodeSchema = object().shape({
nodeType: string().required().oneOf(['mixnode', 'gateway']),
identityKey: string()
.required('An indentity key is required')
.test('valid-id-key', 'A valid identity key is required', (value) => validateKey(value || '', 32)),
sphinxKey: string()
.required('A sphinx key is required')
.test('valid-sphinx-key', 'A valid sphinx key is required', (value) => validateKey(value || '', 32)),
signature: string()
.required('Signature is required')
.test('valid-signature', 'A valid signature is required', (value) => validateKey(value || '', 64)),
host: string()
.required('A host is required')
.test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)),
version: string()
.required('A version is required')
.test('valid-version', 'A valid version is required', (value) => (value ? validateVersion(value) : false)),
advancedOpt: boolean().required(),
location: lazy((value) => {
if (value) {
return string()
.required('A location is required')
.test('valid-location', 'A valid version is required', (locationValueTest) =>
locationValueTest ? validateLocation(locationValueTest) : false,
);
}
return mixed().notRequired();
}),
mixPort: number()
.required('A mixport is required')
.test('valid-mixport', 'A valid mixport is required', (value) => (value ? validateRawPort(value) : false)),
verlocPort: number()
.required('A verloc port is required')
.test('valid-verloc', 'A valid verloc port is required', (value) => (value ? validateRawPort(value) : false)),
httpApiPort: number()
.required('A http-api port is required')
.test('valid-http', 'A valid http-api port is required', (value) => (value ? validateRawPort(value) : false)),
clientsPort: number()
.required('A clients port is required')
.test('valid-clients', 'A valid clients port is required', (value) => (value ? validateRawPort(value) : false)),
});
export default nodeSchema;
@@ -0,0 +1,51 @@
import * as React from 'react';
import { Control, useController } from 'react-hook-form';
import { Checkbox, CheckboxProps, FormControlLabel, FormControlLabelProps, FormGroup, SxProps } from '@mui/material';
interface Props {
name: string;
label: string;
control: Control<any>;
defaultValue: boolean;
muiCheckboxProps?: CheckboxProps;
muiFormControlLabelProps?: FormControlLabelProps;
sx?: SxProps;
}
const CheckboxInput = ({
name,
control,
defaultValue,
label,
muiCheckboxProps,
muiFormControlLabelProps,
sx,
}: Props) => {
const {
field: { onChange, onBlur, value, ref },
} = useController({
name,
control,
defaultValue,
});
return (
<FormGroup sx={sx}>
<FormControlLabel
control={
<Checkbox
onBlur={onBlur}
onChange={onChange}
checked={value}
inputRef={ref}
name={name}
{...muiCheckboxProps}
/>
}
label={label}
{...muiFormControlLabelProps}
/>
</FormGroup>
);
};
export default CheckboxInput;
@@ -0,0 +1,36 @@
import * as React from 'react';
import { Control, useController } from 'react-hook-form';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
interface Props {
name: string;
label: string;
control: Control<any>;
required?: boolean;
fullWidth?: boolean;
errorMessage?: string;
currencyDenom?: string;
}
const CurrencyInput = ({ name, label, control, errorMessage, currencyDenom, required, fullWidth }: Props) => {
const {
field: { onChange },
} = useController({
name,
control,
});
return (
<CurrencyFormField
showCoinMark
required={required}
fullWidth={fullWidth}
label={label}
onChanged={onChange}
denom={currencyDenom}
validationError={errorMessage}
/>
);
};
export default CurrencyInput;
@@ -0,0 +1,54 @@
import React, { useState } from 'react';
import { Typography } from '@mui/material';
import { ActionsMenu, ActionsMenuItem } from 'src/components/ActionsMenu';
import { Bond as BondIcon, Unbond as UnbondIcon } from '../../../svg-icons';
import { GatewayFlow } from '../gateway/types';
import { MixnodeFlow } from '../mixnode/types';
interface Item {
label: string;
flow: MixnodeFlow | GatewayFlow;
icon: React.ReactNode;
description?: string;
}
const NodeMenu = ({ onFlowChange }: { onFlowChange: (flow: MixnodeFlow) => void }) => {
const [isOpen, setIsOpen] = useState(false);
const handleOpen = () => setIsOpen(true);
const handleClose = () => setIsOpen(false);
const handleActionClick = (flow: MixnodeFlow | GatewayFlow) => {
onFlowChange(flow);
handleClose();
};
return (
<ActionsMenu open={isOpen} onOpen={handleOpen} onClose={handleClose}>
<ActionsMenuItem
title="Bond more"
Icon={<BondIcon fontSize="inherit" />}
onClick={() => handleActionClick('bondMore')}
/>
<ActionsMenuItem
title="Unbond"
Icon={<UnbondIcon fontSize="inherit" />}
onClick={() => handleActionClick('unbond')}
/>
<ActionsMenuItem
title="Compound rewards"
Icon={<Typography sx={{ pl: 1 }}>C</Typography>}
description="Add operator rewards to bond"
onClick={() => handleActionClick('compound')}
/>
<ActionsMenuItem
title="Redeem rewards"
Icon={<Typography sx={{ pl: 1 }}>R</Typography>}
description="Add your rewards to bonding pool"
onClick={() => handleActionClick('redeem')}
/>
</ActionsMenu>
);
};
export default NodeMenu;
@@ -0,0 +1,75 @@
import React from 'react';
import {
Box,
Stack,
SxProps,
Table,
TableBody,
TableCell as MUITableCell,
TableContainer,
TableHead,
TableRow,
Tooltip,
Typography,
} from '@mui/material';
import { InfoOutlined } from '@mui/icons-material';
export interface TableCell {
children: React.ReactNode;
color?: string;
align?: 'center' | 'inherit' | 'justify' | 'left' | 'right';
size?: 'small' | 'medium';
sx?: SxProps;
}
export type TableHeader = TableCell & { tooltip?: React.ReactNode };
const CellHeader = ({ children, tooltip, sx, size, align, color }: TableHeader) => (
<MUITableCell sx={{ py: 1.2, color, ...sx }} size={size} align={align}>
{tooltip ? (
<Tooltip title={tooltip} arrow placement="top-start">
<Stack direction="row" alignItems="center" fontSize="0.8rem">
<InfoOutlined fontSize="inherit" sx={{ mr: 0.5 }} />
<Typography>{children}</Typography>
</Stack>
</Tooltip>
) : (
<Typography>{children}</Typography>
)}
</MUITableCell>
);
export type Header = Omit<TableHeader, 'children'> & { header?: React.ReactNode; id: string };
export type Cell = Omit<TableCell, 'children'> & { cell: React.ReactNode; id?: string };
export interface TableProps {
headers: Header[];
cells: Cell[];
}
const NodeTable = ({ headers, cells }: TableProps) => (
<TableContainer component={Box}>
<Table sx={{ minWidth: 650 }} aria-label="node-table">
<TableHead>
<TableRow>
{headers.map(({ header, id, tooltip, sx }) => (
<CellHeader tooltip={tooltip} key={id} sx={sx}>
{header}
</CellHeader>
))}
</TableRow>
</TableHead>
<TableBody>
<TableRow key="node-data">
{cells.map(({ cell, id, align, size, color, sx }) => (
<MUITableCell component="th" scope="row" sx={{ py: 1, color, ...sx }} align={align} size={size} key={id}>
{cell}
</MUITableCell>
))}
</TableRow>
</TableBody>
</Table>
</TableContainer>
);
export default NodeTable;
@@ -0,0 +1,72 @@
import * as React from 'react';
import { Control, useController } from 'react-hook-form';
import {
FormControl,
FormControlLabel,
FormLabel,
FormLabelProps,
Radio,
RadioGroup,
RadioGroupProps,
RadioProps,
} from '@mui/material';
interface Props {
name: string;
label: string;
control: Control<any>;
options: { label: string; value: any }[];
defaultValue: any;
muiRadioGroupProps?: RadioGroupProps;
muiRadioProps?: RadioProps;
muiFormLabelProps?: FormLabelProps;
}
const RadioInput = ({
label,
control,
options,
defaultValue,
name,
muiRadioGroupProps,
muiRadioProps,
muiFormLabelProps,
}: Props) => {
const {
field: { onChange, value, ref },
} = useController({
name,
control,
rules: { required: true },
defaultValue,
});
return (
<FormControl ref={ref}>
<FormLabel
id={`radio-group-label-${name}`}
sx={{
color: 'text.main',
}}
{...muiFormLabelProps}
>
{label}
</FormLabel>
<RadioGroup
value={value}
onChange={onChange}
aria-labelledby={`radio-group-label-${name}`}
name={name}
sx={{
color: 'text.main',
}}
{...muiRadioGroupProps}
>
{options.map(({ value: v, label: l }) => (
<FormControlLabel value={v} control={<Radio color="default" {...muiRadioProps} />} label={l} />
))}
</RadioGroup>
</FormControl>
);
};
export default RadioInput;
@@ -0,0 +1,64 @@
import * as React from 'react';
import { Control, useController } from 'react-hook-form';
import { SxProps, TextField, TextFieldProps } from '@mui/material';
import { RegisterOptions } from 'react-hook-form/dist/types/validator';
interface Props {
name: string;
label: string;
placeholder?: string;
control: Control<any>;
defaultValue?: string;
required?: boolean;
error?: boolean;
muiTextFieldProps?: TextFieldProps;
helperText?: string;
sx?: SxProps;
registerOptions?: RegisterOptions;
disabled?: boolean;
}
const TextFieldInput = ({
name,
label,
control,
defaultValue,
placeholder,
muiTextFieldProps,
required,
error,
helperText,
registerOptions,
sx,
disabled,
}: Props) => {
const {
field: { onChange, onBlur, value, ref },
} = useController({
name,
control,
defaultValue,
rules: registerOptions,
});
return (
<TextField
onChange={onChange}
onBlur={onBlur}
value={value}
name={name}
id={name}
label={label}
variant="outlined"
placeholder={placeholder}
required={required}
inputRef={ref}
error={error}
helperText={helperText}
{...muiTextFieldProps}
sx={sx}
disabled={disabled}
/>
);
};
export default TextFieldInput;
@@ -0,0 +1,8 @@
export { default as NodeTable } from './NodeTable';
export { default as CheckboxInput } from './CheckboxInput';
export { default as RadioInput } from './RadioInput';
export { default as TextFieldInput } from './TextFieldInput';
export { default as CurrencyInput } from './CurrencyInput';
export { default as NodeMenu } from './NodeMenu';
export * from './NodeTable';
@@ -0,0 +1,63 @@
import { useState } from 'react';
import { Stack, Typography } from '@mui/material';
import { NymCard } from 'src/components';
import { IdentityKey } from 'src/components/IdentityKey';
import { BondedGateway } from '../../../context';
import { Cell, Header, NodeMenu, NodeTable } from '../components';
import Unbond from '../unbond';
import { GatewayFlow } from './types';
const headers: Header[] = [
{
header: 'IP',
id: 'ip-header',
sx: { pl: 0, width: 100 },
},
{
header: 'Bond',
id: 'bond-header',
},
{
id: 'menu-button',
size: 'small',
sx: { width: 34, maxWidth: 34 },
},
];
const GatewayCard = ({ gateway }: { gateway: BondedGateway }) => {
const { ip, bond } = gateway;
const [flow, setFlow] = useState<GatewayFlow>(null);
const cells: Cell[] = [
{
cell: ip,
id: 'ip-cell',
sx: { pl: 0 },
},
{
cell: `${bond.amount} ${bond.denom}`,
id: 'bond-cell',
},
{
cell: <NodeMenu onFlowChange={(newFlow) => setFlow(newFlow as GatewayFlow)} />,
id: 'menu-button-cell',
align: 'center',
},
];
return (
<NymCard
title={
<Stack gap={2}>
<Typography variant="h5">Valhalla gateway</Typography>
<IdentityKey identityKey={gateway.identityKey} />
</Stack>
}
>
<NodeTable headers={headers} cells={cells} />
<Unbond node={gateway} show={flow === 'unbond'} onClose={() => setFlow(null)} />
</NymCard>
);
};
export default GatewayCard;
@@ -0,0 +1,3 @@
import GatewayCard from './GatewayCard';
export default GatewayCard;
@@ -0,0 +1 @@
export type GatewayFlow = 'unbond' | null;
+30
View File
@@ -0,0 +1,30 @@
import React, { useContext } from 'react';
import { AppContext } from 'src/context/main';
import { Box } from '@mui/material';
import { useBondingContext, BondingContextProvider } from '../../context';
import { PageLayout } from '../../layouts';
import BondingCard from './bonding';
import MixnodeCard from './mixnode';
import GatewayCard from './gateway';
const Bonding = () => {
const { bondedMixnode, bondedGateway, loading } = useBondingContext();
// TODO display a special UI on loading state
return (
<PageLayout>
{!bondedMixnode && !bondedGateway && <BondingCard />}
{bondedMixnode && <MixnodeCard mixnode={bondedMixnode} />}
{bondedGateway && <GatewayCard gateway={bondedGateway} />}
</PageLayout>
);
};
export const BondingPage = () => {
const { network } = useContext(AppContext);
return (
<BondingContextProvider network={network}>
<Bonding />
</BondingContextProvider>
);
};
@@ -0,0 +1,133 @@
import { useState } from 'react';
import { Button, Stack, Typography } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { Link } from '@nymproject/react/link/Link';
import { NymCard } from 'src/components';
import { IdentityKey } from 'src/components/IdentityKey';
import { NodeStatus } from 'src/components/NodeStatus';
import { BondedMixnode } from '../../../context';
import { Node as NodeIcon } from '../../../svg-icons/node';
import { Cell, Header, NodeMenu, NodeTable } from '../components';
import Unbond from '../unbond';
import BondMore from './bond-more';
import CompoundRewards from './compound';
import NodeSettings from './node-settings';
import RedeemRewards from './redeem';
import { MixnodeFlow } from './types';
const headers: Header[] = [
{
header: 'Stake',
id: 'stake',
sx: { pl: 0 },
},
{
header: 'Bond',
id: 'bond',
},
{
header: 'Stake saturation',
id: 'stake-saturation',
tooltip: 'TODO', // TODO
},
{
header: 'PM',
id: 'profit-margin',
tooltip:
'The percentage of the node rewards that you as the node operator will take before the rest of the reward is shared between you and the delegators.',
},
{
header: 'Node rewards',
id: 'node-rewards',
tooltip: 'This is the total rewards for this node in this epoch including delegates and the operators share.',
},
{
header: 'Operator rewards',
id: 'operator-rewards',
tooltip:
'This is your (operator) new rewards including the PM and cost. You can compound your rewards manually every epoch or unbond your node to redeem them.',
},
{
header: 'No. delegators',
id: 'delegators',
},
{
id: 'menu-button',
size: 'small',
sx: { width: 34, maxWidth: 34 },
},
];
const MixnodeCard = ({ mixnode }: { mixnode: BondedMixnode }) => {
const { stake, bond, stakeSaturation, profitMargin, nodeRewards, operatorRewards, delegators } = mixnode;
const [flow, setFlow] = useState<MixnodeFlow>(null);
const theme = useTheme();
const cells: Cell[] = [
{
cell: `${stake.amount} ${stake.denom}`,
id: 'stake-cell',
sx: { pl: 0 },
},
{
cell: `${bond.amount} ${bond.denom}`,
id: 'bond-cell',
},
{
cell: `${stakeSaturation}%`,
id: 'stake-saturation-cell',
color: stakeSaturation > 100 ? theme.palette.nym.nymWallet.selectionChance.underModerate : undefined,
},
{
cell: `${profitMargin}%`,
id: 'pm-cell',
},
{
cell: `${nodeRewards.amount} ${nodeRewards.denom}`,
id: 'node-rewards-cell',
},
{
cell: `${operatorRewards.amount} ${operatorRewards.denom}`,
id: 'operator-rewards-cell',
},
{
cell: delegators,
id: 'delegators-cell',
},
{
cell: <NodeMenu onFlowChange={(newFlow) => setFlow(newFlow)} />,
},
];
return (
<NymCard
title={
<Stack gap={2}>
<NodeStatus status={mixnode.status} />
<Typography variant="h5">Monster node</Typography>
<IdentityKey identityKey={mixnode.identityKey} />
</Stack>
}
Action={
<Button variant="text" color="secondary" onClick={() => setFlow('nodeSettings')} startIcon={<NodeIcon />}>
Node settings
</Button>
}
>
<NodeTable headers={headers} cells={cells} />
<Typography sx={{ mt: 2 }}>
Check more stats of your node on the{' '}
<Link href="url" target="_blank">
explorer
</Link>
</Typography>
<NodeSettings mixnode={mixnode} show={flow === 'nodeSettings'} onClose={() => setFlow(null)} />
<BondMore mixnode={mixnode} show={flow === 'bondMore'} onClose={() => setFlow(null)} />
<RedeemRewards mixnode={mixnode} show={flow === 'redeem'} onClose={() => setFlow(null)} />
<Unbond node={mixnode} show={flow === 'unbond'} onClose={() => setFlow(null)} />
<CompoundRewards mixnode={mixnode} show={flow === 'compound'} onClose={() => setFlow(null)} />
</NymCard>
);
};
export default MixnodeCard;
@@ -0,0 +1,99 @@
import * as React from 'react';
import { useContext } from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { Box, Divider, Stack, Typography } from '@mui/material';
import { DecCoin } from '@nymproject/types';
import { CurrencyInput, TextFieldInput } from '../../components';
import schema from './schema';
import { AppContext } from '../../../../context';
import { TokenPoolSelector } from '../../../../components';
import { SimpleModal } from '../../../../components/Modals/SimpleModal';
export interface Props {
open: boolean;
onClose: () => void;
onConfirm: (bond: DecCoin, signature: string) => void;
currentBond: DecCoin;
}
interface FormData {
amount: DecCoin;
tokenPool: string;
signature: string;
}
const BondModal = ({ open, onClose, onConfirm, currentBond }: Props) => {
const {
control,
handleSubmit,
reset,
setValue,
formState: { errors },
} = useForm<FormData>({
resolver: yupResolver(schema),
});
const { userBalance, clientDetails } = useContext(AppContext);
return (
<SimpleModal
open={open}
onClose={() => {
reset();
onClose();
}}
onOk={handleSubmit(async (data) => onConfirm(data.amount, data.signature))}
header="Bond more"
subHeader="Bond more tokens on your node and receive more rewards"
okLabel="Next"
okDisabled={Boolean(errors?.amount || errors?.signature)}
>
<Box sx={{ mt: 1 }}>
<form>
<Stack direction="row" spacing={2}>
{userBalance.originalVesting && (
<TokenPoolSelector onSelect={(pool) => setValue('tokenPool', pool)} disabled={false} />
)}
<CurrencyInput
control={control}
required
fullWidth
label="Amount"
name="amount"
currencyDenom={clientDetails?.display_mix_denom}
errorMessage={errors.amount?.amount?.message}
/>
</Stack>
<TextFieldInput
name="signature"
control={control}
defaultValue=""
label="Signature"
placeholder="Signature"
error={Boolean(errors.signature)}
helperText={errors.signature?.message}
required
muiTextFieldProps={{ fullWidth: true }}
sx={{ mt: 2.5 }}
/>
</form>
<Stack direction="row" justifyContent="space-between" mt={3}>
<Typography fontWeight={600}>Account balance</Typography>
<Typography fontWeight={600} textTransform="uppercase">
{userBalance.balance?.printable_balance || 0}
</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between">
<Typography>Current bond</Typography>
<Typography>{`${currentBond.amount} ${currentBond.denom}`}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Typography fontWeight={400}>Est. fee for this transaction will be calculated in the next page</Typography>
</Box>
</SimpleModal>
);
};
export default BondModal;
@@ -0,0 +1,92 @@
import * as React from 'react';
import { useContext, useState } from 'react';
import { DecCoin, TransactionExecuteResult } from '@nymproject/types';
import { Link } from '@nymproject/react/link/Link';
import { Typography } from '@mui/material';
import { ErrorOutline } from '@mui/icons-material';
import { AppContext, BondedMixnode, urls, useBondingContext } from '../../../../context';
import SummaryModal from './SummaryModal';
import { ConfirmationModal } from '../../../../components';
import BondModal from './BondModal';
interface Props {
mixnode: BondedMixnode;
show: boolean;
onClose: () => void;
}
const BondMore = ({ mixnode, show, onClose }: Props) => {
const [addBond, setAddBond] = useState<DecCoin>({ amount: '0', denom: 'nym' });
const [signature, setSignature] = useState<string>();
const [step, setStep] = useState<1 | 2 | 3>(1);
const [tx, setTx] = useState<TransactionExecuteResult>();
const { network } = useContext(AppContext);
const { bondMore, error } = useBondingContext();
const submit = async () => {
const txResult = await bondMore(signature as string, addBond);
if (txResult) {
setStep(3);
}
setTx(txResult);
};
const reset = () => {
setAddBond({ amount: '0', denom: 'nym' });
setSignature('');
setStep(1);
onClose();
};
return (
<>
<BondModal
open={show && step === 1}
onClose={onClose}
onConfirm={async (bond, sig) => {
setAddBond(bond);
setSignature(sig);
setStep(2);
}}
currentBond={mixnode.bond}
/>
<SummaryModal
open={show && step === 2}
onClose={reset}
onConfirm={submit}
onCancel={() => setStep(1)}
currentBond={mixnode.bond}
addBond={addBond}
/>
<ConfirmationModal
open={show && step === 3 && !error}
onClose={reset}
onConfirm={reset}
title="Bonding successful"
confirmButton="Done"
maxWidth="xs"
>
<Typography sx={{ mb: 2 }}>This operation can take up to one hour to process</Typography>
<Link href={`${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`} noIcon>
View on blockchain
</Link>
</ConfirmationModal>
{error && (
<ConfirmationModal
open={show}
onClose={reset}
onConfirm={reset}
title="Operation failed"
confirmButton="Done"
maxWidth="xs"
>
<Typography variant="caption">Error: {error}</Typography>
<ErrorOutline color="error" />
</ConfirmationModal>
)}
</>
);
};
export default BondMore;
@@ -0,0 +1,73 @@
import * as React from 'react';
import { Divider, Stack, Typography } from '@mui/material';
import { DecCoin } from '@nymproject/types';
import { useEffect } from 'react';
import { ErrorOutline } from '@mui/icons-material';
import { SimpleModal } from '../../../../components/Modals/SimpleModal';
import { useBondingContext } from '../../../../context';
import { ConfirmationModal } from '../../../../components';
export interface Props {
open: boolean;
onClose: () => void;
onConfirm: () => Promise<void>;
onCancel: () => void;
currentBond: DecCoin;
addBond: DecCoin;
}
const SummaryModal = ({ open, onClose, onConfirm, onCancel, currentBond, addBond }: Props) => {
const { getFee, fee, error } = useBondingContext();
const fetchFee = async () => {
await getFee('bondMore', {});
};
useEffect(() => {
fetchFee();
}, []);
if (error) {
return (
<ConfirmationModal
open={open}
onClose={onClose}
onConfirm={onClose}
title="Operation failed"
confirmButton="Done"
maxWidth="xs"
>
<Typography variant="caption">Error: {error}</Typography>
<ErrorOutline color="error" />
</ConfirmationModal>
);
}
return (
<SimpleModal
open={open}
onClose={onClose}
onOk={onConfirm}
onBack={onCancel}
header="Bond mor details"
okLabel="Confirm"
>
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Current bond</Typography>
<Typography fontWeight={400}>{`${currentBond.amount} ${currentBond.denom}`}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Additional bond</Typography>
<Typography fontWeight={400}>{`${addBond.amount} ${addBond.denom}`}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Fee for this operation</Typography>
<Typography fontWeight={400}>{fee ? `${fee.amount?.amount} ${fee.amount?.denom}` : ''}</Typography>
</Stack>
</SimpleModal>
);
};
export default SummaryModal;
@@ -0,0 +1,3 @@
import BondMore from './BondMore';
export default BondMore;
@@ -0,0 +1,21 @@
import { object, string } from 'yup';
import { validateAmount, validateKey } from '../../../../utils';
const schema = object().shape({
amount: object().shape({
amount: string()
.required('An amount is required')
.test('valid-amount', 'Pledge error', async function isValidAmount(this, value) {
const isValid = await validateAmount(value || '', '0.01');
if (!isValid) {
return this.createError({ message: 'A valid amount is required (min 0.01)' });
}
return true;
}),
}),
signature: string()
.required('Signature is required')
.test('valid-signature', 'A valid signature is required', (value) => validateKey(value || '', 64)),
});
export default schema;
@@ -0,0 +1,85 @@
import * as React from 'react';
import { useContext, useEffect, useState } from 'react';
import { TransactionExecuteResult } from '@nymproject/types';
import { Link } from '@nymproject/react/link/Link';
import { Typography } from '@mui/material';
import { ErrorOutline } from '@mui/icons-material';
import { AppContext, BondedMixnode, urls, useBondingContext } from '../../../../context';
import SummaryModal from './SummaryModal';
import { ConfirmationModal } from '../../../../components';
interface Props {
mixnode: BondedMixnode;
show: boolean;
onClose: () => void;
}
const CompoundRewards = ({ mixnode, show, onClose }: Props) => {
const [step, setStep] = useState<1 | 2>(1);
const [tx, setTx] = useState<TransactionExecuteResult>();
const { network } = useContext(AppContext);
const { compoundRewards, error, fee, getFee } = useBondingContext();
const fetchFee = async () => {
await getFee('compoundRewards', {});
};
useEffect(() => {
fetchFee();
}, []);
const submit = async () => {
const txResult = await compoundRewards();
if (txResult) {
setStep(2);
}
setTx(txResult?.[0]);
};
const reset = () => {
setStep(1);
onClose();
};
return (
<>
<SummaryModal
open={show && step === 1}
onClose={reset}
onConfirm={submit}
onCancel={reset}
rewards={mixnode.operatorRewards}
fee={fee?.amount}
/>
<ConfirmationModal
open={show && step === 2}
onClose={reset}
onConfirm={reset}
title="Rewards compounded successfuly"
confirmButton="Done"
maxWidth="xs"
>
<Typography sx={{ mb: 2 }}>This operation can take up to one hour to process</Typography>
<Link href={`${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`} noIcon>
View on blockchain
</Link>
</ConfirmationModal>
{error && (
<ConfirmationModal
open={show}
onClose={reset}
onConfirm={reset}
title="Operation failed"
confirmButton="Done"
maxWidth="xs"
>
<Typography variant="caption">Error: {error}</Typography>
<ErrorOutline color="error" />
</ConfirmationModal>
)}
</>
);
};
export default CompoundRewards;
@@ -0,0 +1,39 @@
import * as React from 'react';
import { Divider, Stack, Typography } from '@mui/material';
import { DecCoin } from '@nymproject/types';
import { SimpleModal } from '../../../../components/Modals/SimpleModal';
export interface Props {
open: boolean;
onClose: () => void;
onConfirm: () => Promise<void>;
onCancel: () => void;
rewards: DecCoin;
fee?: DecCoin | null;
}
const SummaryModal = ({ open, onClose, onConfirm, onCancel, rewards, fee }: Props) => (
<SimpleModal
open={open}
onClose={onClose}
onOk={onConfirm}
onBack={onCancel}
header="Compound rewards"
subHeader="Get more rewards by compounding"
okLabel="Compound"
>
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Operator rewards</Typography>
<Typography fontWeight={400}>{`${rewards.amount} ${rewards.denom}`}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Fee for this operation</Typography>
<Typography fontWeight={400}>{fee ? `${fee?.amount} ${fee?.denom}` : ''}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Typography fontWeight={400}>Rewards will be added to your bonding pool</Typography>
</SimpleModal>
);
export default SummaryModal;
@@ -0,0 +1,3 @@
import CompoundRewards from './CompoundRewards';
export default CompoundRewards;
@@ -0,0 +1,3 @@
import MixnodeCard from './MixnodeCard';
export default MixnodeCard;
@@ -0,0 +1,99 @@
import * as React from 'react';
import { useContext, useEffect, useState } from 'react';
import { DecCoin, TransactionExecuteResult } from '@nymproject/types';
import { Link } from '@nymproject/react/link/Link';
import { Typography } from '@mui/material';
import { ErrorOutline } from '@mui/icons-material';
import ProfitMarginModal from './ProfitMarginModal';
import { AppContext, BondedMixnode, urls, useBondingContext } from '../../../../context';
import SummaryModal from './SummaryModal';
import { ConfirmationModal } from '../../../../components';
interface Props {
mixnode: BondedMixnode;
show: boolean;
onClose: () => void;
}
// TODO fetch real estimated operator reward for 10% PM
const MOCK_ESTIMATED_OP_REWARD: DecCoin = { amount: '42', denom: 'nym' };
const NodeSettings = ({ mixnode, show, onClose }: Props) => {
const [status, setStatus] = useState<'success' | 'error'>();
const [profitMargin, setProfitMargin] = useState<number>();
const [step, setStep] = useState<1 | 2>(1);
const [tx, setTx] = useState<TransactionExecuteResult>();
const { network } = useContext(AppContext);
const { updateMixnode, error, fee, getFee } = useBondingContext();
const submit = async () => {
const txResult = await updateMixnode(profitMargin as number);
if (txResult) {
setStatus('success');
} else {
setStatus('error');
}
setTx(txResult);
};
const reset = () => {
setProfitMargin(0);
setStep(1);
onClose();
};
return (
<>
<ProfitMarginModal
open={show && step === 1}
onClose={onClose}
onConfirm={async (pm) => {
setProfitMargin(pm);
setStep(2);
}}
estimatedOpReward={MOCK_ESTIMATED_OP_REWARD}
currentPm={mixnode.profitMargin}
/>
<SummaryModal
open={show && step === 2}
onClose={reset}
onConfirm={submit}
onCancel={() => setStep(1)}
currentPm={mixnode.profitMargin}
newPm={profitMargin as number}
fee={fee?.amount}
/>
{status === 'success' && (
<ConfirmationModal
open={show}
onClose={reset}
onConfirm={reset}
title="Operation successful"
confirmButton="Done"
maxWidth="xs"
>
<Typography sx={{ mb: 2 }}>This operation can take up to one hour to process</Typography>
<Link href={`${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`} noIcon>
View on blockchain
</Link>
</ConfirmationModal>
)}
{status === 'error' && (
<ConfirmationModal
open={show}
onClose={reset}
onConfirm={reset}
title="Operation failed"
confirmButton="Done"
maxWidth="xs"
>
<Typography variant="caption">Error: {error}</Typography>
<ErrorOutline color="error" />
</ConfirmationModal>
)}
</>
);
};
export default NodeSettings;
@@ -0,0 +1,84 @@
import * as React from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { DecCoin } from '@nymproject/types';
import { Box, Divider, Stack, Tooltip, Typography } from '@mui/material';
import { TextFieldInput } from '../../components';
import getSchema from './schema';
import { SimpleModal } from '../../../../components/Modals/SimpleModal';
export interface Props {
open: boolean;
onClose: () => void;
onConfirm: (pm: number) => void;
estimatedOpReward: DecCoin;
currentPm: number;
}
interface FormData {
profitMargin: number;
}
const NodeSettingsModal = ({ open, onClose, onConfirm, estimatedOpReward, currentPm }: Props) => {
const {
control,
handleSubmit,
reset,
formState: { errors },
} = useForm<FormData>({
resolver: yupResolver(getSchema(currentPm)),
defaultValues: {
profitMargin: currentPm,
},
});
return (
<SimpleModal
open={open}
onClose={() => {
reset();
onClose();
}}
onOk={handleSubmit(async (data) => onConfirm(data.profitMargin))}
header="Node Settings"
subHeader="System Variables"
okLabel="Next"
okDisabled={Boolean(errors?.profitMargin)}
>
<Box sx={{ mt: 1 }}>
<form>
<TextFieldInput
name="profitMargin"
control={control}
defaultValue=""
label="Set profit margin"
placeholder="Profit Margin"
error={Boolean(errors.profitMargin)}
helperText={
errors.profitMargin
? errors.profitMargin.message
: 'Your new profit margin will be applied in the next epoch'
}
required
muiTextFieldProps={{ fullWidth: true }}
sx={{ mb: 2.5 }}
/>
</form>
<Stack direction="row" justifyContent="space-between" mt={3}>
<Tooltip
title="Estimated total reward in an epoch for this profit margin if your node is selected in the active set."
arrow
placement="top"
>
<Typography fontWeight={400}>Estimated operator reward for 10% PM</Typography>
</Tooltip>
<Typography fontWeight={400}>{`~${estimatedOpReward.amount} ${estimatedOpReward.denom}`}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Typography fontWeight={400}>Est. fee for this transaction will be calculated in the next page</Typography>
</Box>
</SimpleModal>
);
};
export default NodeSettingsModal;
@@ -0,0 +1,43 @@
import * as React from 'react';
import { Divider, Stack, Typography } from '@mui/material';
import { DecCoin } from '@nymproject/types';
import { SimpleModal } from '../../../../components/Modals/SimpleModal';
export interface Props {
open: boolean;
onClose: () => void;
onConfirm: () => Promise<void>;
onCancel: () => void;
currentPm: number;
newPm: number;
fee?: DecCoin | null;
}
const SummaryModal = ({ open, onClose, onConfirm, onCancel, currentPm, newPm, fee }: Props) => (
<SimpleModal
open={open}
onClose={onClose}
onOk={onConfirm}
onBack={onCancel}
header="Profit margin change"
subHeader="System Variables"
okLabel="Confirm"
>
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Current profit margin</Typography>
<Typography fontWeight={400}>{`${currentPm}%`}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>New profit margin</Typography>
<Typography fontWeight={400}>{`${newPm}%`}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Fee for this operation</Typography>
<Typography fontWeight={400}>{fee ? `${fee?.amount} ${fee?.denom}` : ''}</Typography>
</Stack>
</SimpleModal>
);
export default SummaryModal;
@@ -0,0 +1,3 @@
import NodeSettings from './NodeSettings';
export default NodeSettings;
@@ -0,0 +1,8 @@
import { number, object } from 'yup';
const getSchema = (currentPm: number) =>
object().shape({
profitMargin: number().required('Profit Percentage is required').min(0).max(100).notOneOf([currentPm]),
});
export default getSchema;
@@ -0,0 +1,85 @@
import * as React from 'react';
import { useContext, useEffect, useState } from 'react';
import { TransactionExecuteResult } from '@nymproject/types';
import { Link } from '@nymproject/react/link/Link';
import { Typography } from '@mui/material';
import { ErrorOutline } from '@mui/icons-material';
import { AppContext, BondedMixnode, urls, useBondingContext } from '../../../../context';
import SummaryModal from './SummaryModal';
import { ConfirmationModal } from '../../../../components';
interface Props {
mixnode: BondedMixnode;
show: boolean;
onClose: () => void;
}
const RedeemRewards = ({ mixnode, show, onClose }: Props) => {
const [step, setStep] = useState<1 | 2>(1);
const [tx, setTx] = useState<TransactionExecuteResult>();
const { network } = useContext(AppContext);
const { redeemRewards, error, fee, getFee } = useBondingContext();
const fetchFee = async () => {
await getFee('redeemRewards', {});
};
useEffect(() => {
fetchFee();
}, []);
const submit = async () => {
const txResult = await redeemRewards();
if (txResult) {
setStep(2);
}
setTx(txResult?.[0]);
};
const reset = () => {
setStep(1);
onClose();
};
return (
<>
<SummaryModal
open={show && step === 1}
onClose={reset}
onConfirm={submit}
onCancel={reset}
rewards={mixnode.operatorRewards}
fee={fee?.amount}
/>
<ConfirmationModal
open={show && step === 2}
onClose={reset}
onConfirm={reset}
title="Rewards redemption succesfull"
confirmButton="Done"
maxWidth="xs"
>
<Typography sx={{ mb: 2 }}>This operation can take up to one hour to process</Typography>
<Link href={`${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`} noIcon>
View on blockchain
</Link>
</ConfirmationModal>
{error && (
<ConfirmationModal
open={show}
onClose={reset}
onConfirm={reset}
title="Operation failed"
confirmButton="Done"
maxWidth="xs"
>
<Typography variant="caption">Error: {error}</Typography>
<ErrorOutline color="error" />
</ConfirmationModal>
)}
</>
);
};
export default RedeemRewards;
@@ -0,0 +1,39 @@
import * as React from 'react';
import { Divider, Stack, Typography } from '@mui/material';
import { SimpleModal } from '../../../../components/Modals/SimpleModal';
import { DecCoin } from '@nymproject/types';
export interface Props {
open: boolean;
onClose: () => void;
onConfirm: () => Promise<void>;
onCancel: () => void;
rewards: DecCoin;
fee?: DecCoin | null;
}
const SummaryModal = ({ open, onClose, onConfirm, onCancel, rewards, fee }: Props) => (
<SimpleModal
open={open}
onClose={onClose}
onOk={onConfirm}
onBack={onCancel}
header="Redeem rewards"
subHeader="Claim your rewards"
okLabel="Redeem rewards"
>
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Rewards to redeem</Typography>
<Typography fontWeight={400}>{`${rewards.amount} ${rewards.denom}`}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Fee for this operation</Typography>
<Typography fontWeight={400}>{fee ? `${fee.amount} ${fee.denom}` : ''}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Typography fontWeight={400}>Rewards will be transferred to the account you are logged in with</Typography>
</SimpleModal>
);
export default SummaryModal;
@@ -0,0 +1,3 @@
import RedeemRewards from './RedeemRewards';
export default RedeemRewards;
@@ -0,0 +1 @@
export type MixnodeFlow = 'nodeSettings' | 'bondMore' | 'unbond' | 'compound' | 'redeem' | null;
+67
View File
@@ -0,0 +1,67 @@
import { DecCoin, TNodeType, TransactionExecuteResult } from '@nymproject/types';
export type FormStep = 1 | 2 | 3 | 4;
export type NodeType = TNodeType;
export type BondStatus = 'init' | 'success' | 'error' | 'loading';
export type ACTIONTYPE =
| { type: 'change_bond_type'; payload: NodeType }
| { type: 'set_node_data'; payload: NodeData }
| { type: 'set_amount_data'; payload: AmountData }
| { type: 'set_step'; payload: FormStep }
| { type: 'set_tx'; payload: TransactionExecuteResult | undefined }
| { type: 'set_error'; payload: string | null | undefined }
| { type: 'set_bond_status'; payload: BondStatus }
| { type: 'next_step' }
| { type: 'prev_step' }
| { type: 'show_modal' }
| { type: 'close_modal' }
| { type: 'reset' };
export type NodeIdentity = {
identityKey: string;
sphinxKey: string;
signature: string;
host: string;
version: string;
advancedOpt: boolean;
mixPort: number;
};
export type MixnodeData = NodeIdentity & {
verlocPort: number;
httpApiPort: number;
};
export type MixnodeAmount = {
amount: DecCoin;
tokenPool: string;
profitMargin: number;
};
export type GatewayData = NodeIdentity & {
location: string;
clientsPort: number;
};
export type GatewayAmount = Omit<MixnodeAmount, 'profitMargin'>;
export type NodeData<N = MixnodeData | GatewayData> = {
nodeType: TNodeType;
} & N;
export interface AmountData {
amount: DecCoin;
tokenPool: string;
profitMargin?: number;
}
export interface BondState {
showModal: boolean;
formStep: FormStep;
nodeData?: NodeData;
amountData?: MixnodeAmount | GatewayAmount;
tx?: TransactionExecuteResult;
bondStatus: BondStatus;
error?: string | null;
}
@@ -0,0 +1,49 @@
import * as React from 'react';
import { Divider, Stack, Typography } from '@mui/material';
import { SimpleModal } from '../../../components/Modals/SimpleModal';
import { DecCoin } from '@nymproject/types';
export interface Props {
open: boolean;
onClose: () => void;
onConfirm: () => Promise<void>;
onCancel: () => void;
bond: DecCoin;
rewards?: DecCoin;
fee?: DecCoin | null;
}
const SummaryModal = ({ open, onClose, onConfirm, onCancel, bond, rewards, fee }: Props) => (
<SimpleModal
open={open}
onClose={onClose}
onOk={onConfirm}
onBack={onCancel}
header="Unbond"
subHeader="Unbond and remove your node from the mixnet"
okLabel="Unbond"
>
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Amount to unbond</Typography>
<Typography fontWeight={400}>{`${bond.amount} ${bond.denom}`}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
{rewards?.amount && (
<>
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Operator rewards</Typography>
<Typography fontWeight={400}>{`${rewards.amount} ${rewards.denom}`}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
</>
)}
<Stack direction="row" justifyContent="space-between">
<Typography fontWeight={400}>Fee for this operation</Typography>
<Typography fontWeight={400}>{fee ? `${fee.amount} ${fee.denom}` : ''}</Typography>
</Stack>
<Divider sx={{ my: 1 }} />
<Typography fontWeight={400}>Tokens will be transferred to account you are logged in with now</Typography>
</SimpleModal>
);
export default SummaryModal;
@@ -0,0 +1,131 @@
import * as React from 'react';
import { useContext, useEffect, useState } from 'react';
import { Link } from '@nymproject/react/link/Link';
import { Typography } from '@mui/material';
import { ErrorOutline } from '@mui/icons-material';
import { AppContext, BondedGateway, BondedMixnode, urls, useBondingContext } from '../../../context';
import SummaryModal from './SummaryModal';
import { ConfirmationModal } from '../../../components';
import { LoadingModal } from '../../../components/Modals/LoadingModal';
import { NodeType } from '../types';
interface Props {
node: BondedMixnode | BondedGateway;
show: boolean;
onClose: () => void;
}
type UnbondStatus = 'success' | 'error';
const Unbond = ({ node, show, onClose }: Props) => {
const [step, setStep] = useState<1 | 2>(1);
const [txHash, setTxHash] = useState<string>();
const [status, setStatus] = useState<UnbondStatus>();
const [nodeType, setNodeType] = useState<NodeType>('mixnode');
const { network } = useContext(AppContext);
const { fee, getFee, resetFeeState, feeLoading, feeError, loading, unbondMixnode, unbondGateway, error } =
useBondingContext();
useEffect(() => {
if (error || feeError) {
setStatus('error');
}
}, [error, feeError]);
useEffect(() => {
if ('profitMargin' in node) {
setNodeType('mixnode');
} else {
setNodeType('gateway');
}
}, [node]);
const unbond = async () => {
let tx;
if (nodeType === 'mixnode') {
tx = await unbondMixnode();
} else {
tx = await unbondGateway();
}
if (!tx) {
setStatus('error');
}
setStatus('success');
setTxHash(tx?.transaction_hash);
return tx;
};
const fetchFee = async () => {
if (nodeType === 'mixnode') {
await getFee('unbondMixnode', {});
} else {
await getFee('unbondGateway', {});
}
};
useEffect(() => {
fetchFee();
}, [node]);
const submit = async () => {
if (status === 'error') {
// Fetch fee failed
return;
}
unbond();
resetFeeState();
setStep(2);
};
const reset = () => {
setStep(1);
onClose();
};
if (feeLoading || loading) return <LoadingModal />;
return (
<>
<SummaryModal
open={show && step === 1}
onClose={reset}
onConfirm={submit}
onCancel={reset}
bond={node.bond}
rewards={nodeType === 'mixnode' ? (node as BondedMixnode).operatorRewards : undefined}
fee={fee?.amount}
/>
{status === 'success' && (
<ConfirmationModal
open={show && step === 2}
onClose={reset}
onConfirm={reset}
title="Unbonding succesfull"
confirmButton="Done"
maxWidth="xs"
>
<Typography sx={{ mb: 2 }}>This operation can take up to one hour to process</Typography>
<Link href={`${urls(network).blockExplorer}/transaction/${txHash}`} noIcon>
View on blockchain
</Link>
</ConfirmationModal>
)}
{status === 'error' && (
<ConfirmationModal
open={show}
onClose={reset}
onConfirm={reset}
title="Unbonding failed"
confirmButton="Done"
maxWidth="xs"
>
<Typography variant="caption">Error: {error}</Typography>
<ErrorOutline color="error" />
</ConfirmationModal>
)}
</>
);
};
export default Unbond;
@@ -0,0 +1,3 @@
import Unbond from './Unbond';
export default Unbond;
+2
View File
@@ -7,3 +7,5 @@ export * from './auth';
export * from './settings';
export * from './unbond';
export * from './delegation';
export * from './bonding';
+2
View File
@@ -6,6 +6,7 @@ import {
InclusionProbabilityResponse,
DecCoin,
MixNodeBond,
GatewayBond,
} from '@nymproject/types';
import { Epoch } from 'src/types';
import { invokeWrapper } from './wrapper';
@@ -25,6 +26,7 @@ export const getAllPendingDelegations = async () =>
invokeWrapper<DelegationEvent[]>('get_all_pending_delegation_events');
export const getMixnodeBondDetails = async () => invokeWrapper<MixNodeBond | null>('mixnode_bond_details');
export const getGatewayBondDetails = async () => invokeWrapper<GatewayBond | null>('gateway_bond_details');
export const getOperatorRewards = async (address: string) =>
invokeWrapper<DecCoin>('get_operator_rewards', { address });
+2 -1
View File
@@ -3,7 +3,7 @@ import { Route, Routes } from 'react-router-dom';
import { ApplicationLayout } from 'src/layouts';
import { Terminal } from 'src/pages/terminal';
import { Send } from 'src/components/Send';
import { Bond, Balance, InternalDocs, Receive, Unbond, DelegationPage, Admin, Settings } from '../pages';
import { Bond, Balance, InternalDocs, Receive, Unbond, DelegationPage, Admin, Settings, BondingPage } from '../pages';
export const AppRoutes = () => (
<ApplicationLayout>
@@ -14,6 +14,7 @@ export const AppRoutes = () => (
<Route path="/balance" element={<Balance />} />
<Route path="/receive" element={<Receive />} />
<Route path="/bond" element={<Bond />} />
<Route path="/bonding" element={<BondingPage />} />
<Route path="/unbond" element={<Unbond />} />
<Route path="/delegation" element={<DelegationPage />} />
<Route path="/docs" element={<InternalDocs />} />
+22
View File
@@ -0,0 +1,22 @@
import React from 'react';
import { SvgIcon, SvgIconProps } from '@mui/material';
export const Bonding = (props: SvgIconProps) => (
<SvgIcon {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16 17C14.8954 17 14 16.1046 14 15L14 9C14 7.89543 14.8954 7 16 7L22 7C23.1046 7 24 7.89543 24 9L24 15C24 16.1046 23.1046 17 22 17L16 17ZM16 9L16 15L22 15L22 9L16 9Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 17C0.89543 17 -1.00647e-07 16.1046 -2.24801e-07 15L-8.99206e-07 9C-1.02336e-06 7.89543 0.895429 7 2 7L8 7C9.10457 7 10 7.89543 10 9L10 15C10 16.1046 9.10457 17 8 17L2 17ZM2 9L2 15L8 15L8 9L2 9Z"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6 12C6 11.4477 6.44772 11 7 11H17C17.5523 11 18 11.4477 18 12C18 12.5523 17.5523 13 17 13H7C6.44772 13 6 12.5523 6 12Z"
/>
</SvgIcon>
);
+1
View File
@@ -2,3 +2,4 @@ export * from './delegate';
export * from './undelegate';
export * from './bond';
export * from './unbond';
export * from './bonding';
+11
View File
@@ -64,6 +64,17 @@ declare module '@mui/material/styles' {
nav: {
background: string;
};
mixnodes: {
status: {
active: string;
standby: string;
};
};
selectionChance: {
overModerate: string;
moderate: string;
underModerate: string;
};
}
/**
+29
View File
@@ -56,6 +56,17 @@ const darkMode: NymPaletteVariant = {
nav: {
background: '#292E34',
},
mixnodes: {
status: {
active: '#20D073',
standby: '#5FD7EF',
},
},
selectionChance: {
overModerate: '#20D073',
moderate: '#EBA53D',
underModerate: '#DA465B',
},
};
const lightMode: NymPaletteVariant = {
@@ -80,6 +91,17 @@ const lightMode: NymPaletteVariant = {
nav: {
background: '#FFFFFF',
},
mixnodes: {
status: {
active: '#1CBB67',
standby: '#55C1D7',
},
},
selectionChance: {
overModerate: '#20D073',
moderate: '#EBA53D',
underModerate: '#DA465B',
},
};
/**
@@ -273,6 +295,13 @@ export const getDesignTokens = (mode: PaletteMode): ThemeOptions => {
underline: 'none',
},
},
MuiDialogTitle: {
styleOverrides: {
root: {
fontWeight: 600,
},
},
},
},
palette,
};
@@ -132,8 +132,8 @@ export const CurrencyFormField: React.FC<{
required,
endAdornment: showCoinMark && (
<InputAdornment position="end">
{denom === 'unym' && <CoinMark height="20px" />}
{denom !== 'unym' && <span>NYMT</span>}
{denom === 'NYM' && <CoinMark height="20px" />}
{denom !== 'NYM' && <span>NYMT</span>}
</InputAdornment>
),
...{
+11 -25
View File
@@ -1157,7 +1157,7 @@
dependencies:
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.17.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.17.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
version "7.17.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941"
integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==
@@ -5279,11 +5279,6 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2"
integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==
"@types/lodash@^4.14.175":
version "4.14.179"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.179.tgz#490ec3288088c91295780237d2497a3aa9dfb5c5"
integrity sha512-uwc1x90yCKqGcIOAT6DwOSuxnrAbpkdPsUOZtwrXb4D/6wZs+6qG7QnIawDuZWg0sWpxl+ltIKCaLoMlna678w==
"@types/long@^4.0.1":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"
@@ -13188,11 +13183,6 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lodash-es@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
lodash._reinterpolate@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
@@ -18101,12 +18091,12 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tiny-invariant@^1.0.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9"
integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==
tiny-case@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03"
integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==
tiny-warning@^1.0.0, tiny-warning@^1.0.2, tiny-warning@^1.0.3:
tiny-warning@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
@@ -19542,17 +19532,13 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
yup@^0.32.9:
version "0.32.11"
resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.11.tgz#d67fb83eefa4698607982e63f7ca4c5ed3cf18c5"
integrity sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==
yup@^1.0.0-beta.4:
version "1.0.0-beta.4"
resolved "https://registry.yarnpkg.com/yup/-/yup-1.0.0-beta.4.tgz#df10f42df6aa0212c22aab58d6d026b1610ff8a3"
integrity sha512-g5uuQH2rN+0Z4L/ix8KoYIS6v7vnrykRzM/u7ExSA0WA33vrw2YOUKShBhaueh9N95oz54slgqBQTHx7E6EHwg==
dependencies:
"@babel/runtime" "^7.15.4"
"@types/lodash" "^4.14.175"
lodash "^4.17.21"
lodash-es "^4.17.21"
nanoclone "^0.2.1"
property-expr "^2.0.4"
tiny-case "^1.0.2"
toposort "^2.0.2"
zwitch@^1.0.0: