;
+}
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..c738df4d2a
--- /dev/null
+++ b/explorer/src/components/Delegations/utils/common.ts
@@ -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 => {
+// 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 => {
+ // 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 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));
diff --git a/explorer/src/components/TableToolbar.tsx b/explorer/src/components/TableToolbar.tsx
index c2c0951b9f..51ab38e191 100644
--- a/explorer/src/components/TableToolbar.tsx
+++ b/explorer/src/components/TableToolbar.tsx
@@ -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 = ({
withFilters,
}) => {
const [showNewDelegationModal, setShowNewDelegationModal] = useState(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 = ({
{showNewDelegationModal && (
- 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)}
- />
+ 'amino',
+ }}
+ >
+ 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)}
+ />
+
)}
);