From e69fb369634c70f7fd40f9f6c733f9bd82677035 Mon Sep 17 00:00:00 2001 From: Ignacio Date: Tue, 26 Mar 2024 15:30:19 +0800 Subject: [PATCH] feat: start integrating stake accounts --- public/locales/en/staking.json | 1 + public/locales/zh-CN/staking.json | 1 + public/locales/zh-HK/staking.json | 1 + src/components/icons/solana_wallet.svg | 20 -- src/components/icons/solflare.svg | 11 + .../network_card/popover.module.scss | 64 ------ .../components/network_card/popover.tsx | 155 +++---------- .../network_card/staking_data_box.module.scss | 65 ++++++ .../network_card/staking_data_box.tsx | 212 ++++++++++++++++++ .../staking/lib/staking_sdk/context/index.tsx | 2 +- .../lib/staking_sdk/context/selectors.ts | 23 ++ .../lib/staking_sdk/staking_client_types.ts | 3 +- src/screens/staking/lib/wallet_info.ts | 2 +- 13 files changed, 355 insertions(+), 205 deletions(-) delete mode 100644 src/components/icons/solana_wallet.svg create mode 100644 src/components/icons/solflare.svg create mode 100644 src/screens/staking/components/networks/components/network_grid/components/network_card/staking_data_box.module.scss create mode 100644 src/screens/staking/components/networks/components/network_grid/components/network_card/staking_data_box.tsx diff --git a/public/locales/en/staking.json b/public/locales/en/staking.json index 58e24f80..181b0172 100644 --- a/public/locales/en/staking.json +++ b/public/locales/en/staking.json @@ -105,6 +105,7 @@ "stake desc": "As a token holder, you can utilize our infrastructure by staking your tokens with us", "stake with us!": "Stake with us!", "stake_now": "Stake Now", + "stakeAccounts": "Accounts", "staked by forbole": "Staked by Forbole", "staking para 1": "The action that token holders stake their tokens to validators to secure the network is called staking. Staked tokens are eligible to receive staking rewards. Newly minted tokens and transactions fees are given out as rewards to bonded staking token holders.", "staking para 2": "Stakers stake their staking power to validators, but that does not mean the validators possess or have custody of the tokens staked to them. In other words, validators cannot steal or take away stakers’ tokens. Your staked amount of tokens will not be decreased in most situations, however, if your validator is punished by slashing, your staked token is at risk of being slashed too (see below for “slashing”).", diff --git a/public/locales/zh-CN/staking.json b/public/locales/zh-CN/staking.json index 4331722c..97cc1429 100644 --- a/public/locales/zh-CN/staking.json +++ b/public/locales/zh-CN/staking.json @@ -105,6 +105,7 @@ "stake desc": "作为通证持有人,你可以安心在我们的基础建设上委托质押原生通证", "stake with us!": "与我们一起赌注!", "stake_now": "立即质押", + "stakeAccounts": "Accounts", "staked by forbole": "Forbole 质押", "staking para 1": "代币持有者将其代币质押给验证者以保护网络的行为称为质押。", "staking para 2": "质押者将质押权质押给验证者,但这并不意味着验证者拥有或保管质押给他们的代币。", diff --git a/public/locales/zh-HK/staking.json b/public/locales/zh-HK/staking.json index c2ae6baa..88f2c45d 100644 --- a/public/locales/zh-HK/staking.json +++ b/public/locales/zh-HK/staking.json @@ -105,6 +105,7 @@ "stake desc": "作為通證持有人,你可以安心在我們的基礎建設上委託質押原生通證", "stake with us!": "與我們一起賭注!", "stake_now": "立即質押", + "stakeAccounts": "Accounts", "staked by forbole": "Forbole 質押", "staking para 1": "代幣持有者將其代幣質押給驗證者以保護網路的行為稱為質押。", "staking para 2": "質押者將質押權質押給驗證者,但這並不意味著驗證者擁有或保管質押給他們的代幣。", diff --git a/src/components/icons/solana_wallet.svg b/src/components/icons/solana_wallet.svg deleted file mode 100644 index baa987f7..00000000 --- a/src/components/icons/solana_wallet.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/src/components/icons/solflare.svg b/src/components/icons/solflare.svg new file mode 100644 index 00000000..00b4bf55 --- /dev/null +++ b/src/components/icons/solflare.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/screens/staking/components/networks/components/network_grid/components/network_card/popover.module.scss b/src/screens/staking/components/networks/components/network_grid/components/network_card/popover.module.scss index e12588cc..9dbe3ed9 100644 --- a/src/screens/staking/components/networks/components/network_grid/components/network_card/popover.module.scss +++ b/src/screens/staking/components/networks/components/network_grid/components/network_card/popover.module.scss @@ -102,57 +102,6 @@ $popover-background: linear-gradient( width: 100%; } -.stakingData { - background: #ffffff8f; - border-radius: 8px; - box-shadow: - 0 10px 32px -4px #0226e11a, - 0 6px 14px -6px #0226e11f; - display: flex; - flex-direction: column; - gap: 12px; - line-break: anywhere; - margin-bottom: 8px; - max-width: 100%; - padding: 12px 16px; - width: 100%; - - .unbonding, - .rewards, - .total { - align-items: center; - display: flex; - flex-direction: row; - font-size: 16px; - justify-content: space-between; - width: 100%; - - > div { - font-size: 16px; - font-style: normal; - font-weight: 600; - letter-spacing: 0.032px; - line-height: 20px; - text-shadow: $box-shadow-variant-3; - } - - > div:first-child { - color: #25282d; - text-align: left; - } - - > div:last-child { - text-align: right; - } - } - - .rewards { - > div:nth-child(2) { - color: #059c78; - } - } -} - .buttons { display: flex; flex-direction: column; @@ -177,16 +126,3 @@ $popover-background: linear-gradient( body .stake { font-size: 16px; } - -.totalValue { - > div:nth-child(2) { - color: #616161; - font-size: 14px; - font-style: normal; - font-weight: 400; - letter-spacing: 0.308px; - line-height: 20px; - text-align: right; - text-shadow: $box-shadow-variant-3; - } -} diff --git a/src/screens/staking/components/networks/components/network_grid/components/network_card/popover.tsx b/src/screens/staking/components/networks/components/network_grid/components/network_card/popover.tsx index 44048e2a..e592d2fb 100644 --- a/src/screens/staking/components/networks/components/network_grid/components/network_card/popover.tsx +++ b/src/screens/staking/components/networks/components/network_grid/components/network_card/popover.tsx @@ -6,7 +6,7 @@ import type { ReactNode, SetStateAction, } from "react"; -import { useContext, useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import CtaButton from "@src/components/cta-button"; import EmptyButton from "@src/components/empty-button"; @@ -14,10 +14,7 @@ import HighlightButton from "@src/components/highlight-button"; import CloseIcon from "@src/components/icons/icon_cross.svg"; import IconInfoCircle from "@src/components/icons/info-circle.svg"; import { tooltipId } from "@src/components/tooltip"; -import { - StakingContext, - useStakingRef, -} from "@src/screens/staking/lib/staking_sdk/context"; +import { useStakingRef } from "@src/screens/staking/lib/staking_sdk/context"; import { fetchCoinPriceForNetwork, getNetworkStakingInfo, @@ -27,12 +24,9 @@ import type { NetworkClaimableRewards } from "@src/screens/staking/lib/staking_s import { getAccountsForNetwork, getClaimableRewardsForNetwork, - getCoinPriceForNetwork, getHasNetworkSupportedWallet, getNetworkTVL, getNetworkVotingPower, - getStakedDataForNetwork, - getUnbondingTokensForNetwork, } from "@src/screens/staking/lib/staking_sdk/context/selectors"; import { networkKeyToNetworkId, @@ -42,12 +36,8 @@ import type { Account, StakingNetworkInfo, } from "@src/screens/staking/lib/staking_sdk/core"; -import type { Coin } from "@src/screens/staking/lib/staking_sdk/core/base"; import { WalletId } from "@src/screens/staking/lib/staking_sdk/core/base"; -import { - formatCoin, - formatStakedDataUSD, -} from "@src/screens/staking/lib/staking_sdk/formatters"; +import { formatCoin } from "@src/screens/staking/lib/staking_sdk/formatters"; import { accountHasDelegations, accountHasRewards, @@ -58,6 +48,7 @@ import type { Network, NetworkKey } from "@src/utils/network_info"; import type { ParamsProps } from "../../config"; import * as styles from "./popover.module.scss"; +import StakingDataBox from "./staking_data_box"; type PopOverProps = { canClickNetwork: boolean; @@ -78,6 +69,7 @@ const PopOver = ({ }: PopOverProps) => { const networkNetworkId = networkKeyToNetworkId[network.key as NetworkKey]; const stakingNetworkId = networkKeyToNetworkId[network.key as NetworkKey]; + const nodeRef = useRef(null); const stakingRef = useStakingRef(); @@ -95,10 +87,21 @@ const PopOver = ({ const [stakingNetworkInfo, setStakingNetworkInfo] = useState(null); - const { state: stakingState } = useContext(StakingContext); - + const { state: stakingState } = stakingRef.current; const { hasInit } = stakingState; + useEffect(() => { + if (!nodeRef.current) return; + + const box = nodeRef.current.getBoundingClientRect(); + + if (box.left < 0) { + (nodeRef.current as HTMLElement).style.left = "0"; + } else if (box.right > window.innerWidth) { + (nodeRef.current as HTMLElement).style.right = "0"; + } + }, [nodeRef]); + useEffect(() => { if (stakingNetworkId) { getNetworkStakingInfo(stakingRef.current, stakingNetworkId).then( @@ -109,52 +112,30 @@ const PopOver = ({ } }, [stakingNetworkId, stakingRef]); - const { accounts, claimableRewards, stakedData, unbondingTokens } = - useMemo(() => { - const wallet = WalletId.Keplr; - - const result = { - accounts: null as Account[] | null, - claimableRewards: null as NetworkClaimableRewards | null, - stakedData: null as Coin | null, - unbondingTokens: null as { period: string; text: string } | null, - }; - - if (!!stakingNetworkId && !!wallet) { - result.accounts = getAccountsForNetwork(stakingState, stakingNetworkId); + const { accounts, claimableRewards } = useMemo(() => { + const wallet = WalletId.Keplr; - if (!result.accounts?.length) { - return result; - } + const result = { + accounts: null as Account[] | null, + claimableRewards: null as NetworkClaimableRewards | null, + }; - result.stakedData = getStakedDataForNetwork( - stakingRef.current.state, - stakingNetworkId, - ); + if (!!stakingNetworkId && !!wallet) { + result.accounts = getAccountsForNetwork(stakingState, stakingNetworkId); - result.claimableRewards = - getClaimableRewardsForNetwork( - stakingRef.current.state, - stakingNetworkId, - ) || null; + if (!result.accounts?.length) { + return result; + } - const unbonding = getUnbondingTokensForNetwork( + result.claimableRewards = + getClaimableRewardsForNetwork( stakingRef.current.state, stakingNetworkId, - ); - - if (unbonding) { - result.unbondingTokens = { - period: unbonding.period - ? new Date(Number(unbonding.period) * 1000).toLocaleString() - : "", - text: formatCoin(unbonding.coin, { decimals: 4 }), - }; - } - } + ) || null; + } - return result; - }, [stakingState, stakingNetworkId, stakingRef]); + return result; + }, [stakingState, stakingNetworkId, stakingRef]); useEffect(() => { fetchCoinPriceForNetwork(stakingRef.current, stakingNetworkId); @@ -163,31 +144,13 @@ const PopOver = ({ const accountsWithDelegations = accounts?.filter(accountHasDelegations); const accountsWithRewards = accounts?.filter(accountHasRewards); - const displayedRewards = claimableRewards - ? `+${formatCoin(claimableRewards, { decimals: 4 })}` - : null; - - const displayedStaked = (() => { - if (!stakedData || !stakingNetworkId) return null; - - const coinPrice = getCoinPriceForNetwork( - stakingRef.current.state, - stakingNetworkId, - ); - - if (!coinPrice) return [formatCoin(stakedData)]; - - const stakedDataUSD = formatStakedDataUSD(stakedData, coinPrice); - - return [formatCoin(stakedData), stakedDataUSD].filter(Boolean); - })(); - return (
{ setShowPopover(""); }} + ref={nodeRef} >
{networkImage}
{network.name &&
{network.name}
} - {!![stakedData, claimableRewards, unbondingTokens].filter(Boolean) - .length && ( -
- {displayedStaked && ( -
-
{t("totalStaked")}
-
- {displayedStaked.map((item, itemIdx) => ( -
{item}
- ))} -
-
- )} - {!!claimableRewards && ( -
-
{t("claimableRewards")}
-
- {displayedRewards} -
-
- )} - {!!unbondingTokens && ( -
-
{t("unbondingTokens")}
-
- {unbondingTokens.text} -
-
- )} -
- )} + {!!networkSummary && (
{(() => { diff --git a/src/screens/staking/components/networks/components/network_grid/components/network_card/staking_data_box.module.scss b/src/screens/staking/components/networks/components/network_grid/components/network_card/staking_data_box.module.scss new file mode 100644 index 00000000..fe2f1405 --- /dev/null +++ b/src/screens/staking/components/networks/components/network_grid/components/network_card/staking_data_box.module.scss @@ -0,0 +1,65 @@ +@import "src/styles/sass.scss"; + +.stakingData { + background: #ffffff8f; + border-radius: 8px; + box-shadow: + 0 10px 32px -4px #0226e11a, + 0 6px 14px -6px #0226e11f; + display: flex; + flex-direction: column; + gap: 12px; + line-break: anywhere; + margin-bottom: 8px; + max-width: 100%; + padding: 12px 16px; + width: 100%; + + .unbonding, + .rewards, + .total { + align-items: center; + display: flex; + flex-direction: row; + font-size: 16px; + justify-content: space-between; + width: 100%; + + > div { + font-size: 16px; + font-style: normal; + font-weight: 600; + letter-spacing: 0.032px; + line-height: 20px; + text-shadow: $box-shadow-variant-3; + } + + > div:first-child { + color: #25282d; + text-align: left; + } + + > div:last-child { + text-align: right; + } + } + + .rewards { + > div:nth-child(2) { + color: #059c78; + } + } +} + +.totalValue { + > div:nth-child(2) { + color: #616161; + font-size: 14px; + font-style: normal; + font-weight: 400; + letter-spacing: 0.308px; + line-height: 20px; + text-align: right; + text-shadow: $box-shadow-variant-3; + } +} diff --git a/src/screens/staking/components/networks/components/network_grid/components/network_card/staking_data_box.tsx b/src/screens/staking/components/networks/components/network_grid/components/network_card/staking_data_box.tsx new file mode 100644 index 00000000..8ccfc222 --- /dev/null +++ b/src/screens/staking/components/networks/components/network_grid/components/network_card/staking_data_box.tsx @@ -0,0 +1,212 @@ +import useTranslation from "next-translate/useTranslation"; +import { useEffect, useMemo, useState } from "react"; + +import { tooltipId } from "@src/components/tooltip"; +import { useStakingRef } from "@src/screens/staking/lib/staking_sdk/context"; +import { fetchCoinPriceForNetwork } from "@src/screens/staking/lib/staking_sdk/context/actions"; +import type { NetworkClaimableRewards } from "@src/screens/staking/lib/staking_sdk/context/selectors"; +import { + getClaimableRewardsForNetwork, + getCoinPriceForNetwork, + getStakeAccountsForNetwork, + getStakedDataForNetwork, + getUnbondingTokensForNetwork, +} from "@src/screens/staking/lib/staking_sdk/context/selectors"; +import { networkKeyToNetworkId } from "@src/screens/staking/lib/staking_sdk/core"; +import type { Coin } from "@src/screens/staking/lib/staking_sdk/core/base"; +import { WalletId } from "@src/screens/staking/lib/staking_sdk/core/base"; +import { + formatCoin, + formatStakedDataUSD, +} from "@src/screens/staking/lib/staking_sdk/formatters"; +import type { StakeAccount } from "@src/screens/staking/lib/staking_sdk/staking_client_types"; +import type { Network, NetworkKey } from "@src/utils/network_info"; + +import * as styles from "./staking_data_box.module.scss"; + +type PopOverProps = { + network: Network; +}; + +const StakingDataBox = ({ network }: PopOverProps) => { + const stakingNetworkId = networkKeyToNetworkId[network.key as NetworkKey]; + + const [isDisplayingStakeAccounts, setIsDisplayingStakeAccounts] = + useState(false); + + const stakingRef = useStakingRef(); + + const { t } = useTranslation("staking"); + + const { claimableRewards, stakeAccounts, stakedData, unbondingTokens } = + useMemo(() => { + const wallet = WalletId.Keplr; + + const result = { + claimableRewards: null as NetworkClaimableRewards | null, + stakeAccounts: null as null | StakeAccount[], + stakedData: null as Coin | null, + unbondingTokens: null as { period: string; text: string } | null, + }; + + if (!!stakingNetworkId && !!wallet) { + result.stakeAccounts = getStakeAccountsForNetwork( + stakingRef.current.state, + stakingNetworkId, + ); + + result.stakedData = getStakedDataForNetwork( + stakingRef.current.state, + stakingNetworkId, + ); + + result.claimableRewards = + getClaimableRewardsForNetwork( + stakingRef.current.state, + stakingNetworkId, + ) || null; + + const unbonding = getUnbondingTokensForNetwork( + stakingRef.current.state, + stakingNetworkId, + ); + + if (unbonding) { + result.unbondingTokens = { + period: unbonding.period + ? new Date(Number(unbonding.period) * 1000).toLocaleString() + : "", + text: formatCoin(unbonding.coin, { decimals: 4 }), + }; + } + } + + return result; + }, [stakingNetworkId, stakingRef]); + + useEffect(() => { + fetchCoinPriceForNetwork(stakingRef.current, stakingNetworkId); + }, [stakingRef, stakingNetworkId]); + + const displayedRewards = claimableRewards + ? `+${formatCoin(claimableRewards, { decimals: 4 })}` + : null; + + const displayedStaked = (() => { + if (!stakedData || !stakingNetworkId) return null; + + const coinPrice = getCoinPriceForNetwork( + stakingRef.current.state, + stakingNetworkId, + ); + + if (!coinPrice) return [formatCoin(stakedData)]; + + const stakedDataUSD = formatStakedDataUSD(stakedData, coinPrice); + + return [formatCoin(stakedData), stakedDataUSD].filter(Boolean); + })(); + + if ( + ![ + stakedData, + claimableRewards, + unbondingTokens, + stakeAccounts?.length, + ].filter(Boolean).length + ) { + return null; + } + + const content = (() => { + if (isDisplayingStakeAccounts) { + return ( +
+
+ {" "} + Accounts {stakeAccounts?.length} +
+
+ {stakeAccounts?.map((account, accountIdx) => ( +
+
{account.address}
+
{formatCoin(account)}
+
{account.status}
+
+ ))} +
+
+ ); + } + + return ( + <> + {displayedStaked && ( +
+
{t("totalStaked")}
+
+ {displayedStaked.map((item, itemIdx) => ( +
{item}
+ ))} +
+
+ )} + {!!claimableRewards && ( +
+
{t("claimableRewards")}
+
+ {displayedRewards} +
+
+ )} + {!!unbondingTokens && ( +
+
{t("unbondingTokens")}
+
+ {unbondingTokens.text} +
+
+ )} + {!!stakeAccounts?.length && ( +
+
{t("stakeAccounts")}
+
+ {stakeAccounts.length}{" "} + +
+
+ )} + + ); + })(); + + return
{content}
; +}; + +export default StakingDataBox; diff --git a/src/screens/staking/lib/staking_sdk/context/index.tsx b/src/screens/staking/lib/staking_sdk/context/index.tsx index b3abe542..6cb3cb59 100644 --- a/src/screens/staking/lib/staking_sdk/context/index.tsx +++ b/src/screens/staking/lib/staking_sdk/context/index.tsx @@ -34,7 +34,7 @@ const baseContext: TStakingContext = { state: defaultState, }; -export const StakingContext = createContext(baseContext); +const StakingContext = createContext(baseContext); export const StakingProvider = ({ children }: PropsWithChildren) => { const [state, setState] = useState( diff --git a/src/screens/staking/lib/staking_sdk/context/selectors.ts b/src/screens/staking/lib/staking_sdk/context/selectors.ts index cc728cbf..53d1ceab 100644 --- a/src/screens/staking/lib/staking_sdk/context/selectors.ts +++ b/src/screens/staking/lib/staking_sdk/context/selectors.ts @@ -4,6 +4,7 @@ import type { Account, StakingState } from "../core"; import { walletsSupported } from "../core"; import type { Coin, CoinDenom, StakingNetworkId, WalletId } from "../core/base"; import { mainNetworkDenom } from "../core/base"; +import type { StakeAccount } from "../staking_client_types"; import { filterUniqueAddresses, getClaimableRewardsForAccount, @@ -58,6 +59,28 @@ export const getAccountsForNetwork = ( ); }; +export const getStakeAccountsForNetwork = ( + state: StakingState, + network: StakingNetworkId, +) => { + const accounts = getAccountsForNetwork(state, network); + const uniqueAccounts = new Set(); + + return accounts + .map((account) => account.info?.stakeAccounts) + .flat() + .filter((a): a is StakeAccount => !!a) + .filter((a) => { + if (uniqueAccounts.has(a.address)) { + return false; + } + + uniqueAccounts.add(a.address); + + return true; + }); +}; + export const getStakedDataForNetwork = ( state: StakingState, network: StakingNetworkId, diff --git a/src/screens/staking/lib/staking_sdk/staking_client_types.ts b/src/screens/staking/lib/staking_sdk/staking_client_types.ts index cd3260c1..5decba82 100644 --- a/src/screens/staking/lib/staking_sdk/staking_client_types.ts +++ b/src/screens/staking/lib/staking_sdk/staking_client_types.ts @@ -28,10 +28,11 @@ type Coin = { denom: string; }; -type StakeAccount = { +export type StakeAccount = { address: string; amount: string; denom: string; + status: string; validator_address: string; }; diff --git a/src/screens/staking/lib/wallet_info.ts b/src/screens/staking/lib/wallet_info.ts index 85bbc85a..03543af4 100644 --- a/src/screens/staking/lib/wallet_info.ts +++ b/src/screens/staking/lib/wallet_info.ts @@ -4,7 +4,7 @@ import type { FC } from "react"; import IconKeplr from "@src/components/icons/keplr.svg"; import IconLeap from "@src/components/icons/leap.svg"; import IconPhantom from "@src/components/icons/phantom.svg"; -import IconSolflare from "@src/components/icons/solana_wallet.svg"; +import IconSolflare from "@src/components/icons/solflare.svg"; import { WalletId } from "./staking_sdk/core/base";