Compare commits

...

19 Commits

Author SHA1 Message Date
Yana 61f8b1710a Add FixMatomoProvider 2023-11-29 15:26:08 +02:00
Yana 496b284bcd Add delegate button to each mixnode raw 2023-11-22 19:52:23 +01:00
Yana 1f249a3386 Fix ModalError styles 2023-11-20 16:08:44 +01:00
Yana 8ca7c48094 Add confirmation models 2023-11-15 15:53:37 +00:00
Yana abbcfbb6c2 wip 2023-11-15 12:48:38 +00:00
Yana c0cc019b97 WIP 2023-11-14 14:57:19 +00:00
Yana e8896352a1 Add CosmWasmSigningClient 2023-11-10 17:27:49 +00:00
Yana 278b2e1657 WIP 2023-11-09 17:34:26 +00:00
Yana 87437785f9 Remove identity key, mixId and amount validation 2023-11-09 16:52:14 +00:00
Yana 788a67e9f4 WIP 2023-11-08 16:52:55 +00:00
Yana 4f58a63cb6 WIP 2023-11-08 16:51:38 +00:00
Yana 35e3961f75 WIP 2023-11-07 17:22:51 +00:00
Yana 2dac633873 WIP 2023-11-07 15:06:45 +00:00
Yana 23d08b993c Connect delegate modal to keplr balance 2023-11-07 13:02:00 +00:00
Yana 42b5472886 Add Delegations Modal UI 2023-11-06 21:19:28 +00:00
Yana 48071f11ab Add TokenSVG 2023-11-06 10:50:24 +00:00
Yana 72b5aedad3 fix ui bug in ts.config 2023-11-05 09:35:25 +00:00
Yana 4fa5a1ad37 WIP 2023-10-31 15:59:14 +01:00
Yana bcc450eb9f WIP 2023-10-30 15:03:24 +01:00
72 changed files with 28853 additions and 1031 deletions
+9 -1
View File
@@ -19,6 +19,8 @@
},
"dependencies": {
"@cosmjs/math": "^0.26.2",
"@cosmos-kit/keplr": "^2.4.7",
"@cosmos-kit/react": "^2.9.6",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@mui/icons-material": "^5.0.0",
@@ -27,13 +29,19 @@
"@mui/system": "^5.0.1",
"@mui/x-data-grid": "^5.0.0-beta.5",
"@nymproject/mui-theme": "^1.0.0",
"@nymproject/node-tester": "^1.0.0",
"@nymproject/nym-validator-client": "^0.18.0",
"@nymproject/react": "^1.0.0",
"@nymproject/types": "^1.0.0",
"@tauri-apps/api": "^1.5.1",
"big.js": "^6.2.1",
"bs58": "^5.0.0",
"buffer": "^6.0.3",
"chain-registry": "^1.19.0",
"d3-scale": "^4.0.0",
"date-fns": "^2.24.0",
"lodash": "^4.17.21",
"i18n-iso-countries": "^6.8.0",
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^3.1.4",
@@ -0,0 +1,120 @@
import React from 'react';
import { useChain } from '@cosmos-kit/react';
import { Box, Button, Card, Typography, IconButton } from '@mui/material';
import Big from 'big.js';
import CloseIcon from '@mui/icons-material/Close';
import { useTheme } from '@mui/material/styles';
import { useEffect, useState, useMemo } from 'react';
import '@interchain-ui/react/styles';
import { TokenSVG } from '../icons/TokenSVG';
import { ElipsSVG } from '../icons/ElipsSVG';
export function useIsClient() {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
return isClient;
}
export const uNYMtoNYM = (unym: string, rounding = 6) => {
const nym = Big(unym).div(1000000).toFixed(rounding);
return {
asString: () => {
return nym;
},
asNumber: () => {
return Number(nym);
},
};
};
export const trimAddress = (address = '', trimBy = 6) => {
return `${address.slice(0, trimBy)}...${address.slice(-trimBy)}`;
};
export default function ConnectKeplrWallet() {
const {
username,
connect,
disconnect,
wallet,
openView,
address,
getCosmWasmClient,
isWalletConnected,
isWalletConnecting,
} = useChain('nyx');
const isClient = useIsClient();
const theme = useTheme();
const color = theme.palette.text.primary;
const [balance, setBalance] = useState<{
status: 'loading' | 'success';
data?: string;
}>({ status: 'loading', data: undefined });
useEffect(() => {
const getBalance = async (walletAddress: string) => {
setBalance({ status: 'loading', data: undefined });
const account = await getCosmWasmClient();
const uNYMBalance = await account.getBalance(walletAddress, 'unym');
const NYMBalance = uNYMtoNYM(uNYMBalance.amount).asString();
setBalance({ status: 'success', data: NYMBalance });
};
if (address) {
getBalance(address);
}
}, [address, getCosmWasmClient]);
if (!isClient) return null;
const getGlobalbutton = () => {
if (isWalletConnecting) {
return <Button onClick={() => connect()}>{`Connecting ${wallet?.prettyName}`}</Button>;
}
if (isWalletConnected) {
return (
<Box display={'flex'} alignItems={'center'} gap={2}>
<Box display={'flex'} alignItems={'center'} gap={1}>
<TokenSVG />
<Typography variant="body1" fontWeight={600}>
{balance.data} NYM
</Typography>
</Box>
<Box display={'flex'} alignItems={'center'} gap={1}>
<ElipsSVG />
<Typography variant="body1" fontWeight={600}>
{trimAddress(address, 7)}
</Typography>
</Box>
<IconButton
onClick={async () => {
await disconnect();
// setGlobalStatus(WalletStatus.Disconnected);
}}
>
<CloseIcon sx={{ color: 'white' }} />
</IconButton>
</Box>
);
}
return <Button onClick={() => connect()}>Connect Wallet</Button>;
};
return (
<Box>
<div className="flex justify-start space-x-5">{getGlobalbutton()}</div>
</Box>
);
}
@@ -0,0 +1,48 @@
import React from 'react';
import { FeeDetails } from '@nymproject/types';
import { Box } from '@mui/material';
import { useTheme, Theme } from '@mui/material/styles';
import { SimpleModal } from './SimpleModal';
import { ModalFee } from './ModalFee';
import { ModalDivider } from './ModalDivider';
import { backDropStyles, modalStyles } from './styles';
const storybookStyles = (theme: Theme, isStorybook?: boolean, backdropProps?: object) =>
isStorybook
? {
backdropProps: { ...backDropStyles(theme), ...backdropProps },
sx: modalStyles(theme),
}
: {};
export const ConfirmTx: FCWithChildren<{
open: boolean;
header: string;
subheader?: string;
fee: FeeDetails;
onConfirm: () => Promise<void>;
onClose?: () => void;
onPrev: () => void;
isStorybook?: boolean;
children?: React.ReactNode;
}> = ({ open, fee, onConfirm, onClose, header, subheader, onPrev, children, isStorybook }) => {
const theme = useTheme();
return (
<SimpleModal
open={open}
header={header}
subHeader={subheader}
okLabel="Confirm"
onOk={onConfirm}
onClose={onClose}
onBack={onPrev}
{...storybookStyles(theme, isStorybook)}
>
<Box sx={{ mt: 3 }}>
{children}
<ModalFee fee={fee} isLoading={false} />
<ModalDivider />
</Box>
</SimpleModal>
);
};
@@ -0,0 +1,83 @@
import React from 'react';
import {
Breakpoint,
Button,
Paper,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
SxProps,
Typography,
} from '@mui/material';
export interface ConfirmationModalProps {
open: boolean;
onConfirm: () => void;
onClose?: () => void;
children?: React.ReactNode;
title: React.ReactNode | string;
subTitle?: React.ReactNode | string;
confirmButton: React.ReactNode | string;
disabled?: boolean;
sx?: SxProps;
fullWidth?: boolean;
maxWidth?: Breakpoint;
backdropProps?: object;
}
export const ConfirmationModal = ({
open,
onConfirm,
onClose,
children,
title,
subTitle,
confirmButton,
disabled,
sx,
fullWidth,
maxWidth,
backdropProps,
}: ConfirmationModalProps) => {
const Title = (
<DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}>
{title}
{subTitle &&
(typeof subTitle === 'string' ? (
<Typography fontWeight={400} variant="subtitle1" fontSize={12} color={'grey'}>
{subTitle}
</Typography>
) : (
subTitle
))}
</DialogTitle>
);
const ConfirmButton =
typeof confirmButton === 'string' ? (
<Button onClick={onConfirm} variant="contained" fullWidth disabled={disabled} sx={{ py: 1.6 }}>
<Typography variant="button" fontSize="large">
{confirmButton}
</Typography>
</Button>
) : (
confirmButton
);
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="responsive-dialog-title"
maxWidth={maxWidth || 'sm'}
sx={{ textAlign: 'center', ...sx }}
fullWidth={fullWidth}
BackdropProps={backdropProps}
PaperComponent={Paper}
PaperProps={{ elevation: 0 }}
>
{Title}
<DialogContent>{children}</DialogContent>
<DialogActions sx={{ px: 3, pb: 3 }}>{ConfirmButton}</DialogActions>
</Dialog>
);
};
@@ -0,0 +1,28 @@
import * as React from 'react';
import { useClipboard } from 'use-clipboard-copy';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import DoneIcon from '@mui/icons-material/Done';
import { Box, Tooltip } from '@mui/material';
import { SxProps } from '@mui/system';
import { DelegateSVG } from '../../../icons/DelevateSVG';
import { useChain } from '@cosmos-kit/react';
export const DelegateIconButton: FCWithChildren<{
tooltip?: React.ReactNode;
onDelegate: () => void;
sx?: SxProps;
}> = ({ tooltip, onDelegate, sx }) => {
const { address, getCosmWasmClient, isWalletConnected, getSigningCosmWasmClient } = useChain('nyx');
console.log('isWalletConnected :>> ', isWalletConnected);
const handleDelegateClick = () => {
onDelegate();
};
return (
<Tooltip title={isWalletConnected ? undefined : 'Connect your wallet to delegate'}>
<Box sx={sx} onClick={isWalletConnected ? handleDelegateClick : undefined}>
<DelegateSVG />
</Box>
</Tooltip>
);
};
@@ -0,0 +1,288 @@
import React, { useState, useEffect, ChangeEvent } from 'react';
import { Box, Typography, SxProps, TextField } from '@mui/material';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
import { CurrencyDenom, DecCoin } from '@nymproject/types';
import { SimpleModal } from './SimpleModal';
import { ModalListItem } from './ModalListItem';
import { Console, urls, validateAmount } from '../utils';
import { useChain } from '@cosmos-kit/react';
import { StdFee } from '@cosmjs/amino';
import { ExecuteResult } from '@cosmjs/cosmwasm-stargate';
import { uNYMtoNYM } from '../utils';
import { DelegationModalProps } from './DelegationModal';
const MIN_AMOUNT_TO_DELEGATE = 10;
const MIXNET_CONTRACT_ADDRESS = 'n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr';
// const sandboxContractAddress = 'n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav';
export const DelegateModal: FCWithChildren<{
open: boolean;
onClose: () => void;
onOk?: (delegationModalProps: DelegationModalProps) => void;
initialIdentityKey?: string;
initialMixId?: number;
onIdentityKeyChanged?: (identityKey: string) => void;
onAmountChanged?: (amount: string) => void;
header?: string;
buttonText?: string;
rewardInterval?: string;
// accountBalance?: string;
estimatedReward?: number;
profitMarginPercentage?: string | null;
nodeUptimePercentage?: number | null;
denom: CurrencyDenom;
initialAmount?: string;
hasVestingContract?: boolean;
sx?: SxProps;
backdropProps?: object;
}> = ({
open,
onIdentityKeyChanged,
onAmountChanged,
onClose,
onOk,
header,
buttonText,
initialIdentityKey,
initialMixId,
// accountBalance,
denom,
sx,
backdropProps,
}) => {
const [mixId, setMixId] = useState<number | undefined>(initialMixId);
const [identityKey, setIdentityKey] = useState<string | undefined>(initialIdentityKey);
const [amount, setAmount] = useState<DecCoin | undefined>();
const [isValidated, setValidated] = useState<boolean>(false);
const [errorAmount, setErrorAmount] = useState<string | undefined>();
const [errorIdentityKey, setErrorIdentityKey] = useState<string>();
const [mixIdError, setMixIdError] = useState<string>();
const [cosmWasmSignerClient, setCosmWasmSignerClient] = useState<any>();
const [balance, setBalance] = useState<{
status: 'loading' | 'success';
data?: string;
}>({ status: 'loading', data: undefined });
const { address, getCosmWasmClient, isWalletConnected, getSigningCosmWasmClient } = useChain('nyx');
useEffect(() => {
const getClient = async () => {
await getSigningCosmWasmClient()
.then((res) => {
setCosmWasmSignerClient(res);
console.log('res :>> ', res);
})
.catch((e) => console.log('e :>> ', e));
};
isWalletConnected && getClient();
}, [isWalletConnected]);
const getBalance = async (walletAddress: string) => {
const account = await getCosmWasmClient();
const uNYMBalance = await account.getBalance(walletAddress, 'unym');
const NYMBalance = uNYMtoNYM(uNYMBalance.amount).asString();
setBalance({ status: 'success', data: NYMBalance });
};
useEffect(() => {
if (address) {
getBalance(address);
}
}, [address, getCosmWasmClient]);
const validate = async () => {
let newValidatedValue = true;
let errorAmountMessage;
let errorIdentityKeyMessage;
if (!identityKey) {
newValidatedValue = false;
errorIdentityKeyMessage = 'Please enter a valid identity key';
}
if (amount && !(await validateAmount(amount.amount, '0'))) {
newValidatedValue = false;
errorAmountMessage = 'Please enter a valid amount';
}
if (amount && Number(amount) < MIN_AMOUNT_TO_DELEGATE) {
errorAmountMessage = `Min. delegation amount: ${MIN_AMOUNT_TO_DELEGATE} ${denom.toUpperCase()}`;
newValidatedValue = false;
}
if (!amount?.amount.length) {
newValidatedValue = false;
}
if (!mixId) {
newValidatedValue = false;
}
if (amount && balance.data && +balance.data - +amount <= 0) {
errorAmountMessage = 'Not enough funds';
newValidatedValue = false;
}
setErrorIdentityKey(errorIdentityKeyMessage);
if (mixIdError && !errorIdentityKeyMessage) {
setErrorIdentityKey(mixIdError);
}
setErrorAmount(errorAmountMessage);
setValidated(newValidatedValue);
};
const delegateToMixnode = async (
{
mixId,
}: {
mixId: number;
},
fee: number | StdFee | 'auto' = 'auto',
memo?: string,
funds?: DecCoin[],
): Promise<ExecuteResult> => {
const amount = (Number(funds![0].amount) * 1000000).toString();
const uNymFunds = [{ amount: amount, denom: 'unym' }];
console.log('cosmWasmSignerClient :>> ', cosmWasmSignerClient);
return await cosmWasmSignerClient.execute(
address,
MIXNET_CONTRACT_ADDRESS,
{
delegate_to_mixnode: {
mix_id: mixId,
},
},
fee,
memo,
uNymFunds,
);
};
const handleConfirm = async () => {
const memo: string = 'test delegation';
const fee = { gas: '1000000', amount: [{ amount: '25000', denom: 'unym' }] };
if (mixId && amount && onOk && cosmWasmSignerClient) {
onOk({
status: 'loading',
action: 'delegate',
});
try {
const tx = await delegateToMixnode({ mixId }, fee, memo, [amount]);
onOk({
status: 'success',
action: 'delegate',
message: 'This operation can take up to one hour to process',
transactions: [
{ url: `${urls('MAINNET').blockExplorer}/transaction/${tx.transactionHash}`, hash: tx.transactionHash },
],
});
} catch (e) {
Console.error('Failed to addDelegation', e);
onOk({
status: 'error',
action: 'delegate',
message: (e as Error).message,
});
}
}
};
const handleIdentityKeyChanged = (newIdentityKey: string) => {
setIdentityKey(newIdentityKey);
if (onIdentityKeyChanged) {
onIdentityKeyChanged(newIdentityKey);
}
};
const handleMixIDChanged = (event: ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
setMixId(Number(newValue));
};
const handleAmountChanged = (newAmount: DecCoin) => {
setAmount(newAmount);
if (onAmountChanged) {
onAmountChanged(newAmount.amount);
}
};
React.useEffect(() => {
validate();
}, [amount, identityKey, mixId]);
return (
<SimpleModal
open={open}
onClose={onClose}
// onOk={async () => {
// if (mixId && amount) {
// handleConfirm({ mixId, value: { amount, denom } });
// }
// }}
onOk={async () => handleConfirm()}
header={header || 'Delegate'}
okLabel={buttonText || 'Delegate stake'}
okDisabled={!isValidated}
sx={sx}
backdropProps={backdropProps}
>
<Box sx={{ mt: 3 }} gap={2}>
<IdentityKeyFormField
required
fullWidth
label="Node identity key"
onChanged={handleIdentityKeyChanged}
initialValue={identityKey}
readOnly={Boolean(initialIdentityKey)}
textFieldProps={{
autoFocus: !initialIdentityKey,
}}
showTickOnValid={false}
/>
<Typography
component="div"
textAlign="left"
variant="caption"
sx={{ color: 'error.main', mx: 2, mt: errorIdentityKey && 1 }}
>
{errorIdentityKey}
</Typography>
</Box>
<Box sx={{ mt: 3 }} gap={2}>
<TextField
fullWidth={true}
required
label={'MixID'}
error={mixIdError !== undefined}
helperText={mixIdError}
onChange={handleMixIDChanged}
InputLabelProps={{ shrink: true }}
value={mixId?.toString() || ''}
/>
</Box>
<Box display="flex" gap={2} alignItems="center" sx={{ mt: 3 }}>
<CurrencyFormField
required
fullWidth
label="Amount"
// initialValue={amount}
autoFocus={Boolean(initialIdentityKey)}
onChanged={handleAmountChanged}
denom={denom}
validationError={errorAmount}
/>
</Box>
<Box sx={{ mt: 3 }}>
<ModalListItem label="Account balance" value={`${balance.data} NYM`} divider fontWeight={600} />
</Box>
<ModalListItem label="Est. fee for this transaction will be calculated in the next page" />
</SimpleModal>
);
};
@@ -0,0 +1,80 @@
import React from 'react';
import { Typography, SxProps, Stack } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { LoadingModal } from './LoadingModal';
import { ConfirmationModal } from './ConfirmationModal';
import { ErrorModal } from './ErrorModal';
export type ActionType = 'delegate' | 'undelegate' | 'redeem' | 'redeem-all' | 'compound';
const actionToHeader = (action: ActionType): string => {
// eslint-disable-next-line default-case
switch (action) {
case 'redeem':
return 'Rewards redeemed successfully';
case 'redeem-all':
return 'All rewards redeemed successfully';
case 'delegate':
return 'Delegation successful';
case 'undelegate':
return 'Undelegation successful';
case 'compound':
return 'Rewards compounded successfully';
default:
throw new Error('Unknown type');
}
};
export type DelegationModalProps = {
status: 'loading' | 'success' | 'error';
action: ActionType;
message?: string;
transactions?: {
url: string;
hash: string;
}[];
};
export const DelegationModal: FCWithChildren<
DelegationModalProps & {
open: boolean;
onClose: () => void;
sx?: SxProps;
backdropProps?: object;
children?: React.ReactNode;
}
> = ({ status, action, message, transactions, open, onClose, children, sx, backdropProps }) => {
if (status === 'loading') return <LoadingModal sx={sx} backdropProps={backdropProps} />;
if (status === 'error') {
return (
<ErrorModal message={message} sx={sx} open={open} onClose={onClose}>
{children}
</ErrorModal>
);
}
return (
<ConfirmationModal
open={open}
onConfirm={onClose || (() => {})}
title={actionToHeader(action)}
confirmButton="Done"
>
<Stack alignItems="center" spacing={2} mb={0}>
{message && <Typography>{message}</Typography>}
{transactions?.length === 1 && (
<Link href={transactions[0].url} target="_blank" sx={{ ml: 1 }} text="View on blockchain" noIcon />
)}
{transactions && transactions.length > 1 && (
<Stack alignItems="center" spacing={1}>
<Typography>View the transactions on blockchain:</Typography>
{transactions.map(({ url, hash }) => (
<Link href={url} target="_blank" sx={{ ml: 1 }} text={hash.slice(0, 6)} key={hash} noIcon />
))}
</Stack>
)}
</Stack>
</ConfirmationModal>
);
};
@@ -0,0 +1,28 @@
import React from 'react';
import { Box, Button, Modal, SxProps, Typography } from '@mui/material';
import { modalStyle } from '../../../../../nym-wallet/src/components/Modals/styles';
export const ErrorModal: FCWithChildren<{
open: boolean;
title?: string;
message?: string;
sx?: SxProps;
backdropProps?: object;
onClose: () => void;
children?: React.ReactNode;
}> = ({ children, open, title, message, sx, backdropProps, onClose }) => (
<Modal open={open} onClose={onClose} BackdropProps={backdropProps}>
<Box sx={{ border: (t) => `1px solid #fff`, ...modalStyle, ...sx }} textAlign="center">
<Typography color={(theme) => theme.palette.error.main} mb={1}>
{title || 'Oh no! Something went wrong...'}
</Typography>
<Typography my={5} color="text.primary" sx={{ textOverflow: 'wrap', overflowWrap: 'break-word' }}>
{message}
</Typography>
{children}
<Button variant="contained" onClick={onClose}>
Close
</Button>
</Box>
</Modal>
);
@@ -0,0 +1,35 @@
import React, { useContext } from 'react';
import { Warning } from '@mui/icons-material';
import { FeeDetails } from '@nymproject/types';
import { Alert, AlertTitle, Box } from '@mui/material';
import { isBalanceEnough } from '../utils';
import { AppContext } from '../context';
export const FeeWarning = ({ fee, amount }: { fee: FeeDetails; amount: number }) => {
if (fee.amount && +fee.amount.amount > amount) {
return (
<Alert color="warning" sx={{ mt: 3 }} icon={<Warning />}>
<AlertTitle>Warning: Fees are greater than the reward</AlertTitle>
The fees for redeeming rewards will cost more than the rewards. Are you sure you want to continue?
</Alert>
);
}
return null;
};
export const BalanceWarning = ({ tx, fee }: { fee: string; tx?: string }) => {
const { userBalance } = useContext(AppContext);
const hasEnoughBalanace = isBalanceEnough(fee, tx, userBalance.balance?.amount.amount);
if (hasEnoughBalanace) return null;
return (
<Alert color="warning" icon={<Warning />}>
<AlertTitle>Warning: Transaction amount is greater than your balance</AlertTitle>
The transaction amount (inc fees) is greater than your current balance, which could cause this transaction to
fail.
<Box sx={{ mt: 0.5 }}>Do you want to continue?</Box>
</Alert>
);
};
@@ -0,0 +1,29 @@
import React from 'react';
import { Box, CircularProgress, Modal, Stack, Typography, SxProps } from '@mui/material';
const modalStyle: SxProps = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 500,
bgcolor: 'background.paper',
boxShadow: 24,
borderRadius: '16px',
p: 4,
};
export const LoadingModal: FCWithChildren<{
text?: string;
sx?: SxProps;
backdropProps?: object;
}> = ({ sx, backdropProps, text = 'Please wait...' }) => (
<Modal open BackdropProps={backdropProps}>
<Box sx={{ border: (t) => `1px solid grey`, ...modalStyle, ...sx }} textAlign="center">
<Stack spacing={4} direction="row" alignItems="center">
<CircularProgress />
<Typography sx={{ color: 'text.primary' }}>{text}</Typography>
</Stack>
</Box>
</Modal>
);
@@ -0,0 +1,6 @@
import React from 'react';
import { Box, SxProps } from '@mui/material';
export const ModalDivider: FCWithChildren<{
sx?: SxProps;
}> = ({ sx }) => <Box borderTop="1px solid" borderColor="rgba(141, 147, 153, 0.2)" my={1} sx={sx} />;
@@ -0,0 +1,35 @@
import React from 'react';
import { FeeDetails } from '@nymproject/types';
import { CircularProgress } from '@mui/material';
import { ModalListItem } from './ModalListItem';
import { ModalDivider } from './ModalDivider';
type TFeeProps = { fee?: FeeDetails; isLoading: boolean; error?: string; divider?: boolean };
type TTotalAmountProps = { fee?: FeeDetails; amount?: string; isLoading: boolean; error?: string; divider?: boolean };
const getValue = ({ fee, amount, isLoading, error }: TTotalAmountProps) => {
if (isLoading) return <CircularProgress size={15} />;
if (error && !isLoading) return 'n/a';
if (fee) {
const numericFee = Number(fee.amount?.amount);
const numericAmountToTransfer = Number(amount);
return amount
? `${numericFee + numericAmountToTransfer} ${fee.amount?.denom.toUpperCase()}`
: `${fee.amount?.amount} ${fee.amount?.denom.toUpperCase()}`;
}
return '-';
};
export const ModalFee = ({ fee, isLoading, error, divider }: TFeeProps) => (
<>
<ModalListItem label="Fee for this transaction" value={getValue({ fee, isLoading, error })} />
{divider && <ModalDivider />}
</>
);
export const ModalTotalAmount = ({ fee, amount, isLoading, error, divider }: TTotalAmountProps) => (
<>
<ModalListItem label="Total amount" value={getValue({ fee, amount, isLoading, error })} fontWeight={600} />
{divider && <ModalDivider />}
</>
);
@@ -0,0 +1,32 @@
import React from 'react';
import { Box, Stack, SxProps, Typography, TypographyProps } from '@mui/material';
import { ModalDivider } from '../../../../../nym-wallet/src/components/Modals/ModalDivider';
export const ModalListItem: FCWithChildren<{
label: string;
divider?: boolean;
hidden?: boolean;
fontWeight?: TypographyProps['fontWeight'];
fontSize?: TypographyProps['fontSize'];
light?: boolean;
value?: React.ReactNode;
sxValue?: SxProps;
}> = ({ label, value, hidden, fontWeight, fontSize, divider, sxValue }) => (
<Box sx={{ display: hidden ? 'none' : 'block' }}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography fontSize="smaller" fontWeight={fontWeight} sx={{ color: 'text.primary', fontSize: 14 }}>
{label}
</Typography>
{value && (
<Typography
fontSize="smaller"
fontWeight={fontWeight}
sx={{ color: 'text.primary', fontSize: fontSize || 14, ...sxValue }}
>
{value}
</Typography>
)}
</Stack>
{divider && <ModalDivider />}
</Box>
);
@@ -0,0 +1,106 @@
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 { modalStyle } from '../../../../../nym-wallet/src/components/Modals/styles';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
export const StyledBackButton = ({
onBack,
label,
fullWidth,
sx,
}: {
onBack: () => void;
label?: string;
fullWidth?: boolean;
sx?: SxProps;
}) => (
<Button disableFocusRipple size="large" fullWidth={fullWidth} variant="outlined" onClick={onBack} sx={sx}>
{label || <ArrowBackIosNewIcon fontSize="small" />}
</Button>
);
export const SimpleModal: FCWithChildren<{
open: boolean;
hideCloseIcon?: boolean;
displayErrorIcon?: boolean;
displayInfoIcon?: boolean;
headerStyles?: SxProps;
subHeaderStyles?: SxProps;
buttonFullWidth?: boolean;
onClose?: () => void;
onOk?: () => Promise<void>;
onBack?: () => void;
header: string | React.ReactNode;
subHeader?: string;
okLabel: string;
backLabel?: string;
backButtonFullWidth?: boolean;
okDisabled?: boolean;
sx?: SxProps;
backdropProps?: object;
children?: React.ReactNode;
}> = ({
open,
hideCloseIcon,
displayErrorIcon,
displayInfoIcon,
headerStyles,
subHeaderStyles,
buttonFullWidth,
onClose,
okDisabled,
onOk,
onBack,
header,
subHeader,
okLabel,
backLabel,
backButtonFullWidth,
sx,
children,
backdropProps,
}) => (
<Modal open={open} onClose={onClose} BackdropProps={backdropProps}>
<Box sx={{ border: (t) => `1px solid #fff`, ...modalStyle, ...sx }}>
{displayErrorIcon && <ErrorOutline color="error" sx={{ mb: 3 }} />}
{displayInfoIcon && <InfoOutlinedIcon sx={{ mb: 2, color: 'blue' }} />}
<Stack direction="row" justifyContent="space-between" alignItems="center">
{typeof header === 'string' ? (
<Typography fontSize={20} fontWeight={600} sx={{ color: 'text.primary', ...headerStyles }}>
{header}
</Typography>
) : (
header
)}
{!hideCloseIcon && <CloseIcon onClick={onClose} cursor="pointer" />}
</Stack>
<Typography
mt={subHeader ? 0.5 : 0}
mb={3}
fontSize={12}
color={(theme) => theme.palette.text.secondary}
// sx={{ color: (theme) => theme.palette.nym.nymWallet.text.muted, ...subHeaderStyles }}
>
{subHeader}
</Typography>
{children}
{(onOk || onBack) && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 2, width: buttonFullWidth ? '100%' : null }}>
{onBack && <StyledBackButton onBack={onBack} label={backLabel} fullWidth={backButtonFullWidth} />}
{onOk && (
<Button variant="contained" fullWidth size="large" onClick={onOk} disabled={okDisabled}>
{okLabel}
</Button>
)}
</Box>
)}
</Box>
</Modal>
);
@@ -0,0 +1,21 @@
import { Theme } from '@mui/material/styles';
export const backDropStyles = (theme: Theme) => {
const { mode } = theme.palette;
return {
style: {
left: mode === 'light' ? '0' : '50%',
width: '50%',
},
};
};
export const modalStyles = (theme: Theme) => {
const { mode } = theme.palette;
return { left: mode === 'light' ? '25%' : '75%' };
};
export const dialogStyles = (theme: Theme) => {
const { mode } = theme.palette;
return { left: mode === 'light' ? '-50%' : '50%' };
};
@@ -0,0 +1,4 @@
export const config = {
IS_DEV_MODE: process.env.NODE_ENV === 'development',
LOG_TAURI_OPERATIONS: process.env.NODE_ENV === 'development',
};
@@ -0,0 +1,170 @@
import React, { createContext, Dispatch, SetStateAction, useContext, useEffect, useMemo, useState } from 'react';
import { AccountEntry } from '@nymproject/types';
import { addAccount as addAccountRequest, renameAccount, showMnemonicForAccount } from '../requests';
import { useSnackbar } from 'notistack';
import { AppContext } from './main';
type TAccounts = {
accounts?: AccountEntry[];
selectedAccount?: AccountEntry;
accountToEdit?: AccountEntry;
dialogToDisplay?: TAccountsDialog;
isLoading: boolean;
error?: string;
accountMnemonic: TAccountMnemonic;
setError: Dispatch<SetStateAction<string | undefined>>;
setAccountMnemonic: Dispatch<SetStateAction<TAccountMnemonic>>;
handleAddAccount: (data: { accountName: string; mnemonic: string; password: string }) => void;
setDialogToDisplay: (dialog?: TAccountsDialog) => void;
handleSelectAccount: (data: { accountName: string; password: string }) => Promise<boolean>;
handleAccountToEdit: (accountId: string | undefined) => void;
handleEditAccount: ({
account,
newAccountName,
password,
}: {
account: AccountEntry;
newAccountName: string;
password: string;
}) => Promise<void>;
handleImportAccount: (account: AccountEntry) => void;
handleGetAccountMnemonic: (data: { password: string; accountName: string }) => void;
};
export type TAccountsDialog = 'Accounts' | 'Add' | 'Edit' | 'Import' | 'Mnemonic';
export type TAccountMnemonic = { value?: string; accountName?: string };
export const AccountsContext = createContext({} as TAccounts);
export const AccountsProvider: FCWithChildren = ({ children }) => {
const [accounts, setAccounts] = useState<AccountEntry[]>([]);
const [selectedAccount, setSelectedAccount] = useState<AccountEntry>();
const [accountToEdit, setAccountToEdit] = useState<AccountEntry>();
const [dialogToDisplay, setDialogToDisplay] = useState<TAccountsDialog>();
const [accountMnemonic, setAccountMnemonic] = useState<TAccountMnemonic>({
value: undefined,
accountName: undefined,
});
const [error, setError] = useState<string>();
const [isLoading, setIsLoading] = useState(false);
const { onAccountChange, storedAccounts } = useContext(AppContext);
const { enqueueSnackbar } = useSnackbar();
const handleAddAccount = async ({
accountName,
mnemonic,
password,
}: {
accountName: string;
mnemonic: string;
password: string;
}) => {
setIsLoading(true);
try {
const newAccount = await addAccountRequest({
accountName,
mnemonic,
password,
});
setAccounts((accs) => [...accs, newAccount]);
enqueueSnackbar('New account created', { variant: 'success' });
} catch (e) {
setError(`Error adding account: ${e}`);
throw new Error();
} finally {
setIsLoading(false);
}
};
const handleEditAccount = async ({
account,
newAccountName,
password,
}: {
account: AccountEntry;
newAccountName: string;
password: string;
}) => {
setIsLoading(true);
try {
await renameAccount({ accountName: account.id, newAccountName, password });
setAccounts((accs) =>
accs?.map((acc) => (acc.address === account.address ? { ...acc, id: newAccountName } : acc)),
);
if (selectedAccount?.id === account.id) {
setSelectedAccount({ ...selectedAccount, id: newAccountName });
}
setDialogToDisplay('Accounts');
} catch (e) {
throw new Error(`Error editing account: ${e}`);
} finally {
setIsLoading(false);
}
};
const handleImportAccount = (account: AccountEntry) => setAccounts((accs) => [...(accs ? [...accs] : []), account]);
const handleAccountToEdit = (accountName: string | undefined) =>
setAccountToEdit(accounts?.find((acc) => acc.id === accountName));
const handleSelectAccount = async ({ accountName, password }: { accountName: string; password: string }) => {
try {
await onAccountChange({ accountId: accountName, password });
const match = accounts?.find((acc) => acc.id === accountName);
setSelectedAccount(match);
return true;
} catch (e) {
setError('Error switching account. Please check your password');
return false;
}
};
const handleGetAccountMnemonic = async ({ password, accountName }: { password: string; accountName: string }) => {
try {
setIsLoading(true);
const mnemonic = await showMnemonicForAccount({ password, accountName });
setAccountMnemonic({ value: mnemonic, accountName });
} catch (e) {
setError(e as string);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
if (storedAccounts) {
setAccounts(storedAccounts);
}
if (storedAccounts && !selectedAccount) {
setSelectedAccount(storedAccounts[0]);
}
}, [storedAccounts]);
return (
<AccountsContext.Provider
value={useMemo(
() => ({
error,
setError,
accounts,
selectedAccount,
accountToEdit,
dialogToDisplay,
accountMnemonic,
setDialogToDisplay,
setAccountMnemonic,
isLoading,
handleAddAccount,
handleEditAccount,
handleAccountToEdit,
handleSelectAccount,
handleImportAccount,
handleGetAccountMnemonic,
}),
[accounts, selectedAccount, accountToEdit, dialogToDisplay, isLoading, error, accountMnemonic],
)}
>
{children}
</AccountsContext.Provider>
);
};
@@ -0,0 +1,59 @@
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { sign } from '../requests';
import { Console } from '../utils/console';
// import { AppContext } from './main';
export type TBuyContext = {
loading: boolean;
error?: string;
signMessage: (message: string) => Promise<string | undefined>;
refresh: () => Promise<void>;
};
export const BuyContext = createContext<TBuyContext>({
loading: false,
signMessage: async () => '',
refresh: async () => undefined,
});
export const BuyContextProvider: FCWithChildren = ({ children }): JSX.Element => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string>();
const refresh = useCallback(async () => {
setError(undefined);
}, []);
useEffect(() => {
refresh();
}, [refresh]);
const signMessage = async (message: string) => {
let signature;
setLoading(true);
try {
signature = await sign(message);
} catch (e: any) {
Console.log(`Sign message operation failed: ${e}`);
console.log('`Sign message operation failed: ${e}` :>> ', `Sign message operation failed: ${e}`);
setError(`Sign message operation failed: ${e}`);
} finally {
setLoading(false);
}
return signature;
};
const memoizedValue = useMemo(
() => ({
loading,
error,
refresh,
signMessage,
}),
[loading, error],
);
return <BuyContext.Provider value={memoizedValue}>{children}</BuyContext.Provider>;
};
export const useBuyContext = () => useContext<TBuyContext>(BuyContext);
@@ -0,0 +1,3 @@
export * from './main';
export * from './accounts';
export * from './buy';
@@ -0,0 +1,366 @@
import React, { createContext, useEffect, useMemo, useState } from 'react';
import { forage } from '@tauri-apps/tauri-forage';
import { useNavigate } from 'react-router-dom';
import { useSnackbar } from 'notistack';
import { Account, AccountEntry, MixNodeDetails } from '@nymproject/types';
import { getVersion } from '@tauri-apps/api/app';
import { AppEnv, Network } from '../types';
import { TUseuserBalance, useGetBalance } from '../hooks/useGetBalance';
import {
getEnv,
getMixnodeBondDetails,
listAccounts,
selectNetwork,
signInWithMnemonic,
signInWithPassword,
signOut,
switchAccount,
} from '../requests';
import Big from 'big.js';
import { Console } from '../utils/console';
import { createSignInWindow, getReactState, setReactState } from '../requests/app';
// import { toDisplay } from '../utils';
const toDisplay = (val: string | number | Big, dp = 4) => {
let displayValue;
try {
displayValue = Big(val).toFixed(dp);
} catch (e: any) {
Console.warn(`${displayValue} not a valid decimal number: ${e}`);
}
return displayValue;
};
export const urls = (networkName?: Network) =>
networkName === 'MAINNET'
? {
mixnetExplorer: 'https://mixnet.explorers.guru/',
blockExplorer: 'https://blocks.nymtech.net',
networkExplorer: 'https://explorer.nymtech.net',
}
: {
blockExplorer: `https://${networkName}-blocks.nymtech.net`,
networkExplorer: `https://${networkName}-explorer.nymtech.net`,
};
type TLoginType = 'mnemonic' | 'password';
export type TAppContext = {
mode: 'light' | 'dark';
appEnv?: AppEnv;
appVersion?: string;
clientDetails?: Account;
storedAccounts?: AccountEntry[];
mixnodeDetails?: MixNodeDetails | null;
userBalance: TUseuserBalance;
showAdmin: boolean;
showTerminal: boolean;
network?: Network;
isLoading: boolean;
isAdminAddress: boolean;
error?: string;
loginType?: TLoginType;
showSendModal: boolean;
showReceiveModal: boolean;
onAccountChange: ({ accountId, password }: { accountId: string; password: string }) => void;
handleSwitchMode: () => void;
handleShowSendModal: () => void;
handleShowReceiveModal: () => void;
setIsLoading: (isLoading: boolean) => void;
setError: (value?: string) => void;
switchNetwork: (network: Network) => void;
getBondDetails: () => Promise<void>;
handleShowAdmin: () => void;
logIn: (opts: { type: TLoginType; value: string }) => void;
handleShowTerminal: () => void;
signInWithPassword: (password: string) => void;
logOut: () => void;
keepState: () => Promise<void>;
printBalance: string;
printVestedBalance?: string; // spendable vested token
};
interface RustState {
network?: Network;
loginType?: 'mnemonic' | 'password';
}
export const AppContext = createContext({} as TAppContext);
export const AppProvider: FCWithChildren = ({ children }) => {
const [clientDetails, setClientDetails] = useState<Account>();
const [storedAccounts, setStoredAccounts] = useState<AccountEntry[]>();
const [mixnodeDetails, setMixnodeDetails] = useState<MixNodeDetails | null>(null);
const [network, setNetwork] = useState<Network | undefined>();
const [appEnv, setAppEnv] = useState<AppEnv>();
const [showAdmin, setShowAdmin] = useState(false);
const [showTerminal, setShowTerminal] = useState(false);
const [mode, setMode] = useState<'light' | 'dark'>('light');
const [loginType, setLoginType] = useState<'mnemonic' | 'password'>();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string>();
const [appVersion, setAppVersion] = useState<string>();
const [isAdminAddress, setIsAdminAddress] = useState<boolean>(false);
const [showSendModal, setShowSendModal] = useState(false);
const [showReceiveModal, setShowReceiveModal] = useState(false);
const [printBalance, setPrintBalance] = useState<string>('-');
const [printVestedBalance, setPrintVestedBalance] = useState<string | undefined>();
const userBalance = useGetBalance(clientDetails);
const navigate = useNavigate();
const { enqueueSnackbar } = useSnackbar();
const initFromRustState = async () => {
const stateJson = await getReactState();
if (stateJson) {
const state: RustState = JSON.parse(stateJson);
setNetwork(state.network);
setLoginType(state.loginType);
}
};
useEffect(() => {
initFromRustState();
}, []);
const keepState = async () => {
// add any state from this context to store in the Rust process
const state: RustState = {
network,
loginType,
};
setReactState(JSON.stringify(state));
};
const clearState = () => {
userBalance.clearAll();
setStoredAccounts(undefined);
setNetwork(undefined);
setError(undefined);
setIsLoading(false);
setMixnodeDetails(null);
};
const loadAccount = async (n: Network) => {
try {
const client = await selectNetwork(n);
setClientDetails(client);
} catch (e) {
enqueueSnackbar('Error loading account', { variant: 'error' });
Console.error(e as string);
}
};
const loadStoredAccounts = async () => {
const accounts = await listAccounts();
setStoredAccounts(accounts);
};
const getBondDetails = async () => {
setMixnodeDetails(null);
try {
const mixnode = await getMixnodeBondDetails();
setMixnodeDetails(mixnode);
} catch (e) {
Console.error(e as string);
}
};
const refreshAccount = async (_network: Network) => {
await loadAccount(_network);
if (loginType === 'password') {
await loadStoredAccounts();
}
};
const getModeFromStorage = async () => {
try {
const modeFromStorage = await forage.getItem({ key: 'nym-wallet-mode' })();
if (modeFromStorage) setMode(modeFromStorage);
} catch (e) {
Console.error(e);
}
};
const setModeInStorage = async (newMode: 'light' | 'dark') => {
await forage.setItem({
key: 'nym-wallet-mode',
value: newMode,
})();
};
useEffect(() => {
getVersion().then(setAppVersion);
getModeFromStorage();
}, []);
useEffect(() => {
if (!clientDetails) {
clearState();
navigate('/');
}
}, [clientDetails]);
useEffect(() => {
if (network) {
refreshAccount(network);
getEnv().then(setAppEnv);
}
}, [network]);
useEffect(() => {
const currency = clientDetails?.display_mix_denom.toUpperCase() || 'NYM';
if (userBalance.originalVesting) {
setPrintVestedBalance(`${toDisplay(userBalance.tokenAllocation?.spendableVestedCoins || 0)} ${currency}`);
}
if (userBalance?.balance?.amount) {
setPrintBalance(`${toDisplay(userBalance.balance.amount.amount)} ${currency}`);
} else {
setPrintBalance(`${toDisplay(0)} ${currency}`);
}
}, [userBalance, clientDetails]);
useEffect(() => {
let newValue = false;
if (network && appEnv?.ADMIN_ADDRESS && clientDetails?.client_address) {
try {
const adminAddressMap = JSON.parse(appEnv.ADMIN_ADDRESS);
const adminAddresses = adminAddressMap[network] || [];
if (adminAddresses.length) {
newValue = adminAddresses.includes(clientDetails?.client_address);
if (newValue) {
Console.log('Wallet is in admin mode: ', {
network,
adminAddress: adminAddressMap[network],
clientAddress: clientDetails?.client_address,
});
}
}
} catch (e) {
Console.error('Failed to check admin addresses', e);
}
}
setIsAdminAddress(newValue);
}, [appEnv, network, clientDetails?.client_address]);
const logIn = async ({ type, value }: { type: TLoginType; value: string }) => {
if (value.length === 0) {
setError(`A ${type} must be provided`);
return;
}
try {
setIsLoading(true);
if (type === 'mnemonic') {
await signInWithMnemonic(value);
setLoginType('mnemonic');
} else {
await signInWithPassword(value);
setLoginType('password');
}
setNetwork('MAINNET');
navigate('/balance');
} catch (e) {
setError(e as string);
} finally {
setIsLoading(false);
}
};
const logOut = async () => {
setIsLoading(true);
try {
await signOut();
await setReactState(undefined);
setClientDetails(undefined);
enqueueSnackbar('Successfully logged out', { variant: 'success' });
await createSignInWindow();
} finally {
setIsLoading(false);
}
};
const onAccountChange = async ({ accountId, password }: { accountId: string; password: string }) => {
if (network) {
setIsLoading(true);
try {
await switchAccount({ accountId, password });
await loadAccount(network);
enqueueSnackbar('Account switch success', { variant: 'success', preventDuplicate: true });
} catch (e) {
throw new Error(`Error swtiching account: ${e}`);
} finally {
setIsLoading(false);
}
}
};
const handleShowAdmin = () => setShowAdmin((show) => !show);
const handleShowTerminal = () => setShowTerminal((show) => !show);
const switchNetwork = (_network: Network) => setNetwork(_network);
const handleShowSendModal = () => setShowSendModal((show) => !show);
const handleShowReceiveModal = () => setShowReceiveModal((show) => !show);
const handleSwitchMode = () =>
setMode((currentMode) => {
const newMode = currentMode === 'light' ? 'dark' : 'light';
setModeInStorage(newMode);
return newMode;
});
const memoizedValue = useMemo(
() => ({
mode,
appEnv,
appVersion,
isAdminAddress,
isLoading,
error,
clientDetails,
storedAccounts,
mixnodeDetails,
userBalance,
showAdmin,
showTerminal,
network,
loginType,
setIsLoading,
setError,
signInWithPassword,
switchNetwork,
getBondDetails,
handleShowAdmin,
handleShowTerminal,
logIn,
logOut,
keepState,
onAccountChange,
showSendModal,
showReceiveModal,
handleShowSendModal,
handleShowReceiveModal,
handleSwitchMode,
printBalance,
printVestedBalance,
}),
[
appVersion,
loginType,
isAdminAddress,
mode,
appEnv,
isLoading,
error,
clientDetails,
mixnodeDetails,
userBalance,
showAdmin,
network,
storedAccounts,
showTerminal,
showSendModal,
showReceiveModal,
],
);
return <AppContext.Provider value={memoizedValue}>{children}</AppContext.Provider>;
};
@@ -0,0 +1,59 @@
import { useCallback, useContext, useEffect, useState } from 'react';
import { Console } from '../utils/console';
// eslint-disable-next-line import/no-cycle
import { AppContext } from '../context/main';
import { checkGatewayOwnership, checkMixnodeOwnership, getVestingPledgeInfo } from '../requests';
import { EnumNodeType, TNodeOwnership } from '../types';
const initial: TNodeOwnership = {
hasOwnership: false,
nodeType: undefined,
vestingPledge: undefined,
};
export const useCheckOwnership = () => {
const { clientDetails } = useContext(AppContext);
const [ownership, setOwnership] = useState<TNodeOwnership>(initial);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string>();
const checkOwnership = useCallback(async () => {
const status = { ...initial };
try {
const [ownsMixnode, ownsGateway] = await Promise.all([checkMixnodeOwnership(), checkGatewayOwnership()]);
if (ownsMixnode) {
status.hasOwnership = true;
status.nodeType = EnumNodeType.mixnode;
status.vestingPledge = await getVestingPledgeInfo({
address: clientDetails?.client_address!,
type: EnumNodeType.mixnode,
});
}
if (ownsGateway) {
status.hasOwnership = true;
status.nodeType = EnumNodeType.gateway;
status.vestingPledge = await getVestingPledgeInfo({
address: clientDetails?.client_address!,
type: EnumNodeType.gateway,
});
}
setOwnership(status);
} catch (e) {
Console.error(e as string);
setError(e as string);
setOwnership(initial);
} finally {
setIsLoading(false);
}
}, [clientDetails]);
useEffect(() => {
checkOwnership();
}, [clientDetails]);
return { isLoading, error, ownership, checkOwnership };
};
@@ -0,0 +1,147 @@
/* eslint-disable import/no-cycle */
import { useCallback, useEffect, useState } from 'react';
import { Account, Balance, DecCoin, OriginalVestingResponse, Period, VestingAccountInfo } from '@nymproject/types';
import {
getVestingCoins,
getVestedCoins,
getLockedCoins,
getSpendableCoins,
getOriginalVesting,
getCurrentVestingPeriod,
getVestingAccountInfo,
getSpendableRewardCoins,
getSpendableVestedCoins,
userBalance,
} from '../requests';
import { Console } from '../utils/console';
type TTokenAllocation = {
[key in
| 'vesting'
| 'vested'
| 'locked'
| 'spendable'
| 'spendableRewardCoins'
| 'spendableVestedCoins']: DecCoin['amount'];
};
export type TUseuserBalance = {
error?: string;
balance?: Balance;
tokenAllocation?: TTokenAllocation;
originalVesting?: OriginalVestingResponse;
currentVestingPeriod?: Period;
vestingAccountInfo?: VestingAccountInfo;
isLoading: boolean;
fetchBalance: () => Promise<void>;
fetchTokenAllocation: () => Promise<void>;
clearBalance: () => void;
clearAll: () => void;
refreshBalances: () => Promise<void>;
};
export const useGetBalance = (clientDetails?: Account): TUseuserBalance => {
const [balance, setBalance] = useState<Balance>();
const [error, setError] = useState<string>();
const [tokenAllocation, setTokenAllocation] = useState<TTokenAllocation>();
const [originalVesting, setOriginalVesting] = useState<OriginalVestingResponse>();
const [currentVestingPeriod, setCurrentVestingPeriod] = useState<Period>();
const [vestingAccountInfo, setVestingAccountInfo] = useState<VestingAccountInfo>();
const [isLoading, setIsLoading] = useState(false);
const clearBalance = () => setBalance(undefined);
const clearTokenAllocation = () => setTokenAllocation(undefined);
const clearOriginalVesting = () => setOriginalVesting(undefined);
const fetchTokenAllocation = async () => {
setIsLoading(true);
if (clientDetails?.client_address) {
try {
const [
originalVestingValue,
vestingCoins,
vestedCoins,
lockedCoins,
spendableCoins,
spendableVestedCoins,
spendableRewardCoins,
currentPeriod,
vestingAccountDetail,
] = await Promise.all([
getOriginalVesting(clientDetails?.client_address),
getVestingCoins(clientDetails?.client_address),
getVestedCoins(clientDetails?.client_address),
getLockedCoins(),
getSpendableCoins(),
getSpendableVestedCoins(),
getSpendableRewardCoins(),
getCurrentVestingPeriod(clientDetails?.client_address),
getVestingAccountInfo(clientDetails?.client_address),
]);
setOriginalVesting(originalVestingValue);
setCurrentVestingPeriod(currentPeriod);
setTokenAllocation({
vesting: vestingCoins.amount,
vested: vestedCoins.amount,
locked: lockedCoins.amount,
spendable: spendableCoins.amount,
spendableVestedCoins: spendableVestedCoins.amount,
spendableRewardCoins: spendableRewardCoins.amount,
});
setVestingAccountInfo(vestingAccountDetail);
} catch (e) {
clearTokenAllocation();
clearOriginalVesting();
Console.error(e as string);
}
}
setIsLoading(false);
};
const fetchBalance = useCallback(async () => {
setIsLoading(true);
setError(undefined);
try {
const bal = await userBalance();
setBalance(bal);
} catch (err) {
setError(err as string);
} finally {
setIsLoading(false);
}
}, []);
const clearAll = () => {
clearBalance();
clearTokenAllocation();
clearOriginalVesting();
};
const refreshBalances = async () => {
if (clientDetails?.client_address) {
await fetchBalance();
await fetchTokenAllocation();
} else {
clearAll();
}
};
useEffect(() => {
refreshBalances();
}, [clientDetails]);
return {
error,
isLoading,
balance,
tokenAllocation,
originalVesting,
currentVestingPeriod,
vestingAccountInfo,
fetchBalance,
clearBalance,
clearAll,
fetchTokenAllocation,
refreshBalances,
};
};
@@ -0,0 +1,49 @@
import { DecCoin, FeeDetails } from '@nymproject/types';
import { useState } from 'react';
import { Console } from '../utils/console';
import { getCustomFees } from '../requests';
export function useGetFee() {
const [fee, setFee] = useState<FeeDetails>();
const [isFeeLoading, setIsFeeLoading] = useState(false);
const [feeError, setFeeError] = useState<string>();
async function getFee<T>(operation: (args: T) => Promise<FeeDetails>, args: T) {
try {
setIsFeeLoading(true);
const simulatedFee = await operation(args);
setFee(simulatedFee);
} catch (e) {
// Console.error(e);
setFeeError(e as string);
}
setIsFeeLoading(false);
}
async function setFeeManually(amount: DecCoin) {
try {
setIsFeeLoading(true);
const fees = await getCustomFees({ feesAmount: amount });
setFee(fees);
} catch (e) {
Console.error(e);
setFeeError(e as string);
}
setIsFeeLoading(false);
}
const resetFeeState = () => {
setFee(undefined);
setIsFeeLoading(false);
setFeeError(undefined);
};
return {
fee,
isFeeLoading,
feeError,
getFee,
setFeeManually,
resetFeeState,
};
}
@@ -0,0 +1,29 @@
import { useState, useEffect } from 'react';
export function useKeyPress(targetKey: string): boolean {
// State for keeping track of whether key is pressed
const [keyPressed, setKeyPressed] = useState(false);
// If pressed key is our target key then set to true
function downHandler({ key }: { key: string }): void {
if (key === targetKey) {
setKeyPressed(true);
}
}
// If released key is our target key then set to false
const upHandler = ({ key }: { key: string }): void => {
if (key === targetKey) {
setKeyPressed(false);
}
};
// Add event listeners
useEffect(() => {
window.addEventListener('keydown', downHandler);
window.addEventListener('keyup', upHandler);
// Remove event listeners on cleanup
return () => {
window.removeEventListener('keydown', downHandler);
window.removeEventListener('keyup', upHandler);
};
}, []); // Empty array ensures that effect is only run on mount and unmount
return keyPressed;
}
@@ -0,0 +1,67 @@
import { Account, Balance, AccountEntry } from '@nymproject/types';
import { invokeWrapper } from './wrapper';
export const signInWithMnemonic = async (mnemonic: string): Promise<Account> =>
invokeWrapper('connect_with_mnemonic', { mnemonic });
export const userBalance = async () => invokeWrapper<Balance>('get_balance');
export const createMnemonic = async () => invokeWrapper<string>('create_new_mnemonic');
export const validateMnemonic = async (mnemonic: string) => invokeWrapper<boolean>('validate_mnemonic', { mnemonic });
export const signOut = async () => invokeWrapper<void>('logout');
export const isPasswordCreated = async () => invokeWrapper<boolean>('does_password_file_exist');
export const createPassword = async ({ mnemonic, password }: { mnemonic: string; password: string }) =>
invokeWrapper<void>('create_password', { mnemonic, password });
export const updatePassword = async ({
currentPassword,
newPassword,
}: {
currentPassword: string;
newPassword: string;
}) => invokeWrapper<void>('update_password', { currentPassword, newPassword });
export const signInWithPassword = async (password: string) =>
invokeWrapper<Account>('sign_in_with_password', { password });
export const switchAccount = async ({ accountId, password }: { accountId: string; password: string }) =>
invokeWrapper<Account>('sign_in_with_password_and_account_id', { accountId, password });
export const addAccount = async ({
mnemonic,
password,
accountName,
}: {
mnemonic: string;
password: string;
accountName: string;
}) => invokeWrapper<AccountEntry>('add_account_for_password', { mnemonic, password, accountId: accountName });
export const removeAccount = async ({ password, accountName }: { password: string; accountName: string }) =>
invokeWrapper<void>('remove_account_for_password', { password, innerId: accountName });
export const listAccounts = async () => invokeWrapper<AccountEntry[]>('list_accounts');
export const archiveWalletFile = async () => invokeWrapper<void>('archive_wallet_file');
export const showMnemonicForAccount = async ({ password, accountName }: { password: string; accountName: string }) =>
invokeWrapper<string>('show_mnemonic_for_account_in_password', { password, accountId: accountName });
export const renameAccount = async ({
password,
accountName,
newAccountName,
}: {
password: string;
accountName: string;
newAccountName: string;
}) =>
invokeWrapper<AccountEntry>('rename_account_for_password', {
password,
accountId: accountName,
newAccountId: newAccountName,
});
@@ -0,0 +1,54 @@
import {
Fee,
DecCoin,
SendTxResult,
TransactionExecuteResult,
MixNodeConfigUpdate,
MixNodeCostParams,
GatewayConfigUpdate,
} from '@nymproject/types';
import {
EnumNodeType,
TBondGatewayArgs,
TBondGatewaySignatureArgs,
TBondMixNodeArgs,
TBondMixnodeSignatureArgs,
TUpdateBondArgs,
} from '../types';
import { invokeWrapper } from './wrapper';
export const bondGateway = async (args: TBondGatewayArgs) =>
invokeWrapper<TransactionExecuteResult>('bond_gateway', args);
export const generateGatewayMsgPayload = async (args: Omit<TBondGatewaySignatureArgs, 'tokenPool'>) =>
invokeWrapper<string>('generate_gateway_bonding_msg_payload', args);
export const unbondGateway = async (fee?: Fee) => invokeWrapper<TransactionExecuteResult>('unbond_gateway', { fee });
export const bondMixNode = async (args: TBondMixNodeArgs) =>
invokeWrapper<TransactionExecuteResult>('bond_mixnode', args);
export const generateMixnodeMsgPayload = async (args: Omit<TBondMixnodeSignatureArgs, 'tokenPool'>) =>
invokeWrapper<string>('generate_mixnode_bonding_msg_payload', args);
export const unbondMixNode = async (fee?: Fee) => invokeWrapper<TransactionExecuteResult>('unbond_mixnode', { fee });
export const updateMixnodeCostParams = async (newCosts: MixNodeCostParams, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('update_mixnode_cost_params', { newCosts, fee });
export const updateMixnodeConfig = async (update: MixNodeConfigUpdate, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('update_mixnode_config', { update, fee });
export const updateGatewayConfig = async (update: GatewayConfigUpdate, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('update_gateway_config', { update, fee });
export const send = async (args: { amount: DecCoin; address: string; memo: string; fee?: Fee }) =>
invokeWrapper<SendTxResult>('send', args);
export const unbond = async (type: EnumNodeType) => {
if (type === EnumNodeType.mixnode) return unbondMixNode();
return unbondGateway();
};
export const updateBond = async (args: TUpdateBondArgs) =>
invokeWrapper<TransactionExecuteResult>('update_pledge', args);
@@ -0,0 +1,14 @@
import { invokeWrapper } from './wrapper';
import { AppVersion } from '../types/rust/AppVersion';
export const checkVersion = async () => invokeWrapper<AppVersion>('check_version');
export const createMainWindow = async (): Promise<void> => invokeWrapper<void>('create_main_window');
export const createSignInWindow = async (): Promise<void> => invokeWrapper<void>('create_auth_window');
export const setReactState = async (newState?: string): Promise<void> =>
invokeWrapper<void>('set_react_state', { newState });
export const getReactState = async (): Promise<string | undefined> =>
invokeWrapper<string | undefined>('get_react_state');
@@ -0,0 +1,7 @@
import { TauriContractStateParams } from '../types';
import { invokeWrapper } from './wrapper';
export const getContractParams = async () => invokeWrapper<TauriContractStateParams>('get_contract_settings');
export const setContractParams = async (params: TauriContractStateParams) =>
invokeWrapper<TauriContractStateParams>('update_contract_settings', { params });
@@ -0,0 +1,33 @@
import {
DelegationWithEverything,
DelegationsSummaryResponse,
TransactionExecuteResult,
DecCoin,
FeeDetails,
Fee,
} from '@nymproject/types';
import { invokeWrapper } from './wrapper';
export const getMixNodeDelegationsForCurrentAccount = async () =>
invokeWrapper<DelegationWithEverything[]>('get_all_mix_delegations');
export const getDelegationSummary = async () => invokeWrapper<DelegationsSummaryResponse>('get_delegation_summary');
export const undelegateFromMixnode = async (mixId: number, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('undelegate_from_mixnode', { mixId, fee });
export const undelegateAllFromMixnode = async (
mixId: number,
usesVestingContractTokens: boolean,
fee_liquid?: FeeDetails,
fee_vesting?: FeeDetails,
) =>
invokeWrapper<TransactionExecuteResult[]>('undelegate_all_from_mixnode', {
mixId,
usesVestingContractTokens,
fee_liquid,
fee_vesting,
});
export const delegateToMixnode = async (mixId: number, amount: DecCoin, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('delegate_to_mixnode', { mixId, amount, fee });
@@ -0,0 +1,14 @@
export * from './app';
export * from './account';
export * from './actions';
export * from './contract';
export * from './delegation';
export * from './logging';
export * from './network';
export * from './queries';
export * from './rewards';
export * from './signature';
export * from './simulate';
export * from './utils';
export * from './vesting';
export * from './pendingEvents';
@@ -0,0 +1,3 @@
import { invokeWrapper } from './wrapper';
export const helpLogToggleWindow = async () => invokeWrapper<void>('help_log_toggle_window', {});
@@ -0,0 +1,16 @@
import { Account } from '@nymproject/types';
import { Network } from '../types';
import { invokeWrapper } from './wrapper';
export const selectNetwork = async (network: Network) => invokeWrapper<Account>('switch_network', { network });
export const getSelectedValidatorUrl = async (network: Network) =>
invokeWrapper<string | null>('get_selected_nyxd_url', { network });
export const getDefaultValidatorUrl = async (network: Network) =>
invokeWrapper<string | null>('get_default_nyxd_url', { network });
export const setSelectedValidatorUrl = async (args: { network: Network; url: string }) =>
invokeWrapper<void>('select_nyxd_url', args);
export const resetValidatorUrl = async (network: Network) => invokeWrapper<void>('reset_nyxd_url', { network });
@@ -0,0 +1,4 @@
import { PendingEpochEvent } from '@nymproject/types';
import { invokeWrapper } from './wrapper';
export const getPendingEpochEvents = async () => invokeWrapper<PendingEpochEvent[]>('get_pending_epoch_events');
@@ -0,0 +1,63 @@
import {
DecCoin,
GatewayBond,
InclusionProbabilityResponse,
MixNodeDetails,
MixnodeStatusResponse,
PendingIntervalEvent,
RewardEstimationResponse,
StakeSaturationResponse,
WrappedDelegationEvent,
} from '@nymproject/types';
import { Interval, TGatewayReport, TNodeDescription } from '../types';
import { invokeWrapper } from './wrapper';
export const getAllPendingDelegations = async () =>
invokeWrapper<WrappedDelegationEvent[]>('get_pending_delegation_events');
export const getMixnodeBondDetails = async () => invokeWrapper<MixNodeDetails | null>('mixnode_bond_details');
export const getGatewayBondDetails = async () => invokeWrapper<GatewayBond | null>('gateway_bond_details');
export const getMixnodeAvgUptime = async () => invokeWrapper<number | null>('get_mixnode_avg_uptime');
export const getPendingOperatorRewards = async (address: string) =>
invokeWrapper<DecCoin>('get_pending_operator_rewards', { address });
export const getMixnodeStakeSaturation = async (mixId: number) =>
invokeWrapper<StakeSaturationResponse>('mixnode_stake_saturation', { mixId });
export const getMixnodeRewardEstimation = async (mixId: number) =>
invokeWrapper<RewardEstimationResponse>('mixnode_reward_estimation', { mixId });
export const getMixnodeStatus = async (mixId: number) =>
invokeWrapper<MixnodeStatusResponse>('mixnode_status', { mixId });
export const checkMixnodeOwnership = async () => invokeWrapper<boolean>('owns_mixnode');
export const checkGatewayOwnership = async () => invokeWrapper<boolean>('owns_gateway');
export const getInclusionProbability = async (mixId: number) =>
invokeWrapper<InclusionProbabilityResponse>('mixnode_inclusion_probability', { mixId });
export const getCurrentInterval = async () => invokeWrapper<Interval>('get_current_interval');
export const getNumberOfMixnodeDelegators = async (mixId: number) =>
invokeWrapper<number>('get_number_of_mixnode_delegators', { mixId });
export const getNodeDescription = async (host: string, port: number) =>
invokeWrapper<TNodeDescription>('get_mix_node_description', { host, port });
export const getPendingIntervalEvents = async () =>
invokeWrapper<PendingIntervalEvent[]>('get_pending_interval_events');
export const getGatewayReport = async (identity: string) =>
invokeWrapper<TGatewayReport>('gateway_report', { identity });
export const computeMixnodeRewardEstimation = async (args: {
mixId: number;
performance: string;
pledgeAmount: number;
totalDelegation: number;
profitMarginPercent: string;
intervalOperatingCost: { denom: 'unym'; amount: string };
}) => invokeWrapper<RewardEstimationResponse>('compute_mixnode_reward_estimation', args);
export const getMixnodeUptime = async (mixId: number) => invokeWrapper<number>('get_mixnode_uptime', { mixId });
@@ -0,0 +1,14 @@
import { Fee, FeeDetails, RewardingParams, TransactionExecuteResult } from '@nymproject/types';
import { invokeWrapper } from './wrapper';
export const claimOperatorReward = async (fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('claim_operator_reward', { fee });
export const claimDelegatorRewards = async (mixId: number, fee?: FeeDetails) =>
invokeWrapper<TransactionExecuteResult[]>('claim_locked_and_unlocked_delegator_reward', {
mixId,
fee: fee?.fee,
});
export const getCurrentRewardingParameter = async () =>
invokeWrapper<RewardingParams>('get_current_rewarding_parameters', {});
@@ -0,0 +1,9 @@
import { invokeWrapper } from './wrapper';
export const sign = async (message: string): Promise<string> => invokeWrapper<string>('sign', { message });
export const verify = async (
signatureAsHex: string,
message: string,
publicKeyAsJsonOrAccountAddress?: string | null,
): Promise<string> => invokeWrapper<string>('verify', { publicKeyAsJsonOrAccountAddress, signatureAsHex, message });
@@ -0,0 +1,87 @@
import {
FeeDetails,
DecCoin,
Gateway,
MixNodeCostParams,
MixNodeConfigUpdate,
GatewayConfigUpdate,
} from '@nymproject/types';
import { TBondGatewayArgs, TBondMixNodeArgs, TSimulateUpdateBondArgs } from '../types';
import { invokeWrapper } from './wrapper';
export const simulateBondGateway = async (args: TBondGatewayArgs) =>
invokeWrapper<FeeDetails>('simulate_bond_gateway', args);
export const simulateUnbondGateway = async (args: any) => invokeWrapper<FeeDetails>('simulate_unbond_gateway', args);
export const simulateBondMixnode = async (args: TBondMixNodeArgs) =>
invokeWrapper<FeeDetails>('simulate_bond_mixnode', args);
export const simulateUnbondMixnode = async (args: any) => invokeWrapper<FeeDetails>('simulate_unbond_mixnode', args);
export const simulateUpdateMixnodeCostParams = async (newCosts: MixNodeCostParams) =>
invokeWrapper<FeeDetails>('simulate_update_mixnode_cost_params', { newCosts });
export const simulateUpdateMixnodeConfig = async (update: MixNodeConfigUpdate) =>
invokeWrapper<FeeDetails>('simulate_update_mixnode_config', { update });
export const simulateUpdateGatewayConfig = async (update: GatewayConfigUpdate) =>
invokeWrapper<FeeDetails>('simulate_update_gateway_config', { update });
export const simulateDelegateToMixnode = async (args: { mixId: number; amount: DecCoin }) =>
invokeWrapper<FeeDetails>('simulate_delegate_to_mixnode', args);
export const simulateUndelegateFromMixnode = async (mixId: number) =>
invokeWrapper<FeeDetails>('simulate_undelegate_from_mixnode', { mixId });
export const simulateClaimDelegatorReward = async (mixId: number) =>
invokeWrapper<FeeDetails>('simulate_claim_delegator_reward', { mixId });
export const simulateVestingClaimDelegatorReward = async (mixId: number) =>
invokeWrapper<FeeDetails>('simulate_vesting_claim_delegator_reward', { mixId });
export const simulateVestingUndelegateFromMixnode = async (args: any) =>
invokeWrapper<FeeDetails>('simulate_vesting_undelegate_from_mixnode', args);
export const simulateVestingBondGateway = async (args: { gateway: Gateway; pledge: DecCoin; msgSignature: string }) =>
invokeWrapper<FeeDetails>('simulate_vesting_bond_gateway', args);
export const simulateVestingUnbondGateway = async (args: any) =>
invokeWrapper<FeeDetails>('simulate_vesting_unbond_gateway', args);
export const simulateVestingDelegateToMixnode = async (args: { mixId: number }) =>
invokeWrapper<FeeDetails>('simulate_vesting_delegate_to_mixnode', args);
export const simulateVestingBondMixnode = async (args: TBondMixNodeArgs) =>
invokeWrapper<FeeDetails>('simulate_vesting_bond_mixnode', args);
export const simulateVestingUnbondMixnode = async () => invokeWrapper<FeeDetails>('simulate_vesting_unbond_mixnode');
export const simulateVestingUpdateMixnodeCostParams = async (newCosts: MixNodeCostParams) =>
invokeWrapper<FeeDetails>('simulate_vesting_update_mixnode_cost_params', { newCosts });
export const simulateVestingUpdateMixnodeConfig = async (update: MixNodeConfigUpdate) =>
invokeWrapper<FeeDetails>('simulate_vesting_update_mixnode_config', { update });
export const simulateVestingUpdateGatewayConfig = async (update: GatewayConfigUpdate) =>
invokeWrapper<FeeDetails>('simulate_vesting_update_gateway_config', { update });
export const simulateWithdrawVestedCoins = async (args: any) =>
invokeWrapper<FeeDetails>('simulate_withdraw_vested_coins', args);
export const simulateSend = async ({ address, amount }: { address: string; amount: DecCoin }) =>
invokeWrapper<FeeDetails>('simulate_send', { address, amount });
export const getCustomFees = async ({ feesAmount }: { feesAmount: DecCoin }) =>
invokeWrapper<FeeDetails>('get_custom_fees', { feesAmount });
export const simulateClaimOperatorReward = async () => invokeWrapper<FeeDetails>('simulate_claim_operator_reward');
export const simulateVestingClaimOperatorReward = async () =>
invokeWrapper<FeeDetails>('simulate_vesting_claim_operator_reward');
export const simulateUpdateBond = async (args: TSimulateUpdateBondArgs) =>
invokeWrapper<FeeDetails>('simulate_update_pledge', args);
export const simulateVestingUpdateBond = async (args: TSimulateUpdateBondArgs) =>
invokeWrapper<FeeDetails>('simulate_vesting_update_pledge', args);
@@ -0,0 +1,11 @@
import { MixNodeCostParams } from '@nymproject/types';
import { AppEnv } from '../types';
import { invokeWrapper } from './wrapper';
export const getEnv = async () => invokeWrapper<AppEnv>('get_env');
export const tryConvertIdentityToMixId = async (mixIdentity: string) =>
invokeWrapper<number | null>('try_convert_pubkey_to_mix_id', { mixIdentity });
export const getDefaultMixnodeCostParams = async (profitMarginPercent: string) =>
invokeWrapper<MixNodeCostParams>('default_mixnode_cost_params', { profitMarginPercent });
@@ -0,0 +1,125 @@
import {
TNodeType,
Gateway,
DecCoin,
MixNode,
OriginalVestingResponse,
Period,
PledgeData,
TransactionExecuteResult,
VestingAccountInfo,
MixNodeCostParams,
MixNodeConfigUpdate,
GatewayConfigUpdate,
} from '@nymproject/types';
import { Fee } from '@nymproject/types/dist/types/rust/Fee';
import { invokeWrapper } from './wrapper';
import { TBondGatewaySignatureArgs, TBondMixnodeSignatureArgs, TUpdateBondArgs } from '../types';
export const getLockedCoins = async (): Promise<DecCoin> => invokeWrapper<DecCoin>('locked_coins');
export const getSpendableCoins = async (): Promise<DecCoin> => invokeWrapper<DecCoin>('spendable_coins');
export const getSpendableVestedCoins = async (): Promise<DecCoin> => invokeWrapper<DecCoin>('spendable_vested_coins');
export const getSpendableRewardCoins = async (): Promise<DecCoin> => invokeWrapper<DecCoin>('spendable_reward_coins');
export const getVestingCoins = async (vestingAccountAddress: string): Promise<DecCoin> =>
invokeWrapper<DecCoin>('vesting_coins', { vestingAccountAddress });
export const getVestedCoins = async (vestingAccountAddress: string): Promise<DecCoin> =>
invokeWrapper<DecCoin>('vested_coins', { vestingAccountAddress });
export const getOriginalVesting = async (vestingAccountAddress: string): Promise<OriginalVestingResponse> => {
const res = await invokeWrapper<OriginalVestingResponse>('original_vesting', { vestingAccountAddress });
return { ...res, amount: res.amount };
};
export const getCurrentVestingPeriod = async (address: string) =>
invokeWrapper<Period>('get_current_vesting_period', { address });
export const vestingBondGateway = async ({
gateway,
pledge,
msgSignature,
}: {
gateway: Gateway;
pledge: DecCoin;
msgSignature: string;
}) => invokeWrapper<TransactionExecuteResult>('vesting_bond_gateway', { gateway, msgSignature, pledge });
export const vestingGenerateGatewayMsgPayload = async (args: Omit<TBondGatewaySignatureArgs, 'tokenPool'>) =>
invokeWrapper<string>('vesting_generate_gateway_bonding_msg_payload', args);
export const vestingUnbondGateway = async (fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('vesting_unbond_gateway', { fee });
export const vestingBondMixNode = async ({
mixnode,
costParams,
pledge,
msgSignature,
}: {
mixnode: MixNode;
costParams: MixNodeCostParams;
pledge: DecCoin;
msgSignature: string;
}) => invokeWrapper<TransactionExecuteResult>('vesting_bond_mixnode', { mixnode, costParams, msgSignature, pledge });
export const vestingGenerateMixnodeMsgPayload = async (args: Omit<TBondMixnodeSignatureArgs, 'tokenPool'>) =>
invokeWrapper<string>('vesting_generate_mixnode_bonding_msg_payload', args);
export const vestingUnbondMixnode = async (fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('vesting_unbond_mixnode', { fee });
export const withdrawVestedCoins = async (amount: DecCoin, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('withdraw_vested_coins', { amount, fee });
export const vestingUpdateMixnodeCostParams = async (newCosts: MixNodeCostParams, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('vesting_update_mixnode_cost_params', { newCosts, fee });
export const vestingUpdateMixnodeConfig = async (update: MixNodeConfigUpdate, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('vesting_update_mixnode_config', { update, fee });
export const vestingUpdateGatewayConfig = async (update: GatewayConfigUpdate, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('vesting_update_gateway_config', { update, fee });
export const vestingDelegateToMixnode = async (mixId: number, amount: DecCoin, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('vesting_delegate_to_mixnode', { mixId, amount, fee });
export const vestingUndelegateFromMixnode = async (mixId: number) =>
invokeWrapper<TransactionExecuteResult>('vesting_undelegate_from_mixnode', { mixId });
export const getVestingAccountInfo = async (address: string) =>
invokeWrapper<VestingAccountInfo>('get_account_info', { address });
export const getVestingPledgeInfo = async ({
address,
type,
}: {
address?: string;
type: TNodeType;
}): Promise<PledgeData | undefined> => {
try {
return await invokeWrapper<PledgeData>(`vesting_get_${type}_pledge`, { address });
} catch (e) {
return undefined;
}
};
export const vestingDelegatedFree = async (vestingAccountAddress: string) =>
invokeWrapper<DecCoin>('delegated_free', { vestingAccountAddress });
export const vestingUnbond = async (type: TNodeType) => {
if (type === 'mixnode') return vestingUnbondMixnode();
return vestingUnbondGateway();
};
export const vestingClaimOperatorReward = async (fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('vesting_claim_operator_reward', { fee });
export const vestingClaimDelegatorRewards = async (mixId: number) =>
invokeWrapper<TransactionExecuteResult>('vesting_claim_delegator_reward', { mixId });
export const vestingUpdateBond = async (args: TUpdateBondArgs) =>
invokeWrapper<TransactionExecuteResult>('vesting_update_pledge', args);
@@ -0,0 +1,20 @@
import { invoke } from '@tauri-apps/api';
import { config } from '../config';
import { Console } from '../utils/console';
export async function invokeWrapper<T>(operationName: string, args?: any): Promise<T> {
const res = await invoke<T>(operationName, args);
if (config.LOG_TAURI_OPERATIONS) {
const argsToLog: any = {};
if (args) {
Object.keys(args).forEach((key) => {
// check if the key should be excluded from logs
if (!['mnemonic', 'password', 'currentPassword', 'newPassword'].includes(key)) {
argsToLog[key] = args[key];
}
});
}
Console.log({ operationName, argsToLog, res });
}
return res;
}
@@ -0,0 +1,97 @@
import { DecCoin, Gateway, MixNode, MixNodeCostParams, PledgeData } from '@nymproject/types';
import { Fee } from '@nymproject/types/dist/types/rust/Fee';
// eslint-disable-next-line import/no-cycle
// import { TBondedGateway, TBondedMixnode } from '../context';
export enum EnumNodeType {
mixnode = 'mixnode',
gateway = 'gateway',
}
export type TNodeOwnership = {
hasOwnership: boolean;
nodeType?: EnumNodeType;
vestingPledge?: PledgeData;
};
export type TPendingDelegation = {
block_height: number;
};
export type TDelegation = {
owner: string;
node_identity: string;
amount: DecCoin;
block_height: number;
proxy: string; // proxy address used to delegate the funds on behalf of another address
pending?: TPendingDelegation;
};
export type TBondGatewayArgs = {
gateway: Gateway;
pledge: DecCoin;
msgSignature: string;
fee?: Fee;
};
export type TBondMixNodeArgs = {
mixnode: MixNode;
costParams: MixNodeCostParams;
pledge: DecCoin;
msgSignature: string;
fee?: Fee;
};
export type TBondMixnodeSignatureArgs = {
mixnode: MixNode;
costParams: MixNodeCostParams;
pledge: DecCoin;
tokenPool: 'balance' | 'locked';
};
export type TBondGatewaySignatureArgs = {
gateway: Gateway;
pledge: DecCoin;
tokenPool: 'balance' | 'locked';
};
export type TUpdateBondArgs = {
currentPledge: DecCoin;
newPledge: DecCoin;
fee?: Fee;
};
export type TSimulateUpdateBondArgs = Omit<TUpdateBondArgs, 'fee'>;
export type TNodeDescription = {
name: string;
description: string;
link: string;
location: string;
};
export type TDelegateArgs = {
identity: string;
amount: DecCoin;
};
export type Period = 'Before' | { In: number } | 'After';
export type TAccount = {
name: string;
address: string;
mnemonic: string;
};
export type TGatewayReport = {
identity: string;
owner: string;
last_day: number;
last_hour: number;
most_recent: number;
};
// export const isMixnode = (node: TBondedMixnode | TBondedGateway): node is TBondedMixnode =>
// (node as TBondedMixnode).profitMargin !== undefined;
// export const isGateway = (node: TBondedMixnode | TBondedGateway): node is TBondedGateway => !isMixnode(node);
@@ -0,0 +1,7 @@
export * from './global';
export * from './rust/AppEnv';
export * from './rust/Interval';
export * from './rust/Network';
export * from './rust/StateParams';
export * from './rust/ValidatorUrl';
export * from './rust/ValidatorUrls';
@@ -0,0 +1,5 @@
export interface AppEnv {
ADMIN_ADDRESS: string | null;
SHOW_TERMINAL: string | null;
ENABLE_QA_MODE: string | null;
}
@@ -0,0 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface AppVersion {
current_version: string;
latest_version: string;
is_update_available: boolean;
}
@@ -0,0 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface Epoch {
id: number;
start: bigint;
end: bigint;
duration_seconds: bigint;
}
@@ -0,0 +1,8 @@
export interface Interval {
id: number;
epochs_in_interval: number;
current_epoch_start_unix: bigint;
current_epoch_id: number;
epoch_length_seconds: bigint;
total_elapsed_epochs: number;
}
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Network = 'QA' | 'SANDBOX' | 'MAINNET';
@@ -0,0 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DecCoin } from '@nymproject/types/src/types/rust/DecCoin';
export interface TauriContractStateParams {
minimum_mixnode_pledge: DecCoin;
minimum_gateway_pledge: DecCoin;
minimum_mixnode_delegation: DecCoin | null;
}
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface ValidatorUrl {
url: string;
name: string | null;
}
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ValidatorUrl } from './ValidatorUrl';
export interface ValidatorUrls {
urls: Array<ValidatorUrl>;
}
+1
View File
@@ -0,0 +1 @@
declare type FCWithChildren<P = {}> = React.FC<React.PropsWithChildren<P>>;
+9
View File
@@ -0,0 +1,9 @@
declare module '*.jpeg' {
const value: any;
export default value;
}
declare module '*.jpg' {
const value: any;
export default value;
}
+4
View File
@@ -0,0 +1,4 @@
declare module '*.json' {
const content: any;
export default content;
}
+4
View File
@@ -0,0 +1,4 @@
declare module '*.png' {
const content: any;
export default content;
}
+4
View File
@@ -0,0 +1,4 @@
declare module '*.svg' {
const content: any;
export default content;
}
@@ -0,0 +1,268 @@
import { appWindow } from '@tauri-apps/api/window';
import bs58 from 'bs58';
import Big from 'big.js';
import { valid } from 'semver';
import { add, format, fromUnixTime } from 'date-fns';
import { DecCoin, isValidRawCoin, MixNodeCostParams } from '@nymproject/types';
import { getCurrentInterval, getDefaultMixnodeCostParams, getLockedCoins, getSpendableCoins } from '../requests';
import { Console } from './console';
import { Network } from '../types';
export type TPoolOption = 'balance' | 'locked';
export const uNYMtoNYM = (unym: string, rounding = 6) => {
const nym = Big(unym).div(1000000).toFixed(rounding);
return {
asString: () => nym,
asNumber: () => Number(nym),
};
};
// export const checkHasEnoughFunds = async (balance: string, allocationValue: string): Promise<boolean> => {
// try {
// // const walletValue = await userBalance();
// const remainingBalance = +balance - +allocationValue;
// return remainingBalance >= 0;
// } catch (e) {
// Console.log(e as string);
// return false;
// }
// };
export const checkHasEnoughLockedTokens = async (allocationValue: string) => {
try {
const lockedTokens = await getLockedCoins();
const spendableTokens = await getSpendableCoins();
const remainingBalance = +lockedTokens.amount + +spendableTokens.amount - +allocationValue;
return remainingBalance >= 0;
} catch (e) {
Console.error(e as string);
}
return false;
};
export const checkTokenBalance = (tokenPool: TPoolOption, amount: string, balance: string) => {
let hasEnoughFunds = false;
// if (tokenPool === 'locked') {
// hasEnoughFunds = await checkHasEnoughLockedTokens(amount);
// }
if (tokenPool === 'balance') {
const remainingBalance = +balance - +amount;
hasEnoughFunds = remainingBalance >= 0;
}
return hasEnoughFunds;
};
export const validateKey = (key: string, bytesLength: number): boolean => {
// it must be a valid base58 key
try {
const bytes = bs58.decode(key);
// of length 32
return bytes.length === bytesLength;
} catch (e) {
Console.error(e as string);
return false;
}
};
export const validateAmount = async (
majorAmountAsString: DecCoin['amount'],
minimumAmountAsString: DecCoin['amount'],
): Promise<boolean> => {
// tests basic coin value requirements, like no more than 6 decimal places, value lower than total supply, etc
if (!Number(majorAmountAsString)) {
return false;
}
if (!isValidRawCoin(majorAmountAsString)) {
return false;
}
const majorValueFloat = parseInt(majorAmountAsString, Number(10));
return majorValueFloat >= parseInt(minimumAmountAsString, Number(10));
// this conversion seems really iffy but I'm not sure how to better approach it
};
export const isValidHostname = (value: string) => {
// regex for ipv4 and ipv6 and hhostname- source http://jsfiddle.net/DanielD/8S4nq/
const hostnameRegex =
/((^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))|(^\s*((?=.{1,255}$)(?=.*[A-Za-z].*)[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?)*)\s*$)/;
return hostnameRegex.test(value);
};
export const validateVersion = (version: string): boolean => {
try {
return valid(version) !== null;
} catch (e) {
return false;
}
};
export const validateLocation = (location: string): boolean => {
const locationRegex = /^[a-z]+$/i;
return locationRegex.test(location);
};
export const validateRawPort = (rawPort: number): boolean => !Number.isNaN(rawPort) && rawPort >= 1 && rawPort <= 65535;
export const truncate = (text: string, trim: number) => `${text.substring(0, trim)}...`;
export const isGreaterThan = (a: number, b: number) => a > b;
export const isLessThan = (a: number, b: number) => a < b;
export const randomNumberBetween = (min: number, max: number) => {
const minCeil = Math.ceil(min);
const maxFloor = Math.floor(max);
return Math.floor(Math.random() * (maxFloor - minCeil + 1) + minCeil);
};
export const splice = (size: number, address?: string): string => {
if (address) {
return `${address.slice(0, size)}...${address.slice(-size)}`;
}
return '';
};
export const maximizeWindow = async () => {
await appWindow.maximize();
};
export function removeObjectDuplicates<T extends object, K extends keyof T>(arr: T[], id: K) {
return arr.filter((v, i, a) => a.findIndex((v2) => v2[id] === v[id]) === i);
}
export const isDecimal = (value: number) => value - Math.floor(value) !== 0;
export const attachDefaultOperatingCost = async (profitMarginPercent: string): Promise<MixNodeCostParams> =>
getDefaultMixnodeCostParams(profitMarginPercent);
/**
* Converts a stringified percentage integer (0-100) to a stringified float (0.0-1.0).
*
* @param value - the percentage to convert
* @returns A stringified float
*/
export const toPercentFloatString = (value: string) => (Number(value) / 100).toString();
/**
* Converts a stringified percentage float (0.0-1.0) to a stringified integer (0-100).
*
* @param value - the percentage to convert
* @returns A stringified integer
*/
export const toPercentIntegerString = (value: string) => Math.round(Number(value) * 100).toString();
/**
* Converts a decimal number to a pretty representation
* with fixed decimal places.
*
* @param val - a decimal number of string form
* @param dp - number of decimal places (4 by default ie. 0.0000)
* @returns A prettified decimal number
*/
export const toDisplay = (val: string | number | Big, dp = 4) => {
let displayValue;
try {
displayValue = Big(val).toFixed(dp);
} catch (e: any) {
Console.warn(`${displayValue} not a valid decimal number: ${e}`);
}
return displayValue;
};
/**
* Takes a DecCoin and prettify its amount to a representation
* with fixed decimal places.
*
* @param coin - a DecCoin
* @param dp - number of decimal places to apply to amount (4 by default ie. 0.0000)
* @returns A DecCoin with prettified amount
*/
export const decCoinToDisplay = (coin: DecCoin, dp = 4) => {
const displayCoin = { ...coin };
try {
displayCoin.amount = Big(coin.amount).toFixed(dp);
} catch (e: any) {
Console.warn(`${coin.amount} not a valid decimal number: ${e}`);
}
return displayCoin;
};
/**
* Converts a decimal number of μNYM (micro NYM) to NYM.
*
* @param unym - string representation of a decimal number of μNYM
* @param dp - number of decimal places (4 by default ie. 0.0000)
* @returns The corresponding decimal number in NYM
*/
export const unymToNym = (unym: string | Big, dp = 4) => {
let nym;
try {
nym = Big(unym).div(1_000_000).toFixed(dp);
} catch (e: any) {
Console.warn(`${unym} not a valid decimal number: ${e}`);
}
return nym;
};
/**
*
* Checks if the user's balance is enough to pay the fee
* @param balance - The user's current balance
* @param fee - The fee for the tx
* @param tx - The amount of the tx
* @returns boolean
*
*/
export const isBalanceEnough = (fee: string, tx: string = '0', balance: string = '0') => {
console.log('balance', balance, fee, tx);
try {
return Big(balance).gte(Big(fee).plus(Big(tx)));
} catch (e) {
console.log(e);
return false;
}
};
export const getIntervalAsDate = async () => {
const interval = await getCurrentInterval();
const secondsToNextInterval =
Number(interval.epochs_in_interval - interval.current_epoch_id) * Number(interval.epoch_length_seconds);
const nextInterval = format(
add(new Date(), {
seconds: secondsToNextInterval,
}),
'dd/MM/yyyy, HH:mm',
);
const nextEpoch = format(
add(fromUnixTime(Number(interval.current_epoch_start_unix)), {
seconds: Number(interval.epoch_length_seconds),
}),
'HH:mm',
);
return { nextEpoch, nextInterval };
};
export const urls = (networkName?: Network) =>
networkName === 'MAINNET'
? {
mixnetExplorer: 'https://mixnet.explorers.guru/',
blockExplorer: 'https://blocks.nymtech.net',
networkExplorer: 'https://explorer.nymtech.net',
}
: {
blockExplorer: `https://${networkName}-blocks.nymtech.net`,
networkExplorer: `https://${networkName}-explorer.nymtech.net`,
};
@@ -0,0 +1,10 @@
/* eslint-disable no-console */
import { config } from '../config';
export const Console = {
log: (message?: any, ...optionalParams: any[]) =>
config.IS_DEV_MODE ? console.log(message, ...optionalParams) : undefined,
warn: (message?: any, ...optionalParams: any[]) =>
config.IS_DEV_MODE ? console.warn(message, ...optionalParams) : undefined,
error: (message?: any, ...optionalParams: any[]) => console.error(message, ...optionalParams),
};
@@ -0,0 +1,22 @@
import { Console } from './console';
export type TauriReq<Req extends Function & ((a: any, b?: any) => Promise<any>)> = {
name: Req['name'];
request: () => ReturnType<Req>;
onFulfilled: (value: Awaited<ReturnType<Req>>) => void;
};
async function fireRequests(requests: TauriReq<any>[]) {
const promises = await Promise.allSettled(requests.map((r) => r.request()));
promises.forEach((res, index) => {
if (res.status === 'rejected') {
Console.warn(`${requests[index].name} request fails`, res.reason);
}
if (res.status === 'fulfilled') {
requests[index].onFulfilled(res.value as any);
}
});
}
export default fireRequests;
@@ -0,0 +1,4 @@
export * from './common';
export * from './fireRequests';
export * from './console';
export { default as fireRequests } from './fireRequests';
@@ -0,0 +1,3 @@
export const sleep = (delayMilliseconds: number) =>
// eslint-disable-next-line no-promise-executor-return
new Promise((resolve) => setTimeout(resolve, delayMilliseconds));
@@ -16,6 +16,7 @@ export const MixNodeDetailSection: FCWithChildren<MixNodeDetailProps> = ({ mixNo
const palette = [theme.palette.text.primary];
const isMobile = useIsMobile();
const statusText = React.useMemo(() => getMixNodeStatusText(mixNodeRow.status), [mixNodeRow.status]);
console.log('mixNodeRow :>> ', mixNodeRow);
return (
<Grid container>
<Grid item xs={12} md={6}>
+44
View File
@@ -24,6 +24,13 @@ import { Socials } from './Socials';
import { Footer } from './Footer';
import { DarkLightSwitchDesktop } from './Switch';
import { NavOptionType } from '../context/nav';
import ConnectKeplrWallet from './ConnectKeplrWallet';
import { assets, chains } from 'chain-registry';
import { ChainProvider } from '@cosmos-kit/react';
import { wallets as keplr } from '@cosmos-kit/keplr';
import { useMemo } from 'react';
import '@interchain-ui/react/styles';
const drawerWidth = 255;
const bannerHeight = 80;
@@ -272,6 +279,33 @@ export const Nav: FCWithChildren = ({ children }) => {
}
};
const assetsFixedUp = useMemo(() => {
const nyx = assets.find((a) => a.chain_name === 'nyx');
if (nyx) {
const nyxCoin = nyx.assets.find((a) => a.name === 'nyx');
if (nyxCoin) {
nyxCoin.coingecko_id = 'nyx';
}
nyx.assets = nyx.assets.reverse();
}
return assets;
}, [assets]);
const chainsFixedUp = useMemo(() => {
const nyx = chains.find((c) => c.chain_id === 'nyx');
if (nyx) {
if (!nyx.staking) {
nyx.staking = {
staking_tokens: [{ denom: 'unyx' }],
lock_duration: {
blocks: 10000,
},
};
}
}
return chains;
}, [chains]);
return (
<Box sx={{ display: 'flex' }}>
<AppBar
@@ -341,6 +375,16 @@ export const Nav: FCWithChildren = ({ children }) => {
alignItems: 'center',
}}
>
<ChainProvider
chains={chainsFixedUp}
assetLists={assetsFixedUp}
wallets={[...keplr]}
signerOptions={{
preferredSignType: () => 'amino',
}}
>
<ConnectKeplrWallet />
</ChainProvider>
<Socials />
<DarkLightSwitchDesktop defaultChecked />
</Box>
+96 -2
View File
@@ -1,8 +1,13 @@
import * as React from 'react';
import { Box, TextField, MenuItem, FormControl } from '@mui/material';
import React, { FC, useContext, useEffect, useState, useMemo } from 'react';
import { Box, TextField, MenuItem, FormControl, Button } from '@mui/material';
import Select, { SelectChangeEvent } from '@mui/material/Select';
import { Filters } from './Filters/Filters';
import { useIsMobile } from '../hooks/useIsMobile';
import { DelegateModal } from './Delegations/components/DelegateModal';
import { ChainProvider } from '@cosmos-kit/react';
import { assets, chains } from 'chain-registry';
import { wallets as keplr } from '@cosmos-kit/keplr';
import { DelegationModal } from './Delegations/components/DelegationModal';
const fieldsHeight = '42.25px';
@@ -15,6 +20,17 @@ type TableToolBarProps = {
childrenBefore?: React.ReactNode;
childrenAfter?: React.ReactNode;
};
type ActionType = 'delegate' | 'undelegate' | 'redeem' | 'redeem-all' | 'compound';
type DelegationModalProps = {
status: 'loading' | 'success' | 'error';
action: ActionType;
message?: string;
transactions?: {
url: string;
hash: string;
}[];
};
export const TableToolbar: FCWithChildren<TableToolBarProps> = ({
searchTerm,
@@ -25,6 +41,42 @@ export const TableToolbar: FCWithChildren<TableToolBarProps> = ({
childrenAfter,
withFilters,
}) => {
const [showNewDelegationModal, setShowNewDelegationModal] = useState<boolean>(false);
const [confirmationModalProps, setConfirmationModalProps] = useState<DelegationModalProps | undefined>();
const assetsFixedUp = useMemo(() => {
const nyx = assets.find((a) => a.chain_name === 'nyx');
if (nyx) {
const nyxCoin = nyx.assets.find((a) => a.name === 'nyx');
if (nyxCoin) {
nyxCoin.coingecko_id = 'nyx';
}
nyx.assets = nyx.assets.reverse();
}
return assets;
}, [assets]);
const chainsFixedUp = useMemo(() => {
const nyx = chains.find((c) => c.chain_id === 'nyx');
if (nyx) {
if (!nyx.staking) {
nyx.staking = {
staking_tokens: [{ denom: 'unyx' }],
lock_duration: {
blocks: 10000,
},
};
if (nyx.apis) nyx.apis.rpc = [{ address: 'https://rpc.nymtech.net', provider: 'nym' }];
}
}
return chains;
}, [chains]);
const handleNewDelegation = (delegationModalProps: DelegationModalProps) => {
setShowNewDelegationModal(false);
setConfirmationModalProps(delegationModalProps);
};
const isMobile = useIsMobile();
return (
<Box
@@ -77,6 +129,7 @@ export const TableToolbar: FCWithChildren<TableToolBarProps> = ({
/>
)}
</Box>
<Box
sx={{
display: 'flex',
@@ -86,9 +139,50 @@ export const TableToolbar: FCWithChildren<TableToolBarProps> = ({
marginTop: isMobile ? 2 : 0,
}}
>
<Button
size="large"
variant="contained"
disableElevation
onClick={() => setShowNewDelegationModal(true)}
sx={{ px: 5, color: 'primary.contrastText' }}
>
Delegate
</Button>
{withFilters && <Filters />}
{childrenAfter}
</Box>
{showNewDelegationModal && (
<ChainProvider
chains={chainsFixedUp}
assetLists={assetsFixedUp}
wallets={[...keplr]}
signerOptions={{
preferredSignType: () => 'amino',
}}
>
<DelegateModal
open={showNewDelegationModal}
onClose={() => setShowNewDelegationModal(false)}
header="Delegate"
buttonText="Delegate stake"
denom={'nym'} // clientDetails?.display_mix_denom || 'nym'}
onOk={(delegationModalProps: DelegationModalProps) => handleNewDelegation(delegationModalProps)}
// accountBalance={balance?.printable_balance}
/>
</ChainProvider>
)}
{confirmationModalProps && (
<DelegationModal
{...confirmationModalProps}
open={Boolean(confirmationModalProps)}
onClose={async () => {
setConfirmationModalProps(undefined);
// await fetchBalance();
}}
/>
)}
</Box>
);
};
+23
View File
@@ -0,0 +1,23 @@
import * as React from 'react';
import { useTheme } from '@mui/material/styles';
export const DelegateSVG: FCWithChildren = () => {
const theme = useTheme();
const color = theme.palette.text.primary;
return (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M2.6665 7.99967V9.99967H3.99984V7.99967H2.6665ZM10.6665 4.66634L9.7265 3.72634L8.6665 4.77967V1.33301H7.33317V4.79301L6.25984 3.73967L5.33317 4.66634L7.99984 7.33301L10.6665 4.66634ZM2.6665 11.333H13.3332V9.99967H2.6665V11.333Z"
fill="white"
/>
<path
d="M13.3332 13.6663C13.3332 14.2186 12.8855 14.6663 12.3332 14.6663H3.6665C3.11422 14.6663 2.6665 14.2186 2.6665 13.6663V13.333H13.3332V13.6663Z"
fill="white"
/>
<rect x="12" y="8" width="1.33333" height="2" fill="white" />
<rect x="12" y="11.333" width="1.33333" height="2" fill="white" />
<rect x="2.6665" y="11.333" width="1.33333" height="2" fill="white" />
</svg>
);
};
+26
View File
@@ -0,0 +1,26 @@
import * as React from 'react';
import { useTheme } from '@mui/material/styles';
export const ElipsSVG: FCWithChildren = () => {
const theme = useTheme();
const color = theme.palette.text.primary;
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" viewBox="0 0 24 25" fill="none">
<circle cx="12" cy="12.5" r="12" fill="url(#paint0_angular_2549_7570)" />
<defs>
<radialGradient
id="paint0_angular_2549_7570"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(12 12.5) rotate(90) scale(12)"
>
<stop stop-color="#22D27E" />
<stop offset="1" stop-color="#9002FF" />
</radialGradient>
</defs>
</svg>
);
};
+27
View File
@@ -0,0 +1,27 @@
import * as React from 'react';
import { useTheme } from '@mui/material/styles';
export const TokenSVG: FCWithChildren = () => {
const theme = useTheme();
const color = 'white';
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" viewBox="0 0 24 25" fill="none">
<g clip-path="url(#clip0_2549_7563)">
<path
d="M20.4841 4.01607C15.8041 -0.67593 8.19607 -0.67593 3.51607 4.01607C-1.17593 8.70807 -1.17593 16.3041 3.51607 20.9841C8.20807 25.6761 15.8041 25.6761 20.4841 20.9841C25.1761 16.3041 25.1761 8.69607 20.4841 4.01607ZM19.4521 19.9521C15.3361 24.0681 8.65207 24.0681 4.53607 19.9521C0.42007 15.8361 0.42007 9.15207 4.53607 5.03607C8.65207 0.92007 15.3361 0.92007 19.4521 5.03607C23.5801 9.16407 23.5801 15.8361 19.4521 19.9521Z"
fill={color}
/>
<path
d="M18.48 19.4965V5.50447C17.868 4.92847 17.184 4.42447 16.452 4.02847V17.4085L7.62002 3.98047C6.85202 4.38847 6.14402 4.89247 5.52002 5.49247V19.4965C6.13202 20.0725 6.81602 20.5765 7.54802 20.9725V7.59247L16.38 21.0205C17.148 20.6125 17.856 20.0965 18.48 19.4965Z"
fill={color}
/>
</g>
<defs>
<clipPath id="clip0_2549_7563">
<rect width="24" height="24" fill="white" transform="translate(0 0.5)" />
</clipPath>
</defs>
</svg>
);
};
+109 -2
View File
@@ -19,6 +19,13 @@ import { splice } from '../../utils';
import { getMixNodeStatusColor } from '../../components/MixNodes/Status';
import { MixNodeStatusDropdown } from '../../components/MixNodes/StatusDropdown';
import { Tooltip } from '../../components/Tooltip';
import { DelegateIconButton } from '../../components/Delegations/components/DelegateIconButton';
import { ChainProvider } from '@cosmos-kit/react';
import { assets, chains } from 'chain-registry';
import { wallets as keplr } from '@cosmos-kit/keplr';
import { DelegationModal, DelegationModalProps } from '../../components/Delegations/components/DelegationModal';
import { useMemo, useState } from 'react';
import { DelegateModal } from '../../components/Delegations/components/DelegateModal';
const getCellFontStyle = (theme: Theme, row: MixnodeRowType, textColor?: string) => {
const color = textColor || getMixNodeStatusColor(theme, row.status);
@@ -40,10 +47,59 @@ export const PageMixnodes: FCWithChildren = () => {
const [filteredMixnodes, setFilteredMixnodes] = React.useState<MixNodeResponse>([]);
const [pageSize, setPageSize] = React.useState<string>('10');
const [searchTerm, setSearchTerm] = React.useState<string>('');
const [mixId, setMixId] = useState<number | undefined>();
const [identityKey, setIdentityKey] = useState<string | undefined>();
const [showNewDelegationModal, setShowNewDelegationModal] = useState<boolean>(false);
const [confirmationModalProps, setConfirmationModalProps] = useState<DelegationModalProps | undefined>();
const theme = useTheme();
const { status } = useParams<{ status: MixnodeStatusWithAll | undefined }>();
const navigate = useNavigate();
const assetsFixedUp = useMemo(() => {
const nyx = assets.find((a) => a.chain_name === 'nyx');
if (nyx) {
const nyxCoin = nyx.assets.find((a) => a.name === 'nyx');
if (nyxCoin) {
nyxCoin.coingecko_id = 'nyx';
}
nyx.assets = nyx.assets.reverse();
}
return assets;
}, [assets]);
const chainsFixedUp = useMemo(() => {
const nyx = chains.find((c) => c.chain_id === 'nyx');
if (nyx) {
if (!nyx.staking) {
nyx.staking = {
staking_tokens: [{ denom: 'unyx' }],
lock_duration: {
blocks: 10000,
},
};
if (nyx.apis) nyx.apis.rpc = [{ address: 'https://rpc.nymtech.net', provider: 'nym' }];
}
}
return chains;
}, [chains]);
3;
const openDelegationModal = (identityKey: string, mixId: number) => {
setMixId(mixId);
setIdentityKey(identityKey);
};
React.useEffect(() => {
if (identityKey && mixId) {
setShowNewDelegationModal(true);
}
}, [identityKey, mixId]);
const handleNewDelegation = (delegationModalProps: DelegationModalProps) => {
setShowNewDelegationModal(false);
setConfirmationModalProps(delegationModalProps);
};
const handleSearch = (str: string) => {
setSearchTerm(str.toLowerCase());
};
@@ -88,7 +144,7 @@ export const PageMixnodes: FCWithChildren = () => {
disableColumnMenu: true,
renderHeader: () => <CustomColumnHeading headingTitle="Mix ID" />,
headerClassName: 'MuiDataGrid-header-override',
width: 100,
width: 70,
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
@@ -107,10 +163,27 @@ export const PageMixnodes: FCWithChildren = () => {
disableColumnMenu: true,
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
headerClassName: 'MuiDataGrid-header-override',
width: 170,
width: 190,
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<>
{/* <ChainProvider
chains={chainsFixedUp}
assetLists={assetsFixedUp}
wallets={[...keplr]}
signerOptions={{
preferredSignType: () => 'amino',
}}
>
<DelegateIconButton
sx={{ ...getCellFontStyle(theme, params.row), marginRight: 1 }}
// tooltip={walletConnected ? undefined : 'Connect your wallet to delegate'}
onDelegate={() => {
openDelegationModal(params.value, params.row.mix_id);
console.log('object :>> ', params.value, params.row.mix_id);
}}
/>
</ChainProvider> */}
<CopyToClipboard
sx={{ ...getCellFontStyle(theme, params.row), mr: 1 }}
value={params.value}
@@ -363,6 +436,40 @@ export const PageMixnodes: FCWithChildren = () => {
</Card>
</Grid>
</Grid>
{showNewDelegationModal && (
<ChainProvider
chains={chainsFixedUp}
assetLists={assetsFixedUp}
wallets={[...keplr]}
signerOptions={{
preferredSignType: () => 'amino',
}}
>
<DelegateModal
open={showNewDelegationModal}
onClose={() => setShowNewDelegationModal(false)}
header="Delegate"
buttonText="Delegate stake"
denom={'nym'} // clientDetails?.display_mix_denom || 'nym'}
onOk={(delegationModalProps: DelegationModalProps) => handleNewDelegation(delegationModalProps)}
// accountBalance={balance?.printable_balance}
initialIdentityKey={identityKey}
initialMixId={mixId}
/>
</ChainProvider>
)}
{confirmationModalProps && (
<DelegationModal
{...confirmationModalProps}
open={Boolean(confirmationModalProps)}
onClose={async () => {
setConfirmationModalProps(undefined);
// await fetchBalance();
}}
/>
)}
</>
);
};
+2 -1
View File
@@ -2,7 +2,8 @@
"extends": "../ts-packages/tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"outDir": "./dist"
"outDir": "./dist",
"module": "CommonJS",
},
"include": [
"./src/**/*.ts",
+3
View File
@@ -36,6 +36,9 @@ module.exports = mergeWithRules({
// this can be included automatically by the dev server, however build mode fails if missing
new webpack.HotModuleReplacementPlugin(),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
],
target: 'web',
File diff suppressed because it is too large Load Diff
@@ -25,6 +25,7 @@ export const Mixnodes = () => {
setBusy(true);
const client = await getClient();
const { nodes } = await client.getMixNodesDetailed({});
setMixnodes(nodes);
setBusy(false);
};
+3228 -1025
View File
File diff suppressed because it is too large Load Diff