Merge pull request #255 from cosmos/load-all-validators

Load unbonded and unbonding validators too
This commit is contained in:
Simon Warta 2025-01-15 11:39:40 +01:00 committed by GitHub
commit ea6a6c904d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 97 additions and 29 deletions

View File

@ -12,24 +12,43 @@ import {
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { useChains } from "@/context/ChainsContext";
import { cn } from "@/lib/utils";
import { Validator } from "cosmjs-types/cosmos/staking/v1beta1/staking";
import { Check, ChevronsUpDown } from "lucide-react";
import { useState } from "react";
interface SelectValidatorProps {
readonly validatorAddress: string;
readonly selectedValidatorAddress: string;
readonly setValidatorAddress: (validatorAddress: string) => void;
}
export default function SelectValidator({
validatorAddress,
selectedValidatorAddress,
setValidatorAddress,
}: SelectValidatorProps) {
const {
validatorState: { validators },
validatorState: {
validators: { bonded, unbonding, unbonded },
},
} = useChains();
const [open, setOpen] = useState(false);
const [searchText, setSearchText] = useState("");
// The list of validators includes unbonding and unbonded validators in order to
// be able to do undelegates and redelegates from jailed validators as well as delegate
// to validators who are not yet active.
//
// If this list becomes too long due to spam registrations, we can try to do some
// reasonable filtering here.
const validators = [...bonded, ...unbonding, ...unbonded];
function displayValidator(val: Validator): string {
return val.description.moniker + (val.jailed ? " (jailed)" : "");
}
const selectedValidator = validators.find(
(validatorItem) => selectedValidatorAddress === validatorItem.operatorAddress,
);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
@ -39,9 +58,10 @@ export default function SelectValidator({
aria-expanded={open}
className="mb-4 w-full max-w-[300px] justify-between border-white bg-fuchsia-900 hover:bg-fuchsia-900"
>
{validatorAddress
? validators.find((validatorItem) => validatorAddress === validatorItem.operatorAddress)
?.description.moniker || "Unknown validator"
{selectedValidatorAddress
? selectedValidator
? displayValidator(selectedValidator)
: "Unknown validator"
: "Select validator…"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
@ -68,12 +88,12 @@ export default function SelectValidator({
<Check
className={cn(
"mr-2 h-4 w-4",
validatorAddress === validatorItem.operatorAddress
selectedValidatorAddress === validatorItem.operatorAddress
? "opacity-100"
: "opacity-0",
)}
/>
{validatorItem.description.moniker}
{validatorItem.description.moniker + (validatorItem.jailed ? " (jailed)" : "")}
</CommandItem>
))}
</CommandGroup>

View File

@ -111,7 +111,7 @@ const MsgBeginRedelegateForm = ({
<h2>MsgBeginRedelegate</h2>
<div className="form-item">
<SelectValidator
validatorAddress={validatorSrcAddress}
selectedValidatorAddress={validatorSrcAddress}
setValidatorAddress={setValidatorSrcAddress}
/>
<Input
@ -128,7 +128,7 @@ const MsgBeginRedelegateForm = ({
</div>
<div className="form-item">
<SelectValidator
validatorAddress={validatorDstAddress}
selectedValidatorAddress={validatorDstAddress}
setValidatorAddress={setValidatorDstAddress}
/>
<Input

View File

@ -92,7 +92,7 @@ const MsgDelegateForm = ({ senderAddress, setMsgGetter, deleteMsg }: MsgDelegate
<h2>MsgDelegate</h2>
<div className="form-item">
<SelectValidator
validatorAddress={validatorAddress}
selectedValidatorAddress={validatorAddress}
setValidatorAddress={setValidatorAddress}
/>
<Input

View File

@ -92,7 +92,7 @@ const MsgUndelegateForm = ({ senderAddress, setMsgGetter, deleteMsg }: MsgUndele
<h2>MsgUndelegate</h2>
<div className="form-item">
<SelectValidator
validatorAddress={validatorAddress}
selectedValidatorAddress={validatorAddress}
setValidatorAddress={setValidatorAddress}
/>
<Input

View File

@ -65,7 +65,7 @@ const MsgWithdrawDelegatorRewardForm = ({
<h2>MsgWithdrawDelegatorReward</h2>
<div className="form-item">
<SelectValidator
validatorAddress={validatorAddress}
selectedValidatorAddress={validatorAddress}
setValidatorAddress={setValidatorAddress}
/>
<Input

View File

@ -52,7 +52,8 @@ const OldCreateTxForm = ({ router, senderAddress, accountOnChain }: OldCreateTxF
};
const addMsgWithValidator = (newMsgType: MsgTypeUrl) => {
if (!validators.length) {
const validatorsLoaded = !!validators.bonded.length;
if (!validatorsLoaded) {
loadValidators(chainsDispatch);
}

View File

@ -1,10 +1,10 @@
import { getAllValidators } from "@/lib/staking";
import { emptyAllValidatorsEmpty, getAllValidators } from "@/lib/staking";
import { toastError } from "@/lib/utils";
import { ReactNode, createContext, useContext, useEffect, useReducer } from "react";
import { emptyChain, isChainInfoFilled, setChain, setChains, setChainsError } from "./helpers";
import { getChain, getNodeFromArray, useChainsFromRegistry } from "./service";
import { addLocalChainInStorage, addRecentChainNameInStorage, setChainInUrl } from "./storage";
import { Action, ChainsContextType, State } from "./types";
import { Action, ChainsContextType, Dispatch, State } from "./types";
const ChainsContext = createContext<ChainsContextType | undefined>(undefined);
@ -31,7 +31,7 @@ const chainsReducer = (state: State, action: Action): State => {
return {
...state,
chain: action.payload,
validatorState: { validators: [], status: "initial" },
validatorState: { validators: emptyAllValidatorsEmpty(), status: "initial" },
};
}
case "addNodeAddress": {
@ -66,7 +66,7 @@ export const ChainsProvider = ({ children }: ChainsProviderProps) => {
chain: emptyChain,
chains: { mainnets: new Map(), testnets: new Map(), localnets: new Map() },
newConnection: { action: "edit" },
validatorState: { validators: [], status: "initial" },
validatorState: { validators: emptyAllValidatorsEmpty(), status: "initial" },
});
const { chainItems, chainItemsError } = useChainsFromRegistry();
@ -105,7 +105,10 @@ export const ChainsProvider = ({ children }: ChainsProviderProps) => {
description: "Failed to load validators",
fullError: e instanceof Error ? e : undefined,
});
dispatch({ type: "setValidatorState", payload: { validators: [], status: "error" } });
dispatch({
type: "setValidatorState",
payload: { validators: emptyAllValidatorsEmpty(), status: "error" },
});
}
}
})();
@ -114,7 +117,7 @@ export const ChainsProvider = ({ children }: ChainsProviderProps) => {
return <ChainsContext.Provider value={{ state, dispatch }}>{children}</ChainsContext.Provider>;
};
export const useChains = () => {
export const useChains = (): State & { chainsDispatch: Dispatch } => {
const context = useContext(ChainsContext);
if (context === undefined) {
throw new Error("useChains must be used within a ChainsProvider");

View File

@ -1,5 +1,5 @@
import { Validator } from "cosmjs-types/cosmos/staking/v1beta1/staking";
import { RegistryAsset } from "../../types/chainRegistry";
import { AllValidators } from "@/lib/staking";
export interface ChainsContextType {
readonly state: State;
@ -39,7 +39,7 @@ export interface ChainInfo {
}
export interface ValidatorState {
readonly validators: readonly Validator[];
readonly validators: AllValidators;
readonly status: "initial" | "loading" | "done" | "error";
}

View File

@ -2,24 +2,68 @@ import { QueryClient, StakingExtension, setupStakingExtension } from "@cosmjs/st
import { connectComet } from "@cosmjs/tendermint-rpc";
import { Validator } from "cosmjs-types/cosmos/staking/v1beta1/staking";
const getValidatorsPage = (
const getBondedValidatorsPage = (
queryClient: QueryClient & StakingExtension,
paginationKey: Uint8Array | undefined,
) => queryClient.staking.validators("BOND_STATUS_BONDED", paginationKey);
export const getAllValidators = async (rpcUrl: string): Promise<readonly Validator[]> => {
const validators: Validator[] = [];
const getUnbondingValidatorsPage = (
queryClient: QueryClient & StakingExtension,
paginationKey: Uint8Array | undefined,
) => queryClient.staking.validators("BOND_STATUS_UNBONDING", paginationKey);
const getUnbondedValidatorsPage = (
queryClient: QueryClient & StakingExtension,
paginationKey: Uint8Array | undefined,
) => queryClient.staking.validators("BOND_STATUS_UNBONDED", paginationKey);
export interface AllValidators {
bonded: readonly Validator[];
unbonding: readonly Validator[];
unbonded: readonly Validator[];
}
export function emptyAllValidatorsEmpty(): AllValidators {
return { bonded: [], unbonding: [], unbonded: [] };
}
export const getAllValidators = async (rpcUrl: string): Promise<AllValidators> => {
const bondedValidators: Validator[] = [];
const unbondingValidators: Validator[] = [];
const unbondedValidators: Validator[] = [];
const cometClient = await connectComet(rpcUrl);
const queryClient = QueryClient.withExtensions(cometClient, setupStakingExtension);
let paginationKey: Uint8Array | undefined = undefined;
let paginationKey: Uint8Array | undefined;
// Bonded
paginationKey = undefined;
do {
const response = await getValidatorsPage(queryClient, paginationKey);
validators.push(...response.validators);
const response = await getBondedValidatorsPage(queryClient, paginationKey);
bondedValidators.push(...response.validators);
paginationKey = response.pagination?.nextKey;
} while (paginationKey?.length);
return validators;
// Unbonding
paginationKey = undefined;
do {
const response = await getUnbondingValidatorsPage(queryClient, paginationKey);
unbondingValidators.push(...response.validators);
paginationKey = response.pagination?.nextKey;
} while (paginationKey?.length);
// Unbonded
paginationKey = undefined;
do {
const response = await getUnbondedValidatorsPage(queryClient, paginationKey);
unbondedValidators.push(...response.validators);
paginationKey = response.pagination?.nextKey;
} while (paginationKey?.length);
return {
bonded: bondedValidators,
unbonding: unbondingValidators,
unbonded: unbondedValidators,
};
};