Connect delegate modal to keplr balance

WIP

WIP

WIP

WIP
This commit is contained in:
Yana
2023-11-07 13:02:00 +00:00
committed by fmtabbara
parent f2223dddca
commit 081fec72e5
57 changed files with 2202 additions and 117 deletions
+14 -5
View File
@@ -39,8 +39,17 @@ export const trimAddress = (address = '', trimBy = 6) => {
};
export default function ConnectKeplrWallet() {
const { username, connect, disconnect, wallet, openView, address, getCosmWasmClient, isWalletConnected } =
useChain('nyx');
const {
username,
connect,
disconnect,
wallet,
openView,
address,
getCosmWasmClient,
isWalletConnected,
isWalletConnecting,
} = useChain('nyx');
const isClient = useIsClient();
const theme = useTheme();
@@ -70,9 +79,9 @@ export default function ConnectKeplrWallet() {
if (!isClient) return null;
const getGlobalbutton = () => {
// if (globalStatus === 'Connecting') {
// return <Button onClick={() => connect()}>{`Connecting ${wallet?.prettyName}`}</Button>;
// }
if (isWalletConnecting) {
return <Button onClick={() => connect()}>{`Connecting ${wallet?.prettyName}`}</Button>;
}
if (isWalletConnected) {
return (
<Box display={'flex'} alignItems={'center'} gap={2}>
@@ -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>
);
};
@@ -1,24 +1,22 @@
import React, { useCallback, useContext, useState } from 'react';
import React, { useCallback, useContext, useState, useEffect } from 'react';
import { Box, Typography, SxProps } from '@mui/material';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
import { CurrencyDenom, FeeDetails, DecCoin, decimalToFloatApproximation } from '@nymproject/types';
// import { Console } from 'src/utils/console';
// import { useGetFee } from 'src/hooks/useGetFee';
// import { simulateDelegateToMixnode, simulateVestingDelegateToMixnode, tryConvertIdentityToMixId } from 'src/requests';
import { Console } from '../utils/console';
import { useGetFee } from '../hooks/useGetFee';
import { debounce } from 'lodash';
import { SimpleModal } from './SimpleModal';
import { ModalListItem } from './ModalListItem';
// import { AppContext } from 'src/context';
// import { SimpleModal } from '../Modals/SimpleModal';
// import { ModalListItem } from '../Modals/ModalListItem';
// import { checkTokenBalance, validateAmount, validateKey } from '../../utils';
// import { TokenPoolSelector, TPoolOption } from '../TokenPoolSelector';
// import { ConfirmTx } from '../ConfirmTX';
// import { getMixnodeStakeSaturation } from '../../requests';
// import { ErrorModal } from '../Modals/ErrorModal';
// import { BalanceWarning } from '../FeeWarning';
import { TPoolOption, checkTokenBalance, validateAmount, validateKey } from '../utils';
import { useChain } from '@cosmos-kit/react';
import { uNYMtoNYM } from '../utils';
import { ErrorModal } from './ErrorModal';
import { ConfirmTx } from './ConfirmTX';
import { BalanceWarning } from './FeeWarning';
import { getMixnodeStakeSaturation, simulateDelegateToMixnode, tryConvertIdentityToMixId } from '../requests';
const MIN_AMOUNT_TO_DELEGATE = 10;
@@ -29,8 +27,8 @@ export const DelegateModal: FCWithChildren<{
mixId: number,
identityKey: string,
amount: DecCoin,
// tokenPool: TPoolOption,
// fee?: FeeDetails,
tokenPool: TPoolOption,
fee?: FeeDetails,
) => Promise<void>;
identityKey?: string;
onIdentityKeyChanged?: (identityKey: string) => void;
@@ -38,7 +36,7 @@ export const DelegateModal: FCWithChildren<{
header?: string;
buttonText?: string;
rewardInterval: string;
accountBalance?: string;
// accountBalance?: string;
estimatedReward?: number;
profitMarginPercentage?: string | null;
nodeUptimePercentage?: number | null;
@@ -57,7 +55,7 @@ export const DelegateModal: FCWithChildren<{
buttonText,
identityKey: initialIdentityKey,
rewardInterval,
accountBalance,
// accountBalance,
estimatedReward,
denom,
profitMarginPercentage,
@@ -76,47 +74,45 @@ export const DelegateModal: FCWithChildren<{
const [errorIdentityKey, setErrorIdentityKey] = useState<string>();
const [mixIdError, setMixIdError] = useState<string>();
// const { fee, getFee, resetFeeState, feeError } = useGetFee();
// const { userBalance } = useContext(AppContext);
const { fee, getFee, resetFeeState, feeError } = useGetFee();
// const handleCheckStakeSaturation = async (newMixId: number) => {
// try {
// const newSaturation = decimalToFloatApproximation(
// (await getMixnodeStakeSaturation(newMixId)).uncapped_saturation,
// );
// if (newSaturation && newSaturation > 1) {
// const saturationPercentage = Math.round(newSaturation * 100);
// return { isOverSaturated: true, saturationPercentage };
// }
// return { isOverSaturated: false, saturationPercentage: undefined };
// } catch (e) {
// Console.error('Error fetching the saturation, error:', e);
// return { isOverSaturated: false, saturationPercentage: undefined };
// }
// };
const { username, connect, disconnect, wallet, openView, address, getCosmWasmClient, isWalletConnected } =
useChain('nyx');
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]);
const validate = async () => {
let newValidatedValue = true;
let errorAmountMessage;
let errorIdentityKeyMessage;
// if (!identityKey || !validateKey(identityKey, 32)) {
// newValidatedValue = false;
// errorIdentityKeyMessage = undefined;
// }
if (!identityKey) {
newValidatedValue = false;
errorIdentityKeyMessage = 'Please enter a valid identity key';
}
// if (identityKey && mixId && validateKey(identityKey, 32)) {
// const { isOverSaturated, saturationPercentage } = await handleCheckStakeSaturation(mixId);
// if (isOverSaturated) {
// newValidatedValue = false;
// errorIdentityKeyMessage = `This node is over saturated (${saturationPercentage}%), please select another node`;
// }
// }
// if (amount && !(await validateAmount(amount, '0'))) {
// newValidatedValue = false;
// errorAmountMessage = 'Please enter a valid amount';
// }
if (amount && !(await validateAmount(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()}`;
@@ -127,9 +123,9 @@ export const DelegateModal: FCWithChildren<{
newValidatedValue = false;
}
if (!mixId) {
newValidatedValue = false;
}
// if (!mixId) {
// newValidatedValue = false;
// }
setErrorIdentityKey(errorIdentityKeyMessage);
if (mixIdError && !errorIdentityKeyMessage) {
@@ -141,21 +137,22 @@ export const DelegateModal: FCWithChildren<{
const handleOk = async () => {
if (onOk && amount && identityKey && mixId) {
onOk(mixId, identityKey, { amount, denom }); //tokenPool, fee);
onOk(mixId, identityKey, { amount, denom }, 'balance', fee);
}
};
const handleConfirm = async ({ mixId: id, value }: { mixId: number; value: DecCoin }) => {
const hasEnoughTokens = true; // await checkTokenBalance(tokenPool, value.amount);
const tokenPool = 'balance';
const hasEnoughTokens = checkTokenBalance(tokenPool, value.amount, balance.data || '0');
if (!hasEnoughTokens) {
setErrorAmount('Not enough funds');
return;
}
// if (tokenPool === 'balance') {
// getFee(simulateDelegateToMixnode, { mixId: id, amount: value });
// }
if (tokenPool === 'balance') {
getFee(simulateDelegateToMixnode, { mixId: id, amount: value });
}
// if (tokenPool === 'locked') {
// getFee(simulateVestingDelegateToMixnode, { mixId: id, amount: value });
@@ -184,15 +181,14 @@ export const DelegateModal: FCWithChildren<{
const resolveMixId = useCallback(
debounce(async (idKey) => {
if (!idKey) {
//|| !validateKey(idKey, 32))
if (!idKey || !validateKey(idKey, 32)) {
return;
}
let res;
try {
// res = await tryConvertIdentityToMixId(idKey);
res = await tryConvertIdentityToMixId(idKey);
} catch (e) {
// Console.warn(`failed to resolve mix_id for "${idKey}": ${e}`);
Console.warn(`failed to resolve mix_id for "${idKey}": ${e}`);
return;
}
if (res) {
@@ -209,38 +205,38 @@ export const DelegateModal: FCWithChildren<{
resolveMixId(identityKey);
}, [identityKey]);
// if (fee) {
// return (
// <ConfirmTx
// open
// header="Delegation details"
// fee={fee}
// onClose={onClose}
// onPrev={resetFeeState}
// onConfirm={handleOk}
// >
// {userBalance.balance?.amount.amount && fee?.amount?.amount && (
// <Box sx={{ my: 2 }}>
// <BalanceWarning fee={fee?.amount?.amount} tx={amount} />
// </Box>
// )}
// <ModalListItem label="Node identity key" value={identityKey} divider />
// <ModalListItem label="Amount" value={`${amount} ${denom.toUpperCase()}`} divider />
// </ConfirmTx>
// );
// }
if (fee) {
return (
<ConfirmTx
open
header="Delegation details"
fee={fee}
onClose={onClose}
onPrev={resetFeeState}
onConfirm={handleOk}
>
{balance.data && fee?.amount?.amount && (
<Box sx={{ my: 2 }}>
<BalanceWarning fee={fee?.amount?.amount} tx={amount} />
</Box>
)}
<ModalListItem label="Node identity key" value={identityKey} divider />
<ModalListItem label="Amount" value={`${amount} ${denom.toUpperCase()}`} divider />
</ConfirmTx>
);
}
// if (feeError) {
// return (
// <ErrorModal
// title="Something went wrong while calculating fee. Are you sure you entered a valid node address?"
// message={feeError}
// sx={sx}
// open={open}
// onClose={onClose}
// />
// );
// }
if (feeError) {
return (
<ErrorModal
title="Something went wrong while calculating fee. Are you sure you entered a valid node address?"
message={feeError}
sx={sx}
open={open}
onClose={onClose}
/>
);
}
return (
<SimpleModal
@@ -293,7 +289,7 @@ export const DelegateModal: FCWithChildren<{
/>
</Box>
<Box sx={{ mt: 3 }}>
<ModalListItem label="Account balance" value={accountBalance?.toUpperCase()} divider fontWeight={600} />
<ModalListItem label="Account balance" value={`${balance.data} NYM`} divider fontWeight={600} />
</Box>
<ModalListItem label="Rewards payout interval" value={rewardInterval} hidden divider />
@@ -1,6 +1,6 @@
import React from 'react';
import { Box, Button, Modal, SxProps, Typography } from '@mui/material';
import { modalStyle } from '../../../../nym-wallet/src/components/Modals/styles';
import { modalStyle } from '../../../../../nym-wallet/src/components/Modals/styles';
export const ErrorModal: FCWithChildren<{
open: boolean;
@@ -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,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 />}
</>
);
@@ -1,6 +1,6 @@
import React from 'react';
import { Box, Stack, SxProps, Typography, TypographyProps } from '@mui/material';
import { ModalDivider } from '../../../../nym-wallet/src/components/Modals/ModalDivider';
import { ModalDivider } from '../../../../../nym-wallet/src/components/Modals/ModalDivider';
export const ModalListItem: FCWithChildren<{
label: string;
@@ -3,7 +3,7 @@ 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 { modalStyle } from '../../../../../nym-wallet/src/components/Modals/styles';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
@@ -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,255 @@
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';
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 };
};
@@ -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));
+52 -15
View File
@@ -1,10 +1,12 @@
import * as React from 'react';
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 { useState } from 'react';
import { DelegateModal } from './Delegations/DelegateModal';
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';
const fieldsHeight = '42.25px';
@@ -28,6 +30,32 @@ export const TableToolbar: FCWithChildren<TableToolBarProps> = ({
withFilters,
}) => {
const [showNewDelegationModal, setShowNewDelegationModal] = useState<boolean>(false);
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]);
const isMobile = useIsMobile();
return (
@@ -104,18 +132,27 @@ export const TableToolbar: FCWithChildren<TableToolBarProps> = ({
</Box>
{showNewDelegationModal && (
<DelegateModal
open={showNewDelegationModal}
onClose={() => setShowNewDelegationModal(false)}
onOk={undefined}
header="Delegate"
buttonText="Delegate stake"
denom={'nym'} // clientDetails?.display_mix_denom || 'nym'}
accountBalance={'balance?.printable_balance'}
rewardInterval="weekly"
hasVestingContract={true}
// {...storybookStyles(theme, isStorybook)}
/>
<ChainProvider
chains={chainsFixedUp}
assetLists={assetsFixedUp}
wallets={[...keplr]}
signerOptions={{
preferredSignType: () => 'amino',
}}
>
<DelegateModal
open={showNewDelegationModal}
onClose={() => setShowNewDelegationModal(false)}
onOk={undefined}
header="Delegate"
buttonText="Delegate stake"
denom={'nym'} // clientDetails?.display_mix_denom || 'nym'}
// accountBalance={'balance?.printable_balance' || 'error reading balance'}
rewardInterval="weekly"
hasVestingContract={true}
// {...storybookStyles(theme, isStorybook)}
/>
</ChainProvider>
)}
</Box>
);