Skip to content

Commit

Permalink
Merge pull request #255 from cosmos/load-all-validators
Browse files Browse the repository at this point in the history
Load unbonded and unbonding validators too
  • Loading branch information
webmaster128 authored Jan 15, 2025
2 parents b5fab6c + c20ad7d commit ea6a6c9
Show file tree
Hide file tree
Showing 9 changed files with 97 additions and 29 deletions.
36 changes: 28 additions & 8 deletions components/SelectValidator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ const MsgBeginRedelegateForm = ({
<h2>MsgBeginRedelegate</h2>
<div className="form-item">
<SelectValidator
validatorAddress={validatorSrcAddress}
selectedValidatorAddress={validatorSrcAddress}
setValidatorAddress={setValidatorSrcAddress}
/>
<Input
Expand All @@ -128,7 +128,7 @@ const MsgBeginRedelegateForm = ({
</div>
<div className="form-item">
<SelectValidator
validatorAddress={validatorDstAddress}
selectedValidatorAddress={validatorDstAddress}
setValidatorAddress={setValidatorDstAddress}
/>
<Input
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const MsgWithdrawDelegatorRewardForm = ({
<h2>MsgWithdrawDelegatorReward</h2>
<div className="form-item">
<SelectValidator
validatorAddress={validatorAddress}
selectedValidatorAddress={validatorAddress}
setValidatorAddress={setValidatorAddress}
/>
<Input
Expand Down
3 changes: 2 additions & 1 deletion components/forms/OldCreateTxForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
15 changes: 9 additions & 6 deletions context/ChainsContext/index.tsx
Original file line number Diff line number Diff line change
@@ -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);

Expand All @@ -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": {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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" },
});
}
}
})();
Expand All @@ -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");
Expand Down
4 changes: 2 additions & 2 deletions context/ChainsContext/types.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -39,7 +39,7 @@ export interface ChainInfo {
}

export interface ValidatorState {
readonly validators: readonly Validator[];
readonly validators: AllValidators;
readonly status: "initial" | "loading" | "done" | "error";
}

Expand Down
58 changes: 51 additions & 7 deletions lib/staking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 getBondedValidatorsPage(queryClient, paginationKey);
bondedValidators.push(...response.validators);
paginationKey = response.pagination?.nextKey;
} while (paginationKey?.length);

// 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 getValidatorsPage(queryClient, paginationKey);
validators.push(...response.validators);
const response = await getUnbondedValidatorsPage(queryClient, paginationKey);
unbondedValidators.push(...response.validators);
paginationKey = response.pagination?.nextKey;
} while (paginationKey?.length);

return validators;
return {
bonded: bondedValidators,
unbonding: unbondingValidators,
unbonded: unbondedValidators,
};
};

0 comments on commit ea6a6c9

Please sign in to comment.