diff --git a/components/SelectValidator.tsx b/components/SelectValidator.tsx
new file mode 100644
index 0000000..13d3bc2
--- /dev/null
+++ b/components/SelectValidator.tsx
@@ -0,0 +1,83 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+} from "@/components/ui/command";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { useChains } from "@/context/ChainsContext";
+import { cn } from "@/lib/utils";
+import { Check, ChevronsUpDown } from "lucide-react";
+import { useState } from "react";
+
+interface SelectValidatorProps {
+ readonly validatorAddress: string;
+ readonly setValidatorAddress: (validatorAddress: string) => void;
+}
+
+export default function SelectValidator({
+ validatorAddress,
+ setValidatorAddress,
+}: SelectValidatorProps) {
+ const {
+ validatorState: { validators },
+ } = useChains();
+ const [open, setOpen] = useState(false);
+ const [searchText, setSearchText] = useState("");
+
+ return (
+
+
+
+
+
+
+
+ No framework found.
+
+ {validators?.map((validatorItem) => (
+ {
+ setValidatorAddress(validatorItem.operatorAddress);
+ setOpen(false);
+ }}
+ >
+
+ {validatorItem.description.moniker}
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/components/forms/CreateTxForm/MsgForm/MsgClaimRewardsForm.tsx b/components/forms/CreateTxForm/MsgForm/MsgClaimRewardsForm.tsx
index 46f6dc4..64eb507 100644
--- a/components/forms/CreateTxForm/MsgForm/MsgClaimRewardsForm.tsx
+++ b/components/forms/CreateTxForm/MsgForm/MsgClaimRewardsForm.tsx
@@ -1,3 +1,4 @@
+import SelectValidator from "@/components/SelectValidator";
import { MsgWithdrawDelegatorRewardEncodeObject } from "@cosmjs/stargate";
import { useEffect, useState } from "react";
import { MsgGetter } from "..";
@@ -63,6 +64,10 @@ const MsgClaimRewardsForm = ({
MsgWithdrawDelegatorReward
+
MsgDelegate
+
MsgBeginRedelegate
+
+
MsgUndelegate
+
{
- const { chain } = useChains();
+ const {
+ chain,
+ validatorState: { validators },
+ chainsDispatch,
+ } = useChains();
const [processing, setProcessing] = useState(false);
const [msgTypes, setMsgTypes] = useState
([]);
@@ -45,6 +50,14 @@ const CreateTxForm = ({ router, senderAddress, accountOnChain }: CreateTxFormPro
});
};
+ const addMsgWithValidator = (newMsgType: MsgTypeUrl) => {
+ if (!validators.length) {
+ loadValidators(chainsDispatch);
+ }
+
+ addMsgType(newMsgType);
+ };
+
const createTx = async () => {
try {
setShowTxError(false);
@@ -185,15 +198,18 @@ const CreateTxForm = ({ router, senderAddress, accountOnChain }: CreateTxFormPro
-
-
-
- addMsgType(MsgTypeUrls.Undelegate)} />
+ addMsgWithValidator(MsgTypeUrls.Undelegate)}
+ />
-
addMsgType(MsgTypeUrls.BeginRedelegate)}
+ onClick={() => addMsgWithValidator(MsgTypeUrls.BeginRedelegate)}
/>
@@ -201,7 +217,7 @@ const CreateTxForm = ({ router, senderAddress, accountOnChain }: CreateTxFormPro
addMsgType(MsgTypeUrls.WithdrawDelegatorReward)}
+ onClick={() => addMsgWithValidator(MsgTypeUrls.WithdrawDelegatorReward)}
/>
diff --git a/context/ChainsContext/helpers.tsx b/context/ChainsContext/helpers.tsx
index 0d0cf53..0ac88bd 100644
--- a/context/ChainsContext/helpers.tsx
+++ b/context/ChainsContext/helpers.tsx
@@ -41,6 +41,10 @@ export const setChain = (dispatch: Dispatch, chain: ChainInfo) => {
dispatch({ type: "setChain", payload: chain });
};
+export const loadValidators = (dispatch: Dispatch) => {
+ dispatch({ type: "loadValidators" });
+};
+
export const setNewConnection = (dispatch: Dispatch, newConnection: NewConnection) => {
dispatch({ type: "setNewConnection", payload: newConnection });
};
diff --git a/context/ChainsContext/index.tsx b/context/ChainsContext/index.tsx
index bbfef9b..38fc67e 100644
--- a/context/ChainsContext/index.tsx
+++ b/context/ChainsContext/index.tsx
@@ -1,3 +1,4 @@
+import { getAllValidators } from "@/lib/staking";
import { ReactNode, createContext, useContext, useEffect, useReducer } from "react";
import { emptyChain, isChainInfoFilled, setChain, setChains, setChainsError } from "./helpers";
import { getChain, getNodeFromArray, useChainsFromRegistry } from "./service";
@@ -6,7 +7,7 @@ import { Action, ChainsContextType, State } from "./types";
const ChainsContext = createContext(undefined);
-const chainsReducer = (state: State, action: Action) => {
+const chainsReducer = (state: State, action: Action): State => {
switch (action.type) {
case "setChains": {
return { ...state, chains: action.payload };
@@ -26,11 +27,23 @@ const chainsReducer = (state: State, action: Action) => {
addRecentChainNameInStorage(action.payload.registryName);
setChainInUrl(action.payload, state.chains);
- return { ...state, chain: action.payload };
+ return {
+ ...state,
+ chain: action.payload,
+ validatorState: { validators: [], status: "initial" },
+ };
}
case "addNodeAddress": {
return { ...state, chain: { ...state.chain, nodeAddress: action.payload } };
}
+ case "loadValidators": {
+ return state.validatorState.status === "initial"
+ ? { ...state, validatorState: { ...state.validatorState, status: "loading" } }
+ : state;
+ }
+ case "setValidatorState": {
+ return { ...state, validatorState: action.payload };
+ }
case "setNewConnection": {
return { ...state, newConnection: action.payload };
}
@@ -52,6 +65,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" },
});
const { chainItems, chainItemsError } = useChainsFromRegistry();
@@ -78,6 +92,20 @@ export const ChainsProvider = ({ children }: ChainsProviderProps) => {
})();
}, [state.chain]);
+ useEffect(() => {
+ (async function loadValidators() {
+ if (state.validatorState.status === "loading" && state.chain.nodeAddress) {
+ try {
+ const validators = await getAllValidators(state.chain.nodeAddress);
+ dispatch({ type: "setValidatorState", payload: { validators, status: "done" } });
+ } catch (e) {
+ console.error(e instanceof Error ? e.message : "Failed to load validators");
+ dispatch({ type: "setValidatorState", payload: { validators: [], status: "error" } });
+ }
+ }
+ })();
+ }, [state.chain.nodeAddress, state.validatorState.status]);
+
return {children};
};
diff --git a/context/ChainsContext/types.tsx b/context/ChainsContext/types.tsx
index 5c8c650..20bf452 100644
--- a/context/ChainsContext/types.tsx
+++ b/context/ChainsContext/types.tsx
@@ -1,3 +1,4 @@
+import { Validator } from "cosmjs-types/cosmos/staking/v1beta1/staking";
import { RegistryAsset } from "../../types/chainRegistry";
export interface ChainsContextType {
@@ -8,6 +9,7 @@ export interface ChainsContextType {
export interface State {
readonly chains: ChainItems;
readonly chain: ChainInfo;
+ readonly validatorState: ValidatorState;
readonly newConnection: NewConnection;
readonly chainsError?: string | null;
}
@@ -36,6 +38,11 @@ export interface ChainInfo {
readonly explorerLinks: ExplorerLinks;
}
+export interface ValidatorState {
+ readonly validators: readonly Validator[];
+ readonly status: "initial" | "loading" | "done" | "error";
+}
+
export type ExplorerLinks = {
readonly tx: string;
readonly account: string;
@@ -64,6 +71,13 @@ export type Action =
readonly type: "addNodeAddress";
readonly payload: string;
}
+ | {
+ readonly type: "loadValidators";
+ }
+ | {
+ readonly type: "setValidatorState";
+ readonly payload: ValidatorState;
+ }
| {
readonly type: "setNewConnection";
readonly payload: NewConnection;
diff --git a/lib/staking.ts b/lib/staking.ts
new file mode 100644
index 0000000..217dc56
--- /dev/null
+++ b/lib/staking.ts
@@ -0,0 +1,25 @@
+import { QueryClient, StakingExtension, setupStakingExtension } from "@cosmjs/stargate";
+import { connectComet } from "@cosmjs/tendermint-rpc";
+import { Validator } from "cosmjs-types/cosmos/staking/v1beta1/staking";
+
+const getValidatorsPage = (
+ queryClient: QueryClient & StakingExtension,
+ paginationKey: Uint8Array | undefined,
+) => queryClient.staking.validators("BOND_STATUS_BONDED", paginationKey);
+
+export const getAllValidators = async (rpcUrl: string): Promise => {
+ const validators: Validator[] = [];
+
+ const cometClient = await connectComet(rpcUrl);
+ const queryClient = QueryClient.withExtensions(cometClient, setupStakingExtension);
+
+ let paginationKey: Uint8Array | undefined = undefined;
+
+ do {
+ const response = await getValidatorsPage(queryClient, paginationKey);
+ validators.push(...response.validators);
+ paginationKey = response.pagination?.nextKey;
+ } while (paginationKey?.length);
+
+ return validators;
+};
diff --git a/package-lock.json b/package-lock.json
index ec9f2b0..d27bcd2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"@cosmjs/math": "^0.32.2",
"@cosmjs/proto-signing": "^0.32.2",
"@cosmjs/stargate": "^0.32.2",
+ "@cosmjs/tendermint-rpc": "^0.32.2",
"@cosmjs/utils": "^0.32.2",
"@hookform/resolvers": "^3.3.1",
"@keplr-wallet/types": "^0.12.23",
diff --git a/package.json b/package.json
index d17c38e..f39b7fc 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"@cosmjs/math": "^0.32.2",
"@cosmjs/proto-signing": "^0.32.2",
"@cosmjs/stargate": "^0.32.2",
+ "@cosmjs/tendermint-rpc": "^0.32.2",
"@cosmjs/utils": "^0.32.2",
"@hookform/resolvers": "^3.3.1",
"@keplr-wallet/types": "^0.12.23",