Skip to content

Commit

Permalink
Merge branch 'main' into fix/stake-sheet-validaton-after-submission
Browse files Browse the repository at this point in the history
  • Loading branch information
UrbanWill authored Oct 11, 2024
2 parents dde5fe5 + 24054fa commit d3d08a1
Show file tree
Hide file tree
Showing 16 changed files with 231 additions and 131 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/apps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ jobs:
BASEROW_EXPLORE_AUTH,
BASEROW_CROWDLOANS_AUTH,
LIDO_REWARDS_ADDRESS,
SIMPLESWAP_API_KEY
SIMPLESWAP_API_KEY,
TAOSTATS_API_KEY
environmentVariableVariables: >-
APPLICATION_NAME
environmentVariablePrefix: REACT_APP_
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ jobs:
BASEROW_EXPLORE_AUTH,
BASEROW_CROWDLOANS_AUTH,
LIDO_REWARDS_ADDRESS,
SIMPLESWAP_API_KEY
SIMPLESWAP_API_KEY,
TAOSTATS_API_KEY
environmentVariableVariables: >-
APPLICATION_NAME
environmentVariablePrefix: REACT_APP_
Expand Down
2 changes: 2 additions & 0 deletions apps/portal/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ REACT_APP_POSTHOG_AUTH_TOKEN=
REACT_APP_BASEROW_EXPLORE_AUTH=
REACT_APP_BASEROW_CROWDLOANS_AUTH=
REACT_APP_LIDO_REWARDS_ADDRESS=
REACT_APP_SIMPLESWAP_API_KEY=
REACT_APP_TAOSTATS_API_KEY=
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DEFAULT_DELEGATE, type Delegate } from '../../../../domains/staking/sub
import { useAllDelegateInfos } from '../../../../domains/staking/subtensor/hooks/useAllDelegateInfos'
import { useDelegates } from '../../../../domains/staking/subtensor/hooks/useDelegates'
import StakeTargetSelectorDialog from '../../../recipes/StakeTargetSelectorDialog'
import { useDelegatesStats } from '@/domains/staking/subtensor/hooks/useDelegatesStats'
import { useState } from 'react'
import { useRecoilValue_TRANSITION_SUPPORT_UNSTABLE as useRecoilValue } from 'recoil'

Expand All @@ -15,6 +16,7 @@ type DelegateSelectorDialogProps = {
export const DelegateSelectorDialog = (props: DelegateSelectorDialogProps) => {
const delegates = useDelegates()
const allDelegateInfos = useAllDelegateInfos()
const delegatesStats = useDelegatesStats()

const [highlighted, setHighlighted] = useState(delegates[DEFAULT_DELEGATE] ?? Object.values(delegates)[0])
const nativeTokenAmount = useRecoilValue(useNativeTokenAmountState())
Expand All @@ -38,6 +40,9 @@ export const DelegateSelectorDialog = (props: DelegateSelectorDialogProps) => {
: 1,
'Number of stakers': (a, b) =>
parseInt(b.props.count?.toString?.() ?? '0') - parseInt(a.props.count?.toString?.() ?? '0'),
'Estimated APR': (a, b) =>
parseFloat(b.props.estimatedApr?.replace('%', '') ?? '0') -
parseFloat(a.props.estimatedApr?.replace('%', '') ?? '0'),
// 'Estimated return': (a, b) =>
// BigInt(b.props.estimatedReturn ?? 0n) === BigInt(a.props.estimatedReturn ?? 0n)
// ? 0
Expand All @@ -46,34 +51,40 @@ export const DelegateSelectorDialog = (props: DelegateSelectorDialogProps) => {
// : 1,
}}
>
{Object.values(delegates).map(delegate => (
<StakeTargetSelectorDialog.Item
key={delegate.address}
balanceDescription="Total staked with this delegate"
countDescription="Number of delegate stakers"
estimatedAprDescription="Estimated APR"
talismanRecommendedDescription="Talisman top recommended delegate"
rating={3}
selected={delegate.address === props.selected?.address}
highlighted={delegate.address === highlighted?.address}
name={delegate.name}
talismanRecommended={delegate.address === DEFAULT_DELEGATE}
detailUrl={delegate.url}
count={allDelegateInfos[delegate.address]?.nominators?.length ?? 0}
balance={
nativeTokenAmount
.fromPlanckOrUndefined(allDelegateInfos[delegate.address]?.totalDelegated)
.decimalAmount?.toLocaleString() ?? ''
}
balancePlanck={
nativeTokenAmount.fromPlanckOrUndefined(allDelegateInfos[delegate.address]?.totalDelegated).decimalAmount
?.planck
}
// estimatedReturn={allDelegateInfos[delegate.address]?.return_per_1000}
// estimatedApr={allDelegateInfos[delegate.address]?.apr}
onClick={() => setHighlighted(delegate)}
/>
))}
{Object.values(delegates).map(delegate => {
const formattedApr = Number(
delegatesStats.find(stat => stat.hot_key.ss58 === delegate.address)?.apr
).toLocaleString(undefined, { style: 'percent', maximumFractionDigits: 2 })

return (
<StakeTargetSelectorDialog.Item
key={delegate.address}
balanceDescription="Total staked with this delegate"
countDescription="Number of delegate stakers"
estimatedAprDescription="Estimated APR"
estimatedApr={formattedApr}
talismanRecommendedDescription="Talisman top recommended delegate"
rating={3}
selected={delegate.address === props.selected?.address}
highlighted={delegate.address === highlighted?.address}
name={delegate.name}
talismanRecommended={delegate.address === DEFAULT_DELEGATE}
detailUrl={delegate.url}
count={allDelegateInfos[delegate.address]?.nominators?.length ?? 0}
balance={
nativeTokenAmount
.fromPlanckOrUndefined(allDelegateInfos[delegate.address]?.totalDelegated)
.decimalAmount?.toLocaleString() ?? ''
}
balancePlanck={
nativeTokenAmount.fromPlanckOrUndefined(allDelegateInfos[delegate.address]?.totalDelegated).decimalAmount
?.planck
}
// estimatedReturn={allDelegateInfos[delegate.address]?.return_per_1000}
onClick={() => setHighlighted(delegate)}
/>
)
})}
</StakeTargetSelectorDialog>
)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type Account } from '../../../../domains/accounts'
import { useChainState, useNativeTokenAmountState } from '../../../../domains/chains'
import { useNativeTokenAmountState } from '../../../../domains/chains'
import { useAddStakeForm } from '../../../../domains/staking/subtensor/hooks/forms'
import { useApr } from '../../../../domains/staking/subtensor/hooks/useApr'
import { useDelegateApr } from '../../../../domains/staking/subtensor/hooks/useApr'
import { useStake } from '../../../../domains/staking/subtensor/hooks/useStake'
import { SubtensorStakingForm } from './SubtensorStakingForm'
import { CircularProgressIndicator } from '@talismn/ui'
Expand Down Expand Up @@ -54,6 +54,7 @@ export const StakeForm = (props: StakeFormProps) => {
estimatedRewards={
<Suspense fallback={<CircularProgressIndicator size="1em" />}>
<EstimatedRewards
delegateHotkey={props.delegate}
amount={(amount.decimalAmount?.planck ?? 0n) + (stake.totalStaked.decimalAmount?.planck ?? 0n)}
/>
</Suspense>
Expand Down Expand Up @@ -85,13 +86,13 @@ export const IncompleteSelectionStakeForm = (props: IncompleteSelectionStakeForm
/>
)

const EstimatedRewards = (props: { amount: bigint }) => {
const EstimatedRewards = (props: { amount: bigint; delegateHotkey: string }) => {
const tokenAmount = useRecoilValue(useNativeTokenAmountState())
const genesisHash = useRecoilValue(useChainState())?.genesisHash
const apr = useApr(genesisHash)
// const apr = useApr()
const delegateApr = useDelegateApr(props.delegateHotkey)
const amount = useMemo(
() => tokenAmount.fromPlanck(new BN(props.amount.toString()).muln(apr).toString()),
[apr, props.amount, tokenAmount]
() => tokenAmount.fromPlanck(new BN(props.amount.toString()).muln(delegateApr).toString()),
[delegateApr, props.amount, tokenAmount]
)

if (amount.decimalAmount === undefined) return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
substrateApiState,
useTokenAmountFromPlanck,
} from '../../../../domains/common'
import { useAprFormatted } from '../../../../domains/staking/subtensor/hooks/useApr'
import { useHighestAprFormatted } from '../../../../domains/staking/subtensor/hooks/useApr'
import StakeProvider from '../../../recipes/StakeProvider'
import AnimatedFiatNumber from '../../AnimatedFiatNumber'
import ErrorBoundary from '../../ErrorBoundary'
Expand All @@ -20,7 +20,7 @@ import { useMemo } from 'react'
import { Link } from 'react-router-dom'
import { useRecoilValue, waitForAll } from 'recoil'

const Apr = ({ genesisHash }: { genesisHash?: string }) => <>{useAprFormatted(genesisHash)}</>
const Apr = () => <>{useHighestAprFormatted()}</>

const useAvailableBalance = () => {
const apiId = usePolkadotApiId()
Expand Down Expand Up @@ -90,7 +90,7 @@ const StakeProviderItem = () => {
logo={logo}
chain={name}
chainId={chain?.id}
apr={<Apr genesisHash={chain.genesisHash} />}
apr={<Apr />}
type="Delegation"
provider={name}
unbondingPeriod={<UnlockDuration />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import {
type ChainInfo,
} from '../../../../domains/chains'
import { DEFAULT_DELEGATE, MIN_SUBTENSOR_STAKE } from '../../../../domains/staking/subtensor/atoms/delegates'
import { useAprFormatted } from '../../../../domains/staking/subtensor/hooks/useApr'
import { Delegate } from '../../../../domains/staking/subtensor/atoms/delegates'
import { useDelegateAprFormatted } from '../../../../domains/staking/subtensor/hooks/useApr'
import { useDelegates } from '../../../../domains/staking/subtensor/hooks/useDelegates'
import { useTaostatsToken } from '../../../../domains/staking/subtensor/hooks/useTaostatsToken'
import { useTaostatsVolume24hFormatted } from '../../../../domains/staking/subtensor/hooks/useTaostatsVolume24h'
import { useTotalTaoStakedFormatted } from '../../../../domains/staking/subtensor/hooks/useTotalTaoStakedFormatted'
import { Maybe } from '../../../../util/monads'
import { TalismanHandLoader } from '../../../legacy/TalismanHandLoader'
import { useAccountSelector } from '../../AccountSelector'
Expand All @@ -28,18 +28,22 @@ type StakeSideSheetProps = {
onRequestDismiss: () => unknown
}

const StakeSideSheetContent = (props: Omit<StakeSideSheetProps, 'onRequestDismiss'>) => {
type StakeSideSheetContentProps = Omit<StakeSideSheetProps, 'onRequestDismiss'> & {
delegate: Delegate | undefined
setDelegate: React.Dispatch<React.SetStateAction<Delegate | undefined>>
}

const StakeSideSheetContent = (props: StakeSideSheetContentProps) => {
const [searchParams] = useSearchParams()

const chain = useRecoilValue(useChainState())
const delegates = useDelegates()
const [[account], accountSelector] = useAccountSelector(
useRecoilValue(writeableSubstrateAccountsState),
searchParams.get('account') === null
? 0
: accounts => accounts?.find(x => x.address === searchParams.get('account'))
)
const [delegate, setDelegate] = useState(delegates[DEFAULT_DELEGATE] ?? Object.values(delegates)[0])
const { delegate, setDelegate } = props

const [delegateSelectorOpen, setDelegateSelectorOpen] = useState(false)
const [delegateSelectorInTransition, startDelegateSelectorTransition] = useTransition()
Expand Down Expand Up @@ -101,10 +105,12 @@ const StakeSideSheetContent = (props: Omit<StakeSideSheetProps, 'onRequestDismis
}

const StakeSideSheetForChain = (props: StakeSideSheetProps) => {
const genesisHash = useRecoilValue(useChainState())?.genesisHash
const volume24h = useTaostatsVolume24hFormatted(genesisHash)
const rewards = useAprFormatted(genesisHash)
const token = useTaostatsToken(genesisHash)
const delegates = useDelegates()
const [delegate, setDelegate] = useState(delegates[DEFAULT_DELEGATE] ?? Object.values(delegates)[0])
const { nativeToken } = useRecoilValue(useChainState())

const totalStaked = useTotalTaoStakedFormatted()
const delegateApr = useDelegateAprFormatted(delegate?.address ?? DEFAULT_DELEGATE)

return (
<SubtensorStakingSideSheet
Expand All @@ -113,16 +119,16 @@ const StakeSideSheetForChain = (props: StakeSideSheetProps) => {
info={useMemo(
() => [
{
title: '24h Volume',
content: <>{volume24h}</>,
title: 'Total Staked',
content: <>{totalStaked}</>,
},
{ title: 'Estimated APR', content: <>{rewards}</> },
{ title: 'Estimated APR', content: <>{delegateApr}</> },
],
[rewards, volume24h]
[delegateApr, totalStaked]
)}
minimumStake={
<>
{MIN_SUBTENSOR_STAKE} {token}
{MIN_SUBTENSOR_STAKE} {nativeToken?.symbol}
</>
}
>
Expand All @@ -144,7 +150,7 @@ const StakeSideSheetForChain = (props: StakeSideSheetProps) => {
</div>
}
>
<StakeSideSheetContent {...props} />
<StakeSideSheetContent {...props} delegate={delegate!} setDelegate={setDelegate} />
</Suspense>
</ErrorBoundary>
</SubtensorStakingSideSheet>
Expand Down
97 changes: 59 additions & 38 deletions apps/portal/src/domains/staking/subtensor/atoms/taostats.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,75 @@
import { ValidatorsData } from '../types'
import { delegatesAtom } from './delegates'
import { atom } from 'jotai'
import { atomFamily } from 'jotai/utils'

const TAOSTATS_DATA_URL = 'https://taostats.io/data.json'

// The taostats endpoint has a `network` field, which is the name of the network.
// This field is the only way to tell which network a given set of stats is for.
// This map lets us identify which `network` value to use as an id for each given genesisHash.
const TaostatsNetworkByGenesisHash = new Map([
['0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03', 'Bittensor'],
])

type Taostats = Array<{
network?: string // 'Bittensor'
token?: string // 'TAO'
price?: string // '365.73'
'24h_change'?: string // '12.69'
'24h_volume'?: string // '66990486.579526'
current_supply?: string // '6923726'
total_supply?: string // '21000000'
delegated_supply?: string // '5744443'
market_cap?: string // '2532203699'
next_halvening?: string // '22 October 2025'
daily_return_per_1000t?: string // '0.0010277758870616'
validating_apy?: string // '18.77'
staking_apy?: string // '17.08'
last_updated?: string // '12 June 2024 16:54:08 GMT'
}>
const TAOSTATS_API_KEY = import.meta.env.REACT_APP_TAOSTATS_API_KEY
const TAOSTATS_API_URL = 'https://api.taostats.io/api/v1'

export const taostatsAtom = atom(async () => {
const fetchTaoStats = async ({ page = 1, limit = 200 }: { page: number; limit: number }): Promise<ValidatorsData> => {
try {
const stats = await (await fetch(TAOSTATS_DATA_URL)).json()
return Array.isArray(stats) ? (stats as Taostats) : []
return await (
await fetch(`${TAOSTATS_API_URL}/validator?page=${page}&limit=${limit}`, {
method: 'GET',
headers: {
Authorization: TAOSTATS_API_KEY,
'Content-Type': 'application/json',
},
})
).json()
} catch (cause) {
console.error('Failed to fetch TAO stats', { cause })
return []
throw new Error('Failed to fetch TAO stats', { cause })
}
}

export const taostatsAtom = atom(async () => {
const stats: ValidatorsData = { count: 0, validators: [] }

let page = 1
while (stats.count === 0 || stats.count > stats.validators.length) {
const taoStats = await fetchTaoStats({ page: page, limit: 200 })
stats.count = taoStats.count
stats.validators.push(...taoStats.validators)
page++
}

return stats
})

export const taostatsByChainAtomFamily = atomFamily((genesisHash: string | undefined) => {
if (!genesisHash) return atom(() => Promise.resolve(undefined))
export const activeTaoDelegatesStatsAtom = atom(async get => {
const taostats = await get(taostatsAtom)
const delegates = await get(delegatesAtom)

const activeDelegatesHotKeys = Object.keys(delegates)

const networkName = TaostatsNetworkByGenesisHash.get(genesisHash)
if (!networkName) return atom(() => Promise.resolve(undefined))
const activeDelegates = taostats.validators.filter(validator =>
activeDelegatesHotKeys.includes(validator.hot_key.ss58)
)

return atom(async get => (await get(taostatsAtom))?.find(stats => stats.network === networkName))
return activeDelegates
})

export const stakingAprByChainAtomFamily = atomFamily((genesisHash: string | undefined) =>
export const highestAprTaoValidatorAtom = atom(async get => {
const activeDelegatesStats = await get(activeTaoDelegatesStatsAtom)
const highestAprValidatorStats = activeDelegatesStats.reduce((acc, validator) => {
if (parseFloat(validator.apr) > parseFloat(acc.apr)) {
acc = validator
}
return acc
})

return highestAprValidatorStats
})

export const taoDelegateStatsAtomFamily = atomFamily((hotKey: string) =>
atom(async get => {
const taostats = await get(taostatsByChainAtomFamily(genesisHash))
return parseFloat(taostats?.staking_apy ?? '0.0') / 100
const activeDelegatesStats = await get(activeTaoDelegatesStatsAtom)
return activeDelegatesStats.find(validator => validator.hot_key.ss58 === hotKey)
})
)

export const taoTotalStakedTaoAtom = atom(async get => {
const { system_total_stake } = await get(highestAprTaoValidatorAtom)

return system_total_stake
})
Loading

0 comments on commit d3d08a1

Please sign in to comment.