From af55c5b5cd4e8046e9ba3a25e74b283780661caa Mon Sep 17 00:00:00 2001 From: Ignacio Date: Fri, 1 Mar 2024 10:23:21 +0800 Subject: [PATCH] feat: add validator details page --- src/app/layout.tsx | 5 +- src/app/page.tsx | 11 +--- src/app/validator/page.tsx | 5 ++ src/features/core/components/base-wrapper.tsx | 34 ++++++++++++ .../components/logged-out.tsx | 0 .../{logged-in.tsx => main-page.tsx} | 12 +++-- src/features/staking/components/page.tsx | 25 --------- .../staking/components/validator-page.tsx | 53 +++++++++++++++++++ src/features/staking/context/actions.ts | 17 ++++++ src/features/staking/context/reducer.ts | 18 +++++++ src/features/staking/context/state.tsx | 2 + src/features/staking/lib/core/base.ts | 21 ++++++++ 12 files changed, 163 insertions(+), 40 deletions(-) create mode 100644 src/app/validator/page.tsx create mode 100644 src/features/core/components/base-wrapper.tsx rename src/features/{staking => core}/components/logged-out.tsx (100%) rename src/features/staking/components/{logged-in.tsx => main-page.tsx} (97%) delete mode 100644 src/features/staking/components/page.tsx create mode 100644 src/features/staking/components/validator-page.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2b16ebc..c49538b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -7,6 +7,7 @@ import { Inter } from "next/font/google"; import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; +import BaseWrapper from "@/features/core/components/base-wrapper"; import { StakingProvider } from "@/features/staking/context/provider"; import { dashboardUrl, @@ -33,7 +34,9 @@ export default function RootLayout({ - {children} + + {children} + diff --git a/src/app/page.tsx b/src/app/page.tsx index ce2e415..d7b23f4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,12 +1,5 @@ -import StakingPage from "@/features/staking/components/page"; +import StakingPage from "@/features/staking/components/main-page"; export default function Page() { - return ( -
-

- XION Staking -

- -
- ); + return ; } diff --git a/src/app/validator/page.tsx b/src/app/validator/page.tsx new file mode 100644 index 0000000..e6f969b --- /dev/null +++ b/src/app/validator/page.tsx @@ -0,0 +1,5 @@ +import ValidatorPage from "@/features/staking/components/validator-page"; + +export default function Page() { + return ; +} diff --git a/src/features/core/components/base-wrapper.tsx b/src/features/core/components/base-wrapper.tsx new file mode 100644 index 0000000..c2e905e --- /dev/null +++ b/src/features/core/components/base-wrapper.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { + Abstraxion, + useAbstraxionAccount, + useModal, +} from "@burnt-labs/abstraxion"; + +import LoggedOut from "./logged-out"; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + const { isConnected } = useAbstraxionAccount(); + const [showAbstraction, setShowAbstraxion] = useModal(); + + return ( +
+

+ XION Staking +

+ {isConnected ? children : } + {showAbstraction && ( + { + setShowAbstraxion(false); + }} + /> + )} +
+ ); +} diff --git a/src/features/staking/components/logged-out.tsx b/src/features/core/components/logged-out.tsx similarity index 100% rename from src/features/staking/components/logged-out.tsx rename to src/features/core/components/logged-out.tsx diff --git a/src/features/staking/components/logged-in.tsx b/src/features/staking/components/main-page.tsx similarity index 97% rename from src/features/staking/components/logged-in.tsx rename to src/features/staking/components/main-page.tsx index e28e06f..08bb11e 100644 --- a/src/features/staking/components/logged-in.tsx +++ b/src/features/staking/components/main-page.tsx @@ -35,10 +35,12 @@ const ValidatorItem = ({ return (
-
- {validator.description.moniker} -
-
{validator.operatorAddress}
+ +
+ {validator.description.moniker} +
+
{validator.operatorAddress}
+
- {validators && ( + {!!validators?.items.length && (
Validators:
diff --git a/src/features/staking/components/page.tsx b/src/features/staking/components/page.tsx deleted file mode 100644 index 4aa54ca..0000000 --- a/src/features/staking/components/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; - -import { Abstraxion, useModal } from "@burnt-labs/abstraxion"; - -import { useStaking } from "../context/hooks"; -import LoggedIn from "./logged-in"; -import LoggedOut from "./logged-out"; - -export default function StakingPage() { - const { isConnected } = useStaking(); - const [showAbstraction, setShowAbstraxion] = useModal(); - - return ( - <> - {isConnected ? : } - {showAbstraction && ( - { - setShowAbstraxion(false); - }} - /> - )} - - ); -} diff --git a/src/features/staking/components/validator-page.tsx b/src/features/staking/components/validator-page.tsx new file mode 100644 index 0000000..25100c8 --- /dev/null +++ b/src/features/staking/components/validator-page.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { BondStatus } from "cosmjs-types/cosmos/staking/v1beta1/staking"; +import { useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; + +import { getValidatorDetailsAction } from "../context/actions"; +import { useStaking } from "../context/hooks"; + +export default function ValidatorPage() { + const searchParams = useSearchParams(); + const address = searchParams.get("address"); + const stakingRef = useStaking(); + + const [validatorDetails, setValidatorDetails] = useState + > | null>(null); + + useEffect(() => { + (async () => { + if (address) { + const validatorDetailsResult = await getValidatorDetailsAction( + address, + stakingRef.staking, + ); + + setValidatorDetails(validatorDetailsResult); + } + })(); + }, [address, stakingRef]); + + if (!validatorDetails) { + return
Loading ...
; + } + + return ( +
+
{address}
+
{validatorDetails.description.moniker}
+
{validatorDetails.description.details}
+
{validatorDetails.description.identity}
+
{validatorDetails.description.securityContact}
+
{validatorDetails.description.website}
+
Jailed: {validatorDetails.jailed.toString()}
+
+ Status:{" "} + {validatorDetails.status === BondStatus.BOND_STATUS_BONDED + ? "Bonded" + : validatorDetails.status} +
+
+ ); +} diff --git a/src/features/staking/context/actions.ts b/src/features/staking/context/actions.ts index d75f4f1..cbf47cb 100644 --- a/src/features/staking/context/actions.ts +++ b/src/features/staking/context/actions.ts @@ -5,6 +5,7 @@ import { getDelegations, getRewards, getUnbondingDelegations, + getValidatorDetails, getValidatorsList, setRedelegate, stakeAmount, @@ -17,6 +18,7 @@ import { addUnbondings, setIsInfoLoading, setTokens, + setValidatorDetails, setValidators, } from "./reducer"; import type { StakingContextType, Unbonding } from "./state"; @@ -157,3 +159,18 @@ export const setRedelegateAction = async ( await fetchStakingDataAction(delegatorAddress, staking); }; + +export const getValidatorDetailsAction = async ( + validatorAddress: string, + staking: StakingContextType, +) => { + if (staking.state.validatorDetails?.operatorAddress === validatorAddress) { + return staking.state.validatorDetails; + } + + const details = await getValidatorDetails(validatorAddress); + + staking.dispatch(setValidatorDetails(details)); + + return details; +}; diff --git a/src/features/staking/context/reducer.ts b/src/features/staking/context/reducer.ts index 39da4a8..3fc047f 100644 --- a/src/features/staking/context/reducer.ts +++ b/src/features/staking/context/reducer.ts @@ -23,6 +23,10 @@ export type StakingAction = | { content: StakingState["tokens"]; type: "SET_TOKENS"; + } + | { + content: StakingState["validatorDetails"]; + type: "SET_VALIDATOR_DETAILS"; }; type Content = Extract< @@ -69,6 +73,13 @@ export const addUnbondings = ( type: "ADD_UNBONDINGS", }); +export const setValidatorDetails = ( + content: Content<"SET_VALIDATOR_DETAILS">, +): StakingAction => ({ + content, + type: "SET_VALIDATOR_DETAILS", +}); + // Used for pagination const getUniqueValidators = ( validators: NonNullable["items"], @@ -192,6 +203,13 @@ export const reducer = (state: StakingState, action: StakingAction) => { }; } + case "SET_VALIDATOR_DETAILS": { + return { + ...state, + validatorDetails: action.content, + }; + } + default: action satisfies never; diff --git a/src/features/staking/context/state.tsx b/src/features/staking/context/state.tsx index 46b1583..99344c9 100644 --- a/src/features/staking/context/state.tsx +++ b/src/features/staking/context/state.tsx @@ -30,6 +30,7 @@ export type StakingState = { isInfoLoading: boolean; tokens: Coin | null; unbondings: Paginated; + validatorDetails: null | Validator; validators: Paginated; }; @@ -43,6 +44,7 @@ export const defaultState: StakingState = { isInfoLoading: false, tokens: null, unbondings: null, + validatorDetails: null, validators: null, }; diff --git a/src/features/staking/lib/core/base.ts b/src/features/staking/lib/core/base.ts index b63b385..3c7f154 100644 --- a/src/features/staking/lib/core/base.ts +++ b/src/features/staking/lib/core/base.ts @@ -9,6 +9,7 @@ import type { } from "@cosmjs/stargate"; import BigNumber from "bignumber.js"; import { MsgWithdrawDelegatorReward } from "cosmjs-types/cosmos/distribution/v1beta1/tx"; +import type { Validator } from "cosmjs-types/cosmos/staking/v1beta1/staking"; import { MsgBeginRedelegate, MsgDelegate, @@ -27,6 +28,26 @@ export const getValidatorsList = async () => { return await queryClient.staking.validators("BOND_STATUS_BONDED"); }; +let validatorDetailsRequest: [string, Promise] | null = null; + +export const getValidatorDetails = async (address: string) => { + if (validatorDetailsRequest?.[0] === address) { + return validatorDetailsRequest[1]; + } + + const queryClient = await getStakingQueryClient(); + + const promise = queryClient.staking.validator(address).then((resp) => { + validatorDetailsRequest = null; + + return resp.validator; + }); + + validatorDetailsRequest = [address, promise]; + + return promise; +}; + export const getBalance = async (address: string) => { const client = await StargateClient.connect(rpcEndpoint);