Compare commits

..

36 Commits

Author SHA1 Message Date
Gala 47cae50e68 profit margin only for mixnode 2022-10-04 11:51:34 +02:00
Gala c644956576 wip 2022-09-29 17:08:31 +03:00
Gala c329724f8c Merge branch 'develop' into node-settings-copy 2022-09-29 15:44:52 +03:00
Gala 7dc776f98a wip fixing conflicts 2022-09-22 13:16:39 +02:00
Gala 9717bcbb17 WIP: Merge branch 'develop' into 348-bonding-settings 2022-09-22 12:22:24 +02:00
Gala 978cbc4f00 fix conflicts 2022-09-21 17:03:56 +02:00
Gala ebb06d4beb Merge branch 'develop' - wip fixing clonflicts 2022-09-21 17:03:47 +02:00
Gala 1241a81514 changing alert behaviour 2022-09-13 13:49:42 +02:00
Gala 08a190c1cb Merge branch 'develop' into 348-bonding-settings 2022-09-13 12:44:11 +02:00
Gala 81f36e8da7 some refactor 2022-09-08 13:36:44 +02:00
Gala f230229ce9 change save button label 2022-09-08 12:18:09 +02:00
Gala 912fb4ab38 Merge branch 'develop' into 348-bonding-settings 2022-09-08 12:05:34 +02:00
Gala 99ceabb0b0 using the wallet Tab component 2022-09-06 13:18:43 +02:00
Gala 25df7bcd4d Merge branch 'develop' into 348-bonding-settings 2022-09-02 09:39:21 +02:00
Gala 1cdca7bec3 unbond modal verification step 2022-09-01 16:57:48 +02:00
Gala c809c7733d logic refactor 2022-09-01 15:06:23 +02:00
Gala 7b53003edb wip verification modal 2022-08-31 18:49:47 +02:00
Gala 831d9d2bf8 update alert text 2022-08-31 18:20:40 +02:00
Gala cb7c51ba12 remove node settings modal trigger 2022-08-31 17:37:00 +02:00
Gala 0310f0a8a9 some refactor 2022-08-31 17:23:53 +02:00
Gala bb79d08f6d dynamic values and remove hard coded code 2022-08-31 16:58:53 +02:00
Gala 414c86b500 fix button width 2022-08-31 12:13:40 +02:00
Gala 4304ffcf3c adding notification span 2022-08-31 11:44:23 +02:00
Gala 309b23e18a adding confirmation modals 2022-08-31 10:55:13 +02:00
Gala 52703583f0 adding validation to parameters settings 2022-08-31 10:10:37 +02:00
Gala 6473ef13c6 validate info fields 2022-08-30 18:51:39 +02:00
Gala 9a45f15ba4 wip 2022-08-30 16:49:07 +02:00
Gala 746795b7ce mook bonded node 2022-08-30 12:49:50 +02:00
Gala 8b81247044 Merge branch 'develop' into 348-bonding-settings 2022-08-30 11:08:19 +02:00
Gala c6cd787950 adding unbonding modal 2022-08-19 18:06:04 +02:00
Gala f9ab20b10f more styling in node
settings page
2022-08-18 17:27:28 +02:00
Gala acffd496ed nav styles 2022-08-18 17:07:59 +02:00
Gala 466ac1a1e0 settings general page 2022-08-18 16:39:05 +02:00
Gala d53adcd17e nodesettings page and logic to browse 2022-08-17 18:55:58 +02:00
Gala 36e82e831f Merge branch 'develop' into 348-bonding-settings 2022-08-17 13:55:06 +02:00
Gala cbe0115f01 wip 2022-08-17 11:10:10 +02:00
71 changed files with 2680 additions and 2147 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ MIX_DENOM_DISPLAY=nym
STAKE_DENOM=unyx
STAKE_DENOM_DISPLAY=nyx
DENOMS_EXPONENT=6
MIXNET_CONTRACT_ADDRESS=n1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsd3qaep
MIXNET_CONTRACT_ADDRESS=n1rjzps6qrmdqmf0xz4cn4x4rcmqeqzq6hnzqg4wcvd0r2lyasdq5sepn5s8
VESTING_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav
BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0
COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1ghd753shjuwexxywmgs4xz7x2q732vcn7ty4yw
+1 -1
View File
@@ -160,7 +160,7 @@ mod qa {
pub(crate) const STAKE_DENOM: DenomDetails = DenomDetails::new("unyx", "nyx", 6);
pub(crate) const MIXNET_CONTRACT_ADDRESS: &str =
"n1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsd3qaep";
"n1rjzps6qrmdqmf0xz4cn4x4rcmqeqzq6hnzqg4wcvd0r2lyasdq5sepn5s8";
pub(crate) const VESTING_CONTRACT_ADDRESS: &str =
"n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav";
pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str =
+1 -2
View File
@@ -109,8 +109,7 @@
"ts-jest": "^27.0.5",
"ts-loader": "^9.2.5",
"tsconfig-paths-webpack-plugin": "^3.5.2",
"tslint": "^6.1.3",
"typescript": "^4.8.2",
"typescript": "^4.6.2",
"url-loader": "^4.1.1",
"webpack": "^5.64.3",
"webpack-cli": "^4.8.0",
@@ -1,4 +1,5 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Box, Button, Stack, Typography } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { TBondedMixnode, urls } from 'src/context';
@@ -55,8 +56,11 @@ export const BondedMixnode = ({
network?: Network;
onActionSelect: (action: TBondedMixnodeActions) => void;
}) => {
const navigate = useNavigate();
const { name, stake, bond, stakeSaturation, profitMargin, operatorRewards, delegators, status, identityKey } =
mixnode;
const cells: Cell[] = [
{
cell: `${stake.amount} ${stake.denom}`,
@@ -114,14 +118,16 @@ export const BondedMixnode = ({
</Stack>
}
Action={
<Button
variant="text"
color="secondary"
onClick={() => onActionSelect('nodeSettings')}
startIcon={<NodeIcon />}
>
Settings
</Button>
mixnode.type === 'mixnode' && (
<Button
variant="text"
color="secondary"
onClick={() => navigate('/bonding/node-settings')}
startIcon={<NodeIcon />}
>
Settings
</Button>
)
}
>
<NodeTable headers={headers} cells={cells} />
@@ -12,6 +12,7 @@ import { simulateUpdateMixnodeCostParams, simulateVestingUpdateMixnodeCostParams
import { LoadingModal } from 'src/components/Modals/LoadingModal';
import { FeeDetails } from '@nymproject/types';
//Now we are using the node setting page instead of this modal
export const NodeSettings = ({
currentPm,
isVesting,
@@ -19,13 +20,13 @@ export const NodeSettings = ({
onClose,
onError,
}: {
currentPm: TBondedMixnode['profitMargin'];
isVesting: boolean;
currentPm?: TBondedMixnode['profitMargin'];
isVesting?: boolean;
onConfirm: (profitMargin: string, fee?: FeeDetails) => Promise<void>;
onClose: () => void;
onError: (err: string) => void;
}) => {
const [pm, setPm] = useState(currentPm.toString());
const [pm, setPm] = useState(currentPm?.toString());
const [error, setError] = useState(false);
const { fee, getFee, resetFeeState, isFeeLoading, feeError } = useGetFee();
@@ -52,13 +53,15 @@ export const NodeSettings = ({
return;
}
// TODO: this will have to be updated with allowing users to provide their operating cost in the form
const defaultCostParams = await attachDefaultOperatingCost(toPercentFloatString(pm));
if (pm) {
// TODO: this will have to be updated with allowing users to provide their operating cost in the form
const defaultCostParams = await attachDefaultOperatingCost(toPercentFloatString(pm));
if (isVesting) {
await getFee(simulateVestingUpdateMixnodeCostParams, defaultCostParams);
} else {
await getFee(simulateUpdateMixnodeCostParams, defaultCostParams);
if (isVesting) {
await getFee(simulateVestingUpdateMixnodeCostParams, defaultCostParams);
} else {
await getFee(simulateUpdateMixnodeCostParams, defaultCostParams);
}
}
};
@@ -74,7 +77,7 @@ export const NodeSettings = ({
if (isFeeLoading) return <LoadingModal />;
if (fee)
if (fee && pm)
return (
<ConfirmTx
open
@@ -2,6 +2,7 @@ import React from 'react';
import { Box, Button, Modal, Stack, SxProps, Typography } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import ErrorOutline from '@mui/icons-material/ErrorOutline';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import { StyledBackButton } from 'src/components/StyledBackButton';
import { modalStyle } from './styles';
@@ -9,8 +10,10 @@ export const SimpleModal: React.FC<{
open: boolean;
hideCloseIcon?: boolean;
displayErrorIcon?: boolean;
displayInfoIcon?: boolean;
headerStyles?: SxProps;
subHeaderStyles?: SxProps;
buttonFullWidth?: boolean;
onClose?: () => void;
onOk?: () => Promise<void>;
onBack?: () => void;
@@ -24,8 +27,10 @@ export const SimpleModal: React.FC<{
open,
hideCloseIcon,
displayErrorIcon,
displayInfoIcon,
headerStyles,
subHeaderStyles,
buttonFullWidth,
onClose,
okDisabled,
onOk,
@@ -40,6 +45,7 @@ export const SimpleModal: React.FC<{
<Modal open={open} onClose={onClose} BackdropProps={backdropProps}>
<Box sx={{ border: (t) => `1px solid ${t.palette.nym.nymWallet.modal.border}`, ...modalStyle, ...sx }}>
{displayErrorIcon && <ErrorOutline color="error" sx={{ mb: 3 }} />}
{displayInfoIcon && <InfoOutlinedIcon sx={{ mb: 2, color: (theme) => theme.palette.nym.nymWallet.text.blue }} />}
<Stack direction="row" justifyContent="space-between" alignItems="center">
{typeof header === 'string' ? (
<Typography fontSize={20} fontWeight={600} sx={{ color: 'text.primary', ...headerStyles }}>
@@ -64,8 +70,8 @@ export const SimpleModal: React.FC<{
{children}
{(onOk || onBack) && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 1 }}>
{onBack && <StyledBackButton onBack={onBack} sx={{ mt: 3 }} />}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 2, width: buttonFullWidth ? '100%' : null }}>
{onBack && <StyledBackButton onBack={onBack} />}
{onOk && (
<Button variant="contained" fullWidth size="large" onClick={onOk} disabled={okDisabled} sx={{ mt: 3 }}>
{okLabel}
+11 -11
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { Tab, Tabs as MuiTabs } from '@mui/material';
import { Tab, Tabs as MuiTabs, SxProps } from '@mui/material';
export const Tabs: React.FC<{
tabs: string[];
@@ -7,7 +7,9 @@ export const Tabs: React.FC<{
disabled?: boolean;
onChange?: (event: React.SyntheticEvent, tab: number) => void;
disableActiveTabHighlight?: boolean;
}> = ({ tabs, selectedTab, disabled, disableActiveTabHighlight, onChange }) => (
tabSx?: SxProps;
tabIndicatorStyles?: {};
}> = ({ tabs, selectedTab, disabled, disableActiveTabHighlight, onChange, tabSx, tabIndicatorStyles }) => (
<MuiTabs
value={selectedTab}
onChange={onChange}
@@ -16,17 +18,15 @@ export const Tabs: React.FC<{
borderTop: '1px solid',
borderBottom: '1px solid',
borderColor: (theme) => theme.palette.nym.nymWallet.background.greyStroke,
...tabSx,
}}
textColor="inherit"
TabIndicatorProps={
disableActiveTabHighlight
? {
style: {
opacity: 0,
},
}
: {}
}
TabIndicatorProps={{
style: {
opacity: disableActiveTabHighlight ? 0 : 1,
...tabIndicatorStyles,
},
}}
>
{tabs.map((tabName) => (
<Tab key={tabName} label={tabName} sx={{ textTransform: 'capitalize' }} disabled={disabled} />
+31 -11
View File
@@ -35,6 +35,7 @@ import { attachDefaultOperatingCost, toPercentFloatString, toPercentIntegerStrin
// TODO add relevant data
export type TBondedMixnode = {
type: 'mixnode';
name?: string;
identityKey: string;
stake: DecCoin;
@@ -45,15 +46,26 @@ export type TBondedMixnode = {
delegators: number;
status: MixnodeStatus;
proxy?: string;
host: string;
httpApiPort: number;
mixPort: number;
verlocPort: number;
version: string;
};
export interface TBondedGateway {
type: 'gateway';
name: string;
identityKey: string;
ip: string;
bond: DecCoin;
location?: string; // TODO not yet available, only available in Network Explorer API
proxy?: string;
host: string;
httpApiPort: number;
mixPort: number;
verlocPort: number;
version: string;
}
export type TokenPool = 'locked' | 'balance';
@@ -155,26 +167,33 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
Console.warn(`get_operator_rewards request failed: ${e}`);
}
if (data) {
const { status, stakeSaturation } = await getAdditionalMixnodeDetails(data.bond_information.id);
const { bond_information, rewarding_details } = data;
const { status, stakeSaturation } = await getAdditionalMixnodeDetails(bond_information.id);
const nodeDescription = await getNodeDescription(
data.bond_information.mix_node.host,
data.bond_information.mix_node.http_api_port,
bond_information.mix_node.host,
bond_information.mix_node.http_api_port,
);
setBondedNode({
type: ownership.nodeType,
name: nodeDescription?.name,
identityKey: data.bond_information.mix_node.identity_key,
ip: '',
identityKey: bond_information.mix_node.identity_key,
ip: bond_information.id,
stake: {
amount: calculateStake(data.rewarding_details.operator, data.rewarding_details.delegates).toString(),
denom: data.bond_information.original_pledge.denom,
amount: calculateStake(rewarding_details.operator, data.rewarding_details.delegates).toString(),
denom: bond_information.original_pledge.denom,
},
bond: data.bond_information.original_pledge,
profitMargin: toPercentIntegerString(data.rewarding_details.cost_params.profit_margin_percent),
delegators: data.rewarding_details.unique_delegations,
proxy: data.bond_information.proxy,
bond: bond_information.original_pledge,
profitMargin: toPercentIntegerString(rewarding_details.cost_params.profit_margin_percent),
delegators: rewarding_details.unique_delegations,
proxy: bond_information.proxy,
operatorRewards,
status,
stakeSaturation,
host: bond_information.mix_node.host.replace(/\s/g, ''),
httpApiPort: bond_information.mix_node.http_api_port,
mixPort: bond_information.mix_node.mix_port,
verlocPort: bond_information.mix_node.verloc_port,
version: bond_information.mix_node.version,
} as TBondedMixnode);
}
} catch (e: any) {
@@ -190,6 +209,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
const nodeDescription = await getNodeDescription(data.gateway.host, data.gateway.clients_port);
setBondedNode({
type: ownership.nodeType,
name: nodeDescription?.name,
identityKey: data.gateway.identity_key,
ip: data.gateway.host,
+12
View File
@@ -7,6 +7,7 @@ import { mockSleep } from './utils';
const SLEEP_MS = 1000;
const bondedMixnodeMock: TBondedMixnode = {
type: 'mixnode',
name: 'Monster node',
identityKey: '7mjM2fYbtN6kxMwp1TrmQ4VwPks3URR5pBgWPWhzT98F',
stake: { denom: 'nym', amount: '1234' },
@@ -16,13 +17,24 @@ const bondedMixnodeMock: TBondedMixnode = {
operatorRewards: { denom: 'nym', amount: '1234' },
delegators: 5423,
status: 'active',
host: '1.2.34.5 ',
httpApiPort: 8000,
mixPort: 1789,
verlocPort: 1790,
version: '1.0.2',
};
const bondedGatewayMock: TBondedGateway = {
type: 'gateway',
name: 'Monster node',
identityKey: 'WayM2fYbtN6kxMwp1TrmQ4VwPks3URR5pBgWPWhzT98F',
ip: '112.43.234.57',
bond: { denom: 'nym', amount: '1234' },
host: '1.2.34.5 ',
httpApiPort: 8000,
mixPort: 1789,
verlocPort: 1790,
version: '1.0.2',
};
const TxResultMock: TransactionExecuteResult = {
+199
View File
@@ -0,0 +1,199 @@
import React, { useContext, useState } from 'react';
import { FeeDetails } from '@nymproject/types';
import { TPoolOption } from 'src/components';
import { Bond } from 'src/components/Bonding/Bond';
import { BondedMixnode } from 'src/components/Bonding/BondedMixnode';
import { TBondedMixnodeActions } from 'src/components/Bonding/BondedMixnodeActions';
import { BondGatewayModal } from 'src/components/Bonding/modals/BondGatewayModal';
import { BondMixnodeModal } from 'src/components/Bonding/modals/BondMixnodeModal';
import { ConfirmationDetailProps, ConfirmationDetailsModal } from 'src/components/Bonding/modals/ConfirmationModal';
import { UnbondModal } from 'src/components/Bonding/modals/UnbondModal';
import { ErrorModal } from 'src/components/Modals/ErrorModal';
import { LoadingModal } from 'src/components/Modals/LoadingModal';
import { AppContext, urls } from 'src/context/main';
import { isGateway, isMixnode, TBondGatewayArgs, TBondMixNodeArgs } from 'src/types';
import { BondedGateway } from 'src/components/Bonding/BondedGateway';
import { RedeemRewardsModal } from 'src/components/Bonding/modals/RedeemRewardsModal';
import { BondingContextProvider, useBondingContext, TBondedMixnode } from '../../context';
import { Box } from '@mui/material';
const Bonding = () => {
const [showModal, setShowModal] = useState<'bond-mixnode' | 'bond-gateway' | 'bond-more' | 'unbond' | 'redeem'>();
const [confirmationDetails, setConfirmationDetails] = useState<ConfirmationDetailProps>();
const {
network,
clientDetails,
userBalance: { originalVesting },
} = useContext(AppContext);
const {
bondedNode,
bondMixnode,
bondGateway,
unbond,
updateMixnode,
redeemRewards,
// compoundRewards,
isLoading,
checkOwnership,
} = useBondingContext();
const handleCloseModal = async () => {
setShowModal(undefined);
await checkOwnership();
};
const handleError = (error: string) => {
setShowModal(undefined);
setConfirmationDetails({
status: 'error',
title: 'An error occurred',
subtitle: error,
});
};
const handleBondMixnode = async (data: TBondMixNodeArgs, tokenPool: TPoolOption) => {
setShowModal(undefined);
const tx = await bondMixnode(data, tokenPool);
setConfirmationDetails({
status: 'success',
title: 'Bond successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
return undefined;
};
const handleBondGateway = async (data: TBondGatewayArgs, tokenPool: TPoolOption) => {
setShowModal(undefined);
const tx = await bondGateway(data, tokenPool);
setConfirmationDetails({
status: 'success',
title: 'Bond successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
const handleUnbond = async (fee?: FeeDetails) => {
setShowModal(undefined);
const tx = await unbond(fee);
setConfirmationDetails({
status: 'success',
title: 'Unbond successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
const handleRedeemReward = async (fee?: FeeDetails) => {
setShowModal(undefined);
const tx = await redeemRewards(fee);
setConfirmationDetails({
status: 'success',
title: 'Rewards redeemed successfully',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
const handleBondedMixnodeAction = (action: TBondedMixnodeActions) => {
switch (action) {
case 'bondMore': {
setShowModal('bond-more');
break;
}
case 'unbond': {
setShowModal('unbond');
break;
}
case 'redeem': {
setShowModal('redeem');
break;
}
default: {
return undefined;
}
}
return undefined;
};
return (
<Box sx={{ mt: 4 }}>
{!bondedNode && <Bond disabled={isLoading} onBond={() => setShowModal('bond-mixnode')} />}
{bondedNode && isMixnode(bondedNode) && (
<BondedMixnode
mixnode={bondedNode}
network={network}
onActionSelect={(action) => handleBondedMixnodeAction(action)}
/>
)}
{bondedNode && isGateway(bondedNode) && (
<BondedGateway gateway={bondedNode} onActionSelect={handleBondedMixnodeAction} network={network} />
)}
{showModal === 'bond-mixnode' && (
<BondMixnodeModal
denom={clientDetails?.display_mix_denom || 'nym'}
hasVestingTokens={Boolean(originalVesting)}
onBondMixnode={handleBondMixnode}
onSelectNodeType={() => setShowModal('bond-gateway')}
onClose={() => setShowModal(undefined)}
onError={handleError}
/>
)}
{showModal === 'bond-gateway' && (
<BondGatewayModal
denom={clientDetails?.display_mix_denom || 'nym'}
hasVestingTokens={Boolean(originalVesting)}
onBondGateway={handleBondGateway}
onSelectNodeType={() => setShowModal('bond-mixnode')}
onClose={() => setShowModal(undefined)}
onError={handleError}
/>
)}
{showModal === 'unbond' && bondedNode && (
<UnbondModal
node={bondedNode}
onClose={() => setShowModal(undefined)}
onConfirm={handleUnbond}
onError={handleError}
/>
)}
{showModal === 'redeem' && bondedNode && isMixnode(bondedNode) && (
<RedeemRewardsModal
node={bondedNode}
onClose={() => setShowModal(undefined)}
onConfirm={handleRedeemReward}
onError={handleError}
/>
)}
{confirmationDetails && confirmationDetails.status === 'success' && (
<ConfirmationDetailsModal
title={confirmationDetails.title}
subtitle={confirmationDetails.subtitle || 'This operation can take up to one hour to process'}
status={confirmationDetails.status}
txUrl={confirmationDetails.txUrl}
onClose={() => {
setConfirmationDetails(undefined);
handleCloseModal();
}}
/>
)}
{confirmationDetails && confirmationDetails.status === 'error' && (
<ErrorModal open message={confirmationDetails.subtitle} onClose={() => setConfirmationDetails(undefined)} />
)}
{isLoading && <LoadingModal />}
</Box>
);
};
export const BondingPage = () => (
<BondingContextProvider>
<Bonding />
</BondingContextProvider>
);
@@ -1,5 +1,5 @@
import * as React from 'react';
import { BondingPage } from './index';
import { BondingPage } from './Bonding';
import { MockBondingContextProvider } from '../../context/mocks/bonding';
export default {
+2 -237
View File
@@ -1,237 +1,2 @@
import React, { useContext, useEffect, useState } from 'react';
import { FeeDetails } from '@nymproject/types';
import { TPoolOption } from 'src/components';
import { Bond } from 'src/components/Bonding/Bond';
import { BondedMixnode } from 'src/components/Bonding/BondedMixnode';
import { TBondedMixnodeActions } from 'src/components/Bonding/BondedMixnodeActions';
import { BondGatewayModal } from 'src/components/Bonding/modals/BondGatewayModal';
import { BondMixnodeModal } from 'src/components/Bonding/modals/BondMixnodeModal';
import { ConfirmationDetailProps, ConfirmationDetailsModal } from 'src/components/Bonding/modals/ConfirmationModal';
import { NodeSettings } from 'src/components/Bonding/modals/NodeSettingsModal';
import { UnbondModal } from 'src/components/Bonding/modals/UnbondModal';
import { ErrorModal } from 'src/components/Modals/ErrorModal';
import { LoadingModal } from 'src/components/Modals/LoadingModal';
import { AppContext, urls } from 'src/context/main';
import { isGateway, isMixnode, TBondGatewayArgs, TBondMixNodeArgs } from 'src/types';
import { BondedGateway } from 'src/components/Bonding/BondedGateway';
import { RedeemRewardsModal } from 'src/components/Bonding/modals/RedeemRewardsModal';
import { Box } from '@mui/material';
import { BondingContextProvider, useBondingContext } from '../../context';
const Bonding = () => {
const [showModal, setShowModal] = useState<
'bond-mixnode' | 'bond-gateway' | 'bond-more' | 'unbond' | 'redeem' | 'compound' | 'node-settings'
>();
const [confirmationDetails, setConfirmationDetails] = useState<ConfirmationDetailProps>();
const {
network,
clientDetails,
userBalance: { originalVesting },
} = useContext(AppContext);
const {
bondedNode,
bondMixnode,
bondGateway,
unbond,
updateMixnode,
redeemRewards,
isLoading,
checkOwnership,
error,
} = useBondingContext();
useEffect(() => {
if (error) {
setShowModal(undefined);
setConfirmationDetails({
status: 'error',
title: 'An error occurred',
subtitle: error,
});
}
}, [error]);
const handleCloseModal = async () => {
setShowModal(undefined);
await checkOwnership();
};
const handleError = (e: string) => {
setShowModal(undefined);
setConfirmationDetails({
status: 'error',
title: 'An error occurred',
subtitle: e,
});
};
const handleBondMixnode = async (data: TBondMixNodeArgs, tokenPool: TPoolOption) => {
setShowModal(undefined);
const tx = await bondMixnode(data, tokenPool);
setConfirmationDetails({
status: 'success',
title: 'Bond successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
return undefined;
};
const handleBondGateway = async (data: TBondGatewayArgs, tokenPool: TPoolOption) => {
setShowModal(undefined);
const tx = await bondGateway(data, tokenPool);
setConfirmationDetails({
status: 'success',
title: 'Bond successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
const handleUnbond = async (fee?: FeeDetails) => {
setShowModal(undefined);
const tx = await unbond(fee);
setConfirmationDetails({
status: 'success',
title: 'Unbond successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
const handleUpdateProfitMargin = async (profitMargin: string, fee?: FeeDetails) => {
setShowModal(undefined);
const tx = await updateMixnode(profitMargin, fee);
setConfirmationDetails({
status: 'success',
title: 'Profit margin update successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
const handleRedeemReward = async (fee?: FeeDetails) => {
setShowModal(undefined);
const tx = await redeemRewards(fee);
setConfirmationDetails({
status: 'success',
title: 'Rewards redeemed successfully',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
const handleBondedMixnodeAction = (action: TBondedMixnodeActions) => {
switch (action) {
case 'bondMore': {
setShowModal('bond-more');
break;
}
case 'unbond': {
setShowModal('unbond');
break;
}
case 'redeem': {
setShowModal('redeem');
break;
}
case 'nodeSettings': {
setShowModal('node-settings');
break;
}
default: {
return undefined;
}
}
return undefined;
};
return (
<Box sx={{ mt: 4 }}>
{!bondedNode && <Bond disabled={isLoading} onBond={() => setShowModal('bond-mixnode')} />}
{bondedNode && isMixnode(bondedNode) && (
<BondedMixnode
mixnode={bondedNode}
network={network}
onActionSelect={(action) => handleBondedMixnodeAction(action)}
/>
)}
{bondedNode && isGateway(bondedNode) && (
<BondedGateway gateway={bondedNode} onActionSelect={handleBondedMixnodeAction} network={network} />
)}
{showModal === 'bond-mixnode' && (
<BondMixnodeModal
denom={clientDetails?.display_mix_denom || 'nym'}
hasVestingTokens={Boolean(originalVesting)}
onBondMixnode={handleBondMixnode}
onSelectNodeType={() => setShowModal('bond-gateway')}
onClose={() => setShowModal(undefined)}
onError={handleError}
/>
)}
{showModal === 'bond-gateway' && (
<BondGatewayModal
denom={clientDetails?.display_mix_denom || 'nym'}
hasVestingTokens={Boolean(originalVesting)}
onBondGateway={handleBondGateway}
onSelectNodeType={() => setShowModal('bond-mixnode')}
onClose={() => setShowModal(undefined)}
onError={handleError}
/>
)}
{showModal === 'unbond' && bondedNode && (
<UnbondModal
node={bondedNode}
onClose={() => setShowModal(undefined)}
onConfirm={handleUnbond}
onError={handleError}
/>
)}
{showModal === 'redeem' && bondedNode && isMixnode(bondedNode) && (
<RedeemRewardsModal
node={bondedNode}
onClose={() => setShowModal(undefined)}
onConfirm={handleRedeemReward}
onError={handleError}
/>
)}
{showModal === 'node-settings' && bondedNode && isMixnode(bondedNode) && (
<NodeSettings
currentPm={bondedNode.profitMargin}
isVesting={Boolean(bondedNode.proxy)}
onConfirm={handleUpdateProfitMargin}
onClose={() => setShowModal(undefined)}
onError={handleError}
/>
)}
{confirmationDetails && confirmationDetails.status === 'success' && (
<ConfirmationDetailsModal
title={confirmationDetails.title}
subtitle={confirmationDetails.subtitle || 'This operation can take up to one hour to process'}
status={confirmationDetails.status}
txUrl={confirmationDetails.txUrl}
onClose={() => {
setConfirmationDetails(undefined);
handleCloseModal();
}}
/>
)}
{confirmationDetails && confirmationDetails.status === 'error' && (
<ErrorModal open message={confirmationDetails.subtitle} onClose={() => setConfirmationDetails(undefined)} />
)}
{isLoading && <LoadingModal />}
</Box>
);
};
export const BondingPage = () => (
<BondingContextProvider>
<Bonding />
</BondingContextProvider>
);
export * from './Bonding';
export * from './node-settings';
@@ -0,0 +1,267 @@
import { useEffect, useState } from 'react';
import { Button, Divider, Typography, TextField, Grid, Alert, IconButton } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import CloseIcon from '@mui/icons-material/Close';
import { TBondedMixnode, TBondedGateway } from '../../../../context/bonding';
import { SimpleModal } from '../../../../components/Modals/SimpleModal';
const getNumberlength = (number: number) => {
return number.toString().length;
};
// TODO: adding ip regex that works well
const ipRegex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
// TODO: only accept valid nym wallet versions
const appVersionRegex = /^\d+(?:\.\d+){2}$/gm;
export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBondedGateway }) => {
const { mixPort, verlocPort, httpApiPort, host, version } = bondedNode;
const [buttonActive, setButtonActive] = useState<boolean>(false);
const [open, setOpen] = useState(true);
const [openConfirmationModal, setOpenConfirmationModal] = useState<boolean>(false);
const [mixPortUpdated, setMixPortUpdated] = useState<number>(mixPort);
const [verlocPortUpdated, setVerlocPortUpdated] = useState<number>(verlocPort);
const [httpApiPortUpdated, setHttpApiPortUpdated] = useState<number>(httpApiPort);
const [hostUpdated, setHostUpdated] = useState<string>(host);
const [versionUpdated, setVersionUpdated] = useState<string>(version);
const theme = useTheme();
useEffect(() => {
setButtonActive(true);
if (
mixPortUpdated === mixPort &&
verlocPortUpdated === verlocPort &&
httpApiPortUpdated === httpApiPort &&
hostUpdated === host &&
versionUpdated === version
) {
setButtonActive(false);
}
if (
getNumberlength(mixPortUpdated) !== 4 ||
getNumberlength(verlocPortUpdated) !== 4 ||
getNumberlength(httpApiPortUpdated) !== 4 ||
!versionUpdated.match(appVersionRegex)
) {
setButtonActive(false);
}
}, [mixPortUpdated, verlocPortUpdated, httpApiPortUpdated, hostUpdated, versionUpdated]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { value, id } = e.target;
const numNewValue = parseInt(value) || 0;
switch (id) {
case 'mixPort':
setMixPortUpdated(numNewValue);
break;
case 'verlocPort':
setVerlocPortUpdated(numNewValue);
break;
case 'httpApiPort':
setHttpApiPortUpdated(numNewValue);
break;
case 'host':
setHostUpdated(value);
break;
case 'version':
setVersionUpdated(value);
}
};
return (
<Grid container xs>
{open && (
<Alert
severity="info"
action={
<IconButton
aria-label="close"
color="inherit"
size="small"
onClick={() => {
setOpen(false);
}}
>
<CloseIcon fontSize="inherit" />
</IconButton>
}
sx={{
px: 2,
borderRadius: 0,
bgcolor: 'background.default',
color: (theme) => theme.palette.nym.nymWallet.text.blue,
'& .MuiAlert-icon': { color: (theme) => theme.palette.nym.nymWallet.text.blue, mr: 1 },
}}
>
<strong>Your changes will be ONLY saved on the display.</strong> Remember to change the values on your nodes
config file too.
</Alert>
)}
<Grid container>
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3}>
<Grid item>
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1 }}>
Port
</Typography>
<Typography
variant="body1"
sx={{
fontSize: 14,
mb: 2,
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
}}
>
Change profit margin of your node
</Typography>
</Grid>
<Grid spacing={3} item container alignItems="center" xs={12} md={6}>
<Grid item width={1}>
<TextField
id="mixPort"
type="input"
label="Mix Port"
value={mixPortUpdated}
onChange={(e) => handleChange(e)}
inputProps={{ maxLength: 4 }}
fullWidth
/>
</Grid>
<Grid item width={1}>
<TextField
id="verlocPort"
type="input"
label="Verloc Port"
value={verlocPortUpdated}
onChange={(e) => handleChange(e)}
inputProps={{ maxLength: 4 }}
fullWidth
/>
</Grid>
<Grid item width={1}>
<TextField
id="httpApiPort"
type="input"
label="HTTP port"
value={httpApiPortUpdated}
onChange={(e) => handleChange(e)}
inputProps={{ maxLength: 4 }}
fullWidth
/>
</Grid>
</Grid>
</Grid>
<Divider flexItem />
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3}>
<Grid item>
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1 }}>
Host
</Typography>
<Typography
variant="body1"
sx={{
fontSize: 14,
mb: 2,
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
}}
>
Lock wallet after certain time
</Typography>
</Grid>
<Grid spacing={3} item container alignItems="center" xs={12} md={6}>
<Grid item width={1}>
<TextField
id="host"
type="input"
label="host"
value={hostUpdated}
onChange={(e) => handleChange(e)}
fullWidth
/>
</Grid>
</Grid>
</Grid>
<Divider flexItem />
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3}>
<Grid item>
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1 }}>
Version
</Typography>
<Typography
variant="body1"
sx={{
fontSize: 14,
mb: 2,
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
}}
>
Lock wallet after certain time
</Typography>
</Grid>
<Grid spacing={3} item container alignItems="center" xs={12} md={6}>
<Grid item width={1}>
<TextField
id="version"
type="input"
label="Version"
value={versionUpdated}
onChange={(e) => handleChange(e)}
fullWidth
/>
</Grid>
</Grid>
</Grid>
<Divider flexItem />
<Grid container justifyContent="end">
<Button
size="large"
variant="contained"
disabled={!buttonActive}
onClick={() => setOpenConfirmationModal(true)}
sx={{ m: 3, width: '320px' }}
>
Save all display changes
</Button>
</Grid>
</Grid>
<SimpleModal
open={openConfirmationModal}
header="Your changes were ONLY saved on the display"
subHeader="Remember to change the values
on your nodes config file too."
okLabel="close"
hideCloseIcon
displayInfoIcon
onOk={async () => {
await setOpenConfirmationModal(false);
}}
buttonFullWidth
sx={{
width: '450px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
headerStyles={{
width: '100%',
mb: 1,
textAlign: 'center',
color: theme.palette.nym.nymWallet.text.blue,
fontSize: 16,
textTransform: 'capitalize',
}}
subHeaderStyles={{
width: '100%',
mb: 1,
textAlign: 'center',
color: 'main',
fontSize: 14,
textTransform: 'capitalize',
}}
/>
</Grid>
);
};
@@ -0,0 +1,207 @@
import { useState, useEffect } from 'react';
import { Button, Divider, Typography, TextField, InputAdornment, Grid, Alert, IconButton } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import CloseIcon from '@mui/icons-material/Close';
import { TBondedMixnode, TBondedGateway } from '../../../../context/bonding';
import { SimpleModal } from '../../../../components/Modals/SimpleModal';
export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBondedGateway }) => {
const { bond, type } = bondedNode;
const [buttonActive, setButtonActive] = useState<boolean>(false);
const [open, setOpen] = useState(true);
const [openConfirmationModal, setOpenConfirmationModal] = useState<boolean>(false);
const [profitMarginPercent, setProfitMarginPercent] = useState<string>(
bondedNode.type === 'mixnode' ? bondedNode.profitMargin : '',
);
const [operatorCost, setOperatorCost] = useState<number>(parseInt(bond.amount));
const theme = useTheme();
useEffect(() => {
if (
type === 'mixnode' &&
bondedNode.profitMargin === profitMarginPercent &&
operatorCost === parseInt(bond.amount)
) {
setButtonActive(false);
} else {
setButtonActive(true);
}
}, [profitMarginPercent, operatorCost]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { value, id } = e.target;
const numNewValue = parseInt(value) || 0;
switch (id) {
case 'profitMargin':
setProfitMarginPercent(value);
break;
case 'operatorCost':
setOperatorCost(numNewValue);
break;
}
};
// Something could be useful to update the profitMargin
// const handleUpdateProfitMargin = async (profitMargin: number, fee?: FeeDetails) => {
// setShowModal(undefined);
// const tx = await updateMixnode(profitMargin, fee);
// setConfirmationDetails({
// status: 'success',
// title: 'Profit margin update successful',
// txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
// });
// };
return (
<Grid container xs>
{open && (
<Alert
severity="info"
action={
<IconButton
aria-label="close"
color="inherit"
size="small"
onClick={() => {
setOpen(false);
}}
>
<CloseIcon fontSize="inherit" />
</IconButton>
}
sx={{
width: 1,
px: 2,
borderRadius: 0,
bgcolor: 'background.default',
color: (theme) => theme.palette.nym.nymWallet.text.blue,
'& .MuiAlert-icon': { color: (theme) => theme.palette.nym.nymWallet.text.blue, mr: 1 },
}}
>
<strong>Profit margin can be changed once a month, your changes will be applied in the next interval</strong>
</Alert>
)}
<Grid container direction="column">
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3} spacing={1}>
<Grid item direction="column">
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1 }}>
Profit Margin
</Typography>
<Typography
variant="body1"
sx={{
fontSize: 14,
mb: 2,
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
}}
>
Profit margin can be changed once a month
</Typography>
</Grid>
<Grid spacing={3} item container alignItems="center" xs={12} md={6}>
{type === 'mixnode' && (
<Grid item width={1} spacing={3}>
<TextField
id="profitMargin"
type="input"
label="Profit margin"
value={profitMarginPercent}
onChange={(e) => handleChange(e)}
fullWidth
InputProps={{
endAdornment: (
<InputAdornment position="end">
<span>%</span>
</InputAdornment>
),
}}
/>
</Grid>
)}
</Grid>
</Grid>
<Divider flexItem />
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3} spacing={1}>
<Grid item direction="column">
<Typography variant="body1" sx={{ fontWeight: 600, mb: 1 }}>
Operator cost
</Typography>
<Typography
variant="body1"
sx={{
fontSize: 14,
mb: 2,
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
}}
>
Lock Wallet after a certain time
</Typography>
</Grid>
<Grid spacing={3} item container alignItems="center" xs={12} md={6}>
<Grid item width={1} spacing={3}>
<TextField
id="operatorCost"
type="input"
label="Operator cost"
value={operatorCost}
onChange={(e) => handleChange(e)}
fullWidth
InputProps={{
endAdornment: (
<InputAdornment position="end">
<span>{bond.denom.toUpperCase()}</span>
</InputAdornment>
),
}}
/>
</Grid>
</Grid>
</Grid>
<Divider flexItem />
<Grid container justifyContent="end">
<Button
size="large"
variant="contained"
disabled={!buttonActive}
onClick={() => setOpenConfirmationModal(true)}
sx={{ m: 3, width: '320px' }}
>
Save all display changes
</Button>
</Grid>
</Grid>
<SimpleModal
open={openConfirmationModal}
header="Your changes will take place
in the next interval"
okLabel="close"
hideCloseIcon
displayInfoIcon
onOk={async () => {
await setOpenConfirmationModal(false);
}}
buttonFullWidth
sx={{
width: '320px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
headerStyles={{
width: '100%',
mb: 1,
textAlign: 'center',
color: theme.palette.nym.nymWallet.text.blue,
fontSize: 16,
textTransform: 'capitalize',
}}
subHeaderStyles={{
m: 0,
}}
/>
</Grid>
);
};
@@ -0,0 +1,41 @@
import React, { useContext, useEffect, useState } from 'react';
import { Box, Button, Divider, Grid } from '@mui/material';
import { TBondedMixnode, TBondedGateway } from '../../../../context/bonding';
import { InfoSettings } from './InfoSettings';
import { ParametersSettings } from './ParametersSettings';
const nodeGeneralNav = ['Info', 'Parameters'];
export const NodeGeneralSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBondedGateway }) => {
const [settingsCard, setSettingsCard] = useState<string>(nodeGeneralNav[0]);
//TODO: Check what happens with a gateway
return (
<Box sx={{ pl: 3, pt: 3 }}>
<Grid container direction="row" spacing={3}>
<Grid item container direction="column" xs={3}>
{nodeGeneralNav.map((item) => (
<Button
size="small"
sx={{
fontSize: 14,
color: settingsCard === item ? 'primary.main' : 'inherit',
justifyContent: 'start',
':hover': {
bgcolor: 'transparent',
color: 'primary.main',
},
}}
key={item}
onClick={() => setSettingsCard(item)}
>
{item}
</Button>
))}
</Grid>
<Divider orientation="vertical" flexItem />
{settingsCard === nodeGeneralNav[0] && <InfoSettings bondedNode={bondedNode} />}
{settingsCard === nodeGeneralNav[1] && <ParametersSettings bondedNode={bondedNode} />}
</Grid>
</Box>
);
};
@@ -0,0 +1,149 @@
import React, { useContext, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { FeeDetails } from '@nymproject/types';
import { Box, Typography, Stack, Button, Divider } from '@mui/material';
import { Close } from '@mui/icons-material';
import { useTheme } from '@mui/material/styles';
import { ConfirmationDetailProps, ConfirmationDetailsModal } from 'src/components/Bonding/modals/ConfirmationModal';
import { Node as NodeIcon } from 'src/svg-icons/node';
import { NymCard } from '../../../components';
import { PageLayout } from '../../../layouts';
import { Tabs } from 'src/components/Tabs';
import { useBondingContext, BondingContextProvider } from '../../../context';
import { AppContext, urls } from 'src/context/main';
import { NodeGeneralSettings } from './general-settings';
import { UnbondModal } from '../../../components/Bonding/modals/UnbondModal';
import { nodeSettingsNav } from './node-settings.constant';
export const NodeSettings = () => {
const [confirmationDetails, setConfirmationDetails] = useState<ConfirmationDetailProps>();
const [value, setValue] = React.useState(0);
const theme = useTheme();
const handleChange = (event: React.SyntheticEvent, tab: number) => {
setValue(tab);
};
const { network } = useContext(AppContext);
const { bondedNode, unbond } = useBondingContext();
const navigate = useNavigate();
const handleCloseUnboundModal = () => {
if (nodeSettingsNav.length === 1) {
navigate('/bonding');
} else if (nodeSettingsNav[0] === 'Unbond') {
setValue(1);
} else {
setValue(0);
}
};
const handleUnbond = async (fee?: FeeDetails) => {
const tx = await unbond(fee);
setConfirmationDetails({
status: 'success',
title: 'Unbond successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
};
const handleError = (error: string) => {
setConfirmationDetails({
status: 'error',
title: 'An error occurred',
subtitle: error,
});
};
return (
<PageLayout>
<NymCard
borderless
noPadding
title={
<Stack gap={2} sx={{ py: 0 }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<NodeIcon />
<Typography variant="h6" fontWeight={600}>
Node Settings
</Typography>
</Box>
</Box>
<Box sx={{ width: '100%' }}>
<Tabs
tabs={nodeSettingsNav}
selectedTab={value}
onChange={handleChange}
tabSx={{
bgcolor: 'transparent',
borderBottom: 'none',
borderTop: 'none',
'& button': {
p: 0,
mr: 4,
minWidth: 'none',
fontSize: 16,
},
'& button:hover': {
color: theme.palette.nym.highlight,
opacity: 1,
},
}}
tabIndicatorStyles={{ height: 4, bottom: '6px', borderRadius: '2px' }}
/>
</Box>
</Stack>
}
Action={
<Button
size="small"
sx={{
color: 'text.primary',
}}
onClick={() => navigate('/bonding')}
startIcon={<Close />}
></Button>
}
>
<Divider />
{nodeSettingsNav[value] === 'General' && bondedNode && <NodeGeneralSettings bondedNode={bondedNode} />}
{nodeSettingsNav[value] === 'Unbond' && bondedNode && (
<UnbondModal
node={bondedNode}
onClose={handleCloseUnboundModal}
onConfirm={handleUnbond}
onError={handleError}
/>
)}
{confirmationDetails && confirmationDetails.status === 'success' && (
<ConfirmationDetailsModal
title={confirmationDetails.title}
subtitle={confirmationDetails.subtitle || 'This operation can take up to one hour to process'}
status={confirmationDetails.status}
txUrl={confirmationDetails.txUrl}
onClose={() => {
setConfirmationDetails(undefined);
navigate('/bonding');
}}
/>
)}
</NymCard>
</PageLayout>
);
};
export const NodeSettingsPage = () => (
<BondingContextProvider>
<NodeSettings />
</BondingContextProvider>
);
@@ -0,0 +1,2 @@
// If we want to hide a tab we can remove the tab from the bellow array
export const nodeSettingsNav = ['General', 'Unbond'];
+2 -1
View File
@@ -4,7 +4,7 @@ import { ApplicationLayout } from 'src/layouts';
import { Terminal } from 'src/pages/terminal';
import { Send } from 'src/components/Send';
import { Receive } from '../components/Receive';
import { Balance, InternalDocs, DelegationPage, Admin, BondingPage } from '../pages';
import { Balance, InternalDocs, DelegationPage, Admin, BondingPage, NodeSettingsPage } from '../pages';
export const AppRoutes = () => (
<ApplicationLayout>
@@ -14,6 +14,7 @@ export const AppRoutes = () => (
<Routes>
<Route path="/balance" element={<Balance />} />
<Route path="/bonding" element={<BondingPage />} />
<Route path="/bonding/node-settings" element={<NodeSettingsPage />} />
<Route path="/delegation" element={<DelegationPage />} />
<Route path="/docs" element={<InternalDocs />} />
<Route path="/admin" element={<Admin />} />
+2
View File
@@ -31,6 +31,7 @@ declare module '@mui/material/styles' {
highlight: string;
success: string;
info: string;
red: string;
fee: string;
background: { light: string; dark: string };
text: {
@@ -57,6 +58,7 @@ declare module '@mui/material/styles' {
warn: string;
contrast: string;
grey: string;
blue: string;
};
topNav: {
background: string;
+13
View File
@@ -23,6 +23,7 @@ const nymPalette: NymPalette = {
highlight: '#FB6E4E',
success: '#21D073',
info: '#60D7EF',
red: '#DA465B',
fee: '#967FF0',
background: { light: '#F4F6F8', dark: '#1D2125' },
text: {
@@ -49,6 +50,7 @@ const darkMode: NymPaletteVariant = {
warn: '#FFE600',
contrast: '#1D2125',
grey: '#5B6174',
blue: '#60D7EF',
},
topNav: {
background: '#111826',
@@ -79,6 +81,7 @@ const lightMode: NymPaletteVariant = {
warn: '#FFE600',
contrast: '#FFFFFF',
grey: '#3A4053',
blue: '#514EFB',
},
topNav: {
background: '#111826',
@@ -285,6 +288,16 @@ export const getDesignTokens = (mode: PaletteMode): ThemeOptions => {
},
},
},
MuiToolbar: {
styleOverrides: {
root: {
minWidth: 0,
'@media (min-width: 0px)': {
minHeight: 'fit-content',
},
},
},
},
},
palette,
};
-1
View File
@@ -1 +0,0 @@
node_modules
-7
View File
@@ -1,7 +0,0 @@
{
"trailingComma": "all",
"singleQuote": true,
"printWidth": 120,
"tabWidth": 2,
"semi": false
}
@@ -1,86 +0,0 @@
import Balance from '../tests/pageobjects/balanceScreen'
import Auth from '../tests/pageobjects/authScreens'
const userData = require("../common/user-data.json");
const deleteScript = require("../scripts/deletesavedwallet")
const savedWalletScript = require("../scripts/savedwalletexists")
class Helpers {
// clear wallet data, login, and navigate to QA network
freshMnemonicLoginQaNetwork = async () => {
await deleteScript
await savedWalletScript
await Auth.loginWithMnemonic(userData.mnemonic)
await Balance.selectQa()
}
loginMnemonic = async () => {
await Auth.loginWithMnemonic(userData.mnemonic)
}
//helper to decode mnemonic so plain 24 character passphrase isn't in sight albeit it is presented when ruunning the scripts
// TO-DO figure out what's going on with the decoding bit
decodeBase = async (input) => {
var m = Buffer.from(input, "base64").toString();
return m;
}
navigateAndClick = async (element) => {
await element.waitForClickable({ timeout: 6000 })
await element.click();
}
elementVisible = async (element) => {
await element.waitForDisplayed({ timeout: 6000 })
}
elementClickable = async (element) => {
await element.toBeClickable({ timeout: 8000 })
}
addValueToTextField = async (element, value) => {
await element.addValue(value)
}
verifyStrictText = async (element, expectedText) => {
let error = await element.getText()
expect(error).toStrictEqual(expectedText)
}
verifyPartialText = async (element, expectedText) => {
let error = await element.getText()
expect(error).toContain(expectedText)
}
currentBalance = async (value) => {
return parseFloat(value.split(/\s+/)[0].toString()).toFixed(5)
}
calculateFees = async (beforeBalance, transactionFee, amount, isSend) => {
let fee
if (isSend) {
//send transaction
fee = transactionFee.split(/\s+/)[0]
} else {
//delegate transaction
fee = transactionFee.split(/\s+/)[3]
}
const currentBalance = beforeBalance.split(/\s+/)[0]
console.log("currenttttt 2 ............. = " + currentBalance)
const castCurrentBalance = parseFloat(currentBalance).toFixed(5)
console.log("castttt ............. " + castCurrentBalance)
const transCost = +parseFloat(amount) + +parseFloat(fee).toFixed(5)
console.log("trans ............." + transCost)
let sum = +castCurrentBalance - transCost
return sum.toFixed(5)
}
}
module.exports = new Helpers();
@@ -1,42 +0,0 @@
module.exports = {
//welcome, sign in, create account
homePageErrorMnemonic: "Error parsing bip39 mnemonic",
signInWithoutMnemonic: "A mnemonic must be provided",
signInRandomString: "mnemonic has a word count that is not a multiple of 6:",
signInIncorrectMnemonic: "mnemonic contains an unknown word",
incorrectMnemonicPasswordCreation: "The mnemonic provided is not valid. Please check the mnemonic",
invalidPasswordOnSignIn: "failed to decrypt the given data with the provided password",
signInWithoutPassword: "A password must be provided",
failedToFindWalletFile: "The wallet file is not found",
//headers
mnemonicSignIn: "Enter a mnemonic to sign in",
passwordSignIn: "Enter a password to sign in",
//homePage
qaNetwork: "QA",
sandboxNetwork: "Testnet Sandbox",
mainnetNetwork: "Nym Mainnet",
noNym: "0 NYM",
//send
invalidRecipientAddress: "123",
recipientAddress: "n17tj0a0w6v7r2dc54rnkzfza6s8hxs87rj273a5",
amountToSend: "1",
negativeAmount: "-1",
inferiorAmount: "0.0000001",
confirmedAmount: "1 NYM",
sendDetails: "Send details",
// bond
host: "1.1.1.1",
version: "1.2.1",
// user incorrect data
incorrectMnemonic: "giraffe note order sun cradle bottom crime humble able antique rural donkey guess parent potato tongue truly way disagree exile zebra someone else heat",
randomString:"thisrandomstring",
password:"iAmThePassword1!",
incorrectPassword:"123notvalid",
};
@@ -1,9 +0,0 @@
{
"mnemonic": "giraffe note order sun cradle bottom crime humble able antique rural donkey guess parent potato tongue truly way disagree exile zebra someone else typical",
"qa_address": "n1qqct7gs79yrjncpkumljxeqjsnwvn42j2g3fw4",
"receiver_address": "n167rupnmpput2alw62sz43eelks03zek4fwvjk0",
"amount_to_send": "1",
"identity_key_to_delegate_mix_node": "HqW2HStFHtAZ3PxRaiSCh7xJK6B7swoR1gSmJzH2iV9g",
"identity_key_to_delegate_gateway": "",
"delegate_amount": "10"
}
-31
View File
@@ -1,31 +0,0 @@
{
"name": "wallet-ui-tests",
"version": "1.0.0",
"description": "ui tests for the nym wallet",
"scripts": {
"test": "wdio run wdio.conf.ts",
"test:signup": "wdio run wdio.conf.ts --suite signup",
"test:login": "wdio run wdio.conf.ts --suite login",
"test:balance": "wdio run wdio.conf.ts --suite balance",
"test:nav": "wdio run wdio.conf.ts --suite nav",
"test:send": "wdio run wdio.conf.ts --suite send",
"test:delegation": "wdio run wdio.conf.ts --suite delegation"
},
"author": "",
"license": "MIT",
"dependencies": {
"-": "^0.0.1",
"@types/mocha": "^9.1.1",
"save-dev": "^0.0.1-security",
"ts-node": "^10.6.0",
"wdio": "^6.0.1"
},
"devDependencies": {
"@wdio/cli": "^7.24.0",
"@wdio/local-runner": "^7.16.16",
"@wdio/mocha-framework": "^7.16.15",
"@wdio/spec-reporter": "^7.16.14",
"prettier": "2.5.1",
"typescript": "^4.6.2"
}
}
@@ -1,9 +0,0 @@
const { exec } = require("child_process")
const deleteSavedFile = exec("rm '/home/benedetta/.local/share/nym-wallet/saved-wallet.json'", (err, stdout, stderr) => {
if (err) {
console.error(`${err.message}`)
return
} else
console.log("File deleted")
})
@@ -1,14 +0,0 @@
const { exec } = require("child_process")
// const doesFileExist = exec("test -f /home/benedetta/.local/share/nym-wallet/saved-wallet.json" && "echo '$FILE exists.'" || "echo 'file doesn't exist'")
// scriptExist ? expect(getErrorWarning).toStrictEqual(textConstants.invalidPasswordOnSignIn) : expect(getErrorWarning).toStrictEqual(textConstants.failedToFindWalletFile)
const doesFileExist = exec("test -f /home/benedetta/.local/share/nym-wallet/saved-wallet.json", (err, stdout, stderr) => {
if (err) {
console.error(`${err.message}`)
return
} else
console.log("File: " + stdout)
})
@@ -1,21 +0,0 @@
class Nav {
get lightMode(): Promise<WebdriverIO.Element> { return $("[data-testid='LightModeOutlinedIcon']") }
get darkMode(): Promise<WebdriverIO.Element> { return $("[data-testid='ModeNightOutlinedIcon']") }
get terminalTitle(): Promise<WebdriverIO.Element> { return $("[data-testid='terminal-header']") }
get terminalIcon(): Promise<WebdriverIO.Element> { return $("[data-testid='TerminalIcon']") }
get balance(): Promise<WebdriverIO.Element> { return $("[data-testid='Balance']") }
get send(): Promise<WebdriverIO.Element> { return $("[data-testid='Send']") }
get receive(): Promise<WebdriverIO.Element> { return $("[data-testid='Receive']") }
get bond(): Promise<WebdriverIO.Element> { return $("[data-testid='Bond']") }
get unbond(): Promise<WebdriverIO.Element> { return $("[data-testid='Unbond']") }
get delegation(): Promise<WebdriverIO.Element> { return $("[data-testid='Delegation']") }
get closeIcon(): Promise<WebdriverIO.Element> { return $("[data-testid='CloseIcon']") }
}
export default new Nav()
@@ -1,74 +0,0 @@
import Balance from '../pageobjects/balanceScreen'
class Auth {
//Welcome landing page
get signInButton(): Promise<WebdriverIO.Element> { return $("[data-testid='signIn']") }
get createAccount(): Promise<WebdriverIO.Element> { return $("[data-testid='createAccount']") }
// Existing account sign in option page
get signInMnemonic(): Promise<WebdriverIO.Element> { return $("[data-testid='signInWithMnemonic']") }
get signInPassword(): Promise<WebdriverIO.Element> { return $("[data-testid='signInWithPassword']") }
get backToWelcomePage(): Promise<WebdriverIO.Element> { return $("[data-testid='backToWelcomePage']") }
get forgotPassword(): Promise<WebdriverIO.Element> { return $("[data-testid='forgotPassword']") }
// Sign in with mnemonic page
get mnemonicLoginScreenHeader(): Promise<WebdriverIO.Element> { return $("[data-testid='Enter a mnemonic to sign in']") }
get mnemonicInput(): Promise<WebdriverIO.Element> { return $("[data-testid='mnemonicInput']") }
get signIn(): Promise<WebdriverIO.Element> { return $("[data-testid='signInSubmitButton']") }
get backToSignInOptions(): Promise<WebdriverIO.Element> { return $("[data-testid='backToSignInOptions']") }
get createPassword(): Promise<WebdriverIO.Element> { return $("[data-testid='goToCreatePassword']") }
// Create password step 1/2
get backToMnemonicSignIn(): Promise<WebdriverIO.Element> { return $("[data-testid='backToMnemonicSignIn']") }
get nextToPasswordCreation(): Promise<WebdriverIO.Element> { return $("[data-testid='nextToPasswordCreation']") }
// Create password step 2/2
get password(): Promise<WebdriverIO.Element> { return $("[data-testid='Password']") }
get confirmPassword(): Promise<WebdriverIO.Element> { return $("[data-testid='Confirm password']") }
get createPasswordButton(): Promise<WebdriverIO.Element> { return $("[data-testid='createPasswordButton']") }
get backToStep1PasswordCreation(): Promise<WebdriverIO.Element> { return $("[data-testid='backToStep1PasswordCreation']") }
// Create account step 1/3
get copyMnemonic(): Promise<WebdriverIO.Element> { return $("[data-testid='copyMnemonic']") }
get iSavedMnemonic(): Promise<WebdriverIO.Element> { return $("[data-testid='iSavedMnemonic']") }
get mnemonicPhrase(): Promise<WebdriverIO.Element> { return $("[data-testid='mnemonicPhrase']") }
get backToWelcomePageFromCreate(): Promise<WebdriverIO.Element> { return $("[data-testid='backToWelcome']") }
// Create account step 2/3
get wordIndex(): Promise<WebdriverIO.Element> { return $("[data-testid='wordIndex']") }
get mnemonicWordTile(): Promise<WebdriverIO.Element> { return $("[data-testid='mnemonicWordTile']") }
get nextToStep3(): Promise<WebdriverIO.Element> { return $("[data-testid='nextToStep3']") }
get backToStep1(): Promise<WebdriverIO.Element> { return $("[data-testid='backToStep1']") }
// Create account step 3/3
get nextStorePassword(): Promise<WebdriverIO.Element> { return $("[data-testid='nextStorePassword']") }
get skipPasswordAndSignInWithMnemonic(): Promise<WebdriverIO.Element> { return $("[data-testid='skipPasswordAndSignInWithMnemonic']") }
// Enter password to sign in
get passwordLoginScreenHeader(): Promise<WebdriverIO.Element> { return $("[data-testid='Enter a password to sign in']") }
get enterPassword(): Promise<WebdriverIO.Element> { return $("[data-testid='Enter password']") }
get signInPasswordButton(): Promise<WebdriverIO.Element> { return $("[data-testid='signInPasswordButton']") }
get backToSignInOptionsFromPassword(): Promise<WebdriverIO.Element> { return $("[data-testid='backToSignInOptionsFromPassword']") }
get forgotPasswordButton(): Promise<WebdriverIO.Element> { return $("[data-testid='forgotPasswordButton']") }
// Errors
get error(): Promise<WebdriverIO.Element> { return $("[data-testid='error']") }
//TO-DO get this bit below working
getErrorMessage = async () => {
await (await this.error).waitForDisplayed({ timeout: 1500 })
await (await this.error).getText()
}
//login to the application
loginWithMnemonic = async (mnemonic) => {
await (await this.signInButton).click()
await (await this.signInMnemonic).click()
await (await this.mnemonicInput).waitForDisplayed()
await (await this.mnemonicInput).addValue(mnemonic);
await (await this.signIn).click();
await (await Balance.nymBalance).waitForDisplayed({ timeout: 4000 });
};
}
export default new Auth()
@@ -1,23 +0,0 @@
class Balance {
get balance(): Promise<WebdriverIO.Element> { return $("[data-testid='Balance']") }
get checkBalance(): Promise<WebdriverIO.Element> { return $("[data-testid='check-balance']") }
get nymBalance(): Promise<WebdriverIO.Element> { return $("[data-testid='nym-balance']") }
get copyAccountId(): Promise<WebdriverIO.Element> { return $("[data-testid='copyIcon']") }
get accountNumber(): Promise<WebdriverIO.Element> { return $("[data-testid='accountNumber']") }
get networkDropdown(): Promise<WebdriverIO.Element> { return $("[data-testid='ArrowDropDownIcon']") }
get networkEnv(): Promise<WebdriverIO.Element> { return $("[data-testid='networkEnv']") }
get networkSelectQa(): Promise<WebdriverIO.Element> { return $("[data-testid='QA']") }
selectQa = async () => {
await (await this.networkDropdown).waitForDisplayed({ timeout: 4000 })
await (await this.networkDropdown).click()
await (await this.networkSelectQa).waitForClickable({ timeout: 4000 })
await (await this.networkSelectQa).click()
await (await this.networkEnv).waitForClickable({ timeout: 2000 })
}
}
export default new Balance()
@@ -1,10 +0,0 @@
class Bond {
get bondTitle() { return $("[data-testid='Bond']") }
get mixnodeRadio() { return $("[data-testid='mix-node']") }
get gatewayRadio() { return $("[data-testid='gate-way']") }
get fundsAlert() { return $("[data-testid='fundsAlert']") }
}
export default new Bond()
@@ -1,9 +0,0 @@
class Delegation {
get delegationTitle() { return $("[data-testid='Delegation']") }
get delegateStakeButton() { return $("[data-testid='Delegate stake']") }
get delegateModalHeader() { return $("[data-testid='Delegate']") }
}
export default new Delegation()
@@ -1,7 +0,0 @@
class Receive {
get receiveNymTitle() { return $("[data-testid='Receive NYM']") }
}
export default new Receive()
@@ -1,28 +0,0 @@
class Send {
// send nym form
get sendHeader() { return $("[data-testid='Send']") }
get recipientAddress() { return $("[data-testid='recipientAddress']") }
// get sendAmount() { return $("[data-testid='Amount']") }
get sendAmount() { return $("#mui-5") } // TO-DO fix this selector, using #mui-5 isn't a good solution
get next() { return $("[data-testid='Next']") }
// confirm transaction modal
get sendDetailsHeader() { return $("[data-testid='Send details']") }
get from() { return $("/html/body/div[2]/div[3]/div[2]/div[1]/div[1]") }
get to() { return $("/html/body/div[2]/div[3]/div[2]/div[2]") }
get amount() { return $("/html/body/div[2]/div[3]/div[2]/div[3]") }
get fee() { return $("/html/body/div[2]/div[3]/div[2]/div[4]") }
get confirm() { return $("[data-testid='Confirm']") }
// transaction sent
get viewOnBlockchain() { return $("[data-testid='viewOnBlockchain']") }
get done() { return $("[data-testid='Done']") }
}
export default new Send()
@@ -1,7 +0,0 @@
class Unbond {
get unbondTitle() { return $("[data-testid='Unbond']") }
}
export default new Unbond()
@@ -1,32 +0,0 @@
import Balance from '../../pageobjects/balanceScreen'
import Auth from '../../pageobjects/authScreens'
const textConstants = require("../../../common/text-constants");
const userData = require("../../../common/user-data.json");
const Helper = require('../../../common/helper');
describe('Balance screen displays correctly', () => {
it('selecting qa network', async () => {
//log in
await Helper.loginMnemonic()
// select QA network
await Helper.navigateAndClick(Balance.networkDropdown)
await Helper.navigateAndClick(Balance.networkSelectQa)
// verifty QA network has been selected properly
await Helper.verifyStrictText(Balance.networkEnv, textConstants.qaNetwork)
})
it('copy the account id', async () => {
// ensure the account number contains *something*
await Helper.elementVisible(Balance.accountNumber)
await Helper.verifyPartialText(Balance.accountNumber[1],'1')
await Helper.navigateAndClick(Balance.copyAccountId)
// TO-DO is there a way to verify that the copy worked, aka pasting it somewhere maybe?
})
})
@@ -1,21 +0,0 @@
import Balance from '../../pageobjects/balanceScreen'
import Auth from '../../pageobjects/authScreens'
import Nav from '../../pageobjects/appNavConstants'
import Delegation from '../../pageobjects/delegationScreen'
import Send from '../../pageobjects/sendScreen'
const Helper = require('../../../common/helper');
const textConstants = require("../../../common/text-constants");
const userData = require("../../../common/user-data.json");
describe('Delegate to a mixnode', () => {
it('entering an invalid node identity key', async () => {
//login and navigate to the screen
await Helper.freshMnemonicLoginQaNetwork()
await Helper.navigateAndClick(Nav.delegation)
await Helper.elementVisible(Delegation.delegationTitle)
// TO-DO enter an invalid node
})
})
@@ -1,99 +0,0 @@
import Auth from '../../pageobjects/authScreens'
import Balance from '../../pageobjects/balanceScreen'
import ValidatorClient from '@nymproject/nym-validator-client';
const deleteScript = require("../../../scripts/deletesavedwallet")
const textConstants = require("../../../common/text-constants");
const userData = require("../../../common/user-data.json");
const Helper = require('../../../common/helper');
describe('Create password for existing account and use it to sign in', () => {
it('enter incorrect mnemonic', async () => {
//click through sign in
await Helper.navigateAndClick(Auth.signInButton)
await Helper.navigateAndClick(Auth.signInMnemonic)
//instead of entering mnemonic, click on create a password
await Helper.navigateAndClick(Auth.createPassword)
//enter incorrect mnemonic
await Helper.addValueToTextField(Auth.mnemonicInput, textConstants.incorrectMnemonic)
await Helper.navigateAndClick(Auth.nextToPasswordCreation)
// assert error message is correct
await Helper.verifyStrictText(Auth.error, textConstants.incorrectMnemonicPasswordCreation)
})
it('enter random string', async () => {
// enter random string as mnemonic
await Helper.addValueToTextField(Auth.mnemonicInput, textConstants.randomString)
await Helper.navigateAndClick(Auth.nextToPasswordCreation)
// assert error is correct
await Helper.verifyStrictText(Auth.error, textConstants.incorrectMnemonicPasswordCreation)
})
it('enter correct mnemonic', async () => {
// generate random mnemonic in the backend
const randomMnemonic = ValidatorClient.randomMnemonic();
deleteScript
// use it to continue with password creation flow
await Helper.navigateAndClick(Auth.backToMnemonicSignIn)
await Helper.navigateAndClick(Auth.createPassword)
await Helper.addValueToTextField(Auth.mnemonicInput, randomMnemonic)
await Helper.navigateAndClick(Auth.nextToPasswordCreation)
await Helper.elementVisible(Auth.password)
})
it('create an invalid password', async () => {
// type an invalid password in both fields
await Helper.addValueToTextField(Auth.password, textConstants.incorrectPassword)
await Helper.navigateAndClick(Auth.confirmPassword)
await Helper.addValueToTextField(Auth.confirmPassword, textConstants.incorrectPassword)
// ensure the button to proceed is still disabled
const nextButton = await Auth.createPasswordButton
const isNextDisabled = await nextButton.getAttribute('disabled')
expect(isNextDisabled).toBe("true")
})
it('create a valid password', async () => {
// type a valid password in both fields
await Helper.navigateAndClick(Auth.password)
await Helper.addValueToTextField(Auth.password, textConstants.password)
await Helper.navigateAndClick(Auth.confirmPassword)
await Helper.addValueToTextField(Auth.confirmPassword, textConstants.password)
// verify the password is created and the next screen is visible
await Helper.navigateAndClick(Auth.createPasswordButton)
await Helper.verifyStrictText(Auth.passwordLoginScreenHeader, textConstants.passwordSignIn)
})
it('sign in with no password throws error', async () => {
//click sign without entering a password
await Helper.navigateAndClick(Auth.signInPasswordButton)
// wait for error
await Helper.elementVisible(Auth.error)
// verify error has the correct message
await Helper.verifyStrictText(Auth.error, textConstants.signInWithoutPassword)
})
it('sign in with invalid password throws error', async () => {
// enter invalid password
await Helper.addValueToTextField(Auth.enterPassword, textConstants.incorrectPassword)
await Helper.navigateAndClick(Auth.signInPasswordButton)
// wait for error
await Helper.elementVisible(Auth.error)
await Helper.verifyStrictText(Auth.error, textConstants.invalidPasswordOnSignIn)
})
})
@@ -1,67 +0,0 @@
import Auth from '../../pageobjects/authScreens'
import Balance from '../../pageobjects/balanceScreen'
import ValidatorClient from '@nymproject/nym-validator-client';
import { text } from 'stream/consumers';
const textConstants = require("../../../common/text-constants");
const userData = require("../../../common/user-data.json");
const Helper = require('../../../common/helper');
describe('Wallet sign in functionality with mnemonic', () => {
it('get to the sign in with mnemonic screen', async () => {
// click through to reach the mnemonic sign in
await Helper.navigateAndClick(Auth.signInButton)
await Helper.navigateAndClick(Auth.signInMnemonic)
// verify you are on the right screen by confirming the header
await Helper.verifyStrictText(Auth.mnemonicLoginScreenHeader, textConstants.mnemonicSignIn)
})
it('sign in with no mnemonic throws error', async () => {
await Helper.navigateAndClick(Auth.signIn)
// wait for error
await Helper.elementVisible(Auth.error)
// verify error has the correct message
await Helper.verifyStrictText(Auth.error, textConstants.signInWithoutMnemonic)
})
it('sign in with incorrect mnemonic throws error', async () => {
// enter an incorrect mnemonic string
await Helper.addValueToTextField(Auth.mnemonicInput, textConstants.incorrectMnemonic)
await Helper.navigateAndClick(Auth.signIn)
// verifty error message is correct
await Helper.verifyPartialText(Auth.error, textConstants.signInIncorrectMnemonic)
})
it('sign in with random string throws error', async () => {
// enter a random string not in mnemonic "format"
await Helper.addValueToTextField(Auth.mnemonicInput, textConstants.randomString)
await Helper.navigateAndClick(Auth.signIn)
// verifty error message is correct
await Helper.verifyPartialText(Auth.error, textConstants.signInRandomString)
})
it('should sign in with valid credentials', async () => {
// create new mnemonic
const randomMnemonic = ValidatorClient.randomMnemonic();
// enter mnemonic
await Helper.addValueToTextField(Auth.mnemonicInput, randomMnemonic)
await Helper.navigateAndClick(Auth.signIn)
// verify successful login, balance is visible
await Helper.elementVisible(Balance.balance)
//new accounts will always default to mainnet, so 0 balance
// TO-DO this value sometimes returns " " instead of "0"
await Helper.verifyStrictText(Balance.nymBalance, textConstants.noNym)
})
})
@@ -1,29 +0,0 @@
import Auth from '../../pageobjects/authScreens'
import Balance from '../../pageobjects/balanceScreen'
const textConstants = require("../../../common/text-constants");
const userData = require("../../../common/user-data.json");
const deleteWallet = require("../../../scripts/deletesavedwallet");
const walletExists = require("../../../scripts/savedwalletexists")
const Helper = require('../../../common/helper');
describe('Wallet sign in functionality without creating password', () => {
it('sign in with invalid password and no saved wallet.json file throws error', async () => {
// delete existing saved wallet file
deleteWallet
//click through sign without entering a password
await Helper.navigateAndClick(Auth.signInButton)
await Helper.navigateAndClick(Auth.signInPassword)
// enter invalid password
await Helper.addValueToTextField(Auth.enterPassword,textConstants.incorrectPassword)
await Helper.navigateAndClick(Auth.signInPasswordButton)
// wait for error
await Helper.elementVisible(Auth.error)
// verify error has the correct message
await Helper.verifyStrictText(Auth.error, textConstants.failedToFindWalletFile)
})
})
@@ -1,73 +0,0 @@
import Auth from '../../pageobjects/authScreens'
import Nav from '../../pageobjects/appNavConstants'
import Balance from '../../pageobjects/balanceScreen'
import Send from '../../pageobjects/sendScreen'
import Receive from '../../pageobjects/receiveScreen'
import Bond from '../../pageobjects/bondScreen'
import Unbond from '../../pageobjects/unbondScreen'
import Delegation from '../../pageobjects/delegationScreen'
const userData = require("../../../common/user-data.json");
const Helper = require('../../../common/helper');
describe('Nav Items behave correctly', () => {
it('switch from light to dark mode and back', async () => {
//log in
await Helper.freshMnemonicLoginQaNetwork()
// click on different modes
await Helper.navigateAndClick(Nav.lightMode)
await Helper.navigateAndClick(Nav.darkMode)
await Helper.elementVisible(Nav.lightMode)
})
it('clicking terminal opens the modal', async () => {
// ensure the terminal button opens the terminal
await Helper.elementVisible(Nav.terminalIcon)
await Helper.navigateAndClick(Nav.terminalIcon)
await Helper.elementVisible(Nav.terminalTitle)
await Helper.verifyPartialText(Nav.terminalTitle, 'Terminal')
})
})
describe('Menu items lead to correct screen', () => {
//TO-DO none of this works
//check each menu item opens the right screen/modal
it('check Balance link works', async () => {
await Helper.navigateAndClick(Nav.balance)
await Helper.verifyPartialText(Balance.balance, 'Balance')
})
it('check Send link works', async () => {
await Helper.navigateAndClick(Nav.send)
await Helper.verifyPartialText(Send.sendHeader, 'Send')
await Helper.navigateAndClick(Nav.closeIcon)
})
it('check Receive link works', async () => {
await Helper.navigateAndClick(Nav.receive)
await Helper.verifyPartialText(Receive.receiveNymTitle, 'Receive NYM')
})
it('check Bond link works', async () => {
await Helper.navigateAndClick(Nav.bond)
await Helper.verifyPartialText(Bond.bondTitle, 'Bond')
})
it('check Unbond link works', async () => {
await Helper.navigateAndClick(Nav.unbond)
await Helper.verifyPartialText(Unbond.unbondTitle, 'Unbond')
})
it('check Delegation link works', async () => {
await Helper.navigateAndClick(Nav.delegation)
await Helper.verifyPartialText(Delegation.delegationTitle, 'Delegation')
})
})
@@ -1,101 +0,0 @@
import Auth from '../../pageobjects/authScreens'
import Balance from '../../pageobjects/balanceScreen'
const textConstants = require("../../../common/text-constants");
const userData = require("../../../common/user-data.json");
const deleteScript = require("../../../scripts/deletesavedwallet")
const Helper = require('../../../common/helper');
describe.skip('Create a new account and verify it exists', () => {
it('generate new mnemonic and verify mnemonic words', async () => {
// delete any existing saved-wallet.json
deleteScript
// click through create account flow
await Helper.navigateAndClick(Auth.createAccount)
await Helper.elementVisible(Auth.mnemonicPhrase)
// save mnemonic phrase
let mnemonic = await (await Auth.mnemonicPhrase).getText()
let arrayMnemonic = mnemonic.split(" ")
await Helper.navigateAndClick(Auth.copyMnemonic)
await Helper.navigateAndClick(Auth.iSavedMnemonic)
// verify the mnemonic words in the correct order
let mnemonicWordTiles = await (await Auth.mnemonicWordTile)
let wordTileIndex = await (await Auth.wordIndex)
const wordsArray: any[] = []
for (const word of mnemonicWordTiles) {
const wordText = await word.getText()
const index = arrayMnemonic.indexOf(wordText)
wordsArray.push({ word, index })
}
for (const index of wordTileIndex) {
const indexValue = await index.getText()
const match = wordsArray.find((word) => +word.index === +indexValue - 1)
if (match) {
await match.word.click()
}
}
// ensure that once the task above is complete, the 'next' button is enabled
const nextButton = await Auth.nextToStep3
const isNextDisabled = await nextButton.getAttribute('disabled')
expect(isNextDisabled).toBe(null)
})
it('click skip password', async () => {
// click on skip password creation
await Helper.navigateAndClick(Auth.nextToStep3)
await Helper.navigateAndClick(Auth.skipPasswordAndSignInWithMnemonic)
// can see mnemonic login page
await Helper.elementVisible(Auth.mnemonicInput)
await Helper.navigateAndClick(Auth.backToSignInOptions)
})
it('set up invalid password for new account', async () => {
// enter invalid password in both fields
await Helper.navigateAndClick(Auth.password)
await Helper.addValueToTextField(Auth.password, textConstants.incorrectPassword)
await Helper.navigateAndClick(Auth.confirmPassword)
await Helper.addValueToTextField(Auth.confirmPassword, textConstants.incorrectPassword)
// verify that the 'next' button is still disabled
const nextButton = await Auth.nextStorePassword
const isNextDisabled = await nextButton.getAttribute('disabled')
expect(isNextDisabled).toBe("true")
})
it('set up valid password for new account', async () => {
// enter a valid password in both fields
await Helper.navigateAndClick(Auth.password)
await Helper.addValueToTextField(Auth.password, textConstants.password)
await Helper.navigateAndClick(Auth.confirmPassword)
await Helper.addValueToTextField(Auth.confirmPassword, textConstants.password)
// verify that the 'next' button is clickable
const nextButton = await Auth.nextStorePassword
const isNextDisabled = await nextButton.getAttribute('disabled')
expect(isNextDisabled).toBe(null)
})
it('proceed to login with newly created password', async () => {
// login with a password
await Helper.navigateAndClick(Auth.nextStorePassword)
await Helper.navigateAndClick(Auth.enterPassword)
await Helper.addValueToTextField(Auth.enterPassword, textConstants.password)
await Helper.navigateAndClick(Auth.signInPasswordButton)
// TO-DO for some reason this is failing due to failed to decrypt the wallet etc error
await Helper.elementVisible(Balance.balance)
//new accounts will always default to mainnet, so 0 balance
await Helper.verifyStrictText(Balance.nymBalance, textConstants.noNym)
})
})
@@ -1,62 +0,0 @@
import Balance from '../../pageobjects/balanceScreen'
import Auth from '../../pageobjects/authScreens'
import Nav from '../../pageobjects/appNavConstants'
import Send from '../../pageobjects/sendScreen'
const Helper = require('../../../common/helper');
const textConstants = require("../../../common/text-constants");
const userData = require("../../../common/user-data.json");
describe.skip('Send modal functions correctly', () => {
it('entering an invalid recipient address shows error', async () => {
// sign in with mnemonic and select QA
await Helper.freshMnemonicLoginQaNetwork()
// click on send and check modal appears
await Helper.navigateAndClick(Nav.send)
await Helper.elementVisible(Send.sendHeader)
// add an invalid recipient address
await Helper.addValueToTextField(Send.recipientAddress, textConstants.invalidRecipientAddress)
// TO-DO -- question: should there not be an error message before clicking on Next to warn that the address is invalid?
})
it('entering an valid recipient address with negative amount value shows error', async () => {
await Helper.navigateAndClick(Send.recipientAddress)
// TO-DO figure out how to clear a text field before adding new value
await (Send.recipientAddress).clearValue()
await Helper.addValueToTextField(Send.recipientAddress, userData.receiver_address)
await Helper.navigateAndClick(Send.sendAmount)
await Helper.addValueToTextField(Send.sendAmount, textConstants.negativeAmount)
//next button is still disabled and error message appears
const nextButton = await Send.next
const isNextDisabled = await nextButton.getAttribute('disabled')
expect(isNextDisabled).toBe("true")
})
it('enter a valid recipient and value', async () => {
// enter valid data
await Helper.addValueToTextField(Send.recipientAddress, userData.receiver_address)
const getCurrentBalance = await (await Balance.nymBalance).getText()
await Helper.addValueToTextField(Send.sendAmount, textConstants.amountToSend)
// click on next and verify details
await Helper.navigateAndClick(Send.next)
const fee = await (await Send.fee).getText()
await Helper.verifyPartialText(Send.sendDetailsHeader, textConstants.sendDetails)
await Helper.verifyPartialText(Send.amount, textConstants.confirmedAmount)
await Helper.navigateAndClick(Send.confirm)
await Helper.elementVisible(Send.viewOnBlockchain)
await Helper.elementClickable(Send.done)
// calculate the transaction and verify it has been correctly executed
let sumCost = await Helper.calculateFees(getCurrentBalance, fee, textConstants.amountToSend, true)
const getNewBalance = await (await Balance.nymBalance).getText()
await Helper.navigateAndClick(Send.done)
// TO-DO the following fails with "TypeError: elem[prop] is not a function"
expect(getNewBalance).toEqual(sumCost)
})
})
@@ -1,30 +0,0 @@
import ValidatorClient from '@nymproject/nym-validator-client';
describe.skip('Creating valid account', () => {
it('create mnemonic', async () => {
const benny = "giraffe note order sun cradle bottom crime humble able antique rural donkey guess parent potato tongue truly way disagree exile zebra someone else typical";
const mnemonic = ValidatorClient.randomMnemonic();
console.log(ValidatorClient);
const newAccountClient = await ValidatorClient.connect(mnemonic,
'https://qa-validator.nymtech.net', 'https://qa-validator-api.nymtech.net/api', 'n', 'n1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsd3qaep', 'n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav', 'nym');
const address = newAccountClient.address;
console.log({ address, mnemonic });
const client = await ValidatorClient.connect(
benny, 'https://qa-validator.nymtech.net', 'https://qa-validator-api.nymtech.net/api', 'n', 'n1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsd3qaep', 'n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav', 'nym');
await client.send(address, [{ amount: '10000000', denom: 'unym' }]);
const balance = await client.getBalance(address);
console.log({ balance });
expect(Number.parseFloat(balance.amount)).toBe(10000000);
}).timeout(5000);
})
// the newly created address from the test above:
// address: 'n13l7rwrygs0m3kx3en2eh55dtmwlzm0vskw0hxq',
// mnemonic: 'tree upset require kitten inquiry truck emotion ladder reject elbow page ability spot win board frog child much credit pizza picture hover medal zoo'
// always make sure it's on QA, unless youre on debug branch (~look in nym_path wdio.config.ts to check)
// ENABLE_QA_MODE=true target/release/nym-wallet
@@ -1,15 +0,0 @@
{
"compilerOptions": {
"moduleResolution": "node",
"types": [
"node",
"webdriverio/async",
"@wdio/mocha-framework",
"expect-webdriverio"
],
"target": "es2019"
},
"include": [
"wallet-ui-tests/*"
]
}
-103
View File
@@ -1,103 +0,0 @@
const os = require('os')
const path = require('path')
const { spawn, spawnSync } = require('child_process')
//insert path to binary
const nym_path = '../target/debug/nym_wallet'
let tauriDriver: any
exports.config = {
autoCompileOpts: {
autoCompile: true,
tsNodeOpts: {
transpileOnly: true,
project: 'tsconfig.json',
},
},
specs: ['./test/specs/**/*.ts'],
suites: {
signup: [
'./test/specs/newaccount/*.ts',
],
login: [
'./test/specs/existingaccount/*.ts',
],
balance: [
'./test/specs/balance/*.ts',
],
nav: [
'./test/specs/navbaritems/*.ts',
],
send: [
'./test/specs/send/*.ts',
],
delegation: [
'./test/specs/delegation/*.ts',
],
},
// Patterns to exclude.
exclude: [
// 'path/to/excluded/files'
],
maxInstances: 1,
capabilities: [
{
maxInstances: 1,
'tauri:options': {
application: nym_path,
},
},
],
//
// ===================
// Test Configurations
// ===================
// Define all options that are relevant for the WebdriverIO instance here
//
// Level of logging verbosity: trace | debug | info | warn | error | silent
logLevel: 'info',
bail: 0,
framework: 'mocha',
reporters: ['spec'],
mochaOpts: {
ui: 'bdd',
timeout: 60000,
},
// ===================
// Test Reporters
// ===================
// reporters: [
// [
// "allure",
// {
// outputDir: "allure-results",
// disableWebdriverStepsReporting: true,
// disableWebdriverScreenshotsReporting: true,
// },
// ],
// ],
// this is documentented in the readme - you will need to build the project first
// ensure the rust project is built since we expect this binary to exist for the webdriver sessions
//onPrepare: () => spawnSync("cargo", ["build", "--release"]),
// ensure we are running `tauri-driver` before the session starts so that we can proxy the webdriver requests
beforeSession: () =>
(tauriDriver = spawn(path.resolve(os.homedir(), '.cargo', 'bin', 'tauri-driver'), [], {
stdio: [null, process.stdout, process.stderr],
})),
// afterTest: function (
// test,
// context,
// { error, result, duration, passed, retries }
// ) {
// if (error) {
// browser.takeScreenshot();
// }
// },
// clean up the `tauri-driver` process we spawned at the start of the session
afterSession: () => tauriDriver.kill(),
}
+5
View File
@@ -0,0 +1,5 @@
reports
allure-results
node_modules
.vscode
.idea
+86
View File
@@ -0,0 +1,86 @@
<!--
Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
SPDX-License-Identifier: Apache-2.0
-->
# Nym Wallet Webdriverio testsuite
A webdriverio test suite implementation using tauri driver
with a page object model design. This is to provide quick iterative feedback
on the UI of the nym wallet. Currently, tauri-driver is available to run on Windows and Linux machines.
## Installation prerequisites
- `Yarn`
- `NodeJS >= v16.8.0`
- `Rust & cargo >= v1.56.1`
- `tauri-driver`
- `That you have an existing mnemonic and you can login to the app`
- `Have the details listed below to provide the user-data.json file`
## Key Information
- Please read the instructions on the `nym/tauri-wallet/README.md` in the root of the project on how to build the application
- Please ensure you have the relevant Webdriver kits installed on your machine -
```
linux:
sudo apt-get install -y webkit2gtk-driver
```
```
windows:
download msedgedriver.exe from https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/
```
please visit [Tauri Studio](https://tauri.studio/en/docs/usage/guides/webdriver/introduction), this will specify the additional drivers you need
- The path to run the application is set in the `wdio.conf.js` which lives in the root directory
- Before running the suite you need to build the application and check that the application has
built successfully, if so, you will have an executable sitting in the target directory in `tauri-wallet/target/*/nym_wallet` (refer to point 1)
- The suite will not be able to detect elements on screen if you select a release build, however you can run tests against a release target
## Installation & usage
- `test excution happens inside /webdriver directory`
- `test data needs to be provided inside the user-data.json`
- `check the wdio.conf.cjs to see the test execution along with the path location of the binary`
```
example:
//mnemonic is a base64 enconded value, which is your 24 character passphrase, these values are for illustration purposes
{
"mnemonic" : "dGhpcyBpcyBhIHBhc3NwaHJhc2UK",
"punk_address" : "punk1f3dzkhmunma5ze5q952daxca6371989189",
"receiver_address" : "punk1p0ce82jxxglpmutvhq4mdwgcwf4avm5n1821982",
"amount_to_send" : "1",
"identity_key_to_delegate_mix_node": "value",
"identity_key_to_delegate_gateway" : "value",
"delegate_amount" : "1"
}
```
- `yarn test:runall` - the first test run will take some time to spin up be patient
- You can run tests individually by passing through the script situated in the package.json for example `yarn test:newuser`
Tests are categorised and run by their pages, they follow a sequential flow, if one test case fails before the next execution it may derail the next test.
//todo improve in near future
## Test reporting
Currently the tests use allure reporting, the configuration can be altered in the `wdio.conf.cjs`. At present it takes snapshots of any failing tests, the test output run can be seen in the allure-results directory
Tests ouput:
- <guid-testuite.xml>
- <guid-attachment.png>
If any tests fail in their test run it will produce the stack trace error along with the test in question
## TODO
_Disclaimer_: Still WIP
Implement error handling/ beforeTest() - validating json file exists with data for test execution
Currently this is dev'd against a Linux based OS, not tested against windows yet.
+12
View File
@@ -0,0 +1,12 @@
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "14",
},
},
],
],
};
@@ -0,0 +1,30 @@
module.exports = {
//receivePage
recievePageInformation:
"You can receive tokens by providing this address to the sender",
receivePageHeaderText: "Receive Nym",
//sendPage
sendPunk: "Send punk",
//homePage
homePageErrorMnemonic: "Error parsing bip39 mnemonic",
homePageSignIn: "Sign in",
createOne: "Create one",
walletSuccess:
"Please store your mnemonic in a safe place. You'll need it to access your wallet",
//bondPage // unbondPage
bondAlreadyNoded: "Looks like you already have a mixnode bonded.",
bondNodeHeaderText: "Bond a node or gateway",
unbondNodeHeaderText: "Unbond a mixnode or gateway",
unbondMixNodeText: "Looks like you already have a mixnode bonded.",
unbondMixNode: "UNBOND",
//delegatePage // undelegatePage
delegateHeaderText: "Delegate\nDelegate to mixnode",
nodeIdentityValidationText: "identity is a required field",
amountValidationText: "amount is a required field",
undelegateHeaderText: "Undelegate from a mixnode or gateway",
delegationComplete: "Delegation complete",
};
@@ -0,0 +1,9 @@
{
"mnemonic": "value",
"punk_address": "",
"receiver_address": "",
"amount_to_send": "",
"identity_key_to_delegate_mix_node": "",
"identity_key_to_delegate_gateway": "",
"delegate_amount": ""
}
@@ -0,0 +1,43 @@
class Helpers {
//helper to decode mnemonic so plain 24 character passphrase isn't in sight albeit it is presented when ruunning the scripts
//maybe a show passphrase toggle button?
decodeBase = async (input) => {
var m = Buffer.from(input, "base64").toString();
return m;
};
navigateAndClick = async (element) => {
await element.click();
};
scrollIntoView = async (element) => {
await element.scrollIntoView();
};
currentBalance = async (value) => {
return parseFloat(value.split(/\s+/)[0].toString()).toFixed(5);
};
//todo need to improve calculation - WIP
calculateFees = async (beforeBalance, transactionFee, amount, isSend) => {
let fee;
if (isSend) {
//send transaction
fee = transactionFee.split(/\s+/)[0];
} else {
//delegate transaction
fee = transactionFee.split(/\s+/)[3];
}
const currentBalance = beforeBalance.split(/\s+/)[0];
const castCurrentBalance = parseFloat(currentBalance).toFixed(5);
const transCost = +parseFloat(amount) + +parseFloat(fee).toFixed(5);
let sum = parseFloat(castCurrentBalance) - parseFloat(transCost);
return sum.toFixed(5);
};
}
module.exports = new Helpers();
+27
View File
@@ -0,0 +1,27 @@
{
"name": "tauri_nym_wallet",
"version": "1.0.0",
"private": false,
"license": "MIT",
"scripts": {
"test:runall": "wdio run wdio.conf.cjs",
"test:sendreceive": "wdio run wdio.conf.cjs --suite sendreceive",
"test:home": "wdio run wdio.conf.cjs --suite home",
"test:bond": "wdio run wdio.conf.cjs --suite bond",
"test:delegate": "wdio run wdio.conf.cjs --suite delegate",
"test:newuser": "wdio run wdio.conf.cjs --suite newuser",
"run:prettier": "prettier --write ."
},
"dependencies": {
"@types/node": "^16.11.0",
"@wdio/allure-reporter": "^7.16.1",
"@wdio/cli": "^7.9.1",
"@zxing/browser": "^0.0.9"
},
"devDependencies": {
"@wdio/local-runner": "^7.14.1",
"@wdio/mocha-framework": "^7.14.1",
"@wdio/spec-reporter": "^7.14.1",
"prettier": "2.4.1"
}
}
@@ -0,0 +1,48 @@
class WalletBond {
get header() {
return $(
"#root > div > div:nth-child(2) > div:nth-child(2) > div > div > div > div.MuiCardHeader-root > div > span.MuiTypography-root.MuiCardHeader-subheader.MuiTypography-subtitle1.MuiTypography-colorTextSecondary.MuiTypography-displayBlock"
);
}
get identityKey() {
return $("#identityKey");
}
get sphinxKey() {
return $("#sphinxKey");
}
get amountToBond() {
return $("#amount");
}
get hostInput() {
return $("#host");
}
get versionInput() {
return $("version");
}
get selectAdvancedOptions() {
return $("[type='checkbox']");
}
get mixPort() {
return $("#mixPort");
}
get verlocPort() {
return $("#verlocPort");
}
get httpApiPort() {
return $("#httpApiPort");
}
get bondButton() {
return $("[data-testid='bond-button']");
}
get unBondButton() {
return $("[data-testid='un-bond']");
}
get unBond() {
return $("[data-testid='bond-noded']");
}
get unBondWarning() {
return $("div.MuiAlert-message");
}
}
module.exports = new WalletBond();
@@ -0,0 +1,24 @@
class WalletCreate {
get createAccount() {
return $("[href='#']");
}
get create() {
return $("[data-testid='create-button']");
}
get accountCreatedSuccessfully() {
return $("[data-testid='mnemonic-warning']");
}
get walletMnemonicValue() {
return $("[data-testid='mnemonic-phrase']");
}
get punkAddress() {
return $("[data-testid='wallet-address']");
}
get backToSignIn() {
return $("[data-testid='sign-in-button']");
}
get signInButton() {
return $("[type='submit']");
}
}
module.exports = new WalletCreate();
@@ -0,0 +1,60 @@
class WalletDelegate {
get header() {
return $("[data-testid='Delegate']");
}
get nodeIdentity() {
return $("#identity");
}
get amountToDelegate() {
return $("#amount");
}
get identityValidation() {
return $("#identity-helper-text");
}
get amountToDelegateValidation() {
return $("#amount-helper-text");
}
get delegateStakeButton() {
return $("[data-testid='delegate-button']");
}
get mixNodeRadioButton() {
return $("[data-testid='mix-node']");
}
get gateWayRadioButton() {
return $("[data-testid='gate-way']");
}
get successfullyDelegate() {
return $("[data-testid='delegate-success']");
}
get finishButton() {
return $("[data-testid='finish-button']");
}
get transactionFeeAmount() {
return $("[data-testid='fee-amount']");
}
get accountBalance() {
return $("[data-testid='account-balance']");
}
//Undelegate
get unDelegateHeader() {
return $("[data-testid='Undelegate']");
}
get unNodeIdentity() {
return $("[name='identity']");
}
get unDelegateFeeText() {
return $("[data-testid='fee-amount']");
}
get unDelegateGatewayRadioButton() {
return $("[data-testid='gate-way']");
}
get unMixNodeRadioButton() {
return $("[data-testid='mix-node']");
}
get unDelegateButton() {
return $("[data-testid='submit-button']");
}
}
module.exports = new WalletDelegate();
@@ -0,0 +1,42 @@
class WalletHome {
get balanceCheck() {
return $(
"#root > div > div:nth-child(2) > div:nth-child(2) > div > div > div > div.MuiCardHeader-root > div > span"
);
}
get punkBalance() {
return $("");
}
get punkAddress() {
return $("[data-testid='wallet-address']");
}
get accountBalance() {
return $("[data-testid='account-balance']");
}
get balanceButton() {
return $("[href='/balance']");
}
get sendButton() {
return $("[href='/send']");
}
get receiveButton() {
return $("[href='/receive']");
}
get bondButton() {
return $("[href='/bond']");
}
get unBondButton() {
return $("[href='/unbond']");
}
get delegateButton() {
return $("[href='/delegate']");
}
get unDelegateButton() {
return $("[href='/undelegate']");
}
get logOutButton() {
return $("[data-testid='log-out']");
}
}
module.exports = new WalletHome();
@@ -0,0 +1,31 @@
class WalletLogin {
get signInLabel() {
return $("[data-testid='sign-in']");
}
get mnemonic() {
return $("#mnemonic");
}
get signInButton() {
return $("[type='submit']");
}
get errorValidation() {
return $("[class='MuiAlert-message']");
}
get accountBalance() {
return $("[data-test-id='account-balance']");
}
get accountBalanceText() {
return $("[class='MuiAlert-message']");
}
get walletAddress() {
return $("[data-testid='wallet-address']");
}
//login to the application
enterMnemonic = async (mnemonic) => {
await this.mnemonic.addValue(mnemonic);
await this.signInButton.click();
await this.accountBalance.isExisting();
};
}
module.exports = new WalletLogin();
@@ -0,0 +1,37 @@
class WalletReceive {
get receiveNymHeader() {
return $(
"#root > div > div:nth-child(2) > div:nth-child(2) > div > div > div > div.MuiCardHeader-root > div > span"
);
}
get receiveNymText() {
return $("[data-testid='receive-nym']");
}
get walletAddress() {
return $("[data-testid='client-address']");
}
get copyButton() {
return $("[data-testid='copy-button']");
}
get qrCode() {
return $("[data-testid='qr-code']");
}
WaitForButtonChangeOnCopy = async () => {
await this.copyButton.click();
await this.copyButton.waitForDisplayed({ timeout: 1500 });
await this.copyButton.waitUntil(
async function () {
return (await this.getText()) === "COPIED";
},
{
timeout: 1500,
timeoutMsg: "expected text to be different after 1.5s",
}
);
};
}
module.exports = new WalletReceive();
@@ -0,0 +1,52 @@
class WalletSend {
get fromAddress() {
return $("#from");
}
get toAddress() {
return $("#to");
}
get amount() {
return $("#amount");
}
get nextButton() {
return $("[data-testid='button");
}
get sendHeader() {
return $("[data-testid='Send punk']");
}
get accountBalance() {
return $("[data-testid='account-balance']");
}
get amountReviewAndSend() {
return $("[data-testid='Amount']");
}
get toAddressReviewAndSend() {
return $("[data-testid='To']");
}
get fromAddressReviewAndSend() {
return $("[data-testid='From']");
}
get transferFeeAmount() {
return $("[data-testid='Transfer fee']");
}
get reviewAndSendBackButton() {
return $("[data-testid='back-button']");
}
get sendButton() {
return $("[data-testid='button']");
}
get transactionComplete() {
return $("[data-testid='transaction-complete']");
}
get transactionCompleteRecipient() {
return $("[data-testid='to-address']");
}
get transactionCompleteAmount() {
return $("[data-testid='send-amount']");
}
get finishButton() {
return $("[data-testid='button']");
}
}
module.exports = new WalletSend();
@@ -0,0 +1,22 @@
class WallentUndelegate {
get transactionFee() {
return $("[data-testid='fee-amount']");
}
get mixNodeRadioButton() {
return $("[value='mixnode']");
}
get gatewayRadionButton() {
return $("[value='gateway']");
}
get nodeIdentity() {
return $("#mui-55011");
}
get identityHelper() {
return $("#identity-helper-text");
}
get delegateButton() {
return $("[data-testid='submit-button']");
}
}
module.exports = new WallentUndelegate();
@@ -0,0 +1,54 @@
const userData = require("../../../common/data/user-data.json");
const helper = require("../../../common/helpers/helper");
const walletLogin = require("../../pages/wallet.login");
const textConstants = require("../../../common/constants/text-constants");
const walletHomepage = require("../../pages/wallet.homepage");
const bondPage = require("../../pages/wallet.bond");
describe("bonding and unbonding nodes", () => {
it("should have a node already bonded and validate no input fields are enabled", async () => {
const mnemonic = await helper.decodeBase(userData.mnemonic);
await walletLogin.enterMnemonic(mnemonic);
await helper.navigateAndClick(walletHomepage.bondButton);
await helper.scrollIntoView(bondPage.selectAdvancedOptions);
await bondPage.selectAdvancedOptions.click();
//as bond node is mixed expect all the fields to be disabled
const getText = await bondPage.header.getText();
const getIdentity = await bondPage.identityKey.isEnabled();
const getSphinxKey = await bondPage.sphinxKey.isEnabled();
const amountToBond = await bondPage.amountToBond.isEnabled();
const hostInput = await bondPage.hostInput.isEnabled();
const verlocPort = await bondPage.verlocPort.isEnabled();
const httpApiPort = await bondPage.httpApiPort.isEnabled();
const mixPort = await bondPage.mixPort.isEnabled();
//assert all field are not functional
expect(getText).toEqual(textConstants.bondNodeHeaderText);
expect(getIdentity).toEqual(false);
expect(getSphinxKey).toEqual(false);
expect(amountToBond).toEqual(false);
expect(hostInput).toEqual(false);
expect(verlocPort).toEqual(false);
expect(httpApiPort).toEqual(false);
expect(mixPort).toEqual(false);
});
it("unbond mix monde screen should be present with the option to unbond", async () => {
//we do not want to unbond our node, check that elements are selectable
await helper.scrollIntoView(walletHomepage.unBondButton);
await helper.navigateAndClick(walletHomepage.unBondButton);
const getText = await bondPage.header.getText();
const unbondText = await bondPage.unBondWarning.getText();
await bondPage.unBondButton.isClickable();
//assert all field are not functional
expect(getText).toEqual(textConstants.unbondNodeHeaderText);
expect(unbondText).toEqual(textConstants.unbondMixNodeText);
});
});
@@ -0,0 +1,108 @@
const userData = require("../../../common/data/user-data.json");
const helper = require("../../../common/helpers/helper");
const walletLogin = require("../../pages/wallet.login");
const textConstants = require("../../../common/constants/text-constants");
const walletHomepage = require("../../pages/wallet.homepage");
const delegatePage = require("../../pages/wallet.delegate");
describe("delegate to a mix node or gateway", () => {
it("ensure that fields are enabled for existing user", async () => {
const mnemonic = await helper.decodeBase(userData.mnemonic);
await walletLogin.enterMnemonic(mnemonic);
await helper.navigateAndClick(walletHomepage.delegateButton);
const getText = await delegatePage.header.getText();
expect(getText).toEqual(textConstants.delegateHeaderText);
});
it("submitting the form without input prompts validation errors", async () => {
await delegatePage.delegateStakeButton.click();
const getIdentityValidation =
await delegatePage.identityValidation.getText();
const getAmountValidation =
await delegatePage.amountToDelegateValidation.getText();
expect(getIdentityValidation).toEqual(
textConstants.nodeIdentityValidationText
);
expect(getAmountValidation).toEqual(textConstants.amountValidationText);
});
it("input delegate amount to a mix node then broadcast the transaction then check account balances", async () => {
const balanceText = await delegatePage.accountBalance.getText();
const getTransfeeAmount = await delegatePage.transactionFeeAmount.getText();
await delegatePage.nodeIdentity.setValue(
userData.identity_key_to_delegate_mix_node
);
await delegatePage.amountToDelegate.setValue(userData.delegate_amount);
//transfer fee + amount delegation
const sumCost = await helper.calculateFees(
balanceText,
getTransfeeAmount,
userData.delegate_amount,
false
);
await delegatePage.delegateStakeButton.click();
await delegatePage.successfullyDelegate.waitForClickable({
timeout: 10000,
});
const getConfirmationText =
await delegatePage.successfullyDelegate.getText();
expect(getConfirmationText).toContain(textConstants.delegationComplete);
const availablePunk = await delegatePage.accountBalance.getText();
//expect new account balance - the fee calculation above
await delegatePage.finishButton.click();
expect(await helper.currentBalance(availablePunk)).toEqual(sumCost);
});
it("input amount to stake to a gateway then broadcast the transaction then check account balances", async () => {
const balanceText = await delegatePage.accountBalance.getText();
const getTransfeeAmount = await delegatePage.transactionFeeAmount.getText();
await delegatePage.gateWayRadioButton.click();
await delegatePage.nodeIdentity.setValue(
userData.identity_key_to_delegate_gateway
);
await delegatePage.amountToDelegate.setValue(userData.delegate_amount);
//transfer fee + amount delegation
const sumCost = await helper.calculateFees(
balanceText,
getTransfeeAmount,
userData.delegate_amount,
false
);
await delegatePage.delegateStakeButton.click();
await delegatePage.successfullyDelegate.waitForClickable({
timeout: 10000,
});
const getConfirmationText =
await delegatePage.successfullyDelegate.getText();
expect(getConfirmationText).toContain(textConstants.delegationComplete);
const availablePunk = await delegatePage.accountBalance.getText();
//expect new account balance - the fee calculation above
expect(await helper.currentBalance(availablePunk)).toEqual(sumCost);
});
});
@@ -0,0 +1,45 @@
const userData = require("../../../common/data/user-data.json");
const helper = require("../../../common/helpers/helper");
const walletLogin = require("../../pages/wallet.login");
const homepPage = require("../../pages/wallet.homepage");
const textConstants = require("../../../common/constants/text-constants");
describe("wallet splash screen", () => {
it("should have the sign in header present", async () => {
const signInText = await walletLogin.signInLabel.getText();
expect(signInText).toEqual(textConstants.homePageSignIn);
});
it("submitting the sign in button with no input throws a validation error", async () => {
await walletLogin.signInButton.click();
const errorResponseText = await walletLogin.errorValidation.getText();
expect(errorResponseText).toEqual(textConstants.homePageErrorMnemonic);
});
//currently the punk_address is not fully displayed on the wallet UI
//trim the punk address
it("successfully input mnemonic and log in", async () => {
const mnemonic = await helper.decodeBase(userData.mnemonic);
await walletLogin.enterMnemonic(mnemonic);
await walletLogin.walletAddress.waitForEnabled({ timeout: 5000 });
const getWalletAddress = await walletLogin.walletAddress.getText();
//currently 35 characters are displayed along with three ...
//current hack we can assume this is the correct wallet
const walletTruncated = userData.punk_address.substring(0, 35);
expect(walletTruncated + "...").toContain(getWalletAddress);
});
it("successfully log out the application", async () => {
await helper.scrollIntoView(homepPage.logOutButton);
await homepPage.logOutButton.click();
await walletLogin.signInLabel.waitForEnabled({ timeout: 1500 });
expect(await walletLogin.signInLabel.isDisplayed()).toEqual(true);
});
});
@@ -0,0 +1,28 @@
const userData = require("../../../common/data/user-data.json");
const textConstants = require("../../../common/constants/text-constants");
const helper = require("../../../common/helpers/helper");
const walletLogin = require("../../pages/wallet.login");
const receive = require("../../pages/wallet.receive");
const walletHomepage = require("../../pages/wallet.homepage");
describe("provide the relevant information about a user nym wallet address", () => {
it("should have the receivers address and a qr code present", async () => {
const mnemonic = await helper.decodeBase(userData.mnemonic);
await walletLogin.enterMnemonic(mnemonic);
await helper.navigateAndClick(walletHomepage.receiveButton);
await receive.receiveNymHeader.waitForDisplayed({ timeout: 1500 });
await receive.WaitForButtonChangeOnCopy();
const textHeader = await receive.receiveNymHeader.getText();
const getInformationText = await receive.receiveNymText.getText();
const getPunkAddress = await receive.walletAddress.getText();
expect(getPunkAddress).toEqual(userData.punk_address);
expect(getInformationText).toEqual(textConstants.recievePageInformation);
expect(textConstants.receivePageHeaderText).toEqual(textHeader);
});
});
@@ -0,0 +1,55 @@
const userData = require("../../../common/data/user-data.json");
const helper = require("../../../common/helpers/helper");
const textConstants = require("../../../common/constants/text-constants");
const walletLogin = require("../../pages/wallet.login");
const sendWallet = require("../../pages/wallet.send");
const walletHomepage = require("../../pages/wallet.homepage");
describe("send punk to another a wallet", () => {
it("expect send screen to display the data", async () => {
const mnemonic = await helper.decodeBase(userData.mnemonic);
await walletLogin.enterMnemonic(mnemonic);
await helper.navigateAndClick(walletHomepage.sendButton);
const textHeader = await sendWallet.sendHeader.getText();
expect(textHeader).toContain(textConstants.sendPunk);
});
it("send funds correctly to another punk address", async () => {
//already logged in due to the previous test
const getCurrentBalance = await walletHomepage.accountBalance.getText();
await sendWallet.toAddress.addValue(userData.receiver_address);
await sendWallet.amount.addValue(userData.amount_to_send);
await sendWallet.nextButton.waitForEnabled({ timeout: 3000 });
await sendWallet.nextButton.click();
const transFee = await sendWallet.transferFeeAmount.getText();
await sendWallet.sendButton.click();
await sendWallet.finishButton.waitForClickable({ timeout: 10000 });
let sumCost = await helper.calculateFees(
getCurrentBalance,
transFee,
userData.amount_to_send,
true
);
await walletHomepage.accountBalance.isDisplayed();
const availablePunk = await walletHomepage.accountBalance.getText();
await sendWallet.finishButton.click();
//expect new account balance - the fee calculation above
expect(await helper.currentBalance(availablePunk)).toEqual(sumCost);
});
});
@@ -0,0 +1,32 @@
const userData = require("../../../common/data/user-data.json");
const helper = require("../../../common/helpers/helper");
const walletLogin = require("../../pages/wallet.login");
const walletHomepage = require("../../pages/wallet.homepage");
const unDelegatePage = require("../../pages/wallet.delegate");
describe("un-delegate a mix node or gateway", () => {
it("ensure that fields are enabled for existing user", async () => {
//we are ensuring that the fields are selectable for undelegation
//not proceeding to undelegate a node or gateway
const mnemonic = await helper.decodeBase(userData.mnemonic);
await walletLogin.enterMnemonic(mnemonic);
await helper.scrollIntoView(walletHomepage.unDelegateButton);
await helper.navigateAndClick(walletHomepage.unDelegateButton);
await unDelegatePage.unDelegateButton.waitForClickable({ timeout: 1500 });
await unDelegatePage.unDelegateButton.isEnabled();
await unDelegatePage.unDelegateGatewayRadioButton.click();
await unDelegatePage.unDelegateGatewayRadioButton.isSelected();
const mixNodeRadioButton =
await unDelegatePage.unMixNodeRadioButton.isSelected();
expect(mixNodeRadioButton).toEqual(false);
});
});
@@ -0,0 +1,39 @@
const walletLogin = require("../../pages/wallet.login");
const walletSignUp = require("../../pages/wallet.create");
const textConstants = require("../../../common/constants/text-constants");
describe("non existing wallet holder", () => {
//wallet mnemonic gets pushed here
const DATA = [];
it("create a new account and wallet", async () => {
const signInText = await walletLogin.signInLabel.getText();
expect(signInText).toEqual(textConstants.homePageSignIn);
await walletSignUp.createAccount.click();
//wallet generation takes some time - apply wait
await walletSignUp.create.click();
await walletSignUp.accountCreatedSuccessfully.waitForEnabled({
timeout: 10000,
});
const getWalletText = await walletSignUp.punkAddress.getText();
expect(getWalletText.length).toEqual(43);
const accountCreated =
await walletSignUp.accountCreatedSuccessfully.getText();
expect(accountCreated).toEqual(textConstants.walletSuccess);
const getMnemonic = await walletSignUp.walletMnemonicValue.getText();
DATA.push(getMnemonic);
});
it("navigate back to sign in screen and validate mnemonic works", async () => {
await walletSignUp.backToSignIn.click();
await walletLogin.enterMnemonic(DATA[0]);
await walletLogin.walletAddress.isDisplayed();
});
});
+93
View File
@@ -0,0 +1,93 @@
const os = require("os");
const path = require("path");
const { spawn, spawnSync } = require("child_process");
//insert path to binary
const nym_path = "../target/release/nym-wallet";
exports.config = {
//run sequentially, as using one default user may cause issues for parallel test runs for now
specs: [
"./tests/specs/existinguser/test.wallet.home.js",
"./tests/specs/existinguser/test.wallet.send.js",
"./tests/specs/existinguser/test.wallet.receive.js",
"./tests/specs/existinguser/test.wallet.bond.js",
"./tests/specs/existinguser/test.wallet.delegate.js",
"./tests/specs/newuser/test.wallet.create.js",
],
//run tests by providing --suite {{login}}
suites: {
home: ["./tests/specs/existinguser/test.wallet.home.js"],
sendreceive: [
"./tests/specs/existinguser/test.wallet.send.js",
"./tests/specs/existinguser/test.wallet.receive.js",
],
bond: ["./tests/specs/existinguser/test.wallet.bond.js"],
delegate: [
"./tests/specs/existinguser/test.wallet.delegate.js",
"./tests/specs/existinguser/test.wallet.undelegate.js",
],
newuser: ["./tests/specs/newuser/test.wallet.create.js"],
},
maxInstances: 1,
capabilities: [
{
maxInstances: 1,
"tauri:options": {
application: nym_path,
},
},
],
// ===================
// Test Configurations
// ===================
// Define all options that are relevant for the WebdriverIO instance here
// Level of logging verbosity: trace | debug | info | warn | error | silent
bail: 0,
framework: "mocha",
reporters: ["spec"],
mochaOpts: {
ui: "bdd",
timeout: 60000,
},
logLevel: "silent",
// ===================
// Test Reporters
// ===================
reporters: [
[
"allure",
{
outputDir: "allure-results",
disableWebdriverStepsReporting: true,
disableWebdriverScreenshotsReporting: true,
},
],
],
// this is documentented in the readme - you will need to build the project first
// ensure the rust project is built since we expect this binary to exist for the webdriver sessions
//onPrepare: () => spawnSync("cargo", ["build", "--release"]),
// ensure we are running `tauri-driver` before the session starts so that we can proxy the webdriver requests
beforeSession: () =>
(tauriDriver = spawn(
path.resolve(os.homedir(), ".cargo", "bin", "tauri-driver"),
[],
{ stdio: [null, process.stdout, process.stderr] }
)),
afterTest: function (
test,
context,
{ error, result, duration, passed, retries }
) {
if (error) {
browser.takeScreenshot();
}
},
// clean up the `tauri-driver` process we spawned at the start of the session
afterSession: () => tauriDriver.kill(),
};
File diff suppressed because it is too large Load Diff