diff --git a/common/types/src/delegation.rs b/common/types/src/delegation.rs index 550f17a261..e98c56fd40 100644 --- a/common/types/src/delegation.rs +++ b/common/types/src/delegation.rs @@ -50,7 +50,7 @@ pub struct DelegationWithEverything { pub accumulated_by_delegates: Option, pub accumulated_by_operator: Option, pub block_height: u64, - pub delegated_on_iso_datetime: String, + pub delegated_on_iso_datetime: Option, pub cost_params: Option, pub avg_uptime_percent: Option, @@ -60,6 +60,8 @@ pub struct DelegationWithEverything { pub uses_vesting_contract_tokens: bool, pub unclaimed_rewards: Option, + pub errors: Option, + // DEPRECATED, IF POSSIBLE TRY TO DISCONTINUE USE OF IT! pub pending_events: Vec, pub mixnode_is_unbonding: Option, diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 9573b2af63..2418e313a1 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3005,6 +3005,7 @@ dependencies = [ "schemars", "serde", "tendermint", + "time", ] [[package]] @@ -3442,7 +3443,7 @@ dependencies = [ [[package]] name = "nym_wallet" -version = "1.2.12" +version = "1.2.13" dependencies = [ "async-trait", "base64 0.13.1", @@ -3476,6 +3477,7 @@ dependencies = [ "serde_json", "serde_repr", "strum 0.23.0", + "tap", "tauri", "tauri-build", "tauri-codegen", diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 6d2dc3185d..3c00c10253 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -37,6 +37,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_repr = "0.1" strum = { version = "0.23", features = ["derive"] } +tap = "1" tauri = { version = "=1.2.3", features = ["clipboard-all", "shell-open", "updater", "window-maximize", "window-print"] } #tendermint-rpc = "0.23.0" time = { version = "0.3.17", features = ["local-offset"] } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs index 1a47f7bee9..7a705f6d03 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs @@ -4,6 +4,7 @@ use crate::error::BackendError; use crate::state::WalletState; use crate::vesting::delegate::vesting_undelegate_from_mixnode; +use nym_mixnet_contract_common::mixnode::StakeSaturationResponse; use nym_mixnet_contract_common::MixId; use nym_types::currency::DecCoin; use nym_types::delegation::{Delegation, DelegationWithEverything, DelegationsSummaryResponse}; @@ -18,6 +19,7 @@ use nym_validator_client::nyxd::contract_traits::{ MixnetQueryClient, MixnetSigningClient, NymContractsProvider, PagedMixnetQueryClient, }; use nym_validator_client::nyxd::Fee; +use tap::TapFallible; #[tauri::command] pub async fn get_pending_delegation_events( @@ -165,10 +167,21 @@ pub async fn get_all_mix_delegations( .expect("vesting contract address is not available"); log::info!(" >>> Get delegations"); - let delegations = client.nyxd.get_all_delegator_delegations(&address).await?; + let delegations = client + .nyxd + .get_all_delegator_delegations(&address) + .await + .tap_err(|err| { + log::error!(" <<< Failed to get delegations. Error: {}", err); + })?; log::info!(" <<< {} delegations", delegations.len()); - let pending_events_for_account = get_pending_delegation_events(state.clone()).await?; + let pending_events_for_account = + get_pending_delegation_events(state.clone()) + .await + .tap_err(|err| { + log::error!(" <<< Failed to get pending delegations. Error: {}", err); + })?; log::info!( " <<< {} pending delegation events for account", @@ -178,7 +191,16 @@ pub async fn get_all_mix_delegations( let mut with_everything: Vec = Vec::with_capacity(delegations.len()); for delegation in delegations { - let d = Delegation::from_mixnet_contract(delegation, reg)?; + let mut error_strings: Vec = vec![]; + + let d = Delegation::from_mixnet_contract(delegation.clone(), reg).tap_err(|err| { + log::error!( + " <<< Failed to get delegation for mix id {} from contract. Error: {}", + delegation.mix_id, + err + ); + })?; + let uses_vesting_contract_tokens = d .proxy .as_ref() @@ -194,7 +216,15 @@ pub async fn get_all_mix_delegations( let mixnode = client .nyxd .get_mixnode_details(d.mix_id) - .await? + .await + .tap_err(|err| { + let str_err = format!( + "Failed to get mixnode details for mix_id = {}. Error: {}", + d.mix_id, err + ); + log::error!(" <<< {}", str_err); + error_strings.push(str_err); + })? .mixnode_details; let accumulated_by_operator = mixnode @@ -202,14 +232,32 @@ pub async fn get_all_mix_delegations( .map(|m| { guard.display_coin_from_base_decimal(&base_mix_denom, m.rewarding_details.operator) }) - .transpose()?; + .transpose() + .tap_err(|err| { + let str_err = format!( + "Failed to get operator rewards as a display coin for mix_id = {}. Error: {}", + d.mix_id, err + ); + log::error!(" <<< {}", str_err); + error_strings.push(str_err); + }) + .unwrap_or_default(); let accumulated_by_delegates = mixnode .as_ref() .map(|m| { guard.display_coin_from_base_decimal(&base_mix_denom, m.rewarding_details.delegates) }) - .transpose()?; + .transpose() + .tap_err(|err| { + let str_err = format!( + "Failed to get delegator rewards as a display coin for mix_id = {}. Error: {}", + d.mix_id, err + ); + log::error!(" <<< {}", str_err); + error_strings.push(str_err); + }) + .unwrap_or_default(); let cost_params = mixnode .as_ref() @@ -219,19 +267,48 @@ pub async fn get_all_mix_delegations( reg, ) }) - .transpose()?; + .transpose() + .tap_err(|err| { + let str_err = format!( + "Failed to mixnode cost params for mix_id = {}. Error: {}", + d.mix_id, err + ); + log::error!(" <<< {}", str_err); + error_strings.push(str_err); + }) + .unwrap_or_default(); log::trace!(" >>> Get accumulated rewards: address = {}", address); let pending_reward = client .nyxd .get_pending_delegator_reward(&address, d.mix_id, d.proxy.clone()) - .await?; + .await + .tap_err(|err| { + let str_err = format!( + "Failed to get accumulated rewards for mix_id = {}. Error: {}", + d.mix_id, err + ); + log::error!(" <<< {}", str_err); + error_strings.push(str_err); + }) + .unwrap_or_default(); let accumulated_rewards = match &pending_reward.amount_earned { Some(reward) => { - let amount = guard.attempt_convert_to_display_dec_coin(reward.clone().into())?; - log::trace!(" <<< rewards = {:?}, amount = {}", pending_reward, amount); - Some(amount) + let amount = guard + .attempt_convert_to_display_dec_coin(reward.clone().into()) + .tap_err(|err| { + let str_err = format!("Failed to get convert reward to a display coin for mix_id = {}. Error: {}", d.mix_id, err); + log::error!(" <<< {}", str_err); + error_strings.push(str_err); + }) + .ok(); + log::trace!( + " <<< rewards = {:?}, amount = {:?}", + pending_reward, + amount + ); + amount } None => { log::trace!(" <<< no rewards waiting"); @@ -240,7 +317,23 @@ pub async fn get_all_mix_delegations( }; log::trace!(" >>> Get stake saturation: mix_id = {}", d.mix_id); - let stake_saturation = client.nyxd.get_mixnode_stake_saturation(d.mix_id).await?; + let stake_saturation = client + .nyxd + .get_mixnode_stake_saturation(d.mix_id) + .await + .tap_err(|err| { + let str_err = format!( + "Failed to get stake saturation for mix_id = {}. Error: {}", + d.mix_id, err + ); + log::error!(" <<< {}", str_err); + error_strings.push(str_err); + }) + .unwrap_or(StakeSaturationResponse { + mix_id: d.mix_id, + uncapped_saturation: None, + current_saturation: None, + }); log::trace!(" <<< {:?}", stake_saturation); log::trace!( @@ -251,6 +344,14 @@ pub async fn get_all_mix_delegations( .nym_api .get_mixnode_avg_uptime(d.mix_id) .await + .tap_err(|err| { + let str_err = format!( + "Failed to get average uptime percentage for mix_id = {}. Error: {}", + d.mix_id, err + ); + log::error!(" <<< {}", str_err); + error_strings.push(str_err); + }) .ok() .map(|r| r.avg_uptime); log::trace!(" <<< {:?}", avg_uptime_percent); @@ -262,8 +363,13 @@ pub async fn get_all_mix_delegations( let timestamp = client .nyxd .get_block_timestamp(Some(d.height as u32)) - .await?; - let delegated_on_iso_datetime = timestamp.to_rfc3339(); + .await + .tap_err(|err| { + let str_err = format!("Failed to get block timestamp for height = {} for delegation to mix_id = {}. Error: {}", d.height, d.mix_id, err); + log::error!(" <<< {}", str_err); + error_strings.push(str_err); + }).ok(); + let delegated_on_iso_datetime = timestamp.map(|ts| ts.to_rfc3339()); log::trace!( " <<< timestamp = {:?}, delegated_on_iso_datetime = {:?}", timestamp, @@ -301,6 +407,11 @@ pub async fn get_all_mix_delegations( unclaimed_rewards: accumulated_rewards, pending_events, mixnode_is_unbonding, + errors: if error_strings.is_empty() { + None + } else { + Some(error_strings.join("\n")) + }, }) } log::trace!("<<< {:?}", with_everything); diff --git a/nym-wallet/src/components/Delegation/DelegationItem.tsx b/nym-wallet/src/components/Delegation/DelegationItem.tsx index c405c18d83..96db0c3854 100644 --- a/nym-wallet/src/components/Delegation/DelegationItem.tsx +++ b/nym-wallet/src/components/Delegation/DelegationItem.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import { Chip, IconButton, TableCell, TableRow, Tooltip, Typography } from '@mui/material'; +import { Box, Chip, IconButton, TableCell, TableRow, Tooltip, Typography } from '@mui/material'; import { Link } from '@nymproject/react/link/Link'; import { decimalToPercentage, DelegationWithEverything } from '@nymproject/types'; -import { LockOutlined } from '@mui/icons-material'; -import { isDelegation } from 'src/context/delegations'; +import { LockOutlined, WarningAmberOutlined } from '@mui/icons-material'; +import { isDelegation, useDelegationContext } from 'src/context/delegations'; import { toPercentIntegerString } from 'src/utils'; import { format } from 'date-fns'; import { Undelegate } from 'src/svg-icons'; @@ -29,6 +29,8 @@ export const DelegationItem = ({ nodeIsUnbonded: boolean; onItemActionClick?: (item: DelegationWithEverything, action: DelegationListItemActions) => void; }) => { + const { setDelegationItemErrors } = useDelegationContext(); + const operatingCost = isDelegation(item) && item.cost_params?.interval_operating_cost; const tooltipText = () => { @@ -45,13 +47,26 @@ export const DelegationItem = ({ {nodeIsUnbonded ? ( '-' ) : ( - + + {item.errors && ( + + setDelegationItemErrors({ nodeId: item.node_identity, errors: item.errors! })} + > + + + + )} + + )} @@ -70,7 +85,7 @@ export const DelegationItem = ({ {getStakeSaturation(item)} - {format(new Date(item.delegated_on_iso_datetime), 'dd/MM/yyyy')} + {item.delegated_on_iso_datetime && format(new Date(item.delegated_on_iso_datetime), 'dd/MM/yyyy')} diff --git a/nym-wallet/src/components/Delegation/DelegationList.stories.tsx b/nym-wallet/src/components/Delegation/DelegationList.stories.tsx index 9683d268ca..4b24a3dcfe 100644 --- a/nym-wallet/src/components/Delegation/DelegationList.stories.tsx +++ b/nym-wallet/src/components/Delegation/DelegationList.stories.tsx @@ -34,6 +34,7 @@ export const items: DelegationWithEverything[] = [ uses_vesting_contract_tokens: false, pending_events: [], mixnode_is_unbonding: true, + errors: null, }, { mix_id: 2, @@ -57,6 +58,7 @@ export const items: DelegationWithEverything[] = [ uses_vesting_contract_tokens: true, pending_events: [], mixnode_is_unbonding: true, + errors: null, }, { mix_id: 3, @@ -80,6 +82,7 @@ export const items: DelegationWithEverything[] = [ uses_vesting_contract_tokens: true, pending_events: [], mixnode_is_unbonding: true, + errors: null, }, { mix_id: 4, @@ -103,6 +106,7 @@ export const items: DelegationWithEverything[] = [ uses_vesting_contract_tokens: true, pending_events: [], mixnode_is_unbonding: true, + errors: null, }, { mix_id: 5, @@ -126,6 +130,7 @@ export const items: DelegationWithEverything[] = [ uses_vesting_contract_tokens: true, pending_events: [], mixnode_is_unbonding: true, + errors: null, }, { mix_id: 6, @@ -149,6 +154,7 @@ export const items: DelegationWithEverything[] = [ uses_vesting_contract_tokens: true, pending_events: [], mixnode_is_unbonding: true, + errors: null, }, { mix_id: 7, @@ -172,6 +178,7 @@ export const items: DelegationWithEverything[] = [ uses_vesting_contract_tokens: false, pending_events: [], mixnode_is_unbonding: true, + errors: null, }, { mix_id: 8, @@ -195,6 +202,7 @@ export const items: DelegationWithEverything[] = [ uses_vesting_contract_tokens: true, pending_events: [], mixnode_is_unbonding: true, + errors: null, }, { mix_id: 9, @@ -218,6 +226,7 @@ export const items: DelegationWithEverything[] = [ uses_vesting_contract_tokens: true, pending_events: [], mixnode_is_unbonding: true, + errors: null, }, { mix_id: 10, @@ -241,6 +250,7 @@ export const items: DelegationWithEverything[] = [ uses_vesting_contract_tokens: true, pending_events: [], mixnode_is_unbonding: true, + errors: null, }, { mix_id: 11, @@ -264,6 +274,7 @@ export const items: DelegationWithEverything[] = [ uses_vesting_contract_tokens: true, pending_events: [], mixnode_is_unbonding: true, + errors: null, }, { mix_id: 12, @@ -287,6 +298,7 @@ export const items: DelegationWithEverything[] = [ uses_vesting_contract_tokens: true, pending_events: [], mixnode_is_unbonding: true, + errors: null, }, ]; diff --git a/nym-wallet/src/components/Delegation/DelegationList.tsx b/nym-wallet/src/components/Delegation/DelegationList.tsx index 0396506a37..ebda436025 100644 --- a/nym-wallet/src/components/Delegation/DelegationList.tsx +++ b/nym-wallet/src/components/Delegation/DelegationList.tsx @@ -8,7 +8,8 @@ import { DelegationListItemActions } from './DelegationActions'; import { DelegationItem } from './DelegationItem'; import { PendingDelegationItem } from './PendingDelegationItem'; import { LoadingModal } from '../Modals/LoadingModal'; -import { isDelegation, isPendingDelegation, TDelegations } from '../../context/delegations'; +import { isDelegation, isPendingDelegation, TDelegations, useDelegationContext } from '../../context/delegations'; +import { ErrorModal } from '../Modals/ErrorModal'; export type Order = 'asc' | 'desc'; type AdditionalTypes = { profit_margin_percent: number; operating_cost: number }; @@ -94,6 +95,8 @@ export const DelegationList: FCWithChildren<{ const [order, setOrder] = React.useState('asc'); const [orderBy, setOrderBy] = React.useState('delegated_on_iso_datetime'); + const { delegationItemErrors, setDelegationItemErrors } = useDelegationContext(); + const handleRequestSort = (_: React.MouseEvent, property: any) => { const isAsc = orderBy === property && order === 'asc'; setOrder(isAsc ? 'desc' : 'asc'); @@ -105,6 +108,12 @@ export const DelegationList: FCWithChildren<{ return ( {isLoading && } + setDelegationItemErrors(undefined)} + /> diff --git a/nym-wallet/src/context/delegations.tsx b/nym-wallet/src/context/delegations.tsx index 5f235e0703..d77e52467e 100644 --- a/nym-wallet/src/context/delegations.tsx +++ b/nym-wallet/src/context/delegations.tsx @@ -20,6 +20,7 @@ import { decCoinToDisplay } from 'src/utils'; import { Console } from 'src/utils/console'; export type TDelegationContext = { + delegationItemErrors?: { nodeId: string; errors: string }; isLoading: boolean; delegations?: TDelegations; pendingDelegations?: WrappedDelegationEvent[]; @@ -34,6 +35,7 @@ export type TDelegationContext = { ) => Promise; undelegate: (mix_id: number, fee?: Fee) => Promise; undelegateVesting: (mix_id: number) => Promise; + setDelegationItemErrors: (data: { nodeId: string; errors: string } | undefined) => void; }; export type TDelegationTransaction = { @@ -61,6 +63,7 @@ export const DelegationContext = createContext({ undelegateVesting: () => { throw new Error('Not implemented'); }, + setDelegationItemErrors: () => undefined, }); export const DelegationContextProvider: FC<{ @@ -69,6 +72,7 @@ export const DelegationContextProvider: FC<{ // eslint-disable-next-line @typescript-eslint/no-unused-vars }> = ({ network, children }) => { const [isLoading, setIsLoading] = useState(true); + const [delegationItemErrors, setDelegationItemErrors] = useState<{ nodeId: string; errors: string }>(); const [delegations, setDelegations] = useState(); const [totalDelegations, setTotalDelegations] = useState(); const [totalRewards, setTotalRewards] = useState(); @@ -130,6 +134,7 @@ export const DelegationContextProvider: FC<{ const memoizedValue = useMemo( () => ({ + delegationItemErrors, isLoading, delegations, pendingDelegations, @@ -137,11 +142,20 @@ export const DelegationContextProvider: FC<{ totalRewards, totalDelegationsAndRewards, refresh, + setDelegationItemErrors, addDelegation, undelegate: undelegateFromMixnode, undelegateVesting: vestingUndelegateFromMixnode, }), - [isLoading, delegations, pendingDelegations, totalDelegations, totalRewards, totalDelegationsAndRewards], + [ + isLoading, + delegations, + delegationItemErrors, + pendingDelegations, + totalDelegations, + totalRewards, + totalDelegationsAndRewards, + ], ); return {children}; diff --git a/nym-wallet/src/context/mocks/delegations.tsx b/nym-wallet/src/context/mocks/delegations.tsx index 2a9609b022..0c984d7369 100644 --- a/nym-wallet/src/context/mocks/delegations.tsx +++ b/nym-wallet/src/context/mocks/delegations.tsx @@ -37,6 +37,7 @@ let mockDelegations: DelegationWithEverything[] = [ uses_vesting_contract_tokens: false, pending_events: [], mixnode_is_unbonding: false, + errors: null, }, { mix_id: 5678, @@ -60,6 +61,7 @@ let mockDelegations: DelegationWithEverything[] = [ uses_vesting_contract_tokens: true, pending_events: [], mixnode_is_unbonding: false, + errors: null, }, ]; @@ -69,6 +71,7 @@ export const MockDelegationContextProvider: FCWithChildren = ({ children }) => { const [error, setError] = useState(); const [delegations, setDelegations] = useState(); const [totalDelegations, setTotalDelegations] = useState(); + const [delegationItemErrors, setDelegationItemErrors] = useState<{ nodeId: string; errors: string }>(); const triggerStateUpdate = () => setTrigger(new Date()); @@ -230,6 +233,8 @@ export const MockDelegationContextProvider: FCWithChildren = ({ children }) => { const memoizedValue = useMemo( () => ({ + delegationItemErrors, + setDelegationItemErrors, isLoading, error, delegations, diff --git a/nym-wallet/src/pages/delegation/index.tsx b/nym-wallet/src/pages/delegation/index.tsx index 64a24b41db..3e985a3526 100644 --- a/nym-wallet/src/pages/delegation/index.tsx +++ b/nym-wallet/src/pages/delegation/index.tsx @@ -101,7 +101,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { // Refresh the rewards and delegations periodically when page is mounted useEffect(() => { - const timer = setInterval(refreshWithIntervalUpdate, 1 * 60 * 1000); // every 1 minute + const timer = setInterval(refreshWithIntervalUpdate, 5 * 60 * 1000); // every 5 minutes return () => clearInterval(timer); }, []); diff --git a/ts-packages/types/src/types/rust/DelegationWithEverything.ts b/ts-packages/types/src/types/rust/DelegationWithEverything.ts index ecf949c5ae..7071f291dd 100644 --- a/ts-packages/types/src/types/rust/DelegationWithEverything.ts +++ b/ts-packages/types/src/types/rust/DelegationWithEverything.ts @@ -11,7 +11,7 @@ export interface DelegationWithEverything { accumulated_by_delegates: DecCoin | null; accumulated_by_operator: DecCoin | null; block_height: bigint; - delegated_on_iso_datetime: string; + delegated_on_iso_datetime: string | null; cost_params: MixNodeCostParams | null; avg_uptime_percent: number | null; stake_saturation: string | null; @@ -19,4 +19,5 @@ export interface DelegationWithEverything { unclaimed_rewards: DecCoin | null; pending_events: Array; mixnode_is_unbonding: boolean | null; + errors: string | null; }