From 7e19cdb55c79407983bbe97e6295ef362868ce82 Mon Sep 17 00:00:00 2001 From: Ignacio Date: Tue, 26 Mar 2024 18:42:15 +0800 Subject: [PATCH] feat: apply improvements from solana --- public/images/network/solana.svg | 20 +++ .../components/network_card/index.tsx | 4 +- .../network_card/popover.module.scss | 64 -------- .../components/network_card/popover.tsx | 155 +++++------------- .../network_card/staking_data_box.module.scss | 78 +++++++++ .../network_card/staking_data_box.tsx | 150 +++++++++++++++++ .../staking_widget/index.module.scss | 5 + .../lib/staking_sdk/context/actions.ts | 8 +- .../staking/lib/staking_sdk/context/index.tsx | 2 +- .../staking/lib/staking_sdk/core/cosmos.ts | 2 +- .../staking/lib/staking_sdk/core/index.ts | 2 +- .../staking/lib/staking_sdk/staking_client.ts | 18 +- .../lib/staking_sdk/wallet_operations.ts | 26 ++- .../lib/staking_sdk/wallet_operations/base.ts | 2 + .../staking_sdk/wallet_operations/cosmos.ts | 4 + 15 files changed, 340 insertions(+), 200 deletions(-) create mode 100644 public/images/network/solana.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/images/network/solana.svg b/public/images/network/solana.svg new file mode 100644 index 00000000..4d462bc8 --- /dev/null +++ b/public/images/network/solana.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/screens/staking/components/networks/components/network_grid/components/network_card/index.tsx b/src/screens/staking/components/networks/components/network_grid/components/network_card/index.tsx index c5f77b4a..27d2b57c 100644 --- a/src/screens/staking/components/networks/components/network_grid/components/network_card/index.tsx +++ b/src/screens/staking/components/networks/components/network_grid/components/network_card/index.tsx @@ -71,8 +71,10 @@ const NetworkCard = ({ ); + const forceDisplay = network.key === process.env.NEXT_PUBLIC_STAKING_POPOVER; + const popover = - isEmptyPopover || network.name !== showPopover ? null : ( + !forceDisplay && (isEmptyPopover || network.name !== showPopover) ? null : ( 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 590f8a25..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,50 +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: 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); @@ -161,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 && (
{(() => { @@ -257,10 +182,8 @@ const PopOver = ({ return networkSummary?.bonded > 0 ? [ - t("votingPower"), - `${convertToMoney( - networkSummary.bonded, - )} ${network.denom?.toUpperCase()}`, + network.denom?.toUpperCase(), + convertToMoney(networkSummary.bonded), ] : null; })(); 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..def272e4 --- /dev/null +++ b/src/screens/staking/components/networks/components/network_grid/components/network_card/staking_data_box.module.scss @@ -0,0 +1,78 @@ +@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; + } +} + +.stakeAccount { + align-items: center; + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; +} + +.external { + height: 20px; + width: 20px; +} 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..c474a53b --- /dev/null +++ b/src/screens/staking/components/networks/components/network_grid/components/network_card/staking_data_box.tsx @@ -0,0 +1,150 @@ +import useTranslation from "next-translate/useTranslation"; +import { useEffect, useMemo } 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, + 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 { 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 stakingRef = useStakingRef(); + + const { t } = useTranslation("staking"); + + const { claimableRewards, stakedData, unbondingTokens } = useMemo(() => { + const wallet = WalletId.Keplr; + + const result = { + claimableRewards: null as NetworkClaimableRewards | null, + stakedData: null as Coin | null, + unbondingTokens: null as { period: string; text: string } | null, + }; + + if (!!stakingNetworkId && !!wallet) { + 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].filter(Boolean).length) { + return null; + } + + const content = (() => ( + <> + {displayedStaked && ( +
+
{t("totalStaked")}
+
+ {displayedStaked.map((item, itemIdx) => ( +
{item}
+ ))} +
+
+ )} + {!!claimableRewards && ( +
+
{t("claimableRewards")}
+
+ {displayedRewards} +
+
+ )} + {!!unbondingTokens && ( +
+
{t("unbondingTokens")}
+
+ {unbondingTokens.text} +
+
+ )} + + ))(); + + return
{content}
; +}; + +export default StakingDataBox; diff --git a/src/screens/staking/components/staking_widget/index.module.scss b/src/screens/staking/components/staking_widget/index.module.scss index 0e321144..f6a18409 100644 --- a/src/screens/staking/components/staking_widget/index.module.scss +++ b/src/screens/staking/components/staking_widget/index.module.scss @@ -31,6 +31,11 @@ $wallet-icon-size: 28px; display: flex; margin-left: -8px; } + + &:nth-child(3) { + display: flex; + margin-left: -8px; + } } &:hover { diff --git a/src/screens/staking/lib/staking_sdk/context/actions.ts b/src/screens/staking/lib/staking_sdk/context/actions.ts index 6fc164c4..424a75c6 100644 --- a/src/screens/staking/lib/staking_sdk/context/actions.ts +++ b/src/screens/staking/lib/staking_sdk/context/actions.ts @@ -154,8 +154,8 @@ export const setSelectedAccount = ( }; type FetchAccountResult = { - info: Awaited>; - rewards: Awaited>; + info: Awaited> | undefined; + rewards: Awaited> | undefined; }; const accountsRequests: Record< @@ -198,8 +198,8 @@ export const fetchAccountData = async ( } const newRequest = Promise.all([ - stakingClient.getAddressInfo(networkId, address), - stakingClient.getRewardsInfo(networkId, address), + stakingClient.getAddressInfo(networkId, address).catch(() => undefined), + stakingClient.getRewardsInfo(networkId, address).catch(() => undefined), ]).then(([info, rewards]) => { accountsRequests[id] = undefined; 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/core/cosmos.ts b/src/screens/staking/lib/staking_sdk/core/cosmos.ts index 6d7074be..4c42264a 100644 --- a/src/screens/staking/lib/staking_sdk/core/cosmos.ts +++ b/src/screens/staking/lib/staking_sdk/core/cosmos.ts @@ -5,7 +5,7 @@ import { testnetNetworks, } from "./base"; -export const keplrNetworks = new Set( +export const keplrNetworks = new Set( [ StakingNetworkId.Akash, StakingNetworkId.Celestia, diff --git a/src/screens/staking/lib/staking_sdk/core/index.ts b/src/screens/staking/lib/staking_sdk/core/index.ts index 8ea1c43b..520d3a53 100644 --- a/src/screens/staking/lib/staking_sdk/core/index.ts +++ b/src/screens/staking/lib/staking_sdk/core/index.ts @@ -12,7 +12,7 @@ export const networksWithStaking = new Set([ ...Array.from(cosmosStakingNetworks), ]); -export const walletsSupported = new Set(Array.from(cosmosWallets)); // TODO add WalletId.Leap back when staking with Leap Wallet is reliable +export const walletsSupported = new Set([...Array.from(cosmosWallets)]); export const networkIdToNetworkKey: Record = { [StakingNetworkId.Akash]: "akash", diff --git a/src/screens/staking/lib/staking_sdk/staking_client.ts b/src/screens/staking/lib/staking_sdk/staking_client.ts index d32c856e..6e3d8937 100644 --- a/src/screens/staking/lib/staking_sdk/staking_client.ts +++ b/src/screens/staking/lib/staking_sdk/staking_client.ts @@ -6,7 +6,6 @@ import type { ClaimableRewardsResponse, StakingInfoResponse, } from "./staking_client_types"; -import { normaliseCoin } from "./utils/coins"; const baseUrl = process.env.NEXT_PUBLIC_STAKING_API || "https://staking-api.forbole.com"; @@ -32,16 +31,15 @@ const rewardsDivisor = new BigNumber(10).pow(18); const parseStakingRewards = async (res: ClaimableRewardsResponse) => Array.isArray(res) - ? res - .map((coin) => { - const num = new BigNumber(coin.amount); + ? res.map((coin) => { + const num = new BigNumber(coin.amount); - return { - amount: num.dividedBy(rewardsDivisor).toString(), - denom: coin.denom, - }; - }) - .map(normaliseCoin) + return { + ...coin, + amount: num.dividedBy(rewardsDivisor).toString(), + denom: coin.denom, + }; + }) : res; type StakeResponse = { diff --git a/src/screens/staking/lib/staking_sdk/wallet_operations.ts b/src/screens/staking/lib/staking_sdk/wallet_operations.ts index db1fe6ec..c2266d93 100644 --- a/src/screens/staking/lib/staking_sdk/wallet_operations.ts +++ b/src/screens/staking/lib/staking_sdk/wallet_operations.ts @@ -29,7 +29,18 @@ export const MAX_MEMO = 256; export const stakeAmount = ( opts: StakeOpts, -): Promise> => stakeAmountCosmos(opts); +): Promise> => { + const { account } = opts; + + if ( + keplrNetworks.has(account.networkId) || + leapNetworks.has(account.networkId) + ) { + return stakeAmountCosmos(opts); + } + + throw new Error("Unsupported network"); +}; export const claimRewards = async ( opts: ClaimOpts, @@ -42,7 +53,18 @@ export const getClaimRewardsFee = async ( export const unstake = async ( opts: UnstakeAmount, -): Promise> => unstakeCosmos(opts); +): Promise> => { + const { account } = opts; + + if ( + keplrNetworks.has(account.networkId) || + leapNetworks.has(account.networkId) + ) { + return unstakeCosmos(opts); + } + + throw new Error("Unsupported network"); +}; export const tryToConnectWallets = async ( context: TStakingContext, diff --git a/src/screens/staking/lib/staking_sdk/wallet_operations/base.ts b/src/screens/staking/lib/staking_sdk/wallet_operations/base.ts index c354bc95..690b794e 100644 --- a/src/screens/staking/lib/staking_sdk/wallet_operations/base.ts +++ b/src/screens/staking/lib/staking_sdk/wallet_operations/base.ts @@ -1,7 +1,9 @@ import type { Account } from "../core"; +import type { Coin } from "../core/base"; export type WalletOperationResult = | { + coin?: Coin; error: ErrorType; success: false; } diff --git a/src/screens/staking/lib/staking_sdk/wallet_operations/cosmos.ts b/src/screens/staking/lib/staking_sdk/wallet_operations/cosmos.ts index 3cd4e9bc..40c6d144 100644 --- a/src/screens/staking/lib/staking_sdk/wallet_operations/cosmos.ts +++ b/src/screens/staking/lib/staking_sdk/wallet_operations/cosmos.ts @@ -387,6 +387,10 @@ export const tryToConnectKeplr = async ( const nonNativeChains = Array.from(keplrNonNativeChains); + if (!chainsToConnect.length || !nonNativeChains.length) { + return; + } + try { await keplr.enable(chainsToConnect);