From 67d1f4932a565e8629c5f617c13d8cdbb8c118c9 Mon Sep 17 00:00:00 2001 From: Ignacio Date: Wed, 6 Mar 2024 16:06:17 +0800 Subject: [PATCH] feat: add inactive validators --- package-lock.json | 90 ++++++------- package.json | 4 +- src/features/core/components/base.tsx | 49 +++++-- src/features/core/utils.ts | 12 +- .../staking/components/delegation-details.tsx | 90 +++++++------ .../staking/components/modals/staking.tsx | 41 +++--- .../staking/components/modals/unstaking.tsx | 121 +++++++++++------- .../staking/components/staking-overview.tsx | 37 +++++- .../staking/components/validator-page.tsx | 33 ++--- .../staking/components/validators-table.tsx | 120 ++++++++++------- src/features/staking/context/actions.ts | 54 ++++---- src/features/staking/context/reducer.ts | 23 +++- src/features/staking/context/selectors.ts | 18 +++ src/features/staking/context/state.tsx | 10 +- src/features/staking/lib/core/base.ts | 13 +- src/features/staking/lib/core/constants.ts | 4 + src/features/staking/lib/core/icons.ts | 20 ++- src/features/staking/lib/core/tx.ts | 32 +++++ src/features/staking/lib/formatters.ts | 4 + 19 files changed, 505 insertions(+), 270 deletions(-) diff --git a/package-lock.json b/package-lock.json index 43d4703..c892dd2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,9 @@ "name": "abstraxion-staking-poc", "version": "0.1.0", "dependencies": { - "@burnt-labs/abstraxion": "^1.0.0-alpha.35", + "@burnt-labs/abstraxion": "^1.0.0-alpha.38", "@burnt-labs/constants": "^0.1.0-alpha.6", - "@burnt-labs/ui": "^0.1.0-alpha.6", + "@burnt-labs/ui": "^0.1.0-alpha.7", "@cosmjs/cosmwasm-stargate": "^0.31.3", "@cosmjs/proto-signing": "^0.32.2", "@cosmjs/stargate": "^0.32.2", @@ -374,11 +374,11 @@ } }, "node_modules/@burnt-labs/abstraxion": { - "version": "1.0.0-alpha.36", - "resolved": "https://registry.npmjs.org/@burnt-labs/abstraxion/-/abstraxion-1.0.0-alpha.36.tgz", - "integrity": "sha512-RQ7WECwbDfg13j9O+U9xND5VznpWiEd41jbpSgJvCu8s9tHYfqgYszYU43VibV6zOKYXu2xY/apr0Mx9yIVlMg==", + "version": "1.0.0-alpha.38", + "resolved": "https://registry.npmjs.org/@burnt-labs/abstraxion/-/abstraxion-1.0.0-alpha.38.tgz", + "integrity": "sha512-K5+taltuWrsBrO+c/Ut1hv58UiwXMSqTCOURZkZ0a8kSkaL2ikjy7qSJce/EKK/65xhG/HovQN+IZsZNtob2jQ==", "dependencies": { - "@burnt-labs/abstraxion-core": "1.0.0-alpha.34", + "@burnt-labs/abstraxion-core": "1.0.0-alpha.35", "@burnt-labs/constants": "0.1.0-alpha.6", "@burnt-labs/signers": "0.1.0-alpha.8", "@burnt-labs/ui": "0.1.0-alpha.7", @@ -403,9 +403,9 @@ } }, "node_modules/@burnt-labs/abstraxion-core": { - "version": "1.0.0-alpha.34", - "resolved": "https://registry.npmjs.org/@burnt-labs/abstraxion-core/-/abstraxion-core-1.0.0-alpha.34.tgz", - "integrity": "sha512-SFqTNhkkW2PUruvoK6yGqPkLQhGRpP9VaLj8FjHaWaBybI8EhG7ELKUwG6Vl9Jo5ONotFRnx3OOzpch9XSbgZQ==", + "version": "1.0.0-alpha.35", + "resolved": "https://registry.npmjs.org/@burnt-labs/abstraxion-core/-/abstraxion-core-1.0.0-alpha.35.tgz", + "integrity": "sha512-Sz95+WEdFYeZr3dRcXp3L/TzlLtNtIKsPpGao1cqj+U1+mLWLrnr1oq+WFgkMELlpJfZPZp9l/QeavyGcBjgZA==", "dependencies": { "@burnt-labs/constants": "0.1.0-alpha.6", "@burnt-labs/signers": "0.1.0-alpha.8", @@ -1098,9 +1098,9 @@ } }, "node_modules/@fastify/busboy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", - "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", "engines": { "node": ">=14" } @@ -1300,29 +1300,29 @@ } }, "node_modules/@keplr-wallet/common": { - "version": "0.12.70", - "resolved": "https://registry.npmjs.org/@keplr-wallet/common/-/common-0.12.70.tgz", - "integrity": "sha512-XtVdlYEF5dMjwBugMsuRtq9DnoK9W+VoSYPdWoNcS3lG37m8/rB0Fs+hBW46hJ00Vx/4WqGMepARsvxIubit7w==", + "version": "0.12.72", + "resolved": "https://registry.npmjs.org/@keplr-wallet/common/-/common-0.12.72.tgz", + "integrity": "sha512-raVgr36BaGJHsILAivWzS/OJzc/IUrK3xI7hiGOImqAVxQMMSqBbScCs0wEMqnua1zwRZ6sdbKzl15pCOh9NhQ==", "dependencies": { - "@keplr-wallet/crypto": "0.12.70", - "@keplr-wallet/types": "0.12.70", + "@keplr-wallet/crypto": "0.12.72", + "@keplr-wallet/types": "0.12.72", "buffer": "^6.0.3", "delay": "^4.4.0", "mobx": "^6.1.7" } }, "node_modules/@keplr-wallet/cosmos": { - "version": "0.12.70", - "resolved": "https://registry.npmjs.org/@keplr-wallet/cosmos/-/cosmos-0.12.70.tgz", - "integrity": "sha512-8UZSP/q8wvUs8sc9ap7JMNKEDMRvtKLJ7De7bp/VCSMlJoYiv94/eF2U7Z+cdw2b/r34qyZ+VHlrzPQf9fSM9A==", + "version": "0.12.72", + "resolved": "https://registry.npmjs.org/@keplr-wallet/cosmos/-/cosmos-0.12.72.tgz", + "integrity": "sha512-GqQIY408z3ZK2b0qSb9Ft9qU/TvS8k6z5InUeZ2Qk75SefF6ScRmkrquemp4mI+6LKJX6fMVC20uOzKKuOW0Eg==", "dependencies": { "@ethersproject/address": "^5.6.0", - "@keplr-wallet/common": "0.12.70", - "@keplr-wallet/crypto": "0.12.70", - "@keplr-wallet/proto-types": "0.12.70", - "@keplr-wallet/simple-fetch": "0.12.70", - "@keplr-wallet/types": "0.12.70", - "@keplr-wallet/unit": "0.12.70", + "@keplr-wallet/common": "0.12.72", + "@keplr-wallet/crypto": "0.12.72", + "@keplr-wallet/proto-types": "0.12.72", + "@keplr-wallet/simple-fetch": "0.12.72", + "@keplr-wallet/types": "0.12.72", + "@keplr-wallet/unit": "0.12.72", "bech32": "^1.1.4", "buffer": "^6.0.3", "long": "^4.0.0", @@ -1335,9 +1335,9 @@ "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" }, "node_modules/@keplr-wallet/crypto": { - "version": "0.12.70", - "resolved": "https://registry.npmjs.org/@keplr-wallet/crypto/-/crypto-0.12.70.tgz", - "integrity": "sha512-jw8/jqG8rgkDPWdjnrBOmL1+u+Gjy+gs3hFsODZDQAzV2LNskgH0ySbBo0okVEjl735bUCl7ljfuqlmNIfdGwg==", + "version": "0.12.72", + "resolved": "https://registry.npmjs.org/@keplr-wallet/crypto/-/crypto-0.12.72.tgz", + "integrity": "sha512-lo7vPNDYGMWIxEEtbOoNdaIwE7qT4gkHzEekrf3QZI8inwadPkp8lDm8oAiWIMCNSAF/6qN+SyaImVdrJyETvQ==", "dependencies": { "@ethersproject/keccak256": "^5.5.0", "bip32": "^2.0.6", @@ -1350,33 +1350,33 @@ } }, "node_modules/@keplr-wallet/proto-types": { - "version": "0.12.70", - "resolved": "https://registry.npmjs.org/@keplr-wallet/proto-types/-/proto-types-0.12.70.tgz", - "integrity": "sha512-RubyJhQ4eB90c4DjFQbEyY6uiWAKofi1rMcCTm2JXwKrIPjozzb2b4AeqkeRl4MxlN27DfQ1X69RugBalgev5A==", + "version": "0.12.72", + "resolved": "https://registry.npmjs.org/@keplr-wallet/proto-types/-/proto-types-0.12.72.tgz", + "integrity": "sha512-GVTq9Id9i3hQKUAs2cbP1+k2XpUP+Mih0LTV/1BN2//IHa3BSBsGP8DELlG75Yyz9zGseBgvj36PSv4/wKhnhA==", "dependencies": { "long": "^4.0.0", "protobufjs": "^6.11.2" } }, "node_modules/@keplr-wallet/simple-fetch": { - "version": "0.12.70", - "resolved": "https://registry.npmjs.org/@keplr-wallet/simple-fetch/-/simple-fetch-0.12.70.tgz", - "integrity": "sha512-WYBIWGQgYpJe2sUnXcq2lWpMOS5+e5Q2abMpvaIVgQlwKJC3UVh9srXFYRVKOKfxPc1sc2WZhWztAsEsz33IKg==" + "version": "0.12.72", + "resolved": "https://registry.npmjs.org/@keplr-wallet/simple-fetch/-/simple-fetch-0.12.72.tgz", + "integrity": "sha512-PyiZy4Ykh1y30HaYhyeoctPBwLCgcF0QKebdmRKp9uSRyZri5mOu6DckJa8QCXji4pBZkLitGXjwwnSj2eoheg==" }, "node_modules/@keplr-wallet/types": { - "version": "0.12.70", - "resolved": "https://registry.npmjs.org/@keplr-wallet/types/-/types-0.12.70.tgz", - "integrity": "sha512-oOZEkWVOj/GCrKpcJR8EQKp6SvWHHzuZAwVkRnxJc9UKaaOE6kiOJnu02dl9jLhTziK2/JqUbXOsF/Hopw7xOQ==", + "version": "0.12.72", + "resolved": "https://registry.npmjs.org/@keplr-wallet/types/-/types-0.12.72.tgz", + "integrity": "sha512-gDP+NCPa4seTT1xU9bkIKbMw7N/LPla9/4/amDLIGyFb6OEjfnFkRuReI/cZe/8aEvlkYAKnfB0UMipHobsn5g==", "dependencies": { "long": "^4.0.0" } }, "node_modules/@keplr-wallet/unit": { - "version": "0.12.70", - "resolved": "https://registry.npmjs.org/@keplr-wallet/unit/-/unit-0.12.70.tgz", - "integrity": "sha512-yIfwa7R+z9hJdggGF0lcJN2JEfEP75XI1m3UCzL92P09r8QR5x0ZBxf3nsyt0BQIHlGEqhYaI5orWsJ1srNEQg==", + "version": "0.12.72", + "resolved": "https://registry.npmjs.org/@keplr-wallet/unit/-/unit-0.12.72.tgz", + "integrity": "sha512-egrh0L/uo6MQrAfVi1V8GQB1eml1ZbfWDiv5gpU6AkeAte4Q1DIe8qkYqeJOJ+KW1TurnxDw9L+9/T/i4Lu8Jg==", "dependencies": { - "@keplr-wallet/types": "0.12.70", + "@keplr-wallet/types": "0.12.72", "big-integer": "^1.6.48", "utility-types": "^3.10.0" } @@ -10728,9 +10728,9 @@ } }, "node_modules/zustand": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.1.tgz", - "integrity": "sha512-XlauQmH64xXSC1qGYNv00ODaQ3B+tNPoy22jv2diYiP4eoDKr9LA+Bh5Bc3gplTrFdb6JVI+N4kc1DZ/tbtfPg==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", + "integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==", "dependencies": { "use-sync-external-store": "1.2.0" }, diff --git a/package.json b/package.json index 06eee68..36ce56a 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ "type-check": "tsc --noEmit" }, "dependencies": { - "@burnt-labs/abstraxion": "^1.0.0-alpha.35", + "@burnt-labs/abstraxion": "^1.0.0-alpha.38", "@burnt-labs/constants": "^0.1.0-alpha.6", - "@burnt-labs/ui": "^0.1.0-alpha.6", + "@burnt-labs/ui": "^0.1.0-alpha.7", "@cosmjs/cosmwasm-stargate": "^0.31.3", "@cosmjs/proto-signing": "^0.32.2", "@cosmjs/stargate": "^0.32.2", diff --git a/src/features/core/components/base.tsx b/src/features/core/components/base.tsx index f34917f..64e2a20 100644 --- a/src/features/core/components/base.tsx +++ b/src/features/core/components/base.tsx @@ -5,7 +5,7 @@ import type { FC, PropsWithChildren, ReactNode } from "react"; import { useState } from "react"; import { toast } from "react-toastify"; -import { clipboard } from "@/features/staking/lib/core/icons"; +import { clipboard, loader, search } from "@/features/staking/lib/core/icons"; import { useCore } from "../context/hooks"; import { setPopupOpenId } from "../context/reducer"; @@ -52,13 +52,20 @@ export const NavLink = ({ children, href }: NavLinkProps) => ( type ButtonPillProps = React.DetailedHTMLProps< React.ButtonHTMLAttributes, HTMLButtonElement ->; +> & { + variant?: "danger" | "default"; +}; -export const ButtonPill = ({ className, ...props }: ButtonPillProps) => ( +export const ButtonPill = ({ + className, + variant, + ...props +}: ButtonPillProps) => ( ); }; @@ -231,9 +256,13 @@ type SearchInputProps = React.DetailedHTMLProps< >; export const SearchInput = (props: SearchInputProps) => ( - +
+
+
+
+ +
); diff --git a/src/features/core/utils.ts b/src/features/core/utils.ts index 4726d34..3428a91 100644 --- a/src/features/core/utils.ts +++ b/src/features/core/utils.ts @@ -1,10 +1,12 @@ -export const sortUtil = (a: unknown, b: unknown, sorting: "asc" | "desc") => { +import BigNumber from "bignumber.js"; + +export const sortUtil = (a: unknown, b: unknown, isAsc: boolean) => { if (typeof a !== typeof b) { return 0; } if (typeof a === "string") { - return sorting === "asc" + return isAsc ? a.localeCompare(b as string) : (b as string).localeCompare(a); } @@ -14,7 +16,11 @@ export const sortUtil = (a: unknown, b: unknown, sorting: "asc" | "desc") => { return 0; } - return sorting === "asc" ? a - (b as number) : (b as number) - a; + return isAsc ? a - (b as number) : (b as number) - a; + } + + if (a instanceof BigNumber && b instanceof BigNumber) { + return isAsc ? a.minus(b).toNumber() : b.minus(a).toNumber(); } return 0; diff --git a/src/features/staking/components/delegation-details.tsx b/src/features/staking/components/delegation-details.tsx index bff656e..cc01aa6 100644 --- a/src/features/staking/components/delegation-details.tsx +++ b/src/features/staking/components/delegation-details.tsx @@ -19,11 +19,16 @@ import { } from "../context/actions"; import { useStaking } from "../context/hooks"; import { setModalOpened } from "../context/reducer"; -import { getTotalDelegation, getTotalUnbonding } from "../context/selectors"; +import { + getAllValidators, + getTotalDelegation, + getTotalUnbonding, +} from "../context/selectors"; import type { StakingContextType, StakingState } from "../context/state"; import { useValidatorLogo } from "../hooks"; import { coinIsPositive } from "../lib/core/coins"; import { menu, pointer } from "../lib/core/icons"; +import { cancelUnstake } from "../lib/core/tx"; import { formatCoin, formatCommission, @@ -87,6 +92,7 @@ type DelegationRowProps = { disabled?: boolean; index: number; staking: StakingContextType; + validator?: Validator; }; const DelegationRowBase = ({ @@ -97,21 +103,14 @@ const DelegationRowBase = ({ disabled, index, staking, + validator, }: DelegationRowProps) => { - const [validator, setValidator] = useState(null); const { validatorAddress } = delegation; useEffect(() => { - (async () => { - if (validatorAddress) { - const newValidator = await getAndSetValidatorAction( - validatorAddress, - staking, - ); - - setValidator(newValidator); - } - })(); + if (validatorAddress) { + getAndSetValidatorAction(validatorAddress, staking); + } }, [validatorAddress, staking]); const logo = useValidatorLogo(validator?.description.identity); @@ -145,7 +144,7 @@ const DelegationRowBase = ({
-
+
{ @@ -209,16 +208,27 @@ const DelegationRow = memo(DelegationRowBase); type UnbondingRowProps = { disabled?: boolean; - staking: StakingContextType; + stakingRef: ReturnType; unbonding: NonNullable["items"][number]; + validator?: Validator; }; -const UnbondingRow = ({ disabled, staking, unbonding }: UnbondingRowProps) => { - const validator = (staking.state.validators?.items || []).find( - (v) => v.operatorAddress === unbonding.validator, - ); +const UnbondingRow = ({ + disabled, + stakingRef, + unbonding, + validator, +}: UnbondingRowProps) => { + const { client, staking } = stakingRef; const logo = useValidatorLogo(validator?.description.identity); + const validatorAddress = unbonding.validator; + + useEffect(() => { + if (validatorAddress) { + getAndSetValidatorAction(validatorAddress, staking); + } + }, [validatorAddress, staking]); return (
@@ -245,14 +255,22 @@ const UnbondingRow = ({ disabled, staking, unbonding }: UnbondingRowProps) => {
{formatUnbondingCompletionTime(unbonding.completionTime)}
-
+
{ - // @TODO + if (!client) return; + + const addresses = { + delegator: staking.state.tokens?.denom || "", + validator: unbonding.validator, + }; + + cancelUnstake(addresses, client); }} + variant="danger" > - Cancel + Cancel Unstake
@@ -293,6 +311,8 @@ const DelegationDetails = () => { const [unbondingsSortMethod, setUnbondingsSortMethod] = useState("none"); + const validatorsMap = getAllValidators(staking.state); + const { delegations, unbondings } = staking.state; const hasDelegations = !!delegations?.items.length; @@ -306,16 +326,6 @@ const DelegationDetails = () => {
{hasDelegations && (() => { - const validatorIdToValidator = - staking.state.validators?.items.reduce( - (acc, v) => { - acc[v.operatorAddress] = v; - - return acc; - }, - { ...staking.state.extraValidators }, - ) || {}; - const sortedDelegations = delegations.items.slice().sort((a, b) => { switch (delegationsSortMethod) { case "staked-asc": @@ -323,7 +333,7 @@ const DelegationDetails = () => { return sortUtil( a.balance.amount, b.balance.amount, - delegationsSortMethod === "staked-asc" ? "asc" : "desc", + delegationsSortMethod === "staked-asc", ); case "rewards-asc": @@ -331,19 +341,19 @@ const DelegationDetails = () => { return sortUtil( a.rewards.amount, b.rewards.amount, - delegationsSortMethod === "rewards-asc" ? "asc" : "desc", + delegationsSortMethod === "rewards-asc", ); case "commission-asc": case "commission-desc": { - const validatorA = validatorIdToValidator[a.validatorAddress]; - const validatorB = validatorIdToValidator[b.validatorAddress]; + const validatorA = validatorsMap[a.validatorAddress]; + const validatorB = validatorsMap[b.validatorAddress]; return sortUtil( Number(validatorA?.commission.commissionRates.rate), Number(validatorB?.commission.commissionRates.rate), - delegationsSortMethod === "commission-asc" ? "asc" : "desc", + delegationsSortMethod === "commission-asc", ); } @@ -390,6 +400,7 @@ const DelegationDetails = () => { index={index} key={index} staking={staking} + validator={validatorsMap[delegation.validatorAddress]} /> ))}
@@ -404,7 +415,7 @@ const DelegationDetails = () => { return sortUtil( Number(a.balance.amount), Number(b.balance.amount), - unbondingsSortMethod === "amount-asc" ? "asc" : "desc", + unbondingsSortMethod === "amount-asc", ); case "completion-asc": @@ -412,7 +423,7 @@ const DelegationDetails = () => { return sortUtil( a.completionTime, b.completionTime, - unbondingsSortMethod === "completion-asc" ? "asc" : "desc", + unbondingsSortMethod === "completion-asc", ); default: @@ -448,8 +459,9 @@ const DelegationDetails = () => { {sortedUnbondings.map((unbonding, index) => ( ))}
diff --git a/src/features/staking/components/modals/staking.tsx b/src/features/staking/components/modals/staking.tsx index 2d7f19d..95d4af2 100644 --- a/src/features/staking/components/modals/staking.tsx +++ b/src/features/staking/components/modals/staking.tsx @@ -82,6 +82,23 @@ const StakingModal = () => { >
{(() => { + const getStakingSummary = () => ( + <> +
+ Staked Amount (XION) + {amountXION} + {amountUSD && ( + ${formatToSmallDisplay(amountUSD)} + )} +
+ {!!memo && ( +
+
{memo}
+
+ )} + + ); + if (step === "completed") { return ( <> @@ -95,16 +112,7 @@ const StakingModal = () => { in securing the XION network.
-
- Staked Amount - {amountXION} - $24N -
- {!!memo && ( -
-
{memo}
-
- )} + {getStakingSummary()}
-
- Staked Amount - {amountXION} - $24N -
- {!!memo && ( -
-
{memo}
-
- )} + {getStakingSummary()} + + ); + } + return ( <>
diff --git a/src/features/staking/components/staking-overview.tsx b/src/features/staking/components/staking-overview.tsx index 69b0b61..bef0620 100644 --- a/src/features/staking/components/staking-overview.tsx +++ b/src/features/staking/components/staking-overview.tsx @@ -1,8 +1,9 @@ -import { useAbstraxionAccount } from "@burnt-labs/abstraxion"; +import { useAbstraxionAccount, useModal } from "@burnt-labs/abstraxion"; import { memo } from "react"; import { BodyMedium, + Button, ButtonPill, Heading2, Heading8, @@ -15,10 +16,15 @@ import { getEmptyXionCoin } from "../lib/core/coins"; import { basePath } from "../lib/core/constants"; import { getIsMinimumClaimable } from "../lib/core/tx"; import { formatCoin, formatXionToUSD } from "../lib/formatters"; +import { DivisorVertical } from "./divisor"; + +const divisorStyle = "absolute bottom-[24px] right-[10px] top-[24px]"; +const columnStyle = "relative flex h-full flex-col items-start gap-3 p-[24px]"; const StakingOverview = () => { const { isConnected } = useAbstraxionAccount(); const { staking } = useStaking(); + const [, setShowAbstraxion] = useModal(); if (!isConnected) { return ( @@ -30,7 +36,15 @@ const StakingOverview = () => { }} > Please Log In To View -
Log In
+
+ +
); } @@ -52,26 +66,35 @@ const StakingOverview = () => { gridTemplateColumns: "1fr 1fr 1fr 1fr", }} > -
+
Claimable Rewards
{formatXionToUSD(totalRewards)} - {getIsMinimumClaimable(totalRewards) && ( + {getIsMinimumClaimable(totalRewards) /* @TODO */ && ( Claim )}
{formatCoin(totalRewards)} +
+ +
-
+
APR 15.57% +
+ +
-
+
Delegated Amount {formatXionToUSD(totalDelegation)} {formatCoin(totalDelegation)} +
+ +
-
+
Available For Delegation {formatXionToUSD(availableDelegation)} {formatCoin(availableDelegation)} diff --git a/src/features/staking/components/validator-page.tsx b/src/features/staking/components/validator-page.tsx index 39cefea..110f906 100644 --- a/src/features/staking/components/validator-page.tsx +++ b/src/features/staking/components/validator-page.tsx @@ -2,6 +2,7 @@ import { Button } from "@burnt-labs/ui"; import BigNumber from "bignumber.js"; +import { BondStatus } from "cosmjs-types/cosmos/staking/v1beta1/staking"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; @@ -117,21 +118,23 @@ export default function ValidatorPage() {
)}
-
- -
+ {validatorDetails.status === BondStatus.BOND_STATUS_BONDED && ( +
+ +
+ )}
diff --git a/src/features/staking/components/validators-table.tsx b/src/features/staking/components/validators-table.tsx index be8d2cd..c55b703 100644 --- a/src/features/staking/components/validators-table.tsx +++ b/src/features/staking/components/validators-table.tsx @@ -1,6 +1,7 @@ "use client"; import BigNumber from "bignumber.js"; +import { BondStatus } from "cosmjs-types/cosmos/staking/v1beta1/staking"; import { memo, useState } from "react"; import { @@ -10,6 +11,7 @@ import { Title, } from "@/features/core/components/base"; import { HeaderTitleBase } from "@/features/core/components/table"; +import { sortUtil } from "@/features/core/utils"; import { useStaking } from "../context/hooks"; import { setModalOpened } from "../context/reducer"; @@ -35,9 +37,9 @@ const gridStyle = { type ValidatorItemProps = { disabled?: boolean; - onStake: () => void; + onStake: (() => void) | null; staking: StakingContextType; - validator: NonNullable["items"][number]; + validator: NonNullable["items"][number]; }; const ValidatorRow = ({ @@ -103,11 +105,13 @@ const ValidatorRow = ({ Details
-
- - Delegate - -
+ {onStake && ( +
+ + Delegate + +
+ )}
; const ValidatorsTable = () => { const { isConnected, staking } = useStaking(); const [sortMethod, setSortMethod] = useState("none"); + const [currentTab, setCurrentTab] = useState<"active" | "inactive">("active"); const [searchValue, setSearchValue] = useState(""); - const { validators } = staking.state; + const { validators: validatorsObj } = staking.state; - if (!validators?.items.length) return null; + const validators = + currentTab === "active" + ? validatorsObj.bonded?.items || [] + : (validatorsObj.unbonded?.items || []).concat( + validatorsObj.unbonding?.items || [], + ); - const sortedItems = validators.items + const sortedItems = validators + .filter( + currentTab === "active" + ? (v) => v.status === BondStatus.BOND_STATUS_BONDED + : (v) => + v.status === + (BondStatus.BOND_STATUS_UNBONDED || + BondStatus.BOND_STATUS_UNBONDING), + ) .slice() .sort((a, b) => { if (sortMethod === "none") return 0; @@ -148,42 +166,36 @@ const ValidatorsTable = () => { const votingPowerA = getVotingPowerPerc(a.tokens, staking.state); const votingPowerB = getVotingPowerPerc(b.tokens, staking.state); - if (!votingPowerA || !votingPowerB) return 0; - - return sortMethod === "voting-power-asc" - ? votingPowerA - votingPowerB - : votingPowerB - votingPowerA; + return sortUtil( + votingPowerA, + votingPowerB, + sortMethod === "voting-power-asc", + ); } if (["commission-asc", "commission-desc"].includes(sortMethod)) { const commissionA = parseFloat(a.commission.commissionRates.rate); const commissionB = parseFloat(b.commission.commissionRates.rate); - if (!commissionA || !commissionB) return 0; - - return sortMethod === "commission-asc" - ? commissionA - commissionB - : commissionB - commissionA; + return sortUtil( + commissionA, + commissionB, + sortMethod === "commission-asc", + ); } if (["name-asc", "name-desc"].includes(sortMethod)) { const nameA = a.description.moniker.toLowerCase(); const nameB = b.description.moniker.toLowerCase(); - if (!nameA || !nameB) return 0; - - return sortMethod === "name-asc" - ? nameA.localeCompare(nameB) - : nameB.localeCompare(nameA); + return sortUtil(nameA, nameB, sortMethod === "name-asc"); } if (["staked-asc", "staked-desc"].includes(sortMethod)) { const aTokens = new BigNumber(a.tokens); const bTokens = new BigNumber(b.tokens); - return sortMethod === "staked-asc" - ? aTokens.minus(bTokens).toNumber() - : bTokens.minus(aTokens).toNumber(); + return sortUtil(aTokens, bTokens, sortMethod === "staked-asc"); } return 0; @@ -202,14 +214,32 @@ const ValidatorsTable = () => { return ( <> -
- Validators - setSearchValue(e.target.value)} - value={searchValue} - /> +
+
+ Validators + setSearchValue(e.target.value)} + value={searchValue} + /> +
+
+ + +
-
+
{ { - staking.dispatch( - setModalOpened({ - content: { validator }, - type: "delegate", - }), - ); - }} + onStake={ + currentTab === "active" + ? () => { + staking.dispatch( + setModalOpened({ + content: { validator }, + type: "delegate", + }), + ); + } + : null + } staking={staking} validator={validator} /> diff --git a/src/features/staking/context/actions.ts b/src/features/staking/context/actions.ts index 2d7f5f6..e7e0c42 100644 --- a/src/features/staking/context/actions.ts +++ b/src/features/staking/context/actions.ts @@ -23,27 +23,40 @@ import { setValidatorDetails, setValidators, } from "./reducer"; +import { getAllValidators } from "./selectors"; import type { StakingContextType, Unbonding } from "./state"; export const fetchStakingDataAction = async (staking: StakingContextType) => { try { staking.dispatch(setIsInfoLoading(true)); - const [validators, pool] = await Promise.all([ - getValidatorsList(), - getPool(), - ]); - - staking.dispatch( - setValidators( - { - items: validators.validators, - nextKey: validators.pagination?.nextKey || null, - total: validators.pagination?.total || null, - }, - true, - ), - ); + const [validatorsBonded, validatorsUnbonded, validatorsUnbonding, pool] = + await Promise.all([ + getValidatorsList("BOND_STATUS_BONDED"), + getValidatorsList("BOND_STATUS_UNBONDED"), + getValidatorsList("BOND_STATUS_UNBONDING"), + getPool(), + ]); + + ( + [ + [validatorsBonded, "bonded"], + [validatorsUnbonded, "unbonded"], + [validatorsUnbonding, "unbonding"], + ] as const + ).forEach(([validators, status]) => { + staking.dispatch( + setValidators( + { + items: validators.validators, + nextKey: validators.pagination?.nextKey || null, + total: validators.pagination?.total || null, + }, + true, + status, + ), + ); + }); staking.dispatch(setPool(pool)); @@ -179,9 +192,8 @@ export const getValidatorDetailsAction = async ( } const details = - staking.state.validators?.items.find( - (v) => v.operatorAddress === validatorAddress, - ) || (await getValidatorDetails(validatorAddress)); + getAllValidators(staking.state)[validatorAddress] || + (await getValidatorDetails(validatorAddress)); staking.dispatch(setValidatorDetails(details)); @@ -192,11 +204,7 @@ export const getAndSetValidatorAction = async ( validatorAddress: string, staking: StakingContextType, ) => { - const details = - staking.state.extraValidators[validatorAddress] || - staking.state.validators?.items.find( - (v) => v.operatorAddress === validatorAddress, - ); + const details = getAllValidators(staking.state)[validatorAddress]; if (details) { return details; diff --git a/src/features/staking/context/reducer.ts b/src/features/staking/context/reducer.ts index 43d1086..27fa4b8 100644 --- a/src/features/staking/context/reducer.ts +++ b/src/features/staking/context/reducer.ts @@ -1,4 +1,4 @@ -import type { StakingState } from "./state"; +import type { StakingState, ValidatorStatus } from "./state"; export type StakingAction = | { @@ -12,8 +12,9 @@ export type StakingAction = type: "ADD_UNBONDINGS"; } | { - content: NonNullable; + content: NonNullable; reset: boolean; + status: ValidatorStatus; type: "ADD_VALIDATORS"; } | { @@ -65,9 +66,11 @@ export const setIsInfoLoading = ( export const setValidators = ( validators: Content<"ADD_VALIDATORS">, reset: boolean, + status: ValidatorStatus, ): StakingAction => ({ content: validators, reset, + status, type: "ADD_VALIDATORS", }); @@ -122,7 +125,7 @@ export const setExtraValidators = ( // Used for pagination const getUniqueValidators = ( - validators: NonNullable["items"], + validators: NonNullable["items"], ) => { const validatorIds = new Set(); @@ -171,13 +174,17 @@ const getUniqueUnbondings = ( }); }; -export const reducer = (state: StakingState, action: StakingAction) => { +export const reducer = ( + state: StakingState, + action: StakingAction, +): StakingState => { switch (action.type) { case "SET_TOKENS": return { ...state, tokens: action.content }; case "ADD_VALIDATORS": { - const currentValidators = (!action.reset && state.validators) || { + const currentValidators = (!action.reset && + state.validators[action.status]) || { items: [], nextKey: null, total: null, @@ -192,7 +199,10 @@ export const reducer = (state: StakingState, action: StakingAction) => { return { ...state, - validators: currentValidators, + validators: { + ...state.validators, + [action.status]: currentValidators, + }, }; } @@ -269,7 +279,6 @@ export const reducer = (state: StakingState, action: StakingAction) => { ...state, delegations: null, unbondings: null, - validators: null, }; } diff --git a/src/features/staking/context/selectors.ts b/src/features/staking/context/selectors.ts index 0fd3ca0..dfe096e 100644 --- a/src/features/staking/context/selectors.ts +++ b/src/features/staking/context/selectors.ts @@ -1,4 +1,5 @@ import BigNumber from "bignumber.js"; +import type { Validator } from "cosmjs-types/cosmos/staking/v1beta1/staking"; import { normaliseCoin, sumAllCoins } from "../lib/core/coins"; import type { StakingState } from "./state"; @@ -86,3 +87,20 @@ export const getVotingPowerPerc = ( .div(new BigNumber(pool.bondedTokens)) .toNumber(); }; + +export const getAllValidators = ( + state: StakingState, +): Record => + Object.values(state.validators) + .map((v) => v?.items) + .flat() + .reduce((acc, v) => { + if (!v) { + return acc; + } + + return { + ...acc, + [v.operatorAddress]: v, + }; + }, state.extraValidators); diff --git a/src/features/staking/context/state.tsx b/src/features/staking/context/state.tsx index 42a4b00..74c9452 100644 --- a/src/features/staking/context/state.tsx +++ b/src/features/staking/context/state.tsx @@ -33,6 +33,8 @@ type ModalContent = { type: "delegate" | "rewards" | "undelegate"; } | null; +export type ValidatorStatus = "bonded" | "unbonded" | "unbonding"; + export type StakingState = { delegations: Paginated; extraValidators: Record; @@ -42,7 +44,7 @@ export type StakingState = { tokens: Coin | null; unbondings: Paginated; validatorDetails: null | Validator; - validators: Paginated; + validators: Record>; }; export type StakingContextType = { @@ -59,7 +61,11 @@ export const defaultState: StakingState = { tokens: null, unbondings: null, validatorDetails: null, - validators: null, + validators: { + bonded: null, + unbonded: null, + unbonding: null, + }, }; export const StakingContext = createContext({ diff --git a/src/features/staking/lib/core/base.ts b/src/features/staking/lib/core/base.ts index ef224f2..f275b57 100644 --- a/src/features/staking/lib/core/base.ts +++ b/src/features/staking/lib/core/base.ts @@ -1,3 +1,4 @@ +import type { BondStatusString } from "@cosmjs/stargate/build/modules/staking/queries"; import BigNumber from "bignumber.js"; import type { QueryValidatorsResponse } from "cosmjs-types/cosmos/staking/v1beta1/query"; import type { @@ -10,18 +11,16 @@ import { normaliseCoin } from "./coins"; let validatorsRequest: null | Promise = null; -export const getValidatorsList = async () => { +export const getValidatorsList = async (bondStatus: BondStatusString) => { if (validatorsRequest) return validatorsRequest; const queryClient = await getStakingQueryClient(); - validatorsRequest = queryClient.staking - .validators("BOND_STATUS_BONDED") - .then((res) => { - validatorsRequest = null; + validatorsRequest = queryClient.staking.validators(bondStatus).then((res) => { + validatorsRequest = null; - return res; - }); + return res; + }); return validatorsRequest; }; diff --git a/src/features/staking/lib/core/constants.ts b/src/features/staking/lib/core/constants.ts index 3adf71a..6244b7d 100644 --- a/src/features/staking/lib/core/constants.ts +++ b/src/features/staking/lib/core/constants.ts @@ -18,3 +18,7 @@ export const basePath = export const xionToUSD = 10; export const defaultAvatar = `${basePath}/default-avatar.svg`; + +// Even if this can be retrieved from the params, hardcode it to avoid and +// extra request +export const unbondingDays = isTestnet ? 3 : 21; diff --git a/src/features/staking/lib/core/icons.ts b/src/features/staking/lib/core/icons.ts index 4040686..96e287e 100644 --- a/src/features/staking/lib/core/icons.ts +++ b/src/features/staking/lib/core/icons.ts @@ -31,4 +31,22 @@ export const menu = [ '', '', "", -].join(" "); +].join(""); + +export const loader = [ + '', + '', + "", + '', + "", + '', + "", + "", + "", +].join(""); + +export const search = [ + '', + '', + "", +].join(""); diff --git a/src/features/staking/lib/core/tx.ts b/src/features/staking/lib/core/tx.ts index bf54230..7f7c676 100644 --- a/src/features/staking/lib/core/tx.ts +++ b/src/features/staking/lib/core/tx.ts @@ -8,6 +8,7 @@ import type { import BigNumber from "bignumber.js"; import { MsgWithdrawDelegatorReward } from "cosmjs-types/cosmos/distribution/v1beta1/tx"; import { + MsgCancelUnbondingDelegation, MsgDelegate, MsgUndelegate, } from "cosmjs-types/cosmos/staking/v1beta1/tx"; @@ -155,3 +156,34 @@ export const getIsMinimumClaimable = (amount: Coin) => { return new BigNumber(normalised.amount).gte(minClaimableXion); }; + +export const cancelUnstake = async ( + addresses: StakeAddresses, + client: NonNullable, +) => { + const msg = MsgCancelUnbondingDelegation.fromPartial({ + delegatorAddress: addresses.delegator, + validatorAddress: addresses.validator, + }); + + const messageWrapper = { + typeUrl: "/cosmos.staking.v1beta1.MsgCancelUnbondingDelegation" as string, + value: msg, + } as MsgUndelegateEncodeObject; // cosmjs doesn't have yet this encode object + + const fee = await getCosmosFee({ + address: addresses.delegator, + msgs: [messageWrapper], + }); + + return await client + .signAndBroadcast(addresses.delegator, [messageWrapper], fee) + .then((result) => { + // @TODO + // eslint-disable-next-line no-console + console.log("debug: tx.ts: cancelUnstake: result", result); + + return result; + }) + .catch(handleTxError); +}; diff --git a/src/features/staking/lib/formatters.ts b/src/features/staking/lib/formatters.ts index 87636be..720a15f 100644 --- a/src/features/staking/lib/formatters.ts +++ b/src/features/staking/lib/formatters.ts @@ -55,6 +55,10 @@ export const formatXionToUSD = (coin: Coin | null, compact?: boolean) => { const value = coin ? new BigNumber(normalised.amount) : new BigNumber(0); const usd = value.times(xionToUSD); + if (usd.eq(0)) { + return "$0"; + } + if (!compact && usd.lt(0.01)) { return "<$0.01"; }