Merge pull request #194 from cosmos/feat/validator-dropdown
Add validator dropdown
This commit is contained in:
commit
cc86a74f35
83
components/SelectValidator.tsx
Normal file
83
components/SelectValidator.tsx
Normal file
@ -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 (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
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"
|
||||
: "Select validator…"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0">
|
||||
<Command className="bg-fuchsia-900">
|
||||
<CommandInput
|
||||
placeholder="Search validator…"
|
||||
value={searchText}
|
||||
onValueChange={setSearchText}
|
||||
/>
|
||||
<CommandEmpty>No framework found.</CommandEmpty>
|
||||
<CommandGroup className="max-h-[400px] overflow-y-auto">
|
||||
{validators?.map((validatorItem) => (
|
||||
<CommandItem
|
||||
className="aria-selected:bg-fuchsia-800"
|
||||
key={validatorItem.operatorAddress}
|
||||
onSelect={() => {
|
||||
setValidatorAddress(validatorItem.operatorAddress);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
validatorAddress === validatorItem.operatorAddress
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{validatorItem.description.moniker}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@ -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 = ({
|
||||
</button>
|
||||
<h2>MsgWithdrawDelegatorReward</h2>
|
||||
<div className="form-item">
|
||||
<SelectValidator
|
||||
validatorAddress={validatorAddress}
|
||||
setValidatorAddress={setValidatorAddress}
|
||||
/>
|
||||
<Input
|
||||
label="Validator Address"
|
||||
name="validator-address"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import SelectValidator from "@/components/SelectValidator";
|
||||
import { MsgDelegateEncodeObject } from "@cosmjs/stargate";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MsgGetter } from "..";
|
||||
@ -90,6 +91,10 @@ const MsgDelegateForm = ({ delegatorAddress, setMsgGetter, deleteMsg }: MsgDeleg
|
||||
</button>
|
||||
<h2>MsgDelegate</h2>
|
||||
<div className="form-item">
|
||||
<SelectValidator
|
||||
validatorAddress={validatorAddress}
|
||||
setValidatorAddress={setValidatorAddress}
|
||||
/>
|
||||
<Input
|
||||
label="Validator Address"
|
||||
name="validator-address"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import SelectValidator from "@/components/SelectValidator";
|
||||
import { EncodeObject } from "@cosmjs/proto-signing";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MsgGetter } from "..";
|
||||
@ -106,6 +107,10 @@ const MsgRedelegateForm = ({
|
||||
</button>
|
||||
<h2>MsgBeginRedelegate</h2>
|
||||
<div className="form-item">
|
||||
<SelectValidator
|
||||
validatorAddress={validatorSrcAddress}
|
||||
setValidatorAddress={setValidatorSrcAddress}
|
||||
/>
|
||||
<Input
|
||||
label="Source Validator Address"
|
||||
name="src-validator-address"
|
||||
@ -119,6 +124,10 @@ const MsgRedelegateForm = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<SelectValidator
|
||||
validatorAddress={validatorDstAddress}
|
||||
setValidatorAddress={setValidatorDstAddress}
|
||||
/>
|
||||
<Input
|
||||
label="Destination Validator Address"
|
||||
name="dst-validator-address"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import SelectValidator from "@/components/SelectValidator";
|
||||
import { MsgUndelegateEncodeObject } from "@cosmjs/stargate";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MsgGetter } from "..";
|
||||
@ -94,6 +95,10 @@ const MsgUndelegateForm = ({
|
||||
</button>
|
||||
<h2>MsgUndelegate</h2>
|
||||
<div className="form-item">
|
||||
<SelectValidator
|
||||
validatorAddress={validatorAddress}
|
||||
setValidatorAddress={setValidatorAddress}
|
||||
/>
|
||||
<Input
|
||||
label="Validator Address"
|
||||
name="validator-address"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { loadValidators } from "@/context/ChainsContext/helpers";
|
||||
import { EncodeObject } from "@cosmjs/proto-signing";
|
||||
import { Account, calculateFee } from "@cosmjs/stargate";
|
||||
import { assert } from "@cosmjs/utils";
|
||||
@ -25,7 +26,11 @@ interface CreateTxFormProps {
|
||||
}
|
||||
|
||||
const CreateTxForm = ({ router, senderAddress, accountOnChain }: CreateTxFormProps) => {
|
||||
const { chain } = useChains();
|
||||
const {
|
||||
chain,
|
||||
validatorState: { validators },
|
||||
chainsDispatch,
|
||||
} = useChains();
|
||||
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [msgTypes, setMsgTypes] = useState<readonly MsgTypeUrl[]>([]);
|
||||
@ -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
|
||||
<label>Staking</label>
|
||||
<ul>
|
||||
<li>
|
||||
<Button label="Delegate" onClick={() => addMsgType(MsgTypeUrls.Delegate)} />
|
||||
<Button label="Delegate" onClick={() => addMsgWithValidator(MsgTypeUrls.Delegate)} />
|
||||
</li>
|
||||
<li>
|
||||
<Button label="Undelegate" onClick={() => addMsgType(MsgTypeUrls.Undelegate)} />
|
||||
<Button
|
||||
label="Undelegate"
|
||||
onClick={() => addMsgWithValidator(MsgTypeUrls.Undelegate)}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<Button
|
||||
label="BeginRedelegate"
|
||||
onClick={() => addMsgType(MsgTypeUrls.BeginRedelegate)}
|
||||
onClick={() => addMsgWithValidator(MsgTypeUrls.BeginRedelegate)}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
@ -201,7 +217,7 @@ const CreateTxForm = ({ router, senderAddress, accountOnChain }: CreateTxFormPro
|
||||
<li>
|
||||
<Button
|
||||
label="WithdrawDelegatorReward"
|
||||
onClick={() => addMsgType(MsgTypeUrls.WithdrawDelegatorReward)}
|
||||
onClick={() => addMsgWithValidator(MsgTypeUrls.WithdrawDelegatorReward)}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@ -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 });
|
||||
};
|
||||
|
||||
@ -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<ChainsContextType | undefined>(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 <ChainsContext.Provider value={{ state, dispatch }}>{children}</ChainsContext.Provider>;
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
25
lib/staking.ts
Normal file
25
lib/staking.ts
Normal file
@ -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<readonly Validator[]> => {
|
||||
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;
|
||||
};
|
||||
1
package-lock.json
generated
1
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user