Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Load unbonded and unbonding validators too #255

Merged
merged 5 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
};
};