diff --git a/explorer/src/components/ConnectKeplrWallet.tsx b/explorer/src/components/ConnectKeplrWallet.tsx
index 4fab8b8712..766f96059a 100644
--- a/explorer/src/components/ConnectKeplrWallet.tsx
+++ b/explorer/src/components/ConnectKeplrWallet.tsx
@@ -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 ;
- // }
+ if (isWalletConnecting) {
+ return ;
+ }
if (isWalletConnected) {
return (
diff --git a/explorer/src/components/Delegations/components/ConfirmTX.tsx b/explorer/src/components/Delegations/components/ConfirmTX.tsx
new file mode 100644
index 0000000000..e62a86a23c
--- /dev/null
+++ b/explorer/src/components/Delegations/components/ConfirmTX.tsx
@@ -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;
+ onClose?: () => void;
+ onPrev: () => void;
+ isStorybook?: boolean;
+ children?: React.ReactNode;
+}> = ({ open, fee, onConfirm, onClose, header, subheader, onPrev, children, isStorybook }) => {
+ const theme = useTheme();
+ return (
+
+
+ {children}
+
+
+
+
+ );
+};
diff --git a/explorer/src/components/Delegations/components/DelegateModal.tsx b/explorer/src/components/Delegations/components/DelegateModal.tsx
index eb98c57bac..3828e81857 100644
--- a/explorer/src/components/Delegations/components/DelegateModal.tsx
+++ b/explorer/src/components/Delegations/components/DelegateModal.tsx
@@ -3,7 +3,7 @@ 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 { Console } from '../utils/console';
import { useGetFee } from '../hooks/useGetFee';
// import { simulateDelegateToMixnode, simulateVestingDelegateToMixnode, tryConvertIdentityToMixId } from 'src/requests';
import { debounce } from 'lodash';
@@ -12,15 +12,17 @@ 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 { TPoolOption, 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 { useChain } from '@cosmos-kit/react';
-import { uNYMtoNYM } from '../../ConnectKeplrWallet';
+import { uNYMtoNYM } from '../utils';
+import { ErrorModal } from './ErrorModal';
+import { ConfirmTx } from './ConfirmTX';
+import { BalanceWarning } from './FeeWarning';
const MIN_AMOUNT_TO_DELEGATE = 10;
@@ -31,7 +33,7 @@ export const DelegateModal: FCWithChildren<{
mixId: number,
identityKey: string,
amount: DecCoin,
- // tokenPool: TPoolOption,
+ tokenPool: TPoolOption,
fee?: FeeDetails,
) => Promise;
identityKey?: string;
@@ -102,7 +104,6 @@ export const DelegateModal: FCWithChildren<{
getBalance(address);
}
}, [address, getCosmWasmClient]);
- // const { userBalance } = useContext(AppContext);
// const handleCheckStakeSaturation = async (newMixId: number) => {
// try {
@@ -138,10 +139,10 @@ export const DelegateModal: FCWithChildren<{
// }
// }
- // 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()}`;
@@ -166,21 +167,22 @@ export const DelegateModal: FCWithChildren<{
const handleOk = async () => {
if (onOk && amount && identityKey && mixId) {
- onOk(mixId, identityKey, { amount, denom }, fee); //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 = await checkTokenBalance(tokenPool, value.amount);
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 });
@@ -217,7 +219,7 @@ export const DelegateModal: FCWithChildren<{
try {
// 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) {
@@ -234,38 +236,38 @@ export const DelegateModal: FCWithChildren<{
resolveMixId(identityKey);
}, [identityKey]);
- // if (fee) {
- // return (
- //
- // {userBalance.balance?.amount.amount && fee?.amount?.amount && (
- //
- //
- //
- // )}
- //
- //
- //
- // );
- // }
+ if (fee) {
+ return (
+
+ {balance.data && fee?.amount?.amount && (
+
+
+
+ )}
+
+
+
+ );
+ }
- // if (feeError) {
- // return (
- //
- // );
- // }
+ if (feeError) {
+ return (
+
+ );
+ }
return (
-
+
diff --git a/explorer/src/components/Delegations/components/FeeWarning.tsx b/explorer/src/components/Delegations/components/FeeWarning.tsx
new file mode 100644
index 0000000000..3dfaba39d1
--- /dev/null
+++ b/explorer/src/components/Delegations/components/FeeWarning.tsx
@@ -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 (
+ }>
+ Warning: Fees are greater than the reward
+ The fees for redeeming rewards will cost more than the rewards. Are you sure you want to continue?
+
+ );
+ }
+ 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 (
+ }>
+ Warning: Transaction amount is greater than your balance
+ The transaction amount (inc fees) is greater than your current balance, which could cause this transaction to
+ fail.
+ Do you want to continue?
+
+ );
+};
diff --git a/explorer/src/components/Delegations/components/ModalDivider.tsx b/explorer/src/components/Delegations/components/ModalDivider.tsx
new file mode 100644
index 0000000000..6258e0bfac
--- /dev/null
+++ b/explorer/src/components/Delegations/components/ModalDivider.tsx
@@ -0,0 +1,6 @@
+import React from 'react';
+import { Box, SxProps } from '@mui/material';
+
+export const ModalDivider: FCWithChildren<{
+ sx?: SxProps;
+}> = ({ sx }) => ;
diff --git a/explorer/src/components/Delegations/components/ModalFee.tsx b/explorer/src/components/Delegations/components/ModalFee.tsx
new file mode 100644
index 0000000000..835b73e689
--- /dev/null
+++ b/explorer/src/components/Delegations/components/ModalFee.tsx
@@ -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 ;
+ 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) => (
+ <>
+
+ {divider && }
+ >
+);
+
+export const ModalTotalAmount = ({ fee, amount, isLoading, error, divider }: TTotalAmountProps) => (
+ <>
+
+ {divider && }
+ >
+);
diff --git a/explorer/src/components/Delegations/components/styles.ts b/explorer/src/components/Delegations/components/styles.ts
new file mode 100644
index 0000000000..9b26551767
--- /dev/null
+++ b/explorer/src/components/Delegations/components/styles.ts
@@ -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%' };
+};
diff --git a/explorer/src/components/Delegations/context/buy.tsx b/explorer/src/components/Delegations/context/buy.tsx
index ff251ec631..96e6893731 100644
--- a/explorer/src/components/Delegations/context/buy.tsx
+++ b/explorer/src/components/Delegations/context/buy.tsx
@@ -1,6 +1,6 @@
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { sign } from '../requests';
-// import { Console } from 'src/utils/console';
+import { Console } from '../utils/console';
// import { AppContext } from './main';
export type TBuyContext = {
@@ -34,7 +34,7 @@ export const BuyContextProvider: FCWithChildren = ({ children }): JSX.Element =>
try {
signature = await sign(message);
} catch (e: any) {
- // Console.log(`Sign message operation failed: ${e}`);
+ 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 {
diff --git a/explorer/src/components/Delegations/context/main.tsx b/explorer/src/components/Delegations/context/main.tsx
index a623ddadae..8653234cbb 100644
--- a/explorer/src/components/Delegations/context/main.tsx
+++ b/explorer/src/components/Delegations/context/main.tsx
@@ -18,7 +18,7 @@ import {
} from '../requests';
import Big from 'big.js';
-// import { Console } from '../utils/console';
+import { Console } from '../utils/console';
import { createSignInWindow, getReactState, setReactState } from '../requests/app';
// import { toDisplay } from '../utils';
@@ -27,8 +27,7 @@ const toDisplay = (val: string | number | Big, dp = 4) => {
try {
displayValue = Big(val).toFixed(dp);
} catch (e: any) {
- // Console.warn(`${displayValue} not a valid decimal number: ${e}`);
- console.log('object :>> ', `${displayValue} not a valid decimal number: ${e}`);
+ Console.warn(`${displayValue} not a valid decimal number: ${e}`);
}
return displayValue;
};
@@ -149,7 +148,7 @@ export const AppProvider: FCWithChildren = ({ children }) => {
setClientDetails(client);
} catch (e) {
enqueueSnackbar('Error loading account', { variant: 'error' });
- // Console.error(e as string);
+ Console.error(e as string);
}
};
@@ -164,7 +163,7 @@ export const AppProvider: FCWithChildren = ({ children }) => {
const mixnode = await getMixnodeBondDetails();
setMixnodeDetails(mixnode);
} catch (e) {
- // Console.error(e as string);
+ Console.error(e as string);
}
};
@@ -180,7 +179,7 @@ export const AppProvider: FCWithChildren = ({ children }) => {
const modeFromStorage = await forage.getItem({ key: 'nym-wallet-mode' })();
if (modeFromStorage) setMode(modeFromStorage);
} catch (e) {
- // Console.error(e);
+ Console.error(e);
}
};
@@ -231,15 +230,15 @@ export const AppProvider: FCWithChildren = ({ children }) => {
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,
- // });
+ 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);
+ Console.error('Failed to check admin addresses', e);
}
}
setIsAdminAddress(newValue);
diff --git a/explorer/src/components/Delegations/hooks/useCheckOwnership.ts b/explorer/src/components/Delegations/hooks/useCheckOwnership.ts
index 50de27a1e8..ef171c43e1 100644
--- a/explorer/src/components/Delegations/hooks/useCheckOwnership.ts
+++ b/explorer/src/components/Delegations/hooks/useCheckOwnership.ts
@@ -1,5 +1,5 @@
import { useCallback, useContext, useEffect, useState } from 'react';
-// import { Console } from '../utils/console';
+import { Console } from '../utils/console';
// eslint-disable-next-line import/no-cycle
import { AppContext } from '../context/main';
import { checkGatewayOwnership, checkMixnodeOwnership, getVestingPledgeInfo } from '../requests';
@@ -43,7 +43,7 @@ export const useCheckOwnership = () => {
setOwnership(status);
} catch (e) {
- // Console.error(e as string);
+ Console.error(e as string);
setError(e as string);
setOwnership(initial);
} finally {
diff --git a/explorer/src/components/Delegations/hooks/useGetBalance.ts b/explorer/src/components/Delegations/hooks/useGetBalance.ts
index 8f4b280300..b23eaf1a34 100644
--- a/explorer/src/components/Delegations/hooks/useGetBalance.ts
+++ b/explorer/src/components/Delegations/hooks/useGetBalance.ts
@@ -13,7 +13,7 @@ import {
getSpendableVestedCoins,
userBalance,
} from '../requests';
-// import { Console } from '../utils/console';
+import { Console } from '../utils/console';
type TTokenAllocation = {
[key in
@@ -92,7 +92,7 @@ export const useGetBalance = (clientDetails?: Account): TUseuserBalance => {
} catch (e) {
clearTokenAllocation();
clearOriginalVesting();
- // Console.error(e as string);
+ Console.error(e as string);
}
}
setIsLoading(false);
diff --git a/explorer/src/components/Delegations/hooks/useGetFee.ts b/explorer/src/components/Delegations/hooks/useGetFee.ts
index c38a1714f8..081493f4ca 100644
--- a/explorer/src/components/Delegations/hooks/useGetFee.ts
+++ b/explorer/src/components/Delegations/hooks/useGetFee.ts
@@ -1,6 +1,6 @@
import { DecCoin, FeeDetails } from '@nymproject/types';
import { useState } from 'react';
-// import { Console } from 'src/utils/console';
+import { Console } from '../utils/console';
import { getCustomFees } from '../requests';
export function useGetFee() {
@@ -26,7 +26,7 @@ export function useGetFee() {
const fees = await getCustomFees({ feesAmount: amount });
setFee(fees);
} catch (e) {
- // Console.error(e);
+ Console.error(e);
setFeeError(e as string);
}
setIsFeeLoading(false);
diff --git a/explorer/src/components/Delegations/typings/FC.d.ts b/explorer/src/components/Delegations/typings/FC.d.ts
new file mode 100644
index 0000000000..08ebdfa298
--- /dev/null
+++ b/explorer/src/components/Delegations/typings/FC.d.ts
@@ -0,0 +1 @@
+declare type FCWithChildren
= React.FC>;
diff --git a/explorer/src/components/Delegations/typings/jpeg.d.ts b/explorer/src/components/Delegations/typings/jpeg.d.ts
new file mode 100644
index 0000000000..af2ed72913
--- /dev/null
+++ b/explorer/src/components/Delegations/typings/jpeg.d.ts
@@ -0,0 +1,9 @@
+declare module '*.jpeg' {
+ const value: any;
+ export default value;
+}
+
+declare module '*.jpg' {
+ const value: any;
+ export default value;
+}
diff --git a/explorer/src/components/Delegations/typings/json.d.ts b/explorer/src/components/Delegations/typings/json.d.ts
new file mode 100644
index 0000000000..b72dd46ee5
--- /dev/null
+++ b/explorer/src/components/Delegations/typings/json.d.ts
@@ -0,0 +1,4 @@
+declare module '*.json' {
+ const content: any;
+ export default content;
+}
diff --git a/explorer/src/components/Delegations/typings/png.d.ts b/explorer/src/components/Delegations/typings/png.d.ts
new file mode 100644
index 0000000000..dd84df40a4
--- /dev/null
+++ b/explorer/src/components/Delegations/typings/png.d.ts
@@ -0,0 +1,4 @@
+declare module '*.png' {
+ const content: any;
+ export default content;
+}
diff --git a/explorer/src/components/Delegations/typings/svg.d.ts b/explorer/src/components/Delegations/typings/svg.d.ts
new file mode 100644
index 0000000000..091d25e210
--- /dev/null
+++ b/explorer/src/components/Delegations/typings/svg.d.ts
@@ -0,0 +1,4 @@
+declare module '*.svg' {
+ const content: any;
+ export default content;
+}
diff --git a/explorer/src/components/Delegations/utils/common.ts b/explorer/src/components/Delegations/utils/common.ts
new file mode 100644
index 0000000000..530437933a
--- /dev/null
+++ b/explorer/src/components/Delegations/utils/common.ts
@@ -0,0 +1,273 @@
+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,
+ userBalance,
+} 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 (allocationValue: string): Promise => {
+ try {
+ const walletValue = await userBalance();
+
+ const remainingBalance = +walletValue.amount.amount - +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 = async (tokenPool: TPoolOption, amount: string) => {
+ let hasEnoughFunds = false;
+ if (tokenPool === 'locked') {
+ hasEnoughFunds = await checkHasEnoughLockedTokens(amount);
+ }
+
+ if (tokenPool === 'balance') {
+ hasEnoughFunds = await checkHasEnoughFunds(amount);
+ }
+
+ 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 => {
+ // 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(arr: T[], id: K) {
+ return arr.filter((v, i, a) => a.findIndex((v2) => v2[id] === v[id]) === i);
+}
+
+// export const checkTokenBalance = async (tokenPool: TPoolOption, amount: string) => {
+// let hasEnoughFunds = false;
+// if (tokenPool === 'locked') {
+// hasEnoughFunds = await checkHasEnoughLockedTokens(amount);
+// }
+
+// if (tokenPool === 'balance') {
+// hasEnoughFunds = await checkHasEnoughFunds(amount);
+// }
+
+// return hasEnoughFunds;
+// };
+
+export const isDecimal = (value: number) => value - Math.floor(value) !== 0;
+
+export const attachDefaultOperatingCost = async (profitMarginPercent: string): Promise =>
+ 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 };
+};
diff --git a/explorer/src/components/Delegations/utils/console.ts b/explorer/src/components/Delegations/utils/console.ts
new file mode 100644
index 0000000000..6506c76d2a
--- /dev/null
+++ b/explorer/src/components/Delegations/utils/console.ts
@@ -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),
+};
diff --git a/explorer/src/components/Delegations/utils/fireRequests.ts b/explorer/src/components/Delegations/utils/fireRequests.ts
new file mode 100644
index 0000000000..441f3289a8
--- /dev/null
+++ b/explorer/src/components/Delegations/utils/fireRequests.ts
@@ -0,0 +1,22 @@
+import { Console } from './console';
+
+export type TauriReq Promise)> = {
+ name: Req['name'];
+ request: () => ReturnType;
+ onFulfilled: (value: Awaited>) => void;
+};
+
+async function fireRequests(requests: TauriReq[]) {
+ 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;
diff --git a/explorer/src/components/Delegations/utils/index.ts b/explorer/src/components/Delegations/utils/index.ts
new file mode 100644
index 0000000000..b1a2ade95a
--- /dev/null
+++ b/explorer/src/components/Delegations/utils/index.ts
@@ -0,0 +1,4 @@
+export * from './common';
+export * from './fireRequests';
+export * from './console';
+export { default as fireRequests } from './fireRequests';
diff --git a/explorer/src/components/Delegations/utils/sleep.ts b/explorer/src/components/Delegations/utils/sleep.ts
new file mode 100644
index 0000000000..4f1d101103
--- /dev/null
+++ b/explorer/src/components/Delegations/utils/sleep.ts
@@ -0,0 +1,3 @@
+export const sleep = (delayMilliseconds: number) =>
+ // eslint-disable-next-line no-promise-executor-return
+ new Promise((resolve) => setTimeout(resolve, delayMilliseconds));