Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| efdb8adab8 | |||
| e8632c3762 | |||
| fa354016e0 | |||
| 935ee765e9 | |||
| 4c8e59e6fc | |||
| 067f3e6f1a | |||
| fb0b928598 | |||
| e0e9d03b7f | |||
| 6f09d46dce | |||
| bdef48331b | |||
| 51a6936e51 | |||
| fd456d2952 | |||
| eee1abe593 | |||
| fffad43937 | |||
| 3a79f43a8d | |||
| 2e495f87ab | |||
| 57a9f18f5a | |||
| 0c6a0a9cae | |||
| c80d8d354a | |||
| 3f544dbc69 | |||
| d1e1f15db0 | |||
| 651c314182 | |||
| a57545521d | |||
| da60606921 | |||
| 14f9bf7234 | |||
| c1fa92869a | |||
| c8533e3ec8 | |||
| 06c4dd601d | |||
| 4ff80bbab2 | |||
| d7220b1fec | |||
| d92df9ada3 | |||
| 78ace473c7 | |||
| e877dfe224 | |||
| 30c488484c | |||
| 8cd155b2ac |
@@ -37,6 +37,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
- validator: fixed local docker-compose setup to work on Apple M1 ([#1329])
|
||||
- explorer-api: listen out for SIGTERM and SIGQUIT too, making it play nicely as a system service ([#1482]).
|
||||
- network-requester: fix filter for suffix-only domains ([#1487])
|
||||
- validator-api: listen out for SIGTERM and SIGQUIT too, making it play nicely as a system service ([#1496]).
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -77,6 +78,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
[#1478]: https://github.com/nymtech/nym/pull/1478
|
||||
[#1482]: https://github.com/nymtech/nym/pull/1482
|
||||
[#1487]: https://github.com/nymtech/nym/pull/1487
|
||||
[#1496]: https://github.com/nymtech/nym/pull/1496
|
||||
|
||||
## [nym-connect-v1.0.1](https://github.com/nymtech/nym/tree/nym-connect-v1.0.1) (2022-07-22)
|
||||
|
||||
|
||||
Generated
+1
@@ -3325,6 +3325,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"task",
|
||||
"thiserror",
|
||||
"time 0.3.9",
|
||||
"tokio",
|
||||
|
||||
@@ -507,7 +507,7 @@ mod test {
|
||||
|
||||
for (expected, raw_display) in values {
|
||||
let coin = DecCoin {
|
||||
denom: Network::MAINNET.mix_denom().display.into(),
|
||||
denom: Network::MAINNET.mix_denom().display,
|
||||
amount: raw_display.parse().unwrap(),
|
||||
};
|
||||
let base = reg.attempt_convert_to_base_coin(coin).unwrap();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
EXPLORER_API_URL=https://explorer.nymtech.net/api/v1
|
||||
VALIDATOR_API_URL=https://validator.nymtech.net
|
||||
VALIDATOR_URL=https://rpc.nyx.nodes.guru
|
||||
BIG_DIPPER_URL=https://blocks.nymtech.net
|
||||
CURRENCY_DENOM=unym
|
||||
CURRENCY_STAKING_DENOM=unyx
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
EXPLORER_API_URL=https://qa-explorer.nymtech.net/api/v1
|
||||
VALIDATOR_API_URL=https://qa-validator.nymtech.net
|
||||
VALIDATOR_API_URL=https://qa-validator-api.nymtech.net
|
||||
VALIDATOR_URL=https://qa-validator.nymtech.net
|
||||
BIG_DIPPER_URL=https://qa-blocks.nymtech.net
|
||||
CURRENCY_DENOM=unymt
|
||||
CURRENCY_STAKING_DENOM=unyxt
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// master APIs
|
||||
export const API_BASE_URL = process.env.EXPLORER_API_URL;
|
||||
export const VALIDATOR_API_BASE_URL = process.env.VALIDATOR_API_URL;
|
||||
export const VALIDATOR_URL = process.env.VALIDATOR_URL;
|
||||
export const BIG_DIPPER = process.env.BIG_DIPPER_URL;
|
||||
|
||||
// specific API routes
|
||||
@@ -9,7 +10,7 @@ export const MIXNODE_PING = `${API_BASE_URL}/ping`;
|
||||
export const MIXNODES_API = `${API_BASE_URL}/mix-nodes`;
|
||||
export const MIXNODE_API = `${API_BASE_URL}/mix-node`;
|
||||
export const GATEWAYS_API = `${VALIDATOR_API_BASE_URL}/api/v1/gateways`;
|
||||
export const VALIDATORS_API = `${VALIDATOR_API_BASE_URL}/validators`;
|
||||
export const VALIDATORS_API = `${VALIDATOR_URL}/validators`;
|
||||
export const BLOCK_API = `${VALIDATOR_API_BASE_URL}/block`;
|
||||
export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`;
|
||||
export const UPTIME_STORY_API = `${VALIDATOR_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this.
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
Slider,
|
||||
Typography,
|
||||
Box,
|
||||
@@ -25,6 +24,7 @@ import { useIsMobile } from '../../hooks/useIsMobile';
|
||||
const FilterItem = ({
|
||||
label,
|
||||
id,
|
||||
tooltipInfo,
|
||||
value,
|
||||
marks,
|
||||
scale,
|
||||
@@ -36,6 +36,7 @@ const FilterItem = ({
|
||||
}) => (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography gutterBottom>{label}</Typography>
|
||||
<Typography fontSize={12}>{tooltipInfo}</Typography>
|
||||
<Slider
|
||||
value={value}
|
||||
onChange={(e: Event, newValue: number | number[]) => onChange(id, newValue as number[])}
|
||||
@@ -50,7 +51,7 @@ const FilterItem = ({
|
||||
);
|
||||
|
||||
export const Filters = () => {
|
||||
const { filterMixnodes, fetchMixnodes } = useMainContext();
|
||||
const { filterMixnodes, fetchMixnodes, mixnodes } = useMainContext();
|
||||
const { status } = useParams<{ status: MixnodeStatusWithAll | undefined }>();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
@@ -129,17 +130,23 @@ export const Filters = () => {
|
||||
variant={isMobile ? 'standard' : 'outlined'}
|
||||
action={
|
||||
<Button size="small" onClick={onClearFilters}>
|
||||
Clear
|
||||
CLEAR FILTERS
|
||||
</Button>
|
||||
}
|
||||
sx={{ width: 300 }}
|
||||
>
|
||||
Filters applied
|
||||
{mixnodes?.data?.length} mixnodes matched your criteria
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
<IconButton size="large" onClick={handleToggleShowFilters}>
|
||||
<Tune />
|
||||
</IconButton>
|
||||
<Button
|
||||
size="large"
|
||||
variant="text"
|
||||
color="inherit"
|
||||
endIcon={<Tune />}
|
||||
onClick={handleToggleShowFilters}
|
||||
sx={{ textTransform: 'none' }}
|
||||
>
|
||||
Advanced filters
|
||||
</Button>
|
||||
<Dialog open={showFilters} onClose={handleToggleShowFilters} maxWidth="md" fullWidth>
|
||||
<DialogTitle>Mixnode filters</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
|
||||
@@ -18,6 +18,8 @@ export const generateFilterSchema = (upperSaturationValue?: number) => ({
|
||||
{ label: '90', value: 90 },
|
||||
{ label: '100', value: 100 },
|
||||
],
|
||||
tooltipInfo:
|
||||
'As a delegator you want to chose nodes with lower profit margin, meaning more payout for their delegators',
|
||||
},
|
||||
stakeSaturation: {
|
||||
label: 'Stake saturation (%)',
|
||||
@@ -43,65 +45,29 @@ export const generateFilterSchema = (upperSaturationValue?: number) => ({
|
||||
},
|
||||
],
|
||||
max: upperSaturationValue,
|
||||
tooltipInfo: "Select nodes with <100% saturation. Any additional stake above 100% saturation won't get rewards",
|
||||
},
|
||||
stake: {
|
||||
label: 'Stake (NYM)',
|
||||
id: EnumFilterKey.stake,
|
||||
value: [20, 90],
|
||||
min: 20,
|
||||
max: 90,
|
||||
routingScore: {
|
||||
label: 'Routing score (%)',
|
||||
id: EnumFilterKey.routingScore,
|
||||
value: [0, 100],
|
||||
marks: [
|
||||
{
|
||||
value: 0,
|
||||
label: '1',
|
||||
},
|
||||
{
|
||||
value: 10,
|
||||
label: '10',
|
||||
},
|
||||
{
|
||||
value: 20,
|
||||
label: '100',
|
||||
},
|
||||
{
|
||||
value: 30,
|
||||
label: '1k',
|
||||
},
|
||||
{
|
||||
value: 40,
|
||||
label: '10k',
|
||||
},
|
||||
{
|
||||
value: 50,
|
||||
label: '100k',
|
||||
},
|
||||
{
|
||||
value: 60,
|
||||
label: '1M',
|
||||
},
|
||||
{
|
||||
value: 70,
|
||||
label: '10M',
|
||||
},
|
||||
{
|
||||
value: 80,
|
||||
label: '100M',
|
||||
},
|
||||
{
|
||||
value: 90,
|
||||
label: '1B',
|
||||
},
|
||||
{ label: '0', value: 0 },
|
||||
{ label: '10', value: 10 },
|
||||
{ label: '20', value: 20 },
|
||||
{ label: '30', value: 30 },
|
||||
{ label: '40', value: 40 },
|
||||
{ label: '50', value: 50 },
|
||||
{ label: '60', value: 60 },
|
||||
{ label: '70', value: 70 },
|
||||
{ label: '80', value: 80 },
|
||||
{ label: '90', value: 90 },
|
||||
{ label: '100', value: 100 },
|
||||
],
|
||||
tooltipInfo: 'The higher the routing score the better the performance of the node and so its rewards',
|
||||
},
|
||||
});
|
||||
|
||||
const formatStakeValuesToMinorDenom = ([value_1, value_2]: number[]) => {
|
||||
const lowerValue = 10 ** (value_1 / 10) * 1_000_000;
|
||||
const upperValue = 10 ** (value_2 / 10) * 1_000_000;
|
||||
|
||||
return [lowerValue, upperValue];
|
||||
};
|
||||
|
||||
const formatStakeSaturationValues = ([value_1, value_2]: number[]) => {
|
||||
const lowerValue = value_1 / 100;
|
||||
const upperValue = value_2 / 100;
|
||||
@@ -110,7 +76,7 @@ const formatStakeSaturationValues = ([value_1, value_2]: number[]) => {
|
||||
};
|
||||
|
||||
export const formatOnSave = (filters: TFilters) => ({
|
||||
stake: formatStakeValuesToMinorDenom(filters.stake.value),
|
||||
routingScore: filters.routingScore.value,
|
||||
profitMargin: filters.profitMargin.value,
|
||||
stakeSaturation: formatStakeSaturationValues(filters.stakeSaturation.value),
|
||||
});
|
||||
|
||||
@@ -102,8 +102,8 @@ export const MainContextProvider: React.FC = ({ children }) => {
|
||||
m.mix_node.profit_margin_percent <= filters.profitMargin[1] &&
|
||||
m.stake_saturation >= filters.stakeSaturation[0] &&
|
||||
m.stake_saturation <= filters.stakeSaturation[1] &&
|
||||
+m.pledge_amount.amount + +m.total_delegation.amount >= filters.stake[0] &&
|
||||
+m.pledge_amount.amount + +m.total_delegation.amount <= filters.stake[1],
|
||||
m.avg_uptime >= filters.routingScore[0] &&
|
||||
m.avg_uptime <= filters.routingScore[1],
|
||||
);
|
||||
setMixnodes({ data: filtered, isLoading: false });
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Mark } from '@mui/base';
|
||||
export enum EnumFilterKey {
|
||||
profitMargin = 'profitMargin',
|
||||
stakeSaturation = 'stakeSaturation',
|
||||
stake = 'stake',
|
||||
routingScore = 'routingScore',
|
||||
}
|
||||
|
||||
export type TFilterItem = {
|
||||
@@ -15,6 +15,7 @@ export type TFilterItem = {
|
||||
min?: number;
|
||||
max?: number;
|
||||
scale?: (value: number) => number;
|
||||
tooltipInfo?: string;
|
||||
};
|
||||
|
||||
export type TFilters = { [key in EnumFilterKey]: TFilterItem };
|
||||
|
||||
@@ -6,7 +6,15 @@ import { AccountAvatar } from './AccountAvatar';
|
||||
export const AccountOverview = ({ account, onClick }: { account: AccountEntry; onClick: () => void }) => (
|
||||
<Button
|
||||
startIcon={<AccountAvatar name={account.id} />}
|
||||
sx={{ color: 'text.primary' }}
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
'&:hover': (t) =>
|
||||
t.palette.mode === 'dark'
|
||||
? {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||
}
|
||||
: {},
|
||||
}}
|
||||
onClick={onClick}
|
||||
disableRipple
|
||||
>
|
||||
|
||||
@@ -10,9 +10,9 @@ export default {
|
||||
|
||||
const Template: ComponentStory<typeof ConfirmTx> = (args) => (
|
||||
<ConfirmTx {...args}>
|
||||
<ModalListItem label="Transaction type" value="Bond" divider />
|
||||
<ModalListItem label="Current bond" value="100 NYM" divider />
|
||||
<ModalListItem label="Additional bond" value="50 NYM" divider />
|
||||
<ModalListItem label="Transaction type:" value="Bond" divider />
|
||||
<ModalListItem label="Current bond:" value="100 NYM" divider />
|
||||
<ModalListItem label="Additional bond:" value="50 NYM" divider />
|
||||
</ConfirmTx>
|
||||
);
|
||||
|
||||
|
||||
@@ -168,8 +168,8 @@ export const DelegateModal: React.FC<{
|
||||
onPrev={resetFeeState}
|
||||
onConfirm={handleOk}
|
||||
>
|
||||
<ModalListItem label="Node identity key" value={identityKey} divider />
|
||||
<ModalListItem label="Amount" value={`${amount} ${denom.toUpperCase()}`} divider />
|
||||
<ModalListItem label="Node identity key:" value={identityKey} divider />
|
||||
<ModalListItem label="Amount:" value={`${amount} ${denom.toUpperCase()}`} divider />
|
||||
</ConfirmTx>
|
||||
);
|
||||
}
|
||||
@@ -184,23 +184,24 @@ export const DelegateModal: React.FC<{
|
||||
}
|
||||
}}
|
||||
header={header || 'Delegate'}
|
||||
subHeader="Delegate to mixnode"
|
||||
okLabel={buttonText || 'Delegate stake'}
|
||||
okDisabled={!isValidated}
|
||||
sx={sx}
|
||||
backdropProps={backdropProps}
|
||||
>
|
||||
<IdentityKeyFormField
|
||||
required
|
||||
fullWidth
|
||||
placeholder="Node identity key"
|
||||
onChanged={handleIdentityKeyChanged}
|
||||
initialValue={identityKey}
|
||||
readOnly={Boolean(initialIdentityKey)}
|
||||
textFieldProps={{
|
||||
autoFocus: !initialIdentityKey,
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<IdentityKeyFormField
|
||||
required
|
||||
fullWidth
|
||||
placeholder="Node identity key"
|
||||
onChanged={handleIdentityKeyChanged}
|
||||
initialValue={identityKey}
|
||||
readOnly={Boolean(initialIdentityKey)}
|
||||
textFieldProps={{
|
||||
autoFocus: !initialIdentityKey,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Typography
|
||||
component="div"
|
||||
textAlign="left"
|
||||
@@ -230,29 +231,30 @@ export const DelegateModal: React.FC<{
|
||||
{errorAmount}
|
||||
</Typography>
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<ModalListItem label="Account balance" value={accountBalance} divider />
|
||||
<ModalListItem label="Account balance:" value={accountBalance} divider strong />
|
||||
</Box>
|
||||
|
||||
<ModalListItem label="Rewards payout interval" value={rewardInterval} hidden divider />
|
||||
<ModalListItem label="Rewards payout interval:" value={rewardInterval} hidden divider />
|
||||
<ModalListItem
|
||||
label="Node profit margin"
|
||||
label="Node profit margin:"
|
||||
value={`${profitMarginPercentage ? `${profitMarginPercentage}%` : '-'}`}
|
||||
hidden={profitMarginPercentage === undefined}
|
||||
divider
|
||||
/>
|
||||
<ModalListItem
|
||||
label="Node uptime"
|
||||
label="Node avg. uptime:"
|
||||
value={`${nodeUptimePercentage ? `${nodeUptimePercentage}%` : '-'}`}
|
||||
hidden={nodeUptimePercentage === undefined}
|
||||
divider
|
||||
/>
|
||||
|
||||
<ModalListItem
|
||||
label="Node est. reward per epoch"
|
||||
label="Node est. reward per epoch:"
|
||||
value={`${estimatedReward} ${denom.toUpperCase()}`}
|
||||
hidden
|
||||
divider
|
||||
/>
|
||||
<ModalListItem label="Est. fee for this transaction will be calculated in the next page" />
|
||||
</SimpleModal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -29,9 +29,6 @@ const transactionForDarkTheme = {
|
||||
url: 'https://sandbox-blocks.nymtech.net/transactions/11ED7B9E21534A9421834F52FED5103DC6E982949C06335F5E12EFC71DAF0CFO',
|
||||
hash: '11ED7B9E21534A9421834F52FED5103DC6E982949C06335F5E12EFC71DAF0CF0',
|
||||
};
|
||||
const balance = '104 NYMT';
|
||||
const balanceVested = '12 NYMT';
|
||||
const recipient = 'nymt1923pujepxfnv8dqyxqrl078s4ysf3xn2p7z2xa';
|
||||
|
||||
const Content: React.FC<{ children: React.ReactElement<any, any>; handleClick: () => void }> = ({
|
||||
children,
|
||||
@@ -78,8 +75,6 @@ export const DelegateSuccess = () => {
|
||||
status="success"
|
||||
action="delegate"
|
||||
message="You delegated 5 NYM"
|
||||
recipient={recipient}
|
||||
balance={balance}
|
||||
transactions={theme.palette.mode === 'light' ? [transaction] : [transactionForDarkTheme]}
|
||||
{...storybookStyles(theme)}
|
||||
/>
|
||||
@@ -99,8 +94,6 @@ export const UndelegateSuccess = () => {
|
||||
status="success"
|
||||
action="undelegate"
|
||||
message="You undelegated 5 NYM"
|
||||
recipient={recipient}
|
||||
balance={balance}
|
||||
transactions={theme.palette.mode === 'light' ? [transaction] : [transactionForDarkTheme]}
|
||||
{...storybookStyles(theme)}
|
||||
/>
|
||||
@@ -120,8 +113,6 @@ export const RedeemSuccess = () => {
|
||||
status="success"
|
||||
action="redeem"
|
||||
message="42 NYM"
|
||||
recipient={recipient}
|
||||
balance={balance}
|
||||
transactions={
|
||||
theme.palette.mode === 'light'
|
||||
? [transaction, transaction]
|
||||
@@ -145,9 +136,6 @@ export const RedeemWithVestedSuccess = () => {
|
||||
status="success"
|
||||
action="redeem"
|
||||
message="42 NYM"
|
||||
recipient={recipient}
|
||||
balance={balance}
|
||||
balanceVested={balanceVested}
|
||||
transactions={
|
||||
theme.palette.mode === 'light'
|
||||
? [transaction, transaction]
|
||||
@@ -171,8 +159,6 @@ export const RedeemAllSuccess = () => {
|
||||
status="success"
|
||||
action="redeem-all"
|
||||
message="42 NYM"
|
||||
recipient={recipient}
|
||||
balance={balance}
|
||||
transactions={
|
||||
theme.palette.mode === 'light'
|
||||
? [transaction, transaction]
|
||||
@@ -196,8 +182,6 @@ export const Error = () => {
|
||||
status="error"
|
||||
action="redeem-all"
|
||||
message="Minim esse veniam Lorem id velit Lorem eu eu est. Excepteur labore sunt do proident proident sint aliquip consequat Lorem sint non nulla ad excepteur."
|
||||
recipient={recipient}
|
||||
balance={balance}
|
||||
transactions={theme.palette.mode === 'light' ? [transaction] : [transactionForDarkTheme]}
|
||||
{...storybookStyles(theme)}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Box, Button, Modal, Typography, SxProps } from '@mui/material';
|
||||
import { Box, Button, Modal, Typography, SxProps, Stack } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { Console } from 'src/utils/console';
|
||||
import { modalStyle } from '../Modals/styles';
|
||||
import { LoadingModal } from '../Modals/LoadingModal';
|
||||
import { ConfirmationModal } from '../Modals/ConfirmationModal';
|
||||
|
||||
export type ActionType = 'delegate' | 'undelegate' | 'redeem' | 'redeem-all' | 'compound';
|
||||
|
||||
@@ -15,9 +16,9 @@ const actionToHeader = (action: ActionType): string => {
|
||||
case 'redeem-all':
|
||||
return 'All rewards redeemed successfully';
|
||||
case 'delegate':
|
||||
return 'Delegation complete';
|
||||
return 'Delegation successful';
|
||||
case 'undelegate':
|
||||
return 'Undelegation complete';
|
||||
return 'Undelegation successful';
|
||||
case 'compound':
|
||||
return 'Rewards compounded successfully';
|
||||
default:
|
||||
@@ -29,9 +30,6 @@ export type DelegationModalProps = {
|
||||
status: 'loading' | 'success' | 'error';
|
||||
action: ActionType;
|
||||
message?: string;
|
||||
recipient?: string;
|
||||
balance?: string;
|
||||
balanceVested?: string;
|
||||
transactions?: {
|
||||
url: string;
|
||||
hash: string;
|
||||
@@ -45,20 +43,7 @@ export const DelegationModal: React.FC<
|
||||
sx?: SxProps;
|
||||
backdropProps?: object;
|
||||
}
|
||||
> = ({
|
||||
status,
|
||||
action,
|
||||
message,
|
||||
recipient,
|
||||
balance,
|
||||
balanceVested,
|
||||
transactions,
|
||||
open,
|
||||
onClose,
|
||||
children,
|
||||
sx,
|
||||
backdropProps,
|
||||
}) => {
|
||||
> = ({ status, action, message, transactions, open, onClose, children, sx, backdropProps }) => {
|
||||
if (status === 'loading') return <LoadingModal sx={sx} backdropProps={backdropProps} />;
|
||||
|
||||
if (status === 'error') {
|
||||
@@ -81,54 +66,28 @@ export const DelegationModal: React.FC<
|
||||
}
|
||||
|
||||
transactions?.map((transaction) => Console.log('action', action, 'status', status, 'key', transaction.hash));
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} BackdropProps={backdropProps}>
|
||||
<Box sx={{ ...modalStyle, ...sx }} textAlign="center">
|
||||
<Typography color={(theme) => theme.palette.success.main} mb={1}>
|
||||
{actionToHeader(action)}
|
||||
</Typography>
|
||||
<Typography mb={3} color="text.primary">
|
||||
{message}
|
||||
</Typography>
|
||||
|
||||
{recipient && (
|
||||
<Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}>
|
||||
Recipient: {recipient}
|
||||
</Typography>
|
||||
return (
|
||||
<ConfirmationModal
|
||||
open={open}
|
||||
onConfirm={onClose || (() => {})}
|
||||
title={actionToHeader(action)}
|
||||
confirmButton="Done"
|
||||
>
|
||||
<Stack alignItems="center" spacing={2} mb={0}>
|
||||
{message && <Typography>{message}</Typography>}
|
||||
{transactions?.length === 1 && (
|
||||
<Link href={transactions[0].url} target="_blank" sx={{ ml: 1 }} text="View on blockchain" noIcon />
|
||||
)}
|
||||
{balanceVested ? (
|
||||
<>
|
||||
<Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}>
|
||||
Your current balance: {balance?.toUpperCase()}
|
||||
</Typography>
|
||||
<Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}>
|
||||
({balanceVested.toUpperCase()} is unlocked in your vesting account)
|
||||
</Typography>
|
||||
</>
|
||||
) : (
|
||||
<Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}>
|
||||
Your current balance: {balance?.toUpperCase()}
|
||||
</Typography>
|
||||
)}
|
||||
{transactions && (
|
||||
<Typography mb={1} fontSize="small" color={(theme) => theme.palette.text.secondary}>
|
||||
Check the transaction {transactions.length > 1 ? 'hashes' : 'hash'}:
|
||||
{transactions.map((transaction) => (
|
||||
<Link
|
||||
key={transaction.hash}
|
||||
href={transaction.url}
|
||||
target="_blank"
|
||||
sx={{ ml: 1 }}
|
||||
text={transaction.hash.slice(0, 6)}
|
||||
/>
|
||||
{transactions && transactions.length > 1 && (
|
||||
<Stack alignItems="center" spacing={1}>
|
||||
<Typography>View the transactions on blockchain:</Typography>
|
||||
{transactions.map(({ url, hash }) => (
|
||||
<Link href={url} target="_blank" sx={{ ml: 1 }} text={hash.slice(0, 6)} key={hash} noIcon />
|
||||
))}
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
{children}
|
||||
<Button variant="contained" sx={{ mt: 3 }} size="large" onClick={onClose}>
|
||||
Finish
|
||||
</Button>
|
||||
</Box>
|
||||
</Modal>
|
||||
</Stack>
|
||||
</ConfirmationModal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -55,7 +55,7 @@ export const UndelegateModal: React.FC<{
|
||||
/>
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<ModalListItem label="Delegation amount" value={`${amount} ${currency}`} divider />
|
||||
<ModalListItem label="Delegation amount:" value={`${amount} ${currency}`} divider />
|
||||
</Box>
|
||||
|
||||
<Typography mb={5} fontSize="smaller" sx={{ color: 'text.primary' }}>
|
||||
|
||||
@@ -13,5 +13,5 @@ const getValue = ({ fee, isLoading, error }: TFeeProps) => {
|
||||
};
|
||||
|
||||
export const ModalFee = ({ fee, isLoading, error }: TFeeProps) => (
|
||||
<ModalListItem label="Estimated fee for this operation" value={getValue({ fee, isLoading, error })} />
|
||||
<ModalListItem label="Estimated fee for this operation:" value={getValue({ fee, isLoading, error })} />
|
||||
);
|
||||
|
||||
@@ -7,20 +7,22 @@ export const ModalListItem: React.FC<{
|
||||
divider?: boolean;
|
||||
hidden?: boolean;
|
||||
strong?: boolean;
|
||||
value: React.ReactNode;
|
||||
value?: React.ReactNode;
|
||||
}> = ({ label, value, hidden, divider, strong }) => (
|
||||
<Box sx={{ display: hidden ? 'none' : 'block' }}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography fontSize="smaller" fontWeight={strong ? 600 : undefined} sx={{ color: 'text.primary' }}>
|
||||
{label}:
|
||||
</Typography>
|
||||
<Typography
|
||||
fontSize="smaller"
|
||||
fontWeight={strong ? 600 : undefined}
|
||||
sx={{ color: 'text.primary', textTransform: 'uppercase' }}
|
||||
>
|
||||
{value}
|
||||
{label}
|
||||
</Typography>
|
||||
{value && (
|
||||
<Typography
|
||||
fontSize="smaller"
|
||||
fontWeight={strong ? 600 : undefined}
|
||||
sx={{ color: 'text.primary', textTransform: 'uppercase' }}
|
||||
>
|
||||
{value}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
{divider && <ModalDivider />}
|
||||
</Box>
|
||||
|
||||
@@ -68,9 +68,16 @@ export const Nav = () => {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
marginLeft: 12,
|
||||
marginRight: 12,
|
||||
}}
|
||||
>
|
||||
<List disablePadding>
|
||||
<List
|
||||
disablePadding
|
||||
sx={{
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{routesSchema
|
||||
.filter(({ mode }) => {
|
||||
if (!mode) {
|
||||
@@ -86,17 +93,35 @@ export const Nav = () => {
|
||||
}
|
||||
})
|
||||
.map(({ Icon, onClick, label, route }) => (
|
||||
<ListItem disableGutters key={label} onClick={onClick} sx={{ cursor: 'pointer' }}>
|
||||
<ListItem
|
||||
disableGutters
|
||||
key={label}
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
py: 2,
|
||||
paddingLeft: 3.5,
|
||||
borderRadius: 1,
|
||||
'&:hover': { backgroundColor: (theme) => theme.palette.nym.nymWallet.hover.background },
|
||||
}}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
height: '20px',
|
||||
minWidth: 30,
|
||||
color: location.pathname === route ? 'primary.main' : 'text.primary',
|
||||
}}
|
||||
>
|
||||
<Icon sx={{ fontSize: 20 }} />
|
||||
<Icon
|
||||
sx={{
|
||||
fontSize: 20,
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
sx={{
|
||||
height: '20px',
|
||||
margin: 0,
|
||||
color: location.pathname === route ? 'primary.main' : 'text.primary',
|
||||
'& .MuiListItemText-primary': {
|
||||
fontSize: 14,
|
||||
|
||||
@@ -40,7 +40,16 @@ export const NetworkSelector = () => {
|
||||
<Button
|
||||
variant="text"
|
||||
color="primary"
|
||||
sx={{ color: 'text.primary' }}
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
fontSize: 14,
|
||||
'&:hover': (t) =>
|
||||
t.palette.mode === 'dark'
|
||||
? {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||
}
|
||||
: {},
|
||||
}}
|
||||
onClick={handleClick}
|
||||
disableElevation
|
||||
endIcon={<ArrowDropDown sx={{ color: (theme) => `1px solid ${theme.palette.text.primary}` }} />}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { SxProps } from '@mui/material';
|
||||
import { Box, Card, CardContent, CardHeader } from '@mui/material';
|
||||
import { styled, Theme } from '@mui/material/styles';
|
||||
import { Title } from './Title';
|
||||
@@ -12,16 +13,17 @@ const CardContentNoPadding = styled(CardContent)(() => ({
|
||||
|
||||
export const NymCard: React.FC<{
|
||||
title: string | React.ReactElement;
|
||||
titleSx?: SxProps;
|
||||
subheader?: string;
|
||||
Action?: React.ReactNode;
|
||||
Icon?: React.ReactNode;
|
||||
noPadding?: boolean;
|
||||
borderless?: boolean;
|
||||
dataTestid?: string;
|
||||
}> = ({ title, subheader, Action, Icon, noPadding, borderless, children, dataTestid }) => (
|
||||
}> = ({ title, titleSx, subheader, Action, Icon, noPadding, borderless, children, dataTestid }) => (
|
||||
<Card variant="outlined" sx={{ overflow: 'auto', ...(borderless && { border: 'none', dropShadow: 'none' }) }}>
|
||||
<CardHeader
|
||||
sx={{ p: 3, color: (theme: Theme) => theme.palette.text.primary }}
|
||||
sx={{ p: 3, color: (theme: Theme) => theme.palette.text.primary, ...titleSx }}
|
||||
title={<Title title={title} Icon={Icon} />}
|
||||
subheader={subheader}
|
||||
data-testid={dataTestid || title}
|
||||
|
||||
@@ -39,11 +39,11 @@ export const SendDetailsModal = ({
|
||||
backdropProps={backdropProps}
|
||||
>
|
||||
<Stack gap={0.5} sx={{ mt: 4 }}>
|
||||
<ModalListItem label="From" value={fromAddress} divider />
|
||||
<ModalListItem label="To" value={toAddress} divider />
|
||||
<ModalListItem label="Amount" value={`${amount?.amount} ${denom.toUpperCase()}`} divider />
|
||||
<ModalListItem label="From:" value={fromAddress} divider />
|
||||
<ModalListItem label="To:" value={toAddress} divider />
|
||||
<ModalListItem label="Amount:" value={`${amount?.amount} ${denom.toUpperCase()}`} divider />
|
||||
<ModalListItem
|
||||
label="Fee for this transaction"
|
||||
label="Fee for this transaction:"
|
||||
value={!fee ? 'n/a' : `${fee.amount?.amount} ${fee.amount?.denom}`}
|
||||
divider
|
||||
/>
|
||||
|
||||
@@ -77,8 +77,8 @@ export const SendInputModal = ({
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={0.5} sx={{ mt: 2 }}>
|
||||
<ModalListItem label="Account balance" value={balance} divider strong />
|
||||
<ModalListItem label="Your address" value={fromAddress} divider />
|
||||
<ModalListItem label="Account balance:" value={balance} divider strong />
|
||||
<ModalListItem label="Your address:" value={fromAddress} divider />
|
||||
<Typography fontSize="smaller" sx={{ color: 'text.primary' }}>
|
||||
Est. fee for this transaction will be show on the next page
|
||||
</Typography>
|
||||
|
||||
@@ -26,26 +26,25 @@ export const ApplicationLayout: React.FC = ({ children }) => {
|
||||
sx={{
|
||||
background: (t) => t.palette.nym.nymWallet.nav.background,
|
||||
overflow: 'auto',
|
||||
py: 3,
|
||||
px: 5,
|
||||
py: 5,
|
||||
}}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Box>
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Box sx={{ ml: 5, mb: 3 }}>
|
||||
<NymWordmark height={14} />
|
||||
</Box>
|
||||
<Nav />
|
||||
</Box>
|
||||
{appVersion && (
|
||||
<Box color="#888" mt={8}>
|
||||
<Box color="#888" ml={5} mt={8} fontSize={14}>
|
||||
Version {appVersion}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Container maxWidth="xl">
|
||||
<Container maxWidth="xl" sx={{ px: { sm: 5 } }}>
|
||||
<AppBar />
|
||||
<Box overflow="auto" sx={{ height: () => `calc(100% - ${theme.spacing(10)})` }}>
|
||||
{children}
|
||||
|
||||
@@ -14,6 +14,7 @@ export const BalanceCard = () => {
|
||||
return (
|
||||
<NymCard
|
||||
title="Balance"
|
||||
titleSx={{ fontSize: 20 }}
|
||||
data-testid="check-balance"
|
||||
borderless
|
||||
Action={<ClientAddress withCopy showEntireAddress />}
|
||||
|
||||
@@ -87,12 +87,12 @@ export const TransferModal = ({ onClose }: { onClose: () => void }) => {
|
||||
) : (
|
||||
<>
|
||||
<ModalListItem
|
||||
label="Unlocked transferrable tokens"
|
||||
label="Unlocked transferrable tokens:"
|
||||
value={`${userBalance.tokenAllocation?.spendable} ${clientDetails?.display_mix_denom.toUpperCase()}`}
|
||||
divider
|
||||
/>
|
||||
<ModalListItem
|
||||
label="Est. fee for this transaction"
|
||||
label="Est. fee for this transaction:"
|
||||
value={fee ? `${fee.amount?.amount} ${fee.amount?.denom}` : <CircularProgress size={15} />}
|
||||
divider
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
import React, { useContext } from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Box, Tooltip, Typography } from '@mui/material';
|
||||
import { format } from 'date-fns';
|
||||
import { AppContext } from '../../../context/main';
|
||||
@@ -21,6 +22,9 @@ export const VestingTimeline: React.FC<{ percentageComplete: number }> = ({ perc
|
||||
userBalance: { currentVestingPeriod, vestingAccountInfo },
|
||||
} = useContext(AppContext);
|
||||
|
||||
const theme = useTheme();
|
||||
const { mode } = theme.palette;
|
||||
|
||||
const nextPeriod =
|
||||
typeof currentVestingPeriod === 'object' && !!vestingAccountInfo?.periods
|
||||
? Number(vestingAccountInfo?.periods[currentVestingPeriod.In + 1]?.start_time)
|
||||
@@ -29,19 +33,31 @@ export const VestingTimeline: React.FC<{ percentageComplete: number }> = ({ perc
|
||||
return (
|
||||
<Box display="flex" flexDirection="column" gap={1} position="relative" width="100%">
|
||||
<svg width="100%" height="12">
|
||||
<rect y="2" width="100%" height="6" rx="0" fill="#E6E6E6" />
|
||||
<rect y="2" width={`${percentageComplete}%`} height="6" rx="0" fill="#121726" />
|
||||
<rect y="2" width="100%" height="6" rx="0" fill="text.dark" />
|
||||
<rect
|
||||
y="2"
|
||||
width={`${percentageComplete}%`}
|
||||
height="6"
|
||||
rx="0"
|
||||
fill={mode === 'light' ? 'text.dark' : theme.palette.success.main}
|
||||
/>
|
||||
{vestingAccountInfo?.periods.map((period, i, arr) => (
|
||||
<Marker
|
||||
position={`${calculateMarkerPosition(arr.length, i)}%`}
|
||||
color={+percentageComplete.toFixed(2) >= calculateMarkerPosition(arr.length, i) ? '#121726' : '#B9B9B9'}
|
||||
color={
|
||||
+percentageComplete.toFixed(2) >= calculateMarkerPosition(arr.length, i)
|
||||
? mode === 'light'
|
||||
? 'text.dark'
|
||||
: theme.palette.success.main
|
||||
: '#B9B9B9'
|
||||
}
|
||||
tooltipText={format(new Date(Number(period.start_time) * 1000), 'HH:mm do MMM yyyy')}
|
||||
key={i}
|
||||
/>
|
||||
))}
|
||||
<Marker
|
||||
position="calc(100% - 4px)"
|
||||
color={percentageComplete === 100 ? '#121726' : '#B9B9B9'}
|
||||
color={percentageComplete === 100 ? (mode === 'light' ? 'text.dark' : theme.palette.success.main) : '#B9B9B9'}
|
||||
tooltipText="End of vesting schedule"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
@@ -65,20 +65,44 @@ const VestingSchedule = () => {
|
||||
))}
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell sx={{ borderBottom: 'none', textTransform: 'uppercase' }}>
|
||||
<TableCell
|
||||
sx={{
|
||||
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
|
||||
borderBottom: 'none',
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
{userBalance.tokenAllocation?.vesting || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '}
|
||||
{clientDetails?.display_mix_denom.toUpperCase()}
|
||||
</TableCell>
|
||||
<TableCell align="left" sx={{ borderBottom: 'none' }}>
|
||||
<TableCell
|
||||
align="left"
|
||||
sx={{
|
||||
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
|
||||
borderBottom: 'none',
|
||||
}}
|
||||
>
|
||||
{vestingPeriod(userBalance.currentVestingPeriod, userBalance.originalVesting?.number_of_periods)}
|
||||
</TableCell>
|
||||
<TableCell sx={{ borderBottom: 'none' }}>
|
||||
<TableCell
|
||||
sx={{
|
||||
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
|
||||
borderBottom: 'none',
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Typography variant="body2">{`${vestedPercentage}%`}</Typography>
|
||||
<VestingTimeline percentageComplete={vestedPercentage} />
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell sx={{ borderBottom: 'none', textTransform: 'uppercase' }} align="right">
|
||||
<TableCell
|
||||
sx={{
|
||||
color: (t) => (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'),
|
||||
borderBottom: 'none',
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
align="right"
|
||||
>
|
||||
{userBalance.tokenAllocation?.vested || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '}
|
||||
{clientDetails?.display_mix_denom.toUpperCase()}
|
||||
</TableCell>
|
||||
|
||||
@@ -20,8 +20,8 @@ export const ConfirmationModal = ({
|
||||
}) => (
|
||||
<SimpleModal header="Bond confirmation" open onOk={onConfirm} okLabel="Confirm" hideCloseIcon onBack={onPrev}>
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<ModalListItem label="Mixnode identity" value={identity} />
|
||||
<ModalListItem label="Amount" value={`${amount.amount} ${amount.denom}`} />
|
||||
<ModalListItem label="Mixnode identity:" value={identity} />
|
||||
<ModalListItem label="Amount:" value={`${amount.amount} ${amount.denom}`} />
|
||||
<ModalFee fee={fee} isLoading={false} />
|
||||
</Box>
|
||||
</SimpleModal>
|
||||
|
||||
@@ -138,7 +138,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
|
||||
setConfirmationModalProps({
|
||||
status: 'success',
|
||||
action: 'delegate',
|
||||
message: 'Delegations can take up to one hour to process',
|
||||
message: 'This operation can take up to one hour to process',
|
||||
...balances,
|
||||
transactions: [
|
||||
{ url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`, hash: tx.transaction_hash },
|
||||
@@ -240,11 +240,9 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
|
||||
|
||||
try {
|
||||
const txs = await claimRewards(identityKey, fee);
|
||||
const bal = await userBalance();
|
||||
setConfirmationModalProps({
|
||||
status: 'success',
|
||||
action: 'redeem',
|
||||
balance: bal?.printable_balance || '-',
|
||||
transactions: txs.map((tx) => ({
|
||||
url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`,
|
||||
hash: tx.transaction_hash,
|
||||
@@ -270,11 +268,9 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
|
||||
|
||||
try {
|
||||
const txs = await compoundRewards(identityKey, fee);
|
||||
const bal = await userBalance();
|
||||
setConfirmationModalProps({
|
||||
status: 'success',
|
||||
action: 'compound',
|
||||
balance: bal?.printable_balance || '-',
|
||||
transactions: txs.map((tx) => ({
|
||||
url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`,
|
||||
hash: tx.transaction_hash,
|
||||
|
||||
Vendored
+8
-5
@@ -64,6 +64,9 @@ declare module '@mui/material/styles' {
|
||||
nav: {
|
||||
background: string;
|
||||
};
|
||||
hover: {
|
||||
background: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,7 +85,7 @@ declare module '@mui/material/styles' {
|
||||
/**
|
||||
* Add anything not palette related to the theme here
|
||||
*/
|
||||
interface NymTheme {}
|
||||
interface NymTheme { }
|
||||
|
||||
/**
|
||||
* This augments the definitions of the MUI Theme with the Nym theme, as well as
|
||||
@@ -90,8 +93,8 @@ declare module '@mui/material/styles' {
|
||||
*
|
||||
* IMPORTANT: only add extensions to the interfaces above, do not modify the lines below
|
||||
*/
|
||||
interface Theme extends NymTheme {}
|
||||
interface ThemeOptions extends Partial<NymTheme> {}
|
||||
interface Palette extends NymPaletteAndNymWalletPalette {}
|
||||
interface PaletteOptions extends NymPaletteAndNymWalletPaletteOptions {}
|
||||
interface Theme extends NymTheme { }
|
||||
interface ThemeOptions extends Partial<NymTheme> { }
|
||||
interface Palette extends NymPaletteAndNymWalletPalette { }
|
||||
interface PaletteOptions extends NymPaletteAndNymWalletPaletteOptions { }
|
||||
}
|
||||
|
||||
@@ -56,12 +56,15 @@ const darkMode: NymPaletteVariant = {
|
||||
nav: {
|
||||
background: '#292E34',
|
||||
},
|
||||
hover: {
|
||||
background: '#36393E',
|
||||
},
|
||||
};
|
||||
|
||||
const lightMode: NymPaletteVariant = {
|
||||
mode: 'light',
|
||||
background: {
|
||||
main: '#E5E5E5',
|
||||
main: '#F4F6F8',
|
||||
paper: '#FFFFFF',
|
||||
warn: '#FFE600',
|
||||
grey: '#F5F5F5',
|
||||
@@ -80,6 +83,9 @@ const lightMode: NymPaletteVariant = {
|
||||
nav: {
|
||||
background: '#FFFFFF',
|
||||
},
|
||||
hover: {
|
||||
background: '#F9F9F9',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -58,6 +58,7 @@ gateway-client = { path="../common/client-libs/gateway-client" }
|
||||
mixnet-contract-common = { path= "../common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nymsphinx = { path="../common/nymsphinx" }
|
||||
task = { path = "../common/task" }
|
||||
topology = { path="../common/topology" }
|
||||
validator-api-requests = { path = "validator-api-requests" }
|
||||
validator-client = { path="../common/client-libs/validator-client", features = ["nymd-client"] }
|
||||
|
||||
@@ -14,6 +14,7 @@ use okapi::openapi3::OpenApi;
|
||||
use rocket::Route;
|
||||
use rocket_okapi::openapi_get_routes_spec;
|
||||
use rocket_okapi::settings::OpenApiSettings;
|
||||
use task::ShutdownListener;
|
||||
|
||||
use rocket::fairing::AdHoc;
|
||||
use serde::Serialize;
|
||||
@@ -252,20 +253,26 @@ impl<C> ValidatorCacheRefresher<C> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn run(&self)
|
||||
pub(crate) async fn run(&self, mut shutdown: ShutdownListener)
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
{
|
||||
let mut interval = time::interval(self.caching_interval);
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if let Err(err) = self.refresh_cache().await {
|
||||
error!("Failed to refresh validator cache - {}", err);
|
||||
} else {
|
||||
// relaxed memory ordering is fine here. worst case scenario network monitor
|
||||
// will just have to wait for an additional backoff to see the change.
|
||||
// And so this will not really incur any performance penalties by setting it every loop iteration
|
||||
self.cache.initialised.store(true, Ordering::Relaxed)
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = interval.tick() => {
|
||||
if let Err(err) = self.refresh_cache().await {
|
||||
error!("Failed to refresh validator cache - {}", err);
|
||||
} else {
|
||||
// relaxed memory ordering is fine here. worst case scenario network monitor
|
||||
// will just have to wait for an additional backoff to see the change.
|
||||
// And so this will not really incur any performance penalties by setting it every loop iteration
|
||||
self.cache.initialised.store(true, Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
_ = shutdown.recv() => {
|
||||
trace!("UpdateHandler: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+50
-12
@@ -31,6 +31,7 @@ use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::{fs, process};
|
||||
use task::ShutdownNotifier;
|
||||
use tokio::sync::Notify;
|
||||
// use validator_client::nymd::SigningNymdClient;
|
||||
// use validator_client::ValidatorClientError;
|
||||
@@ -226,14 +227,44 @@ fn parse_args<'a>() -> ArgMatches<'a> {
|
||||
base_app.get_matches()
|
||||
}
|
||||
|
||||
async fn wait_for_interrupt() {
|
||||
if let Err(e) = tokio::signal::ctrl_c().await {
|
||||
error!(
|
||||
"There was an error while capturing SIGINT - {:?}. We will terminate regardless",
|
||||
e
|
||||
);
|
||||
async fn wait_for_interrupt(mut shutdown: ShutdownNotifier) {
|
||||
wait_for_signal().await;
|
||||
|
||||
log::info!("Sending shutdown");
|
||||
shutdown.signal_shutdown().ok();
|
||||
|
||||
log::info!("Waiting for tasks to finish... (Press ctrl-c to force)");
|
||||
shutdown.wait_for_shutdown().await;
|
||||
|
||||
log::info!("Stopping nym validator API");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn wait_for_signal() {
|
||||
use tokio::signal::unix::{signal, SignalKind};
|
||||
let mut sigterm = signal(SignalKind::terminate()).expect("Failed to setup SIGTERM channel");
|
||||
let mut sigquit = signal(SignalKind::quit()).expect("Failed to setup SIGQUIT channel");
|
||||
|
||||
tokio::select! {
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
log::info!("Received SIGINT");
|
||||
},
|
||||
_ = sigterm.recv() => {
|
||||
log::info!("Received SIGTERM");
|
||||
}
|
||||
_ = sigquit.recv() => {
|
||||
log::info!("Received SIGQUIT");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
async fn wait_for_signal() {
|
||||
tokio::select! {
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
log::info!("Received SIGINT");
|
||||
},
|
||||
}
|
||||
println!("Received SIGINT - the network monitor will terminate now");
|
||||
}
|
||||
|
||||
fn setup_logging() {
|
||||
@@ -547,6 +578,7 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
|
||||
let signing_nymd_client = Client::new_signing(&config);
|
||||
|
||||
let liftoff_notify = Arc::new(Notify::new());
|
||||
let shutdown = ShutdownNotifier::default();
|
||||
|
||||
// let's build our rocket!
|
||||
let rocket = setup_rocket(
|
||||
@@ -569,7 +601,8 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
|
||||
// setup our daily uptime updater. Note that if network monitor is disabled, then we have
|
||||
// no data for the updates and hence we don't need to start it up
|
||||
let uptime_updater = HistoricalUptimeUpdater::new(storage.clone());
|
||||
tokio::spawn(async move { uptime_updater.run().await });
|
||||
let shutdown_listener = shutdown.subscribe();
|
||||
tokio::spawn(async move { uptime_updater.run(shutdown_listener).await });
|
||||
|
||||
// spawn the cache refresher
|
||||
let validator_cache_refresher = ValidatorCacheRefresher::new(
|
||||
@@ -578,7 +611,8 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
|
||||
validator_cache.clone(),
|
||||
Some(storage.clone()),
|
||||
);
|
||||
tokio::spawn(async move { validator_cache_refresher.run().await });
|
||||
let shutdown_listener = shutdown.subscribe();
|
||||
tokio::spawn(async move { validator_cache_refresher.run(shutdown_listener).await });
|
||||
|
||||
// spawn rewarded set updater
|
||||
let mut rewarded_set_updater =
|
||||
@@ -593,11 +627,15 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
|
||||
None,
|
||||
);
|
||||
|
||||
let shutdown_listener = shutdown.subscribe();
|
||||
// spawn our cacher
|
||||
tokio::spawn(async move { validator_cache_refresher.run().await });
|
||||
tokio::spawn(async move { validator_cache_refresher.run(shutdown_listener).await });
|
||||
}
|
||||
|
||||
// launch the rocket!
|
||||
// Rocket handles shutdown on it's own, but its shutdown handling should be incorporated
|
||||
// with that of the rest of the tasks.
|
||||
// Currently it's runtime is forcefully terminated once the validator-api exits.
|
||||
let shutdown_handle = rocket.shutdown();
|
||||
tokio::spawn(rocket.launch());
|
||||
|
||||
@@ -610,12 +648,12 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
|
||||
|
||||
// we're ready to go! spawn the network monitor!
|
||||
let runnables = monitor_builder.build().await;
|
||||
runnables.spawn_tasks();
|
||||
runnables.spawn_tasks(&shutdown);
|
||||
} else {
|
||||
info!("Network monitoring is disabled.");
|
||||
}
|
||||
|
||||
wait_for_interrupt().await;
|
||||
wait_for_interrupt(shutdown).await;
|
||||
shutdown_handle.notify();
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -6,6 +6,7 @@ use crypto::asymmetric::{encryption, identity};
|
||||
use futures::channel::mpsc;
|
||||
use gateway_client::bandwidth::BandwidthController;
|
||||
use std::sync::Arc;
|
||||
use task::ShutdownNotifier;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::contract_cache::ValidatorCache;
|
||||
@@ -131,11 +132,13 @@ impl NetworkMonitorRunnables {
|
||||
// TODO: note, that is not exactly doing what we want, because when
|
||||
// `ReceivedProcessor` is constructed, it already spawns a future
|
||||
// this needs to be refactored!
|
||||
pub(crate) fn spawn_tasks(self) {
|
||||
pub(crate) fn spawn_tasks(self, shutdown: &ShutdownNotifier) {
|
||||
let mut packet_receiver = self.packet_receiver;
|
||||
let mut monitor = self.monitor;
|
||||
tokio::spawn(async move { packet_receiver.run().await });
|
||||
tokio::spawn(async move { monitor.run().await });
|
||||
let shutdown_listener = shutdown.subscribe();
|
||||
tokio::spawn(async move { packet_receiver.run(shutdown_listener).await });
|
||||
let shutdown_listener = shutdown.subscribe();
|
||||
tokio::spawn(async move { monitor.run(shutdown_listener).await });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::storage::ValidatorApiStorage;
|
||||
use log::{debug, error, info};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::process;
|
||||
use task::ShutdownListener;
|
||||
use tokio::time::{sleep, Duration, Instant};
|
||||
|
||||
pub(crate) mod gateway_clients_cache;
|
||||
@@ -296,7 +297,7 @@ impl Monitor {
|
||||
self.test_nonce += 1;
|
||||
}
|
||||
|
||||
pub(crate) async fn run(&mut self) {
|
||||
pub(crate) async fn run(&mut self, mut shutdown: ShutdownListener) {
|
||||
self.received_processor.start_receiving();
|
||||
|
||||
// wait for validator cache to be ready
|
||||
@@ -308,9 +309,13 @@ impl Monitor {
|
||||
.spawn_gateways_pinger(self.gateway_ping_interval);
|
||||
|
||||
let mut run_interval = tokio::time::interval(self.run_interval);
|
||||
loop {
|
||||
run_interval.tick().await;
|
||||
self.test_run().await;
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = run_interval.tick() => self.test_run().await,
|
||||
_ = shutdown.recv() => {
|
||||
trace!("UpdateHandler: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use crypto::asymmetric::identity;
|
||||
use futures::channel::mpsc;
|
||||
use futures::StreamExt;
|
||||
use gateway_client::{AcknowledgementReceiver, MixnetMessageReceiver};
|
||||
use task::ShutdownListener;
|
||||
|
||||
pub(crate) type GatewayClientUpdateSender = mpsc::UnboundedSender<GatewayClientUpdate>;
|
||||
pub(crate) type GatewayClientUpdateReceiver = mpsc::UnboundedReceiver<GatewayClientUpdate>;
|
||||
@@ -55,8 +56,8 @@ impl PacketReceiver {
|
||||
.expect("packet processor seems to have crashed!");
|
||||
}
|
||||
|
||||
pub(crate) async fn run(&mut self) {
|
||||
loop {
|
||||
pub(crate) async fn run(&mut self, mut shutdown: ShutdownListener) {
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
// unwrap here is fine as it can only return a `None` if the PacketSender has died
|
||||
// and if that was the case, then the entire monitor is already in an undefined state
|
||||
@@ -67,6 +68,9 @@ impl PacketReceiver {
|
||||
Some((_gateway_id, message)) = self.gateways_reader.stream_map().next() => {
|
||||
self.process_gateway_messages(message)
|
||||
}
|
||||
_ = shutdown.recv() => {
|
||||
trace!("UpdateHandler: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::node_status_api::models::{
|
||||
use crate::node_status_api::ONE_DAY;
|
||||
use crate::storage::ValidatorApiStorage;
|
||||
use log::error;
|
||||
use task::ShutdownListener;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::time::sleep;
|
||||
|
||||
@@ -67,18 +68,23 @@ impl HistoricalUptimeUpdater {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn run(&self) {
|
||||
loop {
|
||||
// start any updates a day after starting the task so that we would have complete data
|
||||
sleep(ONE_DAY).await;
|
||||
if let Err(err) = self.update_uptimes().await {
|
||||
// normally that would have been a warning rather than an error,
|
||||
// however, in this case it implies some underlying issues with our database
|
||||
// that might affect the entire program
|
||||
error!(
|
||||
"We failed to update daily uptimes of active nodes - {}",
|
||||
err
|
||||
)
|
||||
pub(crate) async fn run(&self, mut shutdown: ShutdownListener) {
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = sleep(ONE_DAY) => {
|
||||
if let Err(err) = self.update_uptimes().await {
|
||||
// normally that would have been a warning rather than an error,
|
||||
// however, in this case it implies some underlying issues with our database
|
||||
// that might affect the entire program
|
||||
error!(
|
||||
"We failed to update daily uptimes of active nodes - {}",
|
||||
err
|
||||
)
|
||||
}
|
||||
}
|
||||
_ = shutdown.recv() => {
|
||||
trace!("UpdateHandler: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user