Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 47cae50e68 | |||
| c644956576 | |||
| c329724f8c | |||
| 7dc776f98a | |||
| 9717bcbb17 | |||
| 978cbc4f00 | |||
| ebb06d4beb | |||
| 1241a81514 | |||
| 08a190c1cb | |||
| 81f36e8da7 | |||
| f230229ce9 | |||
| 912fb4ab38 | |||
| 99ceabb0b0 | |||
| 25df7bcd4d | |||
| 1cdca7bec3 | |||
| c809c7733d | |||
| 7b53003edb | |||
| 831d9d2bf8 | |||
| cb7c51ba12 | |||
| 0310f0a8a9 | |||
| bb79d08f6d | |||
| 414c86b500 | |||
| 4304ffcf3c | |||
| 309b23e18a | |||
| 52703583f0 | |||
| 6473ef13c6 | |||
| 9a45f15ba4 | |||
| 746795b7ce | |||
| 8b81247044 | |||
| c6cd787950 | |||
| f9ab20b10f | |||
| acffd496ed | |||
| 466ac1a1e0 | |||
| d53adcd17e | |||
| 36e82e831f | |||
| cbe0115f01 |
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 node’s
|
||||
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 node’s 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'];
|
||||
@@ -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 />} />
|
||||
|
||||
Vendored
+2
@@ -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;
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
node_modules
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
})
|
||||
})
|
||||
-29
@@ -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/*"
|
||||
]
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
reports
|
||||
allure-results
|
||||
node_modules
|
||||
.vscode
|
||||
.idea
|
||||
@@ -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.
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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
Reference in New Issue
Block a user