PR review comments
- Add `historical_node_identity` to `DelegationWithEverything` and populate via `lookup_historical_node_identity` in `delegate.rs` so search works after unbond. - `searchDelegations` searches `historical_node_identity` and guards null/empty `node_identity` with optional chaining. - Acceptance tests: historical identity search, bonded-unbonding vs synthetic branch semantics, empty-identity search safety. - Fix linting
This commit is contained in:
@@ -3,4 +3,8 @@ import type { DecCoin } from "./DecCoin";
|
||||
import type { DelegationEvent } from "./DelegationEvent";
|
||||
import type { NodeCostParams } from "./MixNodeCostParams";
|
||||
|
||||
export type DelegationWithEverything = { owner: string, mix_id: number, node_identity: string, amount: DecCoin, accumulated_by_delegates: DecCoin | null, accumulated_by_operator: DecCoin | null, block_height: bigint, delegated_on_iso_datetime: string | null, cost_params: NodeCostParams | null, avg_uptime_percent: number | null, stake_saturation: string | null, uses_vesting_contract_tokens: boolean, unclaimed_rewards: DecCoin | null, errors: string | null, pending_events: Array<DelegationEvent>, mixnode_is_unbonding: boolean | null, };
|
||||
export type DelegationWithEverything = { owner: string, mix_id: number, node_identity: string,
|
||||
/**
|
||||
* Prior node identity when `node_identity` is synthetic (registry miss after unbond).
|
||||
*/
|
||||
historical_node_identity: string | null, amount: DecCoin, accumulated_by_delegates: DecCoin | null, accumulated_by_operator: DecCoin | null, block_height: bigint, delegated_on_iso_datetime: string | null, cost_params: NodeCostParams | null, avg_uptime_percent: number | null, stake_saturation: string | null, uses_vesting_contract_tokens: boolean, unclaimed_rewards: DecCoin | null, errors: string | null, pending_events: Array<DelegationEvent>, mixnode_is_unbonding: boolean | null, };
|
||||
|
||||
@@ -49,6 +49,8 @@ pub struct DelegationWithEverything {
|
||||
pub owner: String,
|
||||
pub mix_id: NodeId,
|
||||
pub node_identity: String,
|
||||
/// Prior node identity when `node_identity` is synthetic (registry miss after unbond).
|
||||
pub historical_node_identity: Option<String>,
|
||||
pub amount: DecCoin,
|
||||
pub accumulated_by_delegates: Option<DecCoin>,
|
||||
pub accumulated_by_operator: Option<DecCoin>,
|
||||
|
||||
@@ -184,6 +184,39 @@ pub(crate) async fn get_node_information(
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) async fn lookup_historical_node_identity(
|
||||
client: &DirectSigningHttpRpcValidatorClient,
|
||||
node_id: NodeId,
|
||||
error_strings: &mut Vec<String>,
|
||||
) -> Option<String> {
|
||||
match client.nyxd.get_unbonded_nymnode_information(node_id).await {
|
||||
Ok(response) => {
|
||||
if let Some(details) = response.details {
|
||||
return Some(details.identity_key);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let str_err = format!(
|
||||
"Failed to get unbonded nymnode information for node_id = {node_id}. Error: {err}",
|
||||
);
|
||||
log::error!(" <<< {str_err}");
|
||||
error_strings.push(str_err);
|
||||
}
|
||||
}
|
||||
|
||||
match client.nyxd.get_unbonded_mixnode_information(node_id).await {
|
||||
Ok(response) => response.unbonded_info.map(|info| info.identity_key),
|
||||
Err(err) => {
|
||||
let str_err = format!(
|
||||
"Failed to get unbonded mixnode information for mix_id = {node_id}. Error: {err}",
|
||||
);
|
||||
log::error!(" <<< {str_err}");
|
||||
error_strings.push(str_err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: fix later (yeah...)
|
||||
#[allow(deprecated)]
|
||||
#[tauri::command]
|
||||
@@ -425,10 +458,16 @@ pub async fn get_all_mix_delegations(
|
||||
mixnode_is_unbonding
|
||||
);
|
||||
|
||||
let historical_node_identity = match &node_details {
|
||||
Some(node) => Some(node.node_identity.clone()),
|
||||
None => lookup_historical_node_identity(client, d.mix_id, &mut error_strings).await,
|
||||
};
|
||||
|
||||
with_everything.push(DelegationWithEverything {
|
||||
owner: d.owner,
|
||||
mix_id: d.mix_id,
|
||||
node_identity: delegation_node_identity(&node_details, d.mix_id),
|
||||
historical_node_identity,
|
||||
amount: d.amount,
|
||||
block_height: d.height,
|
||||
uses_vesting_contract_tokens,
|
||||
@@ -550,13 +589,10 @@ pub(crate) fn delegation_node_identity(
|
||||
pub(crate) fn delegation_mixnode_is_unbonding(
|
||||
node_details: &Option<NodeInformation>,
|
||||
) -> Option<bool> {
|
||||
node_details.as_ref().map(|m| m.is_unbonding).or_else(|| {
|
||||
if node_details.is_none() {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
match node_details {
|
||||
Some(node) => Some(node.is_unbonding),
|
||||
None => Some(true),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -31,7 +31,13 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
import { format } from 'date-fns';
|
||||
import { Undelegate } from 'src/svg-icons';
|
||||
import { toPercentIntegerString, isFullyUnbondedDelegation, formatUnbondedNodeLabel, shouldHideDelegationFromList, searchDelegations } from 'src/utils';
|
||||
import {
|
||||
toPercentIntegerString,
|
||||
isFullyUnbondedDelegation,
|
||||
formatUnbondedNodeLabel,
|
||||
shouldHideDelegationFromList,
|
||||
searchDelegations,
|
||||
} from 'src/utils';
|
||||
import { InfoTooltip } from '../InfoToolTip';
|
||||
import { DelegationListItemActions, DelegationsActionsMenu } from './DelegationActions';
|
||||
import { PendingDelegationCard } from './PendingDelegationCard';
|
||||
@@ -42,7 +48,7 @@ export type Order = 'asc' | 'desc';
|
||||
type AdditionalTypes = { profit_margin_percent: number; operating_cost: number };
|
||||
export type SortingKeys = keyof AdditionalTypes | keyof DelegationWithEverything;
|
||||
|
||||
const shouldBeFiltered = (item: any): boolean => shouldHideDelegationFromList(item);
|
||||
const shouldBeFiltered = (item: TDelegations[number]): boolean => shouldHideDelegationFromList(item);
|
||||
|
||||
const SORT_FIELD_OPTIONS: { id: SortingKeys; label: string }[] = [
|
||||
{ id: 'delegated_on_iso_datetime', label: 'Delegated on' },
|
||||
@@ -114,9 +120,10 @@ export const DelegationList: FCWithChildren<{
|
||||
|
||||
const searchNeedle = identityFilter.trim().toLowerCase();
|
||||
|
||||
const displayedDelegations = React.useMemo(() => {
|
||||
return searchDelegations(activeDelegations, identityFilter);
|
||||
}, [activeDelegations, identityFilter]);
|
||||
const displayedDelegations = React.useMemo(
|
||||
() => searchDelegations(activeDelegations, identityFilter),
|
||||
[activeDelegations, identityFilter],
|
||||
);
|
||||
|
||||
const activeCount = activeDelegations.length;
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { getAllPendingDelegations, getDelegationSummary } from 'src/requests';
|
||||
import { fetchDelegationSummaryQuery } from './delegationQuery';
|
||||
|
||||
jest.mock('src/utils', () => ({
|
||||
decCoinToDisplay: jest.fn((coin: { amount: string; denom: string }) => coin),
|
||||
}));
|
||||
@@ -7,13 +10,8 @@ jest.mock('src/requests', () => ({
|
||||
getAllPendingDelegations: jest.fn(),
|
||||
}));
|
||||
|
||||
import { fetchDelegationSummaryQuery } from './delegationQuery';
|
||||
import { getAllPendingDelegations, getDelegationSummary } from 'src/requests';
|
||||
|
||||
const mockedGetDelegationSummary = getDelegationSummary as jest.MockedFunction<typeof getDelegationSummary>;
|
||||
const mockedGetAllPendingDelegations = getAllPendingDelegations as jest.MockedFunction<
|
||||
typeof getAllPendingDelegations
|
||||
>;
|
||||
const mockedGetAllPendingDelegations = getAllPendingDelegations as jest.MockedFunction<typeof getAllPendingDelegations>;
|
||||
|
||||
describe('fetchDelegationSummaryQuery', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -37,6 +37,7 @@ let mockDelegations: DelegationWithEverything[] = [
|
||||
uses_vesting_contract_tokens: false,
|
||||
pending_events: [],
|
||||
mixnode_is_unbonding: false,
|
||||
historical_node_identity: null,
|
||||
errors: null,
|
||||
},
|
||||
{
|
||||
@@ -61,6 +62,7 @@ let mockDelegations: DelegationWithEverything[] = [
|
||||
uses_vesting_contract_tokens: true,
|
||||
pending_events: [],
|
||||
mixnode_is_unbonding: false,
|
||||
historical_node_identity: null,
|
||||
errors: null,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -38,9 +38,11 @@ export function searchDelegations(
|
||||
if (!needle) {
|
||||
return delegations;
|
||||
}
|
||||
return delegations.filter(
|
||||
(d) => d.node_identity.toLowerCase().includes(needle) || String(d.mix_id).includes(needle),
|
||||
);
|
||||
return delegations.filter((d) => {
|
||||
const identity = d.node_identity?.toLowerCase() ?? '';
|
||||
const historical = d.historical_node_identity?.toLowerCase() ?? '';
|
||||
return identity.includes(needle) || historical.includes(needle) || String(d.mix_id).includes(needle);
|
||||
});
|
||||
}
|
||||
|
||||
export function isUndelegateOnlyDelegation(item: DelegationWithEverything): boolean {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { DelegationWithEverything } from '@nymproject/types';
|
||||
import { formatDelegationNodeIdentityForDisplay } from './delegationIdentity';
|
||||
import { formatDelegationNodeIdentityForDisplay, isFullyUnbondedDelegation } from './delegationIdentity';
|
||||
import {
|
||||
filterVisibleDelegations,
|
||||
isUndelegateOnlyDelegation,
|
||||
@@ -48,6 +48,45 @@ describe('unbonded delegation wallet visibility acceptance', () => {
|
||||
expect(searchDelegations(visible, EXAMPLE_HISTORICAL_NODE_IDENTITY)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does not throw when search runs on unfiltered rows with empty node_identity', () => {
|
||||
const legacyRow = buildLegacyHiddenUnbondedWalletDelegation();
|
||||
|
||||
expect(searchDelegations([legacyRow], String(EXAMPLE_UNBONDED_MIX_ID))).toHaveLength(1);
|
||||
expect(searchDelegations([legacyRow], 'nonexistent-needle')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('finds the row by historical identity when the backend preserved it', () => {
|
||||
const fixedRow = buildFixedUnbondedWalletDelegation({
|
||||
historicalNodeIdentity: EXAMPLE_HISTORICAL_NODE_IDENTITY,
|
||||
});
|
||||
const visible = filterVisibleDelegations([fixedRow]) as DelegationWithEverything[];
|
||||
|
||||
expect(searchDelegations(visible, EXAMPLE_HISTORICAL_NODE_IDENTITY)).toHaveLength(1);
|
||||
expect(searchDelegations(visible, String(EXAMPLE_UNBONDED_MIX_ID))).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('keeps bonded-but-unbonding rows linked and not undelegate-only', () => {
|
||||
const bondedUnbondingRow: DelegationWithEverything = {
|
||||
...buildFixedUnbondedWalletDelegation(),
|
||||
node_identity: EXAMPLE_HISTORICAL_NODE_IDENTITY,
|
||||
historical_node_identity: EXAMPLE_HISTORICAL_NODE_IDENTITY,
|
||||
mixnode_is_unbonding: true,
|
||||
};
|
||||
|
||||
expect(isFullyUnbondedDelegation(bondedUnbondingRow)).toBe(false);
|
||||
expect(isUndelegateOnlyDelegation(bondedUnbondingRow)).toBe(false);
|
||||
expect(searchDelegations([bondedUnbondingRow], EXAMPLE_HISTORICAL_NODE_IDENTITY)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('treats synthetic registry-miss rows as undelegate-only even with historical identity', () => {
|
||||
const syntheticRow = buildFixedUnbondedWalletDelegation({
|
||||
historicalNodeIdentity: EXAMPLE_HISTORICAL_NODE_IDENTITY,
|
||||
});
|
||||
|
||||
expect(isFullyUnbondedDelegation(syntheticRow)).toBe(true);
|
||||
expect(isUndelegateOnlyDelegation(syntheticRow)).toBe(true);
|
||||
});
|
||||
|
||||
it('formats undelegate confirmation copy without exposing the synthetic prefix', () => {
|
||||
const fixedRow = buildFixedUnbondedWalletDelegation();
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ type UnbondedWalletDelegationOptions = {
|
||||
amount?: string;
|
||||
blockHeight?: bigint;
|
||||
owner?: string;
|
||||
historicalNodeIdentity?: string | null;
|
||||
};
|
||||
|
||||
export function buildFixedUnbondedWalletDelegation(
|
||||
@@ -37,6 +38,7 @@ export function buildFixedUnbondedWalletDelegation(
|
||||
uses_vesting_contract_tokens: false,
|
||||
pending_events: [],
|
||||
mixnode_is_unbonding: true,
|
||||
historical_node_identity: options.historicalNodeIdentity ?? null,
|
||||
errors: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export type DelegationWithEverything = {
|
||||
owner: string;
|
||||
mix_id: number;
|
||||
node_identity: string;
|
||||
historical_node_identity: string | null;
|
||||
amount: DecCoin;
|
||||
accumulated_by_delegates: DecCoin | null;
|
||||
accumulated_by_operator: DecCoin | null;
|
||||
|
||||
Reference in New Issue
Block a user