diff --git a/package-lock.json b/package-lock.json index f33356b..d56ca5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "postcss": "^8.4.35", "prettier": "^3.2.5", "tailwindcss": "^3.4.1", + "ts-unused-exports": "^10.0.1", "typescript": "^5.3.3" } }, @@ -9525,6 +9526,100 @@ "node": ">=8" } }, + "node_modules/ts-unused-exports": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ts-unused-exports/-/ts-unused-exports-10.0.1.tgz", + "integrity": "sha512-nWG8Y96pKem01Hw4j4+Mwuy+L0/9sKT7D61Q+OS3cii9ocQACuV6lu00B9qpiPhF4ReVWw3QYHDqV8+to2wbsg==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "tsconfig-paths": "^3.9.0" + }, + "bin": { + "ts-unused-exports": "bin/ts-unused-exports" + }, + "funding": { + "url": "https://github.com/pzavolinsky/ts-unused-exports?sponsor=1" + }, + "peerDependencies": { + "typescript": ">=3.8.3" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": false + } + } + }, + "node_modules/ts-unused-exports/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ts-unused-exports/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ts-unused-exports/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ts-unused-exports/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/ts-unused-exports/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-unused-exports/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", diff --git a/package.json b/package.json index 298d088..6dc1413 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "next build", "dev": "PORT=4000 next dev", - "lint": "npm run lint:eslint && npm run lint:prettier", + "lint": "bash ./scripts/check_unused_exports.sh && npm run lint:eslint && npm run lint:prettier", "lint:eslint": "eslint . --ext .ts,.tsx", "lint:prettier": "prettier --check .", "start": "next start", @@ -45,6 +45,7 @@ "postcss": "^8.4.35", "prettier": "^3.2.5", "tailwindcss": "^3.4.1", + "ts-unused-exports": "^10.0.1", "typescript": "^5.3.3" } } diff --git a/scripts/check_unused_exports.sh b/scripts/check_unused_exports.sh new file mode 100755 index 0000000..e5d43dc --- /dev/null +++ b/scripts/check_unused_exports.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -e + +./node_modules/.bin/ts-unused-exports tsconfig.json \ + --excludePathsFromReport='next.config' \ + --excludePathsFromReport='tailwind.config' \ + --excludePathsFromReport='.*pages.*' \ + --excludePathsFromReport='.*\.next\.*' diff --git a/src/features/staking/components/main-page.tsx b/src/features/staking/components/main-page.tsx index ae1fd3c..d6d234e 100644 --- a/src/features/staking/components/main-page.tsx +++ b/src/features/staking/components/main-page.tsx @@ -9,15 +9,15 @@ import { toast } from "react-toastify"; import { claimRewardsAction, - setRedelegateAction, stakeValidatorAction, unstakeValidatorAction, } from "../context/actions"; import { useStaking } from "../context/hooks"; +import { getTotalDelegation } from "../context/selectors"; import type { StakingState } from "../context/state"; import type { StakeAddresses } from "../lib/core/base"; -import { formatCoin } from "../lib/core/coins"; import { chainId } from "../lib/core/constants"; +import { formatCoin } from "../lib/formatters"; import { keybaseClient } from "../lib/utils/keybase-client"; import DebugAccount from "./debug-account"; @@ -99,6 +99,8 @@ function StakingPage() { [validators], ); + const totalDelegation = getTotalDelegation(staking.state); + return ( <>
@@ -120,6 +122,11 @@ function StakingPage() { Tokens: {formatCoin(tokens)}
)} + {totalDelegation && ( +
+ Total delegation: {formatCoin(totalDelegation)} +
+ )} {isInfoLoading &&
Loading ...
} {!!delegations?.items.length && ( @@ -198,24 +205,6 @@ function StakingPage() { Claim rewards )} - ); diff --git a/src/features/staking/components/validator-page.tsx b/src/features/staking/components/validator-page.tsx index 1ae1b82..208b863 100644 --- a/src/features/staking/components/validator-page.tsx +++ b/src/features/staking/components/validator-page.tsx @@ -1,6 +1,5 @@ "use client"; -import BigNumber from "bignumber.js"; import { BondStatus } from "cosmjs-types/cosmos/staking/v1beta1/staking"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; @@ -8,6 +7,8 @@ import { useEffect, useState } from "react"; import { getValidatorDetailsAction } from "../context/actions"; import { useStaking } from "../context/hooks"; +import { getVotingPowerPerc } from "../context/selectors"; +import { formatCommission, formatVotingPowerPerc } from "../lib/formatters"; import { keybaseClient } from "../lib/utils/keybase-client"; export default function ValidatorPage() { @@ -15,8 +16,6 @@ export default function ValidatorPage() { const address = searchParams.get("address"); const stakingRef = useStaking(); - const { pool } = stakingRef.staking.state; - const [logo, setLogo] = useState(null); const [validatorDetails, setValidatorDetails] = useStateLoading ...; } - const parseCommissionRate = (commissionRate: string) => { - const comission = new BigNumber(commissionRate) - .div(new BigNumber(10).pow(18)) - .toNumber(); - - return `${(comission * 100).toFixed(0)}%`; - }; - - const votingPowerPercentage = (() => { - const validatorTokens = validatorDetails.tokens; - - if (!validatorTokens || typeof pool?.bondedTokens !== "string") { - return null; - } - - const perc = new BigNumber(validatorTokens) - .div(new BigNumber(pool.bondedTokens)) - .toNumber(); - - const percNum = perc < 0.0001 ? "<0.1" : (perc * 100).toFixed(1); + const votingPowerPerc = getVotingPowerPerc( + validatorDetails.tokens, + stakingRef.staking.state, + ); - return `${percNum}%`; - })(); + const votingPowerPercStr = formatVotingPowerPerc(votingPowerPerc); return (
@@ -87,7 +69,17 @@ export default function ValidatorPage() {
{validatorDetails.description.website}
Commission:{" "} - {parseCommissionRate(validatorDetails.commission.commissionRates.rate)} + {formatCommission(validatorDetails.commission.commissionRates.rate)} +
+
+ Max commission:{" "} + {formatCommission(validatorDetails.commission.commissionRates.maxRate)} +
+
+ Max commission change:{" "} + {formatCommission( + validatorDetails.commission.commissionRates.maxChangeRate, + )}
Jailed: {validatorDetails.jailed.toString()}
@@ -96,9 +88,7 @@ export default function ValidatorPage() { ? "Bonded" : validatorDetails.status}
- {votingPowerPercentage && ( -
Voting Power: {votingPowerPercentage}
- )} + {votingPowerPercStr &&
Voting Power: {votingPowerPercStr}
} Back
); diff --git a/src/features/staking/context/actions.ts b/src/features/staking/context/actions.ts index bfcdc95..8834378 100644 --- a/src/features/staking/context/actions.ts +++ b/src/features/staking/context/actions.ts @@ -156,7 +156,9 @@ export const claimRewardsAction = async ( await fetchStakingDataAction(addresses.delegator, staking); }; -export const setRedelegateAction = async ( +// @TODO +// eslint-disable-next-line +const setRedelegateAction = async ( delegatorAddress: string, client: AbstraxionSigningClient, staking: StakingContextType, diff --git a/src/features/staking/context/provider.tsx b/src/features/staking/context/provider.tsx index 7e2d031..ea4e4b1 100644 --- a/src/features/staking/context/provider.tsx +++ b/src/features/staking/context/provider.tsx @@ -7,7 +7,7 @@ import { useStakingSync } from "./hooks"; import { reducer } from "./reducer"; import { StakingContext, defaultState } from "./state"; -export const Wrapper = ({ children }: PropsWithChildren) => { +const Wrapper = ({ children }: PropsWithChildren) => { useStakingSync(); // eslint-disable-next-line react/jsx-no-useless-fragment diff --git a/src/features/staking/context/selectors.ts b/src/features/staking/context/selectors.ts new file mode 100644 index 0000000..58effe0 --- /dev/null +++ b/src/features/staking/context/selectors.ts @@ -0,0 +1,31 @@ +import BigNumber from "bignumber.js"; + +import { sumAllCoins } from "../lib/core/coins"; +import type { StakingState } from "./state"; + +export const getTotalDelegation = (state: StakingState) => { + const { delegations } = state; + + if (!delegations?.items.length) { + return null; + } + + const delegationCoins = delegations.items.map((d) => d.balance); + + return sumAllCoins(delegationCoins); +}; + +export const getVotingPowerPerc = ( + validatorTokens: string, + state: StakingState, +) => { + const { pool } = state; + + if (!validatorTokens || typeof pool?.bondedTokens !== "string") { + return null; + } + + return new BigNumber(validatorTokens) + .div(new BigNumber(pool.bondedTokens)) + .toNumber(); +}; diff --git a/src/features/staking/lib/core/base.ts b/src/features/staking/lib/core/base.ts index 67f324d..c46437c 100644 --- a/src/features/staking/lib/core/base.ts +++ b/src/features/staking/lib/core/base.ts @@ -64,6 +64,9 @@ export const getValidatorDetails = async (address: string) => { return promise; }; +// @TODO: This returns the unbonding time +// const params = await queryClient.staking.params(); + let poolRequest: null | Promise = null; export const getPool = async () => { @@ -210,15 +213,6 @@ export const unstakeAmount = async ( .catch(handleTxError); }; -export const getUnbonding = async ( - address: string, - validatorAddress: string, -) => { - const queryClient = await getStakingQueryClient(); - - return queryClient.staking.unbondingDelegation(address, validatorAddress); -}; - export const claimRewards = async ( addresses: StakeAddresses, client: NonNullable, diff --git a/src/features/staking/lib/core/coins.ts b/src/features/staking/lib/core/coins.ts index 1a5ac7c..97137f7 100644 --- a/src/features/staking/lib/core/coins.ts +++ b/src/features/staking/lib/core/coins.ts @@ -24,23 +24,15 @@ export const normaliseCoin = (coin: Coin) => { }; }; -export const formatCoin = (coin: Coin) => { - const resolved = normaliseCoin(coin); - const amount = new BigNumber(resolved.amount); - - return `${amount.toFormat()} ${resolved.denom}`; -}; - -export const getEmptyXionCoin = () => - ({ amount: "0", denom: "xion" }) satisfies Coin; +const getEmptyXionCoin = () => ({ amount: "0", denom: "xion" }) satisfies Coin; export const sumAllCoins = (coins: Coin[]) => coins.reduce( (acc, coin) => ({ - amount: ( - parseFloat(acc.amount) + parseFloat(normaliseCoin(coin).amount) - ).toString(), - denom: coin.denom, + amount: new BigNumber(acc.amount) + .plus(normaliseCoin(coin).amount) + .toString(), + denom: acc.denom, }), getEmptyXionCoin(), ); diff --git a/src/features/staking/lib/formatters.ts b/src/features/staking/lib/formatters.ts new file mode 100644 index 0000000..fedc0c5 --- /dev/null +++ b/src/features/staking/lib/formatters.ts @@ -0,0 +1,29 @@ +import type { Coin } from "@cosmjs/stargate"; +import BigNumber from "bignumber.js"; + +import { normaliseCoin } from "./core/coins"; + +export const formatCoin = (coin: Coin) => { + const resolved = normaliseCoin(coin); + const amount = new BigNumber(resolved.amount); + + return `${amount.toFormat()} ${resolved.denom}`; +}; + +export const formatVotingPowerPerc = (perc: null | number) => { + if (typeof perc !== "number" || Number.isNaN(perc)) { + return null; + } + + const percNum = perc < 0.0001 ? "<0.1" : (perc * 100).toFixed(1); + + return `${percNum}%`; +}; + +export const formatCommission = (commissionRate: string) => { + const comission = new BigNumber(commissionRate) + .div(new BigNumber(10).pow(18)) + .toNumber(); + + return `${(comission * 100).toFixed(0)}%`; +};