From d540c64bad98c3e7f742f55e4b9fe5ca72e5e3da Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 13 Jan 2025 12:18:31 +0100 Subject: [PATCH 1/5] Load unbonded and unbonding validators too --- components/SelectValidator.tsx | 5 +- components/forms/OldCreateTxForm/index.tsx | 3 +- context/ChainsContext/index.tsx | 12 ++--- context/ChainsContext/types.tsx | 4 +- lib/staking.ts | 58 +++++++++++++++++++--- 5 files changed, 65 insertions(+), 17 deletions(-) diff --git a/components/SelectValidator.tsx b/components/SelectValidator.tsx index d07ec6a..0b3376a 100644 --- a/components/SelectValidator.tsx +++ b/components/SelectValidator.tsx @@ -25,11 +25,13 @@ export default function SelectValidator({ setValidatorAddress, }: SelectValidatorProps) { const { - validatorState: { validators }, + validatorState: { validators: { bonded, unbonding, unbonded} }, } = useChains(); const [open, setOpen] = useState(false); const [searchText, setSearchText] = useState(""); + const validators = [...bonded, ...unbonding, ...unbonded]; + return ( @@ -73,6 +75,7 @@ export default function SelectValidator({ : "opacity-0", )} /> + {validatorItem.jailed ? <>jailed{" "} : null} {validatorItem.description.moniker} ))} diff --git a/components/forms/OldCreateTxForm/index.tsx b/components/forms/OldCreateTxForm/index.tsx index a0e9f15..7e6988e 100644 --- a/components/forms/OldCreateTxForm/index.tsx +++ b/components/forms/OldCreateTxForm/index.tsx @@ -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); } diff --git a/context/ChainsContext/index.tsx b/context/ChainsContext/index.tsx index 03974df..ad8078c 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,7 @@ 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 +114,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, + }; }; From 47f9f2f580c07f0f6ba67dfc6de9bbeec1b61bbf Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 13 Jan 2025 13:58:40 +0100 Subject: [PATCH 2/5] Make jailed annotation more redable --- components/SelectValidator.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/SelectValidator.tsx b/components/SelectValidator.tsx index 0b3376a..a5635e9 100644 --- a/components/SelectValidator.tsx +++ b/components/SelectValidator.tsx @@ -75,8 +75,7 @@ export default function SelectValidator({ : "opacity-0", )} /> - {validatorItem.jailed ? <>jailed{" "} : null} - {validatorItem.description.moniker} + {validatorItem.description.moniker + (validatorItem.jailed ? " (jailed)" : "")} ))} From 6f28cee4822272979c15ce7b61bca3dc93c39774 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 13 Jan 2025 14:19:15 +0100 Subject: [PATCH 3/5] Add comment on validators list --- components/SelectValidator.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/components/SelectValidator.tsx b/components/SelectValidator.tsx index a5635e9..35e68bd 100644 --- a/components/SelectValidator.tsx +++ b/components/SelectValidator.tsx @@ -30,6 +30,12 @@ export default function SelectValidator({ 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]; return ( From c4d787d3203f4eaac3089c94f9289a8ea8f38919 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 13 Jan 2025 15:01:37 +0100 Subject: [PATCH 4/5] Display validator consistently in UI component --- components/SelectValidator.tsx | 18 ++++++++++++------ .../MsgForm/MsgBeginRedelegateForm.tsx | 4 ++-- .../MsgForm/MsgDelegateForm.tsx | 2 +- .../MsgForm/MsgUndelegateForm.tsx | 2 +- .../MsgForm/MsgWithdrawDelegatorRewardForm.tsx | 2 +- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/components/SelectValidator.tsx b/components/SelectValidator.tsx index 35e68bd..acfef13 100644 --- a/components/SelectValidator.tsx +++ b/components/SelectValidator.tsx @@ -12,16 +12,17 @@ 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 { @@ -38,6 +39,12 @@ export default function SelectValidator({ // 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 ( @@ -47,9 +54,8 @@ 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…"} @@ -76,7 +82,7 @@ export default function SelectValidator({ MsgBeginRedelegate
MsgDelegate
MsgUndelegate
MsgWithdrawDelegatorReward
Date: Wed, 15 Jan 2025 09:33:42 +0100 Subject: [PATCH 5/5] Run new prettier --- components/SelectValidator.tsx | 14 ++++++++++---- context/ChainsContext/index.tsx | 5 ++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/components/SelectValidator.tsx b/components/SelectValidator.tsx index acfef13..e5dc55f 100644 --- a/components/SelectValidator.tsx +++ b/components/SelectValidator.tsx @@ -26,7 +26,9 @@ export default function SelectValidator({ setValidatorAddress, }: SelectValidatorProps) { const { - validatorState: { validators: { bonded, unbonding, unbonded} }, + validatorState: { + validators: { bonded, unbonding, unbonded }, + }, } = useChains(); const [open, setOpen] = useState(false); const [searchText, setSearchText] = useState(""); @@ -40,10 +42,12 @@ export default function SelectValidator({ const validators = [...bonded, ...unbonding, ...unbonded]; function displayValidator(val: Validator): string { - return val.description.moniker + (val.jailed ? " (jailed)" : "") + return val.description.moniker + (val.jailed ? " (jailed)" : ""); } - const selectedValidator = validators.find((validatorItem) => selectedValidatorAddress === validatorItem.operatorAddress); + const selectedValidator = validators.find( + (validatorItem) => selectedValidatorAddress === validatorItem.operatorAddress, + ); return ( @@ -55,7 +59,9 @@ export default function SelectValidator({ className="mb-4 w-full max-w-[300px] justify-between border-white bg-fuchsia-900 hover:bg-fuchsia-900" > {selectedValidatorAddress - ? selectedValidator ? displayValidator(selectedValidator): "Unknown validator" + ? selectedValidator + ? displayValidator(selectedValidator) + : "Unknown validator" : "Select validator…"} diff --git a/context/ChainsContext/index.tsx b/context/ChainsContext/index.tsx index ad8078c..9b83ca6 100644 --- a/context/ChainsContext/index.tsx +++ b/context/ChainsContext/index.tsx @@ -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: emptyAllValidatorsEmpty(), status: "error" } }); + dispatch({ + type: "setValidatorState", + payload: { validators: emptyAllValidatorsEmpty(), status: "error" }, + }); } } })();