diff --git a/components/SelectValidator.tsx b/components/SelectValidator.tsx index d07ec6a..e5dc55f 100644 --- a/components/SelectValidator.tsx +++ b/components/SelectValidator.tsx @@ -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 ( @@ -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…"} @@ -68,12 +88,12 @@ export default function SelectValidator({ - {validatorItem.description.moniker} + {validatorItem.description.moniker + (validatorItem.jailed ? " (jailed)" : "")} ))} diff --git a/components/forms/OldCreateTxForm/MsgForm/MsgBeginRedelegateForm.tsx b/components/forms/OldCreateTxForm/MsgForm/MsgBeginRedelegateForm.tsx index 1227598..839420e 100644 --- a/components/forms/OldCreateTxForm/MsgForm/MsgBeginRedelegateForm.tsx +++ b/components/forms/OldCreateTxForm/MsgForm/MsgBeginRedelegateForm.tsx @@ -111,7 +111,7 @@ const MsgBeginRedelegateForm = ({

MsgBeginRedelegate

MsgDelegate
MsgUndelegate
MsgWithdrawDelegatorReward
{ - if (!validators.length) { + const validatorsLoaded = !!validators.bonded.length; + if (!validatorsLoaded) { loadValidators(chainsDispatch); } diff --git a/context/ChainsContext/index.tsx b/context/ChainsContext/index.tsx index 03974df..9b83ca6 100644 --- a/context/ChainsContext/index.tsx +++ b/context/ChainsContext/index.tsx @@ -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(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 {children}; }; -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"); diff --git a/context/ChainsContext/types.tsx b/context/ChainsContext/types.tsx index 20bf452..5d5f1ca 100644 --- a/context/ChainsContext/types.tsx +++ b/context/ChainsContext/types.tsx @@ -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"; } diff --git a/lib/staking.ts b/lib/staking.ts index 217dc56..01e4a9a 100644 --- a/lib/staking.ts +++ b/lib/staking.ts @@ -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 => { - 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 => { + 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, + }; };