Merge pull request #4565 from nymtech/bugfix/delegations

Bug fix: wallet delegations list is empty when RPC node doesn't hold block
This commit is contained in:
Tommy Verrall
2024-05-07 09:07:04 +01:00
committed by GitHub
11 changed files with 203 additions and 31 deletions
+3 -1
View File
@@ -50,7 +50,7 @@ pub struct DelegationWithEverything {
pub accumulated_by_delegates: Option<DecCoin>,
pub accumulated_by_operator: Option<DecCoin>,
pub block_height: u64,
pub delegated_on_iso_datetime: String,
pub delegated_on_iso_datetime: Option<String>,
pub cost_params: Option<MixNodeCostParams>,
pub avg_uptime_percent: Option<u8>,
@@ -60,6 +60,8 @@ pub struct DelegationWithEverything {
pub uses_vesting_contract_tokens: bool,
pub unclaimed_rewards: Option<DecCoin>,
pub errors: Option<String>,
// DEPRECATED, IF POSSIBLE TRY TO DISCONTINUE USE OF IT!
pub pending_events: Vec<DelegationEvent>,
pub mixnode_is_unbonding: Option<bool>,
+3 -1
View File
@@ -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",
+1
View File
@@ -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"] }
@@ -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<DelegationWithEverything> = Vec::with_capacity(delegations.len());
for delegation in delegations {
let d = Delegation::from_mixnet_contract(delegation, reg)?;
let mut error_strings: Vec<String> = 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);
@@ -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 ? (
'-'
) : (
<Link
target="_blank"
href={`${explorerUrl}/network-components/mixnode/${item.mix_id}`}
text={`${item.node_identity.slice(0, 6)}...${item.node_identity.slice(-6)}`}
color="text.primary"
noIcon
/>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{item.errors && (
<Tooltip title="Open to view a list of errors that occurred">
<IconButton
sx={{ mr: 1 }}
size="small"
onClick={() => setDelegationItemErrors({ nodeId: item.node_identity, errors: item.errors! })}
>
<WarningAmberOutlined color="warning" fontSize="small" />
</IconButton>
</Tooltip>
)}
<Link
target="_blank"
href={`${explorerUrl}/network-components/mixnode/${item.mix_id}`}
text={`${item.node_identity.slice(0, 6)}...${item.node_identity.slice(-6)}`}
color="text.primary"
noIcon
/>
</Box>
)}
</TableCell>
<TableCell sx={{ color: 'inherit' }}>
@@ -70,7 +85,7 @@ export const DelegationItem = ({
</TableCell>
<TableCell sx={{ color: 'inherit' }}>{getStakeSaturation(item)}</TableCell>
<TableCell sx={{ color: 'inherit' }}>
{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')}
</TableCell>
<TableCell sx={{ color: 'inherit' }}>
<Typography style={{ textTransform: 'uppercase', fontSize: 'inherit' }}>
@@ -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,
},
];
@@ -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<Order>('asc');
const [orderBy, setOrderBy] = React.useState<SortingKeys>('delegated_on_iso_datetime');
const { delegationItemErrors, setDelegationItemErrors } = useDelegationContext();
const handleRequestSort = (_: React.MouseEvent<unknown>, property: any) => {
const isAsc = orderBy === property && order === 'asc';
setOrder(isAsc ? 'desc' : 'asc');
@@ -105,6 +108,12 @@ export const DelegationList: FCWithChildren<{
return (
<TableContainer>
{isLoading && <LoadingModal text="Please wait. Refreshing..." />}
<ErrorModal
open={Boolean(delegationItemErrors)}
title={`Delegation errors for Node ID ${delegationItemErrors?.nodeId || 'unknown'}`}
message={delegationItemErrors?.errors || 'oops'}
onClose={() => setDelegationItemErrors(undefined)}
/>
<Table sx={{ width: '100%' }}>
<EnhancedTableHead order={order} orderBy={orderBy} onRequestSort={handleRequestSort} />
<TableBody>
+15 -1
View File
@@ -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<TransactionExecuteResult>;
undelegate: (mix_id: number, fee?: Fee) => Promise<TransactionExecuteResult>;
undelegateVesting: (mix_id: number) => Promise<TransactionExecuteResult>;
setDelegationItemErrors: (data: { nodeId: string; errors: string } | undefined) => void;
};
export type TDelegationTransaction = {
@@ -61,6 +63,7 @@ export const DelegationContext = createContext<TDelegationContext>({
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<undefined | TDelegations>();
const [totalDelegations, setTotalDelegations] = useState<undefined | string>();
const [totalRewards, setTotalRewards] = useState<undefined | string>();
@@ -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 <DelegationContext.Provider value={memoizedValue}>{children}</DelegationContext.Provider>;
@@ -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<string>();
const [delegations, setDelegations] = useState<undefined | DelegationWithEverything[]>();
const [totalDelegations, setTotalDelegations] = useState<undefined | string>();
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,
+1 -1
View File
@@ -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);
}, []);
@@ -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<DelegationEvent>;
mixnode_is_unbonding: boolean | null;
errors: string | null;
}