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);