diff --git a/.env.development b/.env.development index bedc6e1fc..f49ee50f1 100644 --- a/.env.development +++ b/.env.development @@ -12,7 +12,7 @@ VITE_STABLECOIN_ASSET_ID=10 VITE_FF_DISPLAY_ASSET_ENABLED=true VITE_FF_ADD_TOKEN=true VITE_REFERENDUM_LINK=https://hydration.subsquare.io/democracy/referendum -VITE_REFERENDUM_DATA_URL=https://hydration.subsquare.io/api/democracy/referendums +VITE_REFERENDUM_DATA_URL=https://hydration.subsquare.io/api/gov2/referendums VITE_EVM_CHAIN_ID=222222 VITE_EVM_NATIVE_ASSET_ID=20 VITE_MIGRATION_TRIGGER_DOMAIN="deploy-preview-1334--testnet-hydra-app.netlify.app" diff --git a/.env.production b/.env.production index 33a14b79c..a51fc6773 100644 --- a/.env.production +++ b/.env.production @@ -12,7 +12,7 @@ VITE_STABLECOIN_ASSET_ID=10 VITE_FF_DISPLAY_ASSET_ENABLED=false VITE_FF_ADD_TOKEN=true VITE_REFERENDUM_LINK=https://hydration.subsquare.io/democracy/referendum -VITE_REFERENDUM_DATA_URL=https://hydration.subsquare.io/api/democracy/referendums +VITE_REFERENDUM_DATA_URL=https://hydration.subsquare.io/api/gov2/referendums VITE_EVM_CHAIN_ID=222222 VITE_EVM_NATIVE_ASSET_ID=20 VITE_MIGRATION_TRIGGER_DOMAIN="app.hydradx.io" diff --git a/.env.rococo b/.env.rococo index f3b60811a..f4e52b5ba 100644 --- a/.env.rococo +++ b/.env.rococo @@ -12,7 +12,7 @@ VITE_STABLECOIN_ASSET_ID=10 VITE_FF_DISPLAY_ASSET_ENABLED=false VITE_FF_ADD_TOKEN=false VITE_REFERENDUM_LINK=https://hydration.subsquare.io/democracy/referendum -VITE_REFERENDUM_DATA_URL=https://hydration.subsquare.io/api/democracy/referendums +VITE_REFERENDUM_DATA_URL=https://hydration.subsquare.io/api/gov2/referendums VITE_EVM_CHAIN_ID= VITE_EVM_NATIVE_ASSET_ID=20 VITE_MIGRATION_TRIGGER_DOMAIN="" diff --git a/package.json b/package.json index 40f31186d..177afbd9f 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@galacticcouncil/math-xyk": "^1.0.0", "@galacticcouncil/sdk": "^5.1.0", "@galacticcouncil/ui": "^5.2.3", - "@galacticcouncil/xcm-cfg": "^6.0.0", + "@galacticcouncil/xcm-cfg": "^6.0.1", "@galacticcouncil/xcm-core": "^5.5.0", "@galacticcouncil/xcm-sdk": "^7.0.1", "@hookform/resolvers": "^3.3.4", diff --git a/src/api/democracy.ts b/src/api/democracy.ts index 90a0361df..af3ef045c 100644 --- a/src/api/democracy.ts +++ b/src/api/democracy.ts @@ -4,9 +4,15 @@ import { QUERY_KEYS } from "utils/queryKeys" import { useAccount } from "sections/web3-connect/Web3Connect.utils" import { useRpcProvider } from "providers/rpcProvider" import { undefinedNoop } from "utils/helpers" -import { PalletDemocracyVoteAccountVote } from "@polkadot/types/lookup" -import BN from "bignumber.js" +import { + PalletDemocracyVoteAccountVote, + PalletReferendaReferendumStatus, + PalletReferendaCurve, +} from "@polkadot/types/lookup" +import BN, { BigNumber } from "bignumber.js" import { BN_0 } from "utils/constants" +import { humanizeUnderscoredString } from "utils/formatting" +import { useActiveRpcUrlList } from "./provider" const REFERENDUM_DATA_URL = import.meta.env.VITE_REFERENDUM_DATA_URL as string @@ -65,6 +71,38 @@ export const useReferendumInfo = (referendumIndex: string) => { ) } +export const useOpenGovReferendas = () => { + const rpcUrlList = useActiveRpcUrlList() + const { api, isLoaded } = useRpcProvider() + + return useQuery( + QUERY_KEYS.openGovReferendas(rpcUrlList.join(".")), + getOpenGovRegerendas(api), + { + enabled: isLoaded, + }, + ) +} + +const getOpenGovRegerendas = (api: ApiPromise) => async () => { + const newReferendumsRaw = + await api.query.referenda.referendumInfoFor.entries() + + // get only ongoing referenas so far + return newReferendumsRaw.reduce< + Array<{ id: string; referendum: PalletReferendaReferendumStatus }> + >((acc, [key, dataRaw]) => { + const id = key.args[0].toString() + const data = dataRaw.unwrap() + + if (!data.isNone && data.isOngoing) { + acc.push({ id, referendum: data.asOngoing }) + } + + return acc + }, []) +} + export const getReferendums = (api: ApiPromise, accountId?: string) => async () => { const [referendumRaw, votesRaw] = await Promise.all([ @@ -228,3 +266,51 @@ export const getAccountUnlockedVotes = return unlockedVotes } + +export const useReferendaTracks = () => { + const rpcUrlList = useActiveRpcUrlList() + const { api, isLoaded } = useRpcProvider() + + return useQuery( + QUERY_KEYS.referendaTracks(rpcUrlList.join(".")), + async () => { + const tracks = await api.consts.referenda.tracks + + const data: Map = new Map( + tracks.map(([key, dataRaw]) => [ + key.toString(), + { + name: dataRaw.name.toString(), + nameHuman: humanizeUnderscoredString(dataRaw.name.toString()), + maxDeciding: dataRaw.maxDeciding.toBigNumber(), + decisionDeposit: dataRaw.decisionDeposit.toBigNumber(), + preparePeriod: dataRaw.preparePeriod.toBigNumber(), + decisionPeriod: dataRaw.decisionPeriod.toBigNumber(), + confirmPeriod: dataRaw.confirmPeriod.toBigNumber(), + minEnactmentPeriod: dataRaw.minEnactmentPeriod.toBigNumber(), + minApproval: dataRaw.minApproval, + minSupport: dataRaw.minSupport, + }, + ]), + ) + + return data + }, + { + enabled: isLoaded, + }, + ) +} + +export type TReferenda = { + name: string + nameHuman: string + maxDeciding: BigNumber + decisionDeposit: BigNumber + preparePeriod: BigNumber + decisionPeriod: BigNumber + confirmPeriod: BigNumber + minEnactmentPeriod: BigNumber + minApproval: PalletReferendaCurve + minSupport: PalletReferendaCurve +} diff --git a/src/api/provider.ts b/src/api/provider.ts index cf2189cbc..da5ab1d03 100644 --- a/src/api/provider.ts +++ b/src/api/provider.ts @@ -50,7 +50,7 @@ export const PROVIDERS: ProviderProps[] = [ }, { name: "Dwellir", - url: "wss://hydradx-rpc.dwellir.com", + url: "wss://hydration-rpc.n.dwellir.com", indexerUrl: "https://explorer.hydradx.cloud/graphql", squidUrl: "https://galacticcouncil.squids.live/hydration-pools:prod/api/graphql", diff --git a/src/api/staking.ts b/src/api/staking.ts index b10293aa2..3f02cbeb7 100644 --- a/src/api/staking.ts +++ b/src/api/staking.ts @@ -7,7 +7,6 @@ import request, { gql } from "graphql-request" import { useActiveProvider } from "./provider" import { useRpcProvider } from "providers/rpcProvider" import { useAccount } from "sections/web3-connect/Web3Connect.utils" -import { undefinedNoop } from "utils/helpers" interface ISubscanData { code: number @@ -46,19 +45,23 @@ export type TStakingPosition = Awaited< ReturnType> > -export const useCirculatingSupply = () => { +export const useHDXSupplyFromSubscan = () => { return useQuery( - QUERY_KEYS.circulatingSupply, + QUERY_KEYS.hdxSupply, async () => { - const res = await getCirculatingSupply()() + const res = await getHDXSupplyFromSubscan()() - return res.data.detail["HDX"].available_balance + const data = res.data.detail["HDX"] + return { + totalIssuance: data.total_issuance, + circulatingSupply: data.available_balance, + } }, { retry: 0 }, ) } -const getCirculatingSupply = () => async () => { +const getHDXSupplyFromSubscan = () => async () => { const res = await fetch("https://hydration.api.subscan.io/api/scan/token") const data: Promise = res.json() @@ -112,7 +115,7 @@ const getStake = (api: ApiPromise, address: string | undefined) => async () => { const getStakingPosition = (api: ApiPromise, id: number) => async () => { const [position, votesRes] = await Promise.all([ api.query.staking.positions(id), - api.query.staking.positionVotes(id), + api.query.staking.votes(id), ]) const positionData = position.unwrap() @@ -122,6 +125,7 @@ const getStakingPosition = (api: ApiPromise, id: number) => async () => { id: BN amount: BN conviction: string + //@ts-ignore }> = await votesRes.votes.reduce(async (acc, [key, data]) => { const prevAcc = await acc const id = key.toBigNumber() @@ -202,29 +206,6 @@ type TStakingInitialized = StakeEventBase & { name: "Staking.StakingInitialized" } -type TPositionBalance = StakeEventBase & - ( - | { - name: "Staking.PositionCreated" - args: { - positionId: string - stake: string - who: string - } - } - | { - name: "Staking.StakeAdded" - args: { - lockedRewards: string - positionId: string - slashedPoints: string - stake: string - totalStake: string - who: string - } - } - ) - export type TAccumulatedRpsUpdated = StakeEventBase & { name: "Staking.AccumulatedRpsUpdated" args: { @@ -251,18 +232,6 @@ export const useStakingEvents = () => { }) } -export const useStakingPositionBalances = (positionId?: string) => { - const { indexerUrl } = useActiveProvider() - - return useQuery( - QUERY_KEYS.stakingPositionBalances(positionId), - positionId - ? getStakingPositionBalances(indexerUrl, positionId) - : undefinedNoop, - { enabled: !!positionId }, - ) -} - const getAccumulatedRpsUpdatedEvents = (indexerUrl: string) => async () => { return { ...(await request<{ @@ -307,36 +276,7 @@ const getStakingInitializedEvents = (indexerUrl: string) => async () => { } } -const getStakingPositionBalances = - (indexerUrl: string, positionId: string) => async () => { - return { - ...(await request<{ - events: Array - }>( - indexerUrl, - gql` - query StakingPositionBalances($positionId: String!) { - events( - where: { - name_contains: "Staking" - args_jsonContains: { positionId: $positionId } - } - orderBy: [block_height_ASC] - ) { - name - block { - height - } - args - } - } - `, - { positionId }, - )), - } - } - -export const useProcessedVotesIds = () => { +export const useVotesRewardedIds = () => { const { account } = useAccount() const { api } = useRpcProvider() @@ -345,7 +285,7 @@ export const useProcessedVotesIds = () => { return [] } - const processedVotesRes = await api.query.staking.processedVotes.entries( + const processedVotesRes = await api.query.staking.votesRewarded.entries( account.address, ) @@ -363,7 +303,8 @@ export const usePositionVotesIds = () => { const { api } = useRpcProvider() return useMutation(async (positionId: number) => { - const positionVotesRes = await api.query.staking.positionVotes(positionId) + const positionVotesRes = await api.query.staking.votes(positionId) + //@ts-ignore const positionVotesIds = positionVotesRes.votes.map(([position]) => position.toString(), ) diff --git a/src/api/volume.ts b/src/api/volume.ts index 5318b4ae3..3801b8554 100644 --- a/src/api/volume.ts +++ b/src/api/volume.ts @@ -9,8 +9,8 @@ import { PROVIDERS, useActiveProvider } from "./provider" import { u8aToHex } from "@polkadot/util" import { decodeAddress, encodeAddress } from "@polkadot/util-crypto" import { HYDRA_ADDRESS_PREFIX } from "utils/api" -import { useBestNumber } from "./chain" import { millisecondsInHour, millisecondsInMinute } from "date-fns/constants" +import { useRpcProvider } from "providers/rpcProvider" export type TradeType = { name: @@ -263,63 +263,68 @@ const squidUrl = const VOLUME_BLOCK_COUNT = 7200 //24 hours export const useXYKSquidVolumes = (addresses: string[]) => { - const { data: bestNumber } = useBestNumber() + const { api, isLoaded } = useRpcProvider() return useQuery( QUERY_KEYS.xykSquidVolumes(addresses), - bestNumber - ? async () => { - const hexAddresses = addresses.map((address) => - u8aToHex(decodeAddress(address)), - ) - const startBlockNumber = - bestNumber.parachainBlockNumber.toNumber() - VOLUME_BLOCK_COUNT - const { xykPoolHistoricalVolumesByPeriod } = await request<{ - xykPoolHistoricalVolumesByPeriod: { - nodes: { - poolId: string - assetAId: number - assetAVolume: string - assetBId: number - assetBVolume: string - }[] - } - }>( - squidUrl, - gql` - query XykVolume($poolIds: [String!]!, $startBlockNumber: Int!) { - xykPoolHistoricalVolumesByPeriod( - filter: { - poolIds: $poolIds - startBlockNumber: $startBlockNumber - } - ) { - nodes { - poolId - assetAId - assetAVolume - assetBId - assetBVolume - } - } - } - `, - { poolIds: hexAddresses, startBlockNumber }, - ) + async () => { + const hexAddresses = addresses.map((address) => + u8aToHex(decodeAddress(address)), + ) - const { nodes = [] } = xykPoolHistoricalVolumesByPeriod + const endBlockNumber = (await api.derive.chain.bestNumber()).toNumber() + const startBlockNumber = endBlockNumber - VOLUME_BLOCK_COUNT - return nodes.map((node) => ({ - poolId: encodeAddress(node.poolId, HYDRA_ADDRESS_PREFIX), - assetId: node.assetAId.toString(), - assetIdB: node.assetBId.toString(), - volume: node.assetAVolume, - })) + const { xykPoolHistoricalVolumesByPeriod } = await request<{ + xykPoolHistoricalVolumesByPeriod: { + nodes: { + poolId: string + assetAId: number + assetAVolume: string + assetBId: number + assetBVolume: string + }[] } - : undefinedNoop, + }>( + squidUrl, + gql` + query XykVolume( + $poolIds: [String!]! + $startBlockNumber: Int! + $endBlockNumber: Int! + ) { + xykPoolHistoricalVolumesByPeriod( + filter: { + poolIds: $poolIds + startBlockNumber: $startBlockNumber + endBlockNumber: $endBlockNumber + } + ) { + nodes { + poolId + assetAId + assetAVolume + assetBId + assetBVolume + } + } + } + `, + { poolIds: hexAddresses, startBlockNumber, endBlockNumber }, + ) + + const { nodes = [] } = xykPoolHistoricalVolumesByPeriod + + return nodes.map((node) => ({ + poolId: encodeAddress(node.poolId, HYDRA_ADDRESS_PREFIX), + assetId: node.assetAId.toString(), + assetIdB: node.assetBId.toString(), + volume: node.assetAVolume, + })) + }, { - enabled: !!bestNumber && !!addresses.length, + enabled: isLoaded && !!addresses.length, staleTime: millisecondsInHour, refetchInterval: millisecondsInMinute, }, diff --git a/src/assets/icons/Calendar.svg b/src/assets/icons/Calendar.svg new file mode 100644 index 000000000..2059dd01e --- /dev/null +++ b/src/assets/icons/Calendar.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/ChevronFull.svg b/src/assets/icons/ChevronFull.svg new file mode 100644 index 000000000..3cf878f65 --- /dev/null +++ b/src/assets/icons/ChevronFull.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/ChevronRightSmall.svg b/src/assets/icons/ChevronRightSmall.svg deleted file mode 100644 index 250dad790..000000000 --- a/src/assets/icons/ChevronRightSmall.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/assets/icons/StakingVote.svg b/src/assets/icons/StakingVote.svg new file mode 100644 index 000000000..be1ac6e31 --- /dev/null +++ b/src/assets/icons/StakingVote.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/components/Dropdown/DropdownRebranded.styled.ts b/src/components/Dropdown/DropdownRebranded.styled.ts new file mode 100644 index 000000000..06ed9e43d --- /dev/null +++ b/src/components/Dropdown/DropdownRebranded.styled.ts @@ -0,0 +1,114 @@ +import { css, keyframes } from "@emotion/react" +import styled from "@emotion/styled" +import * as DropdownMenu from "@radix-ui/react-dropdown-menu" +import { theme } from "theme" + +export const fadeInKeyframes = keyframes` + 0% { + opacity: 0; + transform: scale(0.96); + } + + 100% { + opacity: 1; + transform: scale(1); + } +` +export const SItem = styled(DropdownMenu.Item)` + color: rgba(${theme.rgbColors.white}, 0.5); + font-size: 14px; + line-height: 26px; + + padding: 0 8px; + + font-weight: 500; + + display: flex; + align-items: center; + gap: 10px; + + border-radius: 8px; + + transition: all ${theme.transitions.default}; + + cursor: pointer; + + &:focus-visible, + &:hover { + outline: none; + color: ${theme.colors.basic100}; + background: rgba(${theme.rgbColors.white}, 0.06); + } + + svg { + color: ${theme.colors.pink500}; + } +` +export const SContent = styled(DropdownMenu.Content)` + display: flex; + flex-direction: column; + gap: 4px; + + background: #0d1525; + + border-radius: 12px; + + animation: 150ms cubic-bezier(0.16, 1, 0.3, 1) ${fadeInKeyframes}; + box-shadow: 0px 40px 70px 0px rgba(0, 0, 0, 0.8); + + padding: 10px; + + overflow-y: scroll; + max-height: 400px; + + z-index: ${theme.zIndices.toast}; +` +export const STrigger = styled(DropdownMenu.Trigger)` + all: unset; + + padding: 8px 14px; + + border-radius: 32px; + + min-width: 12px; + min-height: 12px; + + background: #0d1525; + color: ${theme.colors.brightBlue300}; + + transition: background ${theme.transitions.default}; + + border: 1px solid rgba(124, 127, 138, 0.2); + + display: flex; + align-items: center; + justify-content: center; + + cursor: pointer; + + overflow: hidden; + position: relative; + + &:hover { + opacity: 0.7; + } + + &[data-state="open"] { + & > div > span { + transition: transform ${theme.transitions.default}; + transform: rotate(180deg); + } + } + + ${({ disabled }) => + disabled && + css` + background: rgba(${theme.rgbColors.basic100}, 0.06); + color: ${theme.colors.darkBlue300}; + + border: 1px solid ${theme.colors.darkBlue300}; + + pointer-events: none; + cursor: not-allowed; + `} +` diff --git a/src/components/Dropdown/DropdownRebranded.tsx b/src/components/Dropdown/DropdownRebranded.tsx new file mode 100644 index 000000000..db63beb9a --- /dev/null +++ b/src/components/Dropdown/DropdownRebranded.tsx @@ -0,0 +1,71 @@ +import * as DropdownMenu from "@radix-ui/react-dropdown-menu" + +import React, { ReactNode } from "react" +import { SContent, SItem, STrigger } from "./DropdownRebranded.styled" +import { Text } from "components/Typography/Text/Text" +import Chevron from "assets/icons/ChevronFull.svg?react" +import { Icon } from "components/Icon/Icon" + +export type TDropdownItem = { + key: string + icon?: ReactNode + label: ReactNode + onSelect?: () => void + disabled?: boolean +} + +export type DropdownProps = { + items: Array + children: ReactNode + onSelect: (key: TDropdownItem) => void + asChild?: boolean + align?: "start" | "center" | "end" +} + +export const Dropdown: React.FC = ({ + items, + children, + onSelect, + asChild, + align, +}) => { + return ( + + {asChild ? ( + {children} + ) : ( + {children} + )} + + + {items.map((i) => ( + onSelect(i)}> + {i.icon} + {i.label} + + ))} + + + + ) +} + +export const DropdownTriggerContent = ({ + title, + value, +}: { + title: string + value?: string +}) => { + return ( +
+ + {title}: + + + {value} + + } sx={{ color: "basic400" }} /> +
+ ) +} diff --git a/src/components/InfoTooltip/InfoTooltip.tsx b/src/components/InfoTooltip/InfoTooltip.tsx index 3eff52a79..3e8041208 100644 --- a/src/components/InfoTooltip/InfoTooltip.tsx +++ b/src/components/InfoTooltip/InfoTooltip.tsx @@ -9,6 +9,7 @@ type InfoTooltipProps = { children?: ReactNode type?: "default" | "black" side?: Tooltip.TooltipContentProps["side"] + align?: Tooltip.TooltipContentProps["align"] asChild?: boolean preventDefault?: boolean } @@ -18,6 +19,7 @@ export function InfoTooltip({ children, type = "default", side = "bottom", + align = "start", asChild = false, preventDefault, }: InfoTooltipProps) { @@ -49,7 +51,7 @@ export function InfoTooltip({ { + const { t } = useTranslation() + + return ( + + +
+ } css={{ color: "#DFB1F3" }} /> + + {t("referenda.empty.title")} + +
+
+ + + +
+ + {t("referenda.empty.desc.first")} + + + {t("referenda.empty.desc.second")} + +
+
+ ) +} diff --git a/src/components/ReferendumCard/Referenda.tsx b/src/components/ReferendumCard/Referenda.tsx new file mode 100644 index 000000000..3b48feb20 --- /dev/null +++ b/src/components/ReferendumCard/Referenda.tsx @@ -0,0 +1,262 @@ +import { TReferenda, useReferendumInfo } from "api/democracy" +import VoteIcon from "assets/icons/StakingVote.svg?react" +import { Separator } from "components/Separator/Separator" +import { Spacer } from "components/Spacer/Spacer" +import { Text } from "components/Typography/Text/Text" +import { useTranslation } from "react-i18next" +import { BN_0 } from "utils/constants" +import { + SHeader, + SOpenGovContainer, + SProgressBarContainer, + SThresholdLine, + STrackBadge, + SVoteButton, +} from "./ReferendumCard.styled" +import { Icon } from "components/Icon/Icon" +import { LinearProgress } from "components/Progress" +import { theme } from "theme" +import { PalletReferendaReferendumStatus } from "@polkadot/types/lookup" +import { useAssets } from "providers/assets" +import { + getPerbillPercentage, + useMinApprovalThreshold, + useReferendaVotes, + useSupportThreshold, +} from "./Referenda.utils" +import Skeleton from "react-loading-skeleton" +import { InfoTooltip } from "components/InfoTooltip/InfoTooltip" + +export const OpenGovReferenda = ({ + id, + referenda, + track, + totalIssuance, +}: { + id: string + referenda: PalletReferendaReferendumStatus + track: TReferenda + totalIssuance?: string +}) => { + const { native } = useAssets() + const { t } = useTranslation() + + const { data: subscanInfo, isLoading } = useReferendumInfo(id) + + const minApprovalThreshold = useMinApprovalThreshold(track, referenda) + const { threshold, maxSupportBarValue, barPercentage, markPercentage } = + useSupportThreshold(track, referenda, totalIssuance ?? "0") + const votes = useReferendaVotes(referenda) + + const isNoVotes = votes.percAyes.eq(0) && votes.percNays.eq(0) + const isOneWayVote = votes.percAyes.eq(0) || votes.percNays.eq(0) + + return ( + + +
+ {track && } + + + #{id} + +
+
+ + + +
+ {isLoading ? ( + + ) : ( + + {subscanInfo?.title ?? "N/a"} + + )} + + + +
+ +
+ {isNoVotes ? ( + + ) : ( + <> + + + + )} + +
+
+
+
+ + {t("toast.sidebar.referendums.aye")} + +
+ + {t("value.compact", { + value: votes.ayes, + numberSuffix: "HDX", + })} + + + {native.symbol} + + + {t("value.percentage", { + value: votes.percAyes, + numberPrefix: "(", + numberSuffix: "%)", + })} + +
+
+
+ + {t("threshold")} + + + {t("value.percentage", { value: minApprovalThreshold })} + +
+
+ + {t("toast.sidebar.referendums.nay")} + +
+ + {t("value.compact", { value: votes.nays })} + + + {native.symbol} + + + {t("value.percentage", { + value: votes.percNays, + numberPrefix: "(", + numberSuffix: "%)", + })} + +
+
+
+
+ + +
+ + +
+ + +
+
+
+
+
+ + {t("value.percentage", { value: BN_0 })} + +
+ +
+ + {t("threshold")} + + + {getPerbillPercentage(threshold?.toNumber())} + +
+ +
+ + {getPerbillPercentage(maxSupportBarValue)} + +
+
+
+
+ + + +
+ + } /> + {t("referenda.btn.vote")} + +
+
+ ) +} + +const TrackBadge = ({ title }: { title: string }) => { + return {title} +} + +const StateBadge = ({ + referenda, +}: { + referenda: PalletReferendaReferendumStatus +}) => { + const { t } = useTranslation() + let state + + const decisionDeposit = referenda.decisionDeposit.toString() + const deciding = referenda.deciding.unwrapOr(null) + + if (!decisionDeposit || !deciding) { + state = t("referenda.state.preparing") + } else if (!deciding.confirming.toString()) { + state = t("referenda.state.deciding") + } else { + state = t("referenda.state.confirming") + } + + return ( + + {state} + + ) +} diff --git a/src/components/ReferendumCard/Referenda.utils.ts b/src/components/ReferendumCard/Referenda.utils.ts new file mode 100644 index 000000000..ab25d68c5 --- /dev/null +++ b/src/components/ReferendumCard/Referenda.utils.ts @@ -0,0 +1,149 @@ +import { useBestNumber } from "api/chain" +import { useMemo } from "react" +import { getCurveData, getDecidingEndPercentage } from "utils/opengov" +import BN from "bignumber.js" +import { PalletReferendaReferendumStatus } from "@polkadot/types/lookup" +import { BN_0, BN_NAN } from "utils/constants" +import { isNil } from "utils/helpers" +import { TReferenda } from "api/democracy" + +export const getPerbillPercentage = (perbill = 0) => { + if (isNil(perbill) || perbill <= 0) { + return "0.0%" + } + + const precision = perbill > 10 * Math.pow(10, 7) ? 1 : 2 + return ((perbill / Math.pow(10, 9)) * 100).toFixed(precision) + "%" +} + +export const useMinApprovalThreshold = ( + track: TReferenda, + referenda: PalletReferendaReferendumStatus, +) => { + const { data: blockNumbers } = useBestNumber() + const blockNumber = blockNumbers?.parachainBlockNumber + .toBigNumber() + .toString() + + return useMemo(() => { + if (track && blockNumber) { + const decidingSince = referenda.deciding.unwrapOr(null)?.since.toString() + + const percentage = getDecidingEndPercentage( + track.decisionPeriod.toString(), + decidingSince, + blockNumber, + ) + + const getApprovalThreshold = getCurveData(track, "minApproval") + + return BN(getApprovalThreshold?.(percentage.toNumber()) ?? BN_NAN).times( + 100, + ) + } + }, [track, blockNumber, referenda]) +} + +export const useReferendaVotes = ( + referenda: PalletReferendaReferendumStatus, +) => { + return useMemo(() => { + if (!referenda) + return { ayes: BN_0, nays: BN_0, percAyes: BN_0, percNays: BN_0 } + + const ayes = referenda.tally.ayes.toBigNumber().shiftedBy(-12) + const nays = referenda.tally.nays.toBigNumber().shiftedBy(-12) + + const votesSum = ayes.plus(nays) + + let percAyes = BN_0 + let percNays = BN_0 + + if (!votesSum.isZero()) { + percAyes = ayes.div(votesSum).times(100) + percNays = nays.div(votesSum).times(100) + } + + return { ayes, nays, percAyes, percNays } + }, [referenda]) +} + +export const useSupportThreshold = ( + track: TReferenda, + referenda: PalletReferendaReferendumStatus, + issuance: string, +) => { + const { data: blockNumbers } = useBestNumber() + const blockNumber = blockNumbers?.parachainBlockNumber + .toBigNumber() + .toString() + + const support = useMemo( + () => + BN(referenda.tally.support.toString()) + .div(issuance) + .multipliedBy(Math.pow(10, 9)) + .toNumber(), + [issuance, referenda.tally.support], + ) + + const supportThreshold = useMemo(() => { + if (track && blockNumber) { + const decidingSince = referenda.deciding.unwrapOr(null)?.since.toString() + + const percentage = getDecidingEndPercentage( + track.decisionPeriod.toString(), + decidingSince, + blockNumber, + ) + + const getApprovalThreshold = getCurveData(track, "minSupport") + + return BN(getApprovalThreshold?.(percentage.toNumber()) ?? BN_NAN) + } + }, [track, blockNumber, referenda]) + + const threshold = useMemo( + () => supportThreshold?.shiftedBy(9), + [supportThreshold], + ) + + const maxSupportBarValue = useMemo( + () => + BN.max(support, threshold?.toNumber() ?? 0) + .multipliedBy(1.25) + .toNumber(), + [support, threshold], + ) + + const barPercentage = useMemo(() => { + if (!support || !maxSupportBarValue) { + return 0 + } + + // when the decision period reach end, we show 100% for support bar, + // because support threshold require 0% at the end + if (maxSupportBarValue <= 0) { + return 100 + } + + return BN(support).div(maxSupportBarValue).times(100).toNumber() + }, [support, maxSupportBarValue]) + + const markPercentage = useMemo(() => { + if (!maxSupportBarValue || !threshold) { + return 0 + } + + return threshold.div(maxSupportBarValue).times(100).toNumber() + }, [threshold, maxSupportBarValue]) + + return { + support, + supportThreshold, + threshold, + maxSupportBarValue, + barPercentage, + markPercentage, + } +} diff --git a/src/components/ReferendumCard/ReferendaSkeleton.tsx b/src/components/ReferendumCard/ReferendaSkeleton.tsx new file mode 100644 index 000000000..653427531 --- /dev/null +++ b/src/components/ReferendumCard/ReferendaSkeleton.tsx @@ -0,0 +1,64 @@ +import Skeleton from "react-loading-skeleton" +import { + SHeader, + SOpenGovContainer, + SProgressBarContainer, + SVoteButton, +} from "./ReferendumCard.styled" +import { Separator } from "components/Separator/Separator" +import { Spacer } from "components/Spacer/Spacer" +import { Icon } from "components/Icon/Icon" +import { useTranslation } from "react-i18next" +import VoteIcon from "assets/icons/StakingVote.svg?react" + +export const ReferendaSkeleton = () => { + const { t } = useTranslation() + + return ( + + + + + + + +
+ + + + + +
span": { width: "100%", position: "relative", top: -7 }, + }} + > + + +
+
+ + + + +
span": { width: "100%", position: "relative", top: -7 }, + }} + > + +
+
+
+ + + + + } /> + {t("referenda.btn.vote")} + +
+ ) +} diff --git a/src/components/ReferendumCard/ReferendumCard.styled.ts b/src/components/ReferendumCard/ReferendumCard.styled.ts index 77b08a7a3..0d6e20288 100644 --- a/src/components/ReferendumCard/ReferendumCard.styled.ts +++ b/src/components/ReferendumCard/ReferendumCard.styled.ts @@ -2,76 +2,113 @@ import { css } from "@emotion/react" import styled from "@emotion/styled" import { theme } from "theme" -export const SContainer = styled.a<{ type: "staking" | "toast" }>` - padding: 16px; +export const SHeader = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + + padding: 0 16px; +` + +export const SVotedBage = styled.div` + display: flex; + gap: 4px; + align-items: center; + + color: ${theme.colors.basic900}; + background: ${theme.colors.brightBlue600}; + + text-transform: uppercase; + font-size: 13px; + line-height: normal; + font-family: GeistSemiBold; + + border-radius: 2px; + + padding: 4px 8px; +` + +export const SOpenGovContainer = styled.div<{ type: "staking" | "toast" }>` + padding: 16px 0px; ${({ type }) => type === "toast" ? css` border-radius: ${theme.borderRadius.default}px; ` : css` - border-radius: ${theme.borderRadius.medium}px; + border-radius: 16px; + border: 1px solid #372244; position: relative; - - :before { - content: ""; - position: absolute; - inset: 0; - - border-radius: ${theme.borderRadius.medium}px; - padding: 1px; // a width of the border - - background: linear-gradient( - 180deg, - rgba(152, 176, 214, 0.27) 0%, - rgba(163, 177, 199, 0.15) 66.67%, - rgba(158, 167, 180, 0.2) 100% - ); - -webkit-mask: - linear-gradient(#fff 0 0) content-box, - linear-gradient(#fff 0 0); - -webkit-mask-composite: xor; - mask-composite: exclude; - pointer-events: none; - } `} - background: ${theme.colors.darkBlue700}; + background: #240e32; +` - transition: background ${theme.transitions.default}; +export const STrackBadge = styled.div` + display: flex; + padding: 6px 8px; - cursor: pointer; + align-items: center; + gap: 4px; - &:hover { - background: ${theme.colors.darkBlue401}; - } + border-radius: 32px; - &:active { - background: ${theme.colors.darkBlue400}; - } -` + font-size: 11px; + text-transform: uppercase; + color: ${theme.colors.brightBlue100}; -export const SHeader = styled.div` - display: flex; - align-items: center; - justify-content: space-between; + background-color: rgba(133, 209, 255, 0.2); ` -export const SVotedBage = styled.div` +export const SVoteButton = styled.a<{ disabled: boolean }>` + border-radius: 32px; + background: #372244; + display: flex; - gap: 4px; align-items: center; + justify-content: center; - color: ${theme.colors.basic900}; - background: ${theme.colors.brightBlue600}; + width: 82px; + height: 38px; + color: ${theme.colors.white}; + font-size: 12px; text-transform: uppercase; - font-size: 13px; - line-height: normal; - font-family: GeistSemiBold; - border-radius: 2px; + gap: 8px; - padding: 4px 8px; + cursor: pointer; + + &:hover { + opacity: 0.7; + } + &:active { + opacity: 0.8; + } + + ${({ disabled }) => + disabled && + css` + pointer-events: none; + opacity: 0.2; + `} +` + +export const SProgressBarContainer = styled.div` + padding: 7px 11px; + + background: rgba(77, 82, 95, 0.1); + + border-radius: 12px; + border: 1px solid rgba(124, 127, 138, 0.2); +` + +export const SThresholdLine = styled.div<{ percentage: string }>` + position: absolute; + top: -10px; + background: #dfb1f3; + width: 1px; + height: 32px; + left: ${({ percentage }) => percentage}%; ` diff --git a/src/components/ReferendumCard/ReferendumCard.tsx b/src/components/ReferendumCard/ReferendumCard.tsx deleted file mode 100644 index b2c33fe3e..000000000 --- a/src/components/ReferendumCard/ReferendumCard.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import { PalletDemocracyReferendumInfo } from "@polkadot/types/lookup" -import { useReferendumInfo } from "api/democracy" -import IconArrow from "assets/icons/IconArrow.svg?react" -import GovernanceIcon from "assets/icons/GovernanceIcon.svg?react" -import { Separator } from "components/Separator/Separator" -import { Spacer } from "components/Spacer/Spacer" -import { Text } from "components/Typography/Text/Text" -import { useMemo } from "react" -import { useTranslation } from "react-i18next" -import { BN_0, BN_10, PARACHAIN_BLOCK_TIME } from "utils/constants" -import { SContainer, SHeader, SVotedBage } from "./ReferendumCard.styled" -import { ReferendumCardSkeleton } from "./ReferendumCardSkeleton" -import { ReferendumCardProgress } from "./ReferendumCardProgress" -import { Icon } from "components/Icon/Icon" -import BN from "bignumber.js" -import { useBestNumber } from "api/chain" -import { customFormatDuration } from "utils/formatting" - -const REFERENDUM_LINK = import.meta.env.VITE_REFERENDUM_LINK as string - -type Props = { - id: string - referendum: PalletDemocracyReferendumInfo - type: "toast" | "staking" - voted: boolean -} - -export const ReferendumCard = ({ id, referendum, type, voted }: Props) => { - const { t } = useTranslation() - - const info = useReferendumInfo(id) - const bestNumber = useBestNumber() - - const votes = useMemo(() => { - if (!referendum.isOngoing) - return { ayes: BN_0, nays: BN_0, percAyes: BN_0, percNays: BN_0 } - - const ayes = referendum.asOngoing.tally.ayes - .toBigNumber() - .div(BN_10.pow(12)) - const nays = referendum.asOngoing.tally.nays - .toBigNumber() - .div(BN_10.pow(12)) - - const votesSum = ayes.plus(nays) - - let percAyes = BN_0 - let percNays = BN_0 - - if (!votesSum.isZero()) { - percAyes = ayes.div(votesSum).times(100) - percNays = nays.div(votesSum).times(100) - } - - return { ayes, nays, percAyes, percNays } - }, [referendum]) - - const diff = BN(info?.data?.onchainData.meta.end ?? 0) - .minus(bestNumber.data?.parachainBlockNumber.toBigNumber() ?? 0) - .times(PARACHAIN_BLOCK_TIME) - .toNumber() - const endDate = customFormatDuration({ end: diff * 1000 }) - - return info.isLoading || !info.data ? ( - - ) : ( - - -
- - #{info.data.referendumIndex} - - - {"//"} - - - {endDate && - t(`duration.${endDate.isPositive ? "left" : "ago"}`, { - duration: endDate.duration, - })} - -
- -
- {voted && ( - - {t("toast.sidebar.referendums.voted")} - } - /> - - )} - } /> -
-
- - - - - {info.data.title} - - - - - - - - -
- - {t("toast.sidebar.referendums.aye")} - - - {t("toast.sidebar.referendums.nay")} - -
- - - -
- - {t("toast.sidebar.referendums.value", { - value: votes.ayes, - percent: votes.percAyes, - })} - - - {t("toast.sidebar.referendums.value", { - value: votes.nays, - percent: votes.percNays, - })} - -
-
- ) -} diff --git a/src/components/ReferendumCard/ReferendumCardProgress.tsx b/src/components/ReferendumCard/ReferendumCardProgress.tsx deleted file mode 100644 index e9c906033..000000000 --- a/src/components/ReferendumCard/ReferendumCardProgress.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import BN from "bignumber.js" -import { LinearProgress } from "components/Progress" -import { theme } from "theme" - -export type ReferendumCardProgressProps = { - percAyes: BN - percNays: BN -} - -export const ReferendumCardProgress: React.FC = ({ - percAyes, - percNays, -}) => { - const isNoVotes = percAyes.eq(0) && percNays.eq(0) - return ( -
- {isNoVotes ? ( - - ) : ( - <> - - - - )} -
- ) -} diff --git a/src/components/ReferendumCard/ReferendumCardRococo.tsx b/src/components/ReferendumCard/ReferendumCardRococo.tsx deleted file mode 100644 index 3a25e0502..000000000 --- a/src/components/ReferendumCard/ReferendumCardRococo.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { PalletDemocracyReferendumInfo } from "@polkadot/types/lookup" -import IconArrow from "assets/icons/IconArrow.svg?react" -import GovernanceIcon from "assets/icons/GovernanceIcon.svg?react" -import { Separator } from "components/Separator/Separator" -import { Spacer } from "components/Spacer/Spacer" -import { Text } from "components/Typography/Text/Text" -import { useMemo } from "react" -import { useTranslation } from "react-i18next" -import { BN_0, BN_10, PARACHAIN_BLOCK_TIME } from "utils/constants" -import { SContainer, SHeader, SVotedBage } from "./ReferendumCard.styled" -import { ReferendumCardProgress } from "./ReferendumCardProgress" -import { Icon } from "components/Icon/Icon" -import { useBestNumber } from "api/chain" -import { customFormatDuration } from "utils/formatting" - -type Props = { - id: string - referendum: PalletDemocracyReferendumInfo - type: "toast" | "staking" - rpc: string - voted: boolean -} - -export const ReferendumCardRococo = ({ - id, - referendum, - type, - rpc, - voted, -}: Props) => { - const { t } = useTranslation() - - const bestNumber = useBestNumber() - - const votes = useMemo(() => { - if (!referendum.isOngoing) - return { ayes: BN_0, nays: BN_0, percAyes: BN_0, percNays: BN_0 } - - const ayes = referendum.asOngoing.tally.ayes - .toBigNumber() - .div(BN_10.pow(12)) - const nays = referendum.asOngoing.tally.nays - .toBigNumber() - .div(BN_10.pow(12)) - - const votesSum = ayes.plus(nays) - - let percAyes = BN_0 - let percNays = BN_0 - - if (!votesSum.isZero()) { - percAyes = ayes.div(votesSum).times(100) - percNays = nays.div(votesSum).times(100) - } - - return { ayes, nays, percAyes, percNays } - }, [referendum]) - - const diff = referendum.asOngoing.end - .toBigNumber() - .minus(bestNumber.data?.parachainBlockNumber.toBigNumber() ?? 0) - .times(PARACHAIN_BLOCK_TIME) - .toNumber() - const endDate = customFormatDuration({ end: diff * 1000 }) - - return ( - - -
- - #{id.toString()} - - - {"//"} - - - {endDate && - t(`duration.${endDate.isPositive ? "left" : "ago"}`, { - duration: endDate.duration, - })} - -
- -
- {voted && ( - - {t("toast.sidebar.referendums.voted")} - } - /> - - )} - } /> -
-
- - - - - For Rococo testnet, please participate in referenda through polkadot.js - apps, please click on this tile - - - - - - - - -
- - {t("toast.sidebar.referendums.aye")} - - - {t("toast.sidebar.referendums.nay")} - -
- - - -
- - {t("toast.sidebar.referendums.value", { - value: votes.ayes, - percent: votes.percAyes, - })} - - - {t("toast.sidebar.referendums.value", { - value: votes.nays, - percent: votes.percNays, - })} - -
-
- ) -} diff --git a/src/components/ReferendumCard/ReferendumCardSkeleton.tsx b/src/components/ReferendumCard/ReferendumCardSkeleton.tsx deleted file mode 100644 index c370d27b7..000000000 --- a/src/components/ReferendumCard/ReferendumCardSkeleton.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import Skeleton from "react-loading-skeleton" -import IconArrow from "assets/icons/IconArrow.svg?react" -import { Separator } from "components/Separator/Separator" -import { SContainer, SHeader } from "./ReferendumCard.styled" -import { Spacer } from "components/Spacer/Spacer" -import { Text } from "components/Typography/Text/Text" -import { useTranslation } from "react-i18next" -import { Icon } from "components/Icon/Icon" - -export const ReferendumCardSkeleton = ({ - type, -}: { - type: "toast" | "staking" -}) => { - const { t } = useTranslation() - - const isToastCard = type === "toast" - - return ( - - - - } /> - - - - - - - - -
span": { width: "100%" } }} - > - - -
- - - -
- - {t("toast.sidebar.referendums.aye")} - - - {t("toast.sidebar.referendums.nay")} - -
- - - -
- - -
-
- ) -} diff --git a/src/components/Toast/sidebar/group/ToastSidebarGroup.tsx b/src/components/Toast/sidebar/group/ToastSidebarGroup.tsx index ee6dcbb97..0f75a88cd 100644 --- a/src/components/Toast/sidebar/group/ToastSidebarGroup.tsx +++ b/src/components/Toast/sidebar/group/ToastSidebarGroup.tsx @@ -11,11 +11,11 @@ import { SToggle, } from "./ToastSidebarGroup.styled" -type Props = { title: string; children: ReactNode } +type Props = { title: string; children: ReactNode; open?: boolean } -export const ToastSidebarGroup = ({ title, children }: Props) => { +export const ToastSidebarGroup = ({ title, children, open = true }: Props) => { const { t } = useTranslation() - const [isOpen, toggle] = useToggle(true) + const [isOpen, toggle] = useToggle(open) return (
diff --git a/src/components/Toast/sidebar/referendums/ToastSidebarReferendums.tsx b/src/components/Toast/sidebar/referendums/ToastSidebarReferendums.tsx index 6f8d9e818..369f4aae5 100644 --- a/src/components/Toast/sidebar/referendums/ToastSidebarReferendums.tsx +++ b/src/components/Toast/sidebar/referendums/ToastSidebarReferendums.tsx @@ -1,37 +1,42 @@ -import { useReferendums } from "api/democracy" -import { ReferendumCard } from "components/ReferendumCard/ReferendumCard" -import { useTranslation } from "react-i18next" +import { + TReferenda, + useOpenGovReferendas, + useReferendaTracks, +} from "api/democracy" +import { OpenGovReferenda } from "components/ReferendumCard/Referenda" +import { useHDXSupplyFromSubscan } from "api/staking" import { ToastSidebarGroup } from "components/Toast/sidebar/group/ToastSidebarGroup" -import { PASEO_WS_URL, useProviderRpcUrlStore } from "api/provider" -import { ReferendumCardRococo } from "components/ReferendumCard/ReferendumCardRococo" +import { useTranslation } from "react-i18next" export const ToastSidebarReferendums = () => { const { t } = useTranslation() - const referendums = useReferendums("ongoing") - const providers = useProviderRpcUrlStore() - const rococoProvider = [PASEO_WS_URL, "mining-rpc.hydradx.io"].find( - (rpc) => - (providers.rpcUrl ?? import.meta.env.VITE_PROVIDER_URL) === - `wss://${rpc}`, - ) - - if (!referendums.data?.length) return null + const openGovQuery = useOpenGovReferendas() + const tracks = useReferendaTracks() + const { data: hdxSupply } = useHDXSupplyFromSubscan() return ( - +
- {referendums.data.map((referendum) => - rococoProvider ? ( - - ) : ( - - ), - )} + {openGovQuery.data?.length && tracks.data + ? openGovQuery.data.map((referendum) => { + const track = tracks.data.get( + referendum.referendum.track.toString(), + ) as TReferenda + + return ( + + ) + }) + : null}
) diff --git a/src/i18n/locales/en/translations.json b/src/i18n/locales/en/translations.json index 548ee3794..211c43e8d 100644 --- a/src/i18n/locales/en/translations.json +++ b/src/i18n/locales/en/translations.json @@ -17,6 +17,7 @@ "submit": "Submit", "continue": "Continue", "transfer": "Transfer", + "threshold": "Threshold", "24Volume": "24h volume", "days": "Days", "hours": "Hours", @@ -774,7 +775,7 @@ "stats.overview.chart.tvl.label.time": "{{ date, HH:mm}}", "stats.overview.chart.switcher.tvl": "tvl", "stats.overview.chart.switcher.volume": "volume", - "stats.overview.referenda.title": "Referenda", + "stats.overview.referenda.title": "Ongoing referenda", "stats.overview.referenda.desc": "Participate in governance to speed up the rate at which you can claim rewards.", "stats.overview.referenda.emptyState": "Currently there are no ongoing referenda to vote in.", "stats.overview.table.assets.header.title": "Omnipool assets", @@ -1241,5 +1242,14 @@ "claimingRange.modal.warning.title": "Are you sure?", "claimingRange.modal.warning.description": " Don't claim rewards too early. Claiming forfeits any non-claimable rewards to the other LPs. Before claiming, make sure that your claim threshold is configured accordingly.", "claimingRange.modal.description1": "After joining a farm, a portion of the accumulated rewards is locked. These rewards unlock over time as you remain in the farm and follow loyalty factor curve", - "claimingRange.modal.description2": "Claiming forfeits the locked part of the rewards. Use this setting to tweak your preference over claiming faster or losing less rewards (default). This allows you to compound your rewards with ease." + "claimingRange.modal.description2": "Claiming forfeits the locked part of the rewards. Use this setting to tweak your preference over claiming faster or losing less rewards (default). This allows you to compound your rewards with ease.", + "referenda.ongoing": "Ongoing Referenda", + "referenda.support": "Support: {{value}}", + "referenda.empty.title": "No Referenda", + "referenda.empty.desc.first": "Nothing here.", + "referenda.empty.desc.second": " Currently there are no ongoing referenda to vote in.", + "referenda.btn.vote": "Vote", + "referenda.state.preparing": "Preparing", + "referenda.state.deciding": "Deciding", + "referenda.state.confirming": "Confirming" } diff --git a/src/sections/lending/components/transactions/Warnings/IsolationModeWarning.tsx b/src/sections/lending/components/transactions/Warnings/IsolationModeWarning.tsx index 80372ea73..b00b2d718 100644 --- a/src/sections/lending/components/transactions/Warnings/IsolationModeWarning.tsx +++ b/src/sections/lending/components/transactions/Warnings/IsolationModeWarning.tsx @@ -1,5 +1,4 @@ import { Alert } from "components/Alert" -import { Link } from "sections/lending/components/primitives/Link" import { Text } from "components/Typography/Text/Text" interface IsolationModeWarningProps { diff --git a/src/sections/staking/StakingPage.utils.ts b/src/sections/staking/StakingPage.utils.ts index 3678de517..026ac1b18 100644 --- a/src/sections/staking/StakingPage.utils.ts +++ b/src/sections/staking/StakingPage.utils.ts @@ -5,18 +5,16 @@ import { useAccount } from "sections/web3-connect/Web3Connect.utils" import { TAccumulatedRpsUpdated, TStakingPosition, - useCirculatingSupply, + useHDXSupplyFromSubscan, useStake, useStakingConsts, useStakingEvents, - useStakingPositionBalances, } from "api/staking" import { useTokenBalance, useTokenLocks } from "api/balances" import { getHydraAccountAddress } from "utils/api" import { useDisplayPrice } from "utils/displayAsset" import { BN_0, - BN_100, BN_BILL, BN_QUINTILL, PARACHAIN_BLOCK_TIME, @@ -97,15 +95,13 @@ export const useStakeData = () => { const { account } = useAccount() const stake = useStake(account?.address) - const circulatingSupply = useCirculatingSupply() + const { data: hdxSupply, isLoading: isSupplyLoading } = + useHDXSupplyFromSubscan() const accountAssets = useAccountAssets() const locks = useTokenLocks(native.id) const spotPrice = useDisplayPrice(native.id) - const positionBalances = useStakingPositionBalances( - stake.data?.positionId?.toString(), - ) - const referendas = useReferendums("finished") + const circulatingSupply = hdxSupply?.circulatingSupply const balance = accountAssets.data?.accountAssetsMap.get(native.id)?.balance const vestLocks = locks.data?.reduce( @@ -125,17 +121,10 @@ export const useStakeData = () => { const availableBalance = BigNumber.max(0, rawAvailableBalance) - const queries = [ - stake, - circulatingSupply, - locks, - spotPrice, - positionBalances, - referendas, - accountAssets, - ] + const queries = [stake, locks, spotPrice] - const isLoading = queries.some((query) => query.isInitialLoading) + const isLoading = + queries.some((query) => query.isInitialLoading) || isSupplyLoading const data = useMemo(() => { if (isLoading) return undefined @@ -147,7 +136,7 @@ export const useStakeData = () => { const totalStake = stake.data?.totalStake ?? 0 const supplyStaked = BN(totalStake) - .div(Number(circulatingSupply.data ?? 1)) + .div(Number(circulatingSupply ?? 1)) .decimalPlaces(4) .multipliedBy(100) @@ -155,68 +144,11 @@ export const useStakeData = () => { .multipliedBy(spotPrice.data?.spotPrice ?? 1) .shiftedBy(-native.decimals) - const circulatingSupplyData = BN(circulatingSupply.data ?? 0).shiftedBy( + const circulatingSupplyData = BN(circulatingSupply ?? 0).shiftedBy( -native.decimals, ) const stakePosition = stake.data?.stakePosition - let averagePercentage = BN_0 - let amountOfReferends = 0 - - if (stakePosition) { - const initialPositionBalance = BN( - positionBalances.data?.events.find( - (event) => event.name === "Staking.PositionCreated", - )?.args.stake ?? 0, - ) - - const allReferendaPercentages = - referendas.data?.reduce((acc, referenda) => { - const endReferendaBlockNumber = - referenda.referendum.asFinished.end.toBigNumber() - - if (endReferendaBlockNumber.gt(stakePosition.createdAt)) { - amountOfReferends++ - - if (referenda.amount && referenda.conviction) { - /* staked position value when a referenda is over */ - let positionBalance = initialPositionBalance - - positionBalances.data?.events.forEach((event) => { - if (event.name === "Staking.StakeAdded") { - const eventOccurBlockNumber = BN(event.block.height) - - if ( - endReferendaBlockNumber.gte(eventOccurBlockNumber) && - positionBalance.lt(event.args.totalStake) - ) { - positionBalance = BN(event.args.totalStake) - } - } - }) - - const percentageOfVotedReferenda = referenda.amount - .div(positionBalance) - .multipliedBy(CONVICTIONS[referenda.conviction.toLowerCase()]) - .div(CONVICTIONS["locked6x"]) - .multipliedBy(100) - - return acc.plus(percentageOfVotedReferenda) - } - } - - return acc - }, BN_0) ?? BN(0) - - averagePercentage = - allReferendaPercentages.isZero() && !amountOfReferends - ? BN_100 - : allReferendaPercentages.div(amountOfReferends) - } - - const rewardBoostPersentage = referendas.data?.length - ? averagePercentage - : BN_100 return { supplyStaked, @@ -229,16 +161,13 @@ export const useStakeData = () => { stakePosition: stakePosition ? { ...stake.data?.stakePosition, - rewardBoostPersentage, } : undefined, } }, [ availableBalance, - circulatingSupply.data, + circulatingSupply, isLoading, - positionBalances.data?.events, - referendas.data, spotPrice.data?.spotPrice, stake.data?.minStake, stake.data?.positionId, diff --git a/src/sections/staking/sections/dashboard/StakingDashboard.tsx b/src/sections/staking/sections/dashboard/StakingDashboard.tsx index 7e95e4a79..4dec83dcf 100644 --- a/src/sections/staking/sections/dashboard/StakingDashboard.tsx +++ b/src/sections/staking/sections/dashboard/StakingDashboard.tsx @@ -3,11 +3,12 @@ import { AvailableRewards } from "./components/AvailableRewards/AvailableRewards import { StakingInputSection } from "./components/StakingInputSection/StakingInputSection" import { useAccount } from "sections/web3-connect/Web3Connect.utils" import { Stats } from "./components/Stats/Stats" -import { Referenda, ReferendaWrapper } from "./components/Referenda/Referenda" +import { OpenGovReferendas } from "./components/Referenda/Referendas" import { useStakeData } from "sections/staking/StakingPage.utils" import { useRpcProvider } from "providers/rpcProvider" import { useMedia } from "react-use" import { theme } from "theme" +import { ReferendaSkeleton } from "components/ReferendumCard/ReferendaSkeleton" export const StakingDashboard = () => { const { isLoaded } = useRpcProvider() @@ -24,7 +25,7 @@ export const StakingSkeleton = () => {
- {!isDesktop && } + {!isDesktop && }
{ css={{ flex: 2 }} > - {isDesktop && } + {isDesktop && }
) @@ -51,7 +52,7 @@ export const StakingData = () => { {showGuide && } {account && staking.data?.positionId && } - {!isDesktop && } + {!isDesktop && }
{ css={{ flex: 2 }} > - {isDesktop && } + {isDesktop && }
) diff --git a/src/sections/staking/sections/dashboard/components/AvailableRewards/AvailableRewards.tsx b/src/sections/staking/sections/dashboard/components/AvailableRewards/AvailableRewards.tsx index e3b0b78e7..1a943254f 100644 --- a/src/sections/staking/sections/dashboard/components/AvailableRewards/AvailableRewards.tsx +++ b/src/sections/staking/sections/dashboard/components/AvailableRewards/AvailableRewards.tsx @@ -19,7 +19,7 @@ import { TOAST_MESSAGES } from "state/toasts" import { theme } from "theme" import { useRpcProvider } from "providers/rpcProvider" import { useAccount } from "sections/web3-connect/Web3Connect.utils" -import { useProcessedVotesIds } from "api/staking" +import { useVotesRewardedIds } from "api/staking" import { BN_0 } from "utils/constants" import { Graph } from "components/Graph/Graph" import { XAxis, YAxis } from "recharts" @@ -37,7 +37,7 @@ export const AvailableRewards = () => { const spotPrice = useDisplayPrice(native.id) const refetch = useRefetchAccountAssets() - const processedVotes = useProcessedVotesIds() + const votesRewarded = useVotesRewardedIds() const { createTransaction } = useStore() const queryClient = useQueryClient() @@ -62,7 +62,7 @@ export const AvailableRewards = () => { return memo }, {} as ToastMessage) - const processedVoteIds = await processedVotes.mutateAsync() + const processedVoteIds = await votesRewarded.mutateAsync() await createTransaction( { diff --git a/src/sections/staking/sections/dashboard/components/Referenda/Referenda.tsx b/src/sections/staking/sections/dashboard/components/Referenda/Referenda.tsx deleted file mode 100644 index c59cab7c9..000000000 --- a/src/sections/staking/sections/dashboard/components/Referenda/Referenda.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useReferendums } from "api/democracy" -import { ReferendumCard } from "components/ReferendumCard/ReferendumCard" -import { ReferendumCardSkeleton } from "components/ReferendumCard/ReferendumCardSkeleton" -import { Text } from "components/Typography/Text/Text" -import { useTranslation } from "react-i18next" -import { SContainer } from "sections/staking/StakingPage.styled" -import GovernanceIcon from "assets/icons/GovernanceIcon.svg?react" -import { Icon } from "components/Icon/Icon" -import { ReferendumCardRococo } from "components/ReferendumCard/ReferendumCardRococo" -import { PASEO_WS_URL, useProviderRpcUrlStore } from "api/provider" -import { theme } from "theme" - -type ReferendaProps = { - loading: boolean - data?: ReturnType["data"] -} - -export const ReferendaWrapper = () => { - const referendums = useReferendums("ongoing") - - return -} - -export const Referenda = ({ data, loading }: ReferendaProps) => { - const { t } = useTranslation() - const providers = useProviderRpcUrlStore() - const rococoProvider = [PASEO_WS_URL, "mining-rpc.hydradx.io"].find( - (rpc) => - (providers.rpcUrl ?? import.meta.env.VITE_PROVIDER_URL) === - `wss://${rpc}`, - ) - - return ( - - - {t("stats.overview.referenda.title")} - - {loading ? ( - - ) : data?.length ? ( -
- - {t("stats.overview.referenda.desc")} - - {data.map((referendum) => - rococoProvider ? ( - - ) : ( - - ), - )} -
- ) : ( -
- } /> - - {t("stats.overview.referenda.emptyState")} - -
- )} -
- ) -} diff --git a/src/sections/staking/sections/dashboard/components/Referenda/Referendas.tsx b/src/sections/staking/sections/dashboard/components/Referenda/Referendas.tsx new file mode 100644 index 000000000..2d9331f62 --- /dev/null +++ b/src/sections/staking/sections/dashboard/components/Referenda/Referendas.tsx @@ -0,0 +1,115 @@ +import { + TReferenda, + useOpenGovReferendas, + useReferendaTracks, +} from "api/democracy" +import { Text } from "components/Typography/Text/Text" +import { useTranslation } from "react-i18next" +import { useHDXSupplyFromSubscan } from "api/staking" +import { + Dropdown, + DropdownTriggerContent, +} from "components/Dropdown/DropdownRebranded" +import { useMemo, useState } from "react" +import { OpenGovReferenda } from "components/ReferendumCard/Referenda" +import { NoReferenda } from "components/ReferendumCard/NoReferenda" +import { ReferendaSkeleton } from "components/ReferendumCard/ReferendaSkeleton" + +const defaultFilter = { key: "all", label: "ALL" } + +export const OpenGovReferendas = () => { + const { t } = useTranslation() + const openGovQuery = useOpenGovReferendas() + const tracks = useReferendaTracks() + const { data: hdxSupply, isLoading: isSupplyLoading } = + useHDXSupplyFromSubscan() + + const [filter, setFilter] = useState(defaultFilter.key) + + const trackItems = useMemo( + () => + tracks.data + ? [ + defaultFilter, + ...Array.from(tracks.data.entries()).map((track) => ({ + key: track[0], + label: track[1].nameHuman, + })), + ].sort((a, b) => a.label.localeCompare(b.label)) + : null, + [tracks.data], + ) + + const filtredReferenda = useMemo(() => { + if (openGovQuery.data?.length && trackItems) { + if (filter !== defaultFilter.key) { + return openGovQuery.data.filter( + (referenda) => referenda.referendum.track.toString() === filter, + ) + } else { + return openGovQuery.data + } + } + }, [openGovQuery.data, trackItems, filter]) + + const isLoading = + openGovQuery.isLoading || isSupplyLoading || tracks.isLoading + + return ( +
+
+ + {t("stats.overview.referenda.title")} + + {trackItems && ( + setFilter(item.key)} + > + item.key === filter)?.label} + /> + + )} +
+ + {isLoading ? ( + + ) : ( +
+ {filtredReferenda?.length && tracks.data ? ( + filtredReferenda.map((referendum) => { + const track = tracks.data.get( + referendum.referendum.track.toString(), + ) as TReferenda + + return ( + + ) + }) + ) : ( + + )} +
+ )} + + {t("stats.overview.referenda.desc")} + +
+ ) +} diff --git a/src/sections/staking/sections/dashboard/components/StakingInputSection/Stake/Stake.tsx b/src/sections/staking/sections/dashboard/components/StakingInputSection/Stake/Stake.tsx index bbb155cab..804009a44 100644 --- a/src/sections/staking/sections/dashboard/components/StakingInputSection/Stake/Stake.tsx +++ b/src/sections/staking/sections/dashboard/components/StakingInputSection/Stake/Stake.tsx @@ -16,7 +16,7 @@ import { TOAST_MESSAGES } from "state/toasts" import { useRpcProvider } from "providers/rpcProvider" import { useAccount } from "sections/web3-connect/Web3Connect.utils" import { Web3ConnectModalButton } from "sections/web3-connect/modal/Web3ConnectModalButton" -import { useProcessedVotesIds } from "api/staking" +import { useVotesRewardedIds } from "api/staking" import { useAssets } from "providers/assets" import { useRefetchAccountAssets } from "api/deposits" @@ -41,7 +41,7 @@ export const Stake = ({ const form = useForm<{ amount: string }>() - const processedVotes = useProcessedVotesIds() + const votesRewarded = useVotesRewardedIds() const onSubmit = async (values: FormValues) => { const amount = getFixedPointAmount(values.amount, 12).toString() @@ -69,7 +69,7 @@ export const Stake = ({ }, {} as ToastMessage) if (isStakePosition) { - const processedVoteIds = await processedVotes.mutateAsync() + const processedVoteIds = await votesRewarded.mutateAsync() transaction = await createTransaction( { @@ -98,7 +98,7 @@ export const Stake = ({ } await queryClient.invalidateQueries(QUERY_KEYS.stake(account?.address)) - await queryClient.invalidateQueries(QUERY_KEYS.circulatingSupply) + await queryClient.invalidateQueries(QUERY_KEYS.hdxSupply) refetchAccountAssets() } diff --git a/src/sections/staking/sections/dashboard/components/StakingInputSection/Unstake/Unstake.tsx b/src/sections/staking/sections/dashboard/components/StakingInputSection/Unstake/Unstake.tsx index bb73b75dc..6d664c9ca 100644 --- a/src/sections/staking/sections/dashboard/components/StakingInputSection/Unstake/Unstake.tsx +++ b/src/sections/staking/sections/dashboard/components/StakingInputSection/Unstake/Unstake.tsx @@ -14,7 +14,7 @@ import { QUERY_KEYS } from "utils/queryKeys" import { TOAST_MESSAGES } from "state/toasts" import { useRpcProvider } from "providers/rpcProvider" import { useAccount } from "sections/web3-connect/Web3Connect.utils" -import { usePositionVotesIds, useProcessedVotesIds } from "api/staking" +import { usePositionVotesIds, useVotesRewardedIds } from "api/staking" import { useAssets } from "providers/assets" import { useRefetchAccountAssets } from "api/deposits" @@ -41,8 +41,8 @@ export const Unstake = ({ }, }) - const processedVotes = useProcessedVotesIds() - const positionVotes = usePositionVotesIds() + const votesRewarded = useVotesRewardedIds() + const votes = usePositionVotesIds() const onSubmit = async () => { if (!positionId) return null @@ -64,8 +64,8 @@ export const Unstake = ({ return memo }, {} as ToastMessage) - const pendingVoteIds = await positionVotes.mutateAsync(positionId) - const processedVoteIds = await processedVotes.mutateAsync() + const pendingVoteIds = await votes.mutateAsync(positionId) + const processedVoteIds = await votesRewarded.mutateAsync() const voteIds = [...pendingVoteIds, ...processedVoteIds] @@ -82,7 +82,7 @@ export const Unstake = ({ ) await queryClient.invalidateQueries(QUERY_KEYS.stake(account?.address)) - await queryClient.invalidateQueries(QUERY_KEYS.circulatingSupply) + await queryClient.invalidateQueries(QUERY_KEYS.hdxSupply) refetchAccountAssets() if (!transaction.isError) { diff --git a/src/utils/formatting.ts b/src/utils/formatting.ts index 6350e6610..0f1fab15d 100644 --- a/src/utils/formatting.ts +++ b/src/utils/formatting.ts @@ -404,3 +404,7 @@ export const wsToHttp = (url: string) => url.replace(/^(ws)(s)?:\/\//, (_, _insecure, secure) => secure ? "https://" : "http://", ) + +export const humanizeUnderscoredString = (value: string) => { + return value.split("_").join(" ").toUpperCase() +} diff --git a/src/utils/opengov.ts b/src/utils/opengov.ts new file mode 100644 index 000000000..e75ad6aa3 --- /dev/null +++ b/src/utils/opengov.ts @@ -0,0 +1,85 @@ +import BigNumber from "bignumber.js" +import { TReferenda } from "api/democracy" +import { BN_0 } from "./constants" + +export const getCurveData = ( + track: TReferenda, + field: "minApproval" | "minSupport", +) => { + const curve = track[field] + + if (!curve) { + return null + } + + if (curve.isReciprocal) { + const { factor, xOffset, yOffset } = curve.asReciprocal + return makeReciprocalCurve( + factor.toString(), + xOffset.toString(), + yOffset.toString(), + ) + } + + if (curve.isLinearDecreasing) { + const { length, floor, ceil } = curve.asLinearDecreasing + + return makeLinearCurve(length.toNumber(), floor.toNumber(), ceil.toNumber()) + } + + return null +} + +export const makeReciprocalCurve = ( + factor: string, + xOffset: string, + yOffset: string, +) => { + return (percentage: number) => { + const x = percentage * Math.pow(10, 9) + + const v = new BigNumber(factor) + .div(new BigNumber(x).plus(xOffset)) + .multipliedBy(Math.pow(10, 9)) + .toFixed(0, BigNumber.ROUND_DOWN) + + const calcValue = new BigNumber(v) + .plus(yOffset) + .div(Math.pow(10, 9)) + .toString() + return BigNumber.max(calcValue, 0).toString() + } +} + +export const makeLinearCurve = ( + length: number, + floor: number, + ceil: number, +) => { + return (percentage: number) => { + const x = percentage * Math.pow(10, 9) + + const xValue = BigNumber.min(x, length) + + const slope = new BigNumber(ceil).minus(floor).dividedBy(length) + const deducted = slope.multipliedBy(xValue).toString() + + const perbill = new BigNumber(ceil) + .minus(deducted) + .toFixed(0, BigNumber.ROUND_DOWN) + const calcValue = new BigNumber(perbill).div(Math.pow(10, 9)).toString() + return BigNumber.max(calcValue, 0).toString() + } +} + +export const getDecidingEndPercentage = ( + decisionPeriod: string, + decidingSince: string | undefined, + endHeight: string, +) => { + if (decidingSince === undefined) return BN_0 + + const gone = BigNumber(endHeight).minus(decidingSince) + + return BigNumber.min(gone.div(decisionPeriod), 1) +} diff --git a/src/utils/queryKeys.ts b/src/utils/queryKeys.ts index bf9e27355..c5fe596fe 100644 --- a/src/utils/queryKeys.ts +++ b/src/utils/queryKeys.ts @@ -220,12 +220,14 @@ export const QUERY_KEYS = { accountAddress, type, ], + openGovReferendas: (url: string) => ["openGovReferendas", url], + referendaTracks: (url: string) => ["referendaTracks", url], referendumVotes: (accountAddress?: string) => [ QUERY_KEY_PREFIX, "referendumVotes", accountAddress, ], - referendumInfo: (id: string) => [QUERY_KEY_PREFIX, id, "referendumInfo"], + referendumInfo: (id: string) => [id, "referendumInfo"], stats: ( type: ChartType, timeframe?: StatsTimeframe, @@ -238,16 +240,13 @@ export const QUERY_KEYS = { return key }, - circulatingSupply: ["circulatingSupply"], + hdxSupply: ["hdxSupply"], stake: (address: string | undefined) => ["stake", address], staking: ["staking"], stakingPosition: (id: number | undefined) => ["totalStaking", id], stakingConsts: ["stakingConsts"], stakingEvents: ["stakingEvents"], - stakingPositionBalances: (positionId: Maybe) => [ - "positionBalances", - positionId, - ], + stableswapPools: ["stableswapPools"], stableswapPool: (id?: string) => ["stableswapPool", id], lbpPool: ["lbpPool"], bondEvents: (id?: Maybe, myEvents?: boolean) => [ diff --git a/yarn.lock b/yarn.lock index 1cbee3fff..f0e8d4fb2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2294,10 +2294,10 @@ lit "^3.1.4" ts-debounce "^4.0.0" -"@galacticcouncil/xcm-cfg@^6.0.0": - version "6.0.0" - resolved "https://registry.yarnpkg.com/@galacticcouncil/xcm-cfg/-/xcm-cfg-6.0.0.tgz#098f379d2638c24ba21243a54c8e650617b27915" - integrity sha512-Y+wk1LohE0OJIfqzObtKNLacgTiO5cW0xqRKZEN6uBK3W67fsnms6vBeYdnQXebZn8jLKNxN8bNE6vg/2OoSXA== +"@galacticcouncil/xcm-cfg@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@galacticcouncil/xcm-cfg/-/xcm-cfg-6.0.1.tgz#04c6b3618d335a031eb312feb1b10af1d68992db" + integrity sha512-GV5LWbUvlOv+5kYjIsyEeEGtrE4aaDMV8N6sKfZCOK7RGZ0fBqsi6DSeRKot//ybvXL4Zy+hXGVM6UnuvGKYRg== dependencies: "@galacticcouncil/xcm-core" "^5.5.0" @@ -11663,7 +11663,7 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -"prettier-fallback@npm:prettier@^3", prettier@^2.8.0, prettier@^3.1.1, prettier@^3.3.2: +"prettier-fallback@npm:prettier@^3": version "3.3.2" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a" integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA== @@ -11675,6 +11675,11 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" +prettier@^2.8.0, prettier@^3.1.1, prettier@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a" + integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA== + pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" @@ -12984,7 +12989,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -14003,7 +14015,16 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@7.0.0, wrap-ansi@^6.2.0, wrap-ansi@^8.1.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@7.0.0, wrap-ansi@^6.2.0, wrap-ansi@^8.1.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==