Skip to content

Commit

Permalink
feat: TAO staking (#1146)
Browse files Browse the repository at this point in the history
* feat: TAO staking

* fix: remove unused config value

* fix: remove unused config value

* fix: tidy up

* fix: tidy up

* feat: implemented product feedback

* feat: implemented product feedback

* feat: implemented more product feedback

* feat: implemented more product feedback

* feat: chainflip ux rework (#1141)

* feat: wip chainflip ux rework

* feat: quote, side panel and more

* feat: select rebuild, connect eth, balances, max, and more

* feat: balances stuff

* feat: everything working, 3 minor todos remaining

* feat: arbitrary target address, history compatibility, expired status, fix max, more

* feat: better handling of evm accounts

* fix: missing ui recipes

* feat: substrate account prefix

* feat: protocol agnostic swap (#1142)

* feat: allow mainnet to mainnet swap, but what if we can do protocol agnostic swap

* feat: lots of progress on protocol agnostic swap

* feat: chainflip swap ok, remaining accounts ux refactor

* feat: states improvements

* refactor: refactor and cleanup

* feat: improve quote ux

* feat: good accounts ux, hopefully

* fix: swap route name

* feat: usd value and other ux improvements

* fix: selecting to account

* feat: check existential deposit

* Feat/protocol agnostic swap (#1145)

* feat: allow mainnet to mainnet swap, but what if we can do protocol agnostic swap

* feat: lots of progress on protocol agnostic swap

* feat: chainflip swap ok, remaining accounts ux refactor

* feat: states improvements

* refactor: refactor and cleanup

* feat: improve quote ux

* feat: good accounts ux, hopefully

* fix: swap route name

* feat: usd value and other ux improvements

* fix: selecting to account

* feat: check existential deposit

* feat: done

* feat: better mobile responsiveness

* feat: downgrade chainflip sdk and capture swap ids for quest points

* fix: swaps not getting saved correctly

* feat: only swap to and from dot, adjust fee

* feat: show token even before they are added to coingecko

* feat: remove flip token

* feat: better tokens loading, show talisman fees

* feat: fees tooltip and gas estimation

* feat: better gas estimation and fast balances

* fix: balance loading

* fix: yarn lock

* fix: shut up playwright

* fix: crypto

* fix: vite build and better chainflip error

* chore: skip portal storybook deplyment

* feat: enable deploy storybook workflow

* fix: include node polyfills

* fix: missed lockfile

* fix: include qa workflow

* fix: resume workflow

* fix: need qa for portal storybook

* fix: downgrade chainflip sdk: (#1149)

* fix: include withSignedTransaction (#1150)

* fix: update bifrost api (#1152)

* feat: avail nompool staking (#1153)

* feat: added avail to nompool config

* fix: remove serif fallback font

* fix: broken searchbar on portfolio

* fix: use avail extensions in apipromise

* fix: show coming soon instead of apr for avail

* fix: more coming soon text for avail

* fix: add avail indexer

* fix: input width

* feat: improved swap amount (#1154)

* feat: TAO staking

* fix: remove duped bittensor

* fix: dedupe deps

---------

Co-authored-by: Chris Ling <81092286+chrisling-dev@users.noreply.github.com>
Co-authored-by: Will Urban <will.urban.dev@gmail.com>
Co-authored-by: chrisling-dev <chrisling.dev@gmail.com>
  • Loading branch information
4 people committed Jul 25, 2024
1 parent d265ba2 commit 8aca9e0
Show file tree
Hide file tree
Showing 45 changed files with 2,951 additions and 71 deletions.
4 changes: 4 additions & 0 deletions apps/portal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"@chainflip/sdk": "1.3.0",
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@polkadot-api/descriptors": "^0.0.1",
"@polkadot-api/utils": "^0.1.0",
"@polkadot-onboard/core": "^1.1.0",
"@polkadot-onboard/injected-wallets": "^1.1.0",
"@polkadot/api": "^12.0.2",
Expand Down Expand Up @@ -67,6 +69,7 @@
"lucide-react": "^0.394.0",
"md5": "^2.3.0",
"mipd": "^0.0.7",
"polkadot-api": "^0.8.0",
"posthog-js": "^1.93.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand All @@ -78,6 +81,7 @@
"react-winbox": "^1.5.0",
"recoil": "^0.7.7",
"safety-match": "^0.4.4",
"scale-ts": "^1.6.0",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"threads": "^1.7.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const AddStakeForm = (props: AddStakeFormProps) => (
</div>
)

const AddStakeDialog = (props: AddStakeDialogProps) => (
export const AddStakeDialog = (props: AddStakeDialogProps) => (
<AlertDialog
open={props.open}
title="Stake"
Expand All @@ -92,7 +92,7 @@ export type NominationPoolsAddStakeDialogProps = Omit<AddStakeDialogProps, 'mess
export const NominationPoolsAddStakeDialog = (props: NominationPoolsAddStakeDialogProps) => (
<AddStakeDialog
{...props}
message="Increase your stake below. Talisman will automatically stake this in the same nomination pool for you."
message="Increase your stake below. Talisman will automatically stake this in the same nomination pool."
/>
)

Expand All @@ -109,7 +109,7 @@ export const SlpxAddStakeForm = (props: SlpxAddStakeFormProps) => <AddStakeForm
export const SlpxAddStakeDialog = (props: SlpxAddStakeDialogProps) => (
<AddStakeDialog
{...props}
message="Increase your stake below. Talisman will automatically stake this using Bifrost liquid staking for you."
message="Increase your stake below. Talisman will automatically stake this using Bifrost liquid staking."
/>
)

Expand All @@ -118,7 +118,7 @@ export type DappStakingAddStakeDialogProps = Omit<AddStakeDialogProps, 'message'
export const DappStakingAddStakeDialog = (props: DappStakingAddStakeDialogProps) => (
<AddStakeDialog
{...props}
message="Increase your stake below. Talisman will automatically stake this toward your chosen DApp for you."
message="Increase your stake below. Talisman will automatically stake this towards your chosen DApp."
/>
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ const AmountInput = (props: AmountInputProps) => {
Max
</Button>
}
width="10rem"
css={{ fontSize: '3rem' }}
/>
)
Expand Down
1 change: 0 additions & 1 deletion apps/portal/src/components/recipes/StakeForm/StakeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ const AmountInput = (props: AmountInputProps) => (
<TextInput
type="number"
inputMode="decimal"
width="10rem"
placeholder="0.00"
value={props.amount}
onChange={event => props.onChangeAmount(event.target.value)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import StakeTargetSelectorItem, {
PoolSelectorItem,
type StakeTargetSelectorItemProps,
} from './StakeTargetSelectorItem'
import { AlertDialog, Button, Text } from '@talismn/ui'
import { AlertDialog, Button, Text, Select } from '@talismn/ui'
import { ChevronLeft, ChevronRight } from '@talismn/web-icons'
import { motion } from 'framer-motion'
import React, { useState, type ReactElement, type ReactNode } from 'react'
Expand All @@ -16,6 +16,12 @@ export type StakeTargetSelectorDialogProps = {
onRequestDismiss: () => unknown
confirmButtonContent: ReactNode
onConfirm: () => unknown
sortMethods?: {
[key: string]: (
a: ReactElement<StakeTargetSelectorItemProps>,
b: ReactElement<StakeTargetSelectorItemProps>
) => number
}
children: ReactElement<StakeTargetSelectorItemProps> | Array<ReactElement<StakeTargetSelectorItemProps>>
}

Expand All @@ -24,8 +30,13 @@ const ITEMS_PER_PAGE = 9
const StakeTargetSelectorDialog = Object.assign(
(props: StakeTargetSelectorDialogProps) => {
const items = React.Children.toArray(props.children) as Array<ReactElement<StakeTargetSelectorItemProps>>
const [sortMethod, setSortMethod] = useState(
(props.sortMethods ? Object.keys(props.sortMethods)[0] : undefined) ?? 'Default'
)
const selectedItems = items.filter(item => item.props.selected)
const nonSelectedItems = items.filter(item => !item.props.selected)
const nonSelectedItems = items
.filter(item => !item.props.selected)
.sort((sortMethod === 'Default' ? undefined : props.sortMethods?.[sortMethod]) ?? (() => 0))
const highlightedItems = items.filter(item => item.props.highlighted)

const [page, setPage] = useState(0)
Expand Down Expand Up @@ -56,18 +67,34 @@ const StakeTargetSelectorDialog = Object.assign(
>
{selectedItems[0]}
</div>
<div css={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div
css={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: '1.6rem',
marginBottom: '0.6rem',
}}
>
<Text.Body as="h3" css={{ marginTop: '1.6rem', marginBottom: '0.6rem' }}>
{props.selectionLabel}
</Text.Body>
<div css={{ display: 'flex' }}>
<Button variant="noop" onClick={() => setPage(page => page - 1)} hidden={!hasPreviousPage}>
<ChevronLeft />
</Button>
<Button variant="noop" onClick={() => setPage(page => page + 1)} hidden={!hasNextPage}>
<ChevronRight />
</Button>
</div>
{props.sortMethods ? (
<Select
placeholder="Sort delegates"
renderSelected={value => `Sort by: ${value}`}
value={sortMethod}
onChangeValue={setSortMethod}
css={{ minWidth: '22rem' }}
>
{Object.keys(props.sortMethods).length === 0 ? (
<Select.Option headlineContent="Default" value="Default" />
) : null}
{Object.keys(props.sortMethods).map(option => (
<Select.Option key={option} headlineContent={option} value={option} />
))}
</Select>
) : null}
</div>
<motion.div
css={{
Expand Down Expand Up @@ -104,6 +131,20 @@ const StakeTargetSelectorDialog = Object.assign(
: []),
])}
</motion.div>
<div css={{ display: 'flex', justifyContent: 'end', marginTop: '0.6rem' }}>
<Button variant="noop" onClick={() => setPage(page => page - 1)} hidden={!hasPreviousPage}>
<div css={{ display: 'flex', alignItems: 'center', userSelect: 'none' }}>
<ChevronLeft />
<div>Previous Page</div>
</div>
</Button>
<Button variant="noop" onClick={() => setPage(page => page + 1)} hidden={!hasNextPage}>
<div css={{ display: 'flex', alignItems: 'center', userSelect: 'none' }}>
<div>Next Page</div>
<ChevronRight />
</div>
</Button>
</div>
</div>
}
dismissButton={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ export type StakeTargetSelectorItemProps = {
logo?: string
detailUrl?: string
balance: string
balancePlanck?: bigint
balanceDescription: string
estimatedReturn?: number | bigint
estimatedApr?: string
estimatedAprDescription?: string
talismanRecommended: boolean
talismanRecommendedDescription: string
rating?: 0 | 1 | 2 | 3
Expand Down Expand Up @@ -93,29 +97,19 @@ const StakeTargetSelectorItem = (props: StakeTargetSelectorItemProps) => {
css={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '0.6rem' }}
>
<div css={{ display: 'flex', alignItems: 'center' }}>
{/* <Tooltip content="Talisman pool rating">
<div>
{Array(3)
.fill(undefined)
.map((_, index) => (
<Star size="1.4rem" fill={index < props.rating ? 'currentColor' : 'none'} />
))}
</div>
</Tooltip> */}
<Tooltip content={props.countDescription}>
<div css={{ display: 'flex', alignItems: 'center' }}>
<Text.Body
alpha={alpha}
css={{
// marginLeft: '0.8rem',
marginRight: '0.4rem',
}}
>
{props.count}
</Text.Body>
<User size="1.4rem" />
</div>
</Tooltip>
<div css={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
<Tooltip content={props.countDescription}>
{props.count ? (
<div css={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
<Text.Body alpha={alpha}>{props.count}</Text.Body>
<User size="1.4rem" />
</div>
) : null}
</Tooltip>
<Tooltip content={props.estimatedAprDescription}>
{props.estimatedApr ? <div>{props.estimatedApr}</div> : null}
</Tooltip>
</div>
</div>
{props.talismanRecommended && (
<Tooltip content={props.talismanRecommendedDescription}>
Expand Down
4 changes: 4 additions & 0 deletions apps/portal/src/components/widgets/staking/StakeProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ import DappStakingProviders from './dappStaking/StakeProviders'
import LidoStakeProviders from './lido/StakeProviders'
import SlpxStakeProviders from './slpx/StakeProviders'
import NominationPoolsStakeProviders from './substrate/StakeProviders'
import SubtensorStakeProviders from './subtensor/StakeProviders'

const StakeProviders = () => {
return (
<StakeProviderList>
<ErrorBoundary orientation="horizontal">
<NominationPoolsStakeProviders />
</ErrorBoundary>
<ErrorBoundary orientation="horizontal">
<SubtensorStakeProviders />
</ErrorBoundary>
<ErrorBoundary orientation="horizontal">
<DappStakingProviders />
</ErrorBoundary>
Expand Down
6 changes: 6 additions & 0 deletions apps/portal/src/components/widgets/staking/Stakes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import LidoStakes from './lido/Stakes'
import SlpxStakes from './slpx/Stakes'
import PoolStakes from './substrate/PoolStakes'
import ValidatorStakes from './substrate/ValidatorStakes'
import SubtensorStakes from './subtensor/Stakes'
import { Button, HiddenDetails, Text } from '@talismn/ui'
import { StakePosition, StakePositionList } from '@talismn/ui-recipes'
import { useState } from 'react'
Expand Down Expand Up @@ -99,6 +100,11 @@ const Stakes = (props: { hideHeader?: boolean }) => {
</Fragment>
)
})}
<ErrorBoundary orientation="horizontal">
<SuspenseSkeleton>
<SubtensorStakes setShouldRenderLoadingSkeleton={setShouldRenderLoadingSkeleton} />
</SuspenseSkeleton>
</ErrorBoundary>
<ErrorBoundary orientation="horizontal">
<SuspenseSkeleton>
<DappStakes setShouldRenderLoadingSkeleton={setShouldRenderLoadingSkeleton} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import type { Account } from '../../../../domains/accounts'
import { useExtrinsicInBlockOrErrorEffect } from '../../../../domains/common'
import { useAddStakeForm } from '../../../../domains/staking/subtensor/hooks/forms'
import { useDelegate } from '../../../../domains/staking/subtensor/hooks/useDelegate'
import { type Stake } from '../../../../domains/staking/subtensor/hooks/useStake'
import { AddStakeDialog as _AddStakeDialog } from '../../../recipes/AddStakeDialog'
import DelegatePickerDialog from './DelegatePickerDialog'
import { CircularProgressIndicator } from '@talismn/ui'
import { useState } from 'react'

type SubtensorAddStakeDialogProps = {
account: Account
stake: Stake
delegate: string
onRequestDismiss: () => void
}

const SubtensorAddStakeDialog = (props: SubtensorAddStakeDialogProps) => {
const { input, setInput, amount, transferable, resulting, extrinsic, ready, error } = useAddStakeForm(
props.account,
props.stake,
props.delegate
)

useExtrinsicInBlockOrErrorEffect(() => props.onRequestDismiss(), extrinsic)

const delegateName = useDelegate(props.delegate)?.name

return (
<_AddStakeDialog
message={
delegateName
? `Increase your stake below. Talisman will automatically stake this towards the ${delegateName} delegate.`
: `Increase your stake below. Talisman will automatically stake this towards your chosen delegate.`
}
confirmState={extrinsic.state === 'loading' ? 'pending' : !ready ? 'disabled' : undefined}
isError={error !== undefined}
availableToStake={transferable.decimalAmount.toLocaleString()}
amount={input}
onChangeAmount={setInput}
onRequestMaxAmount={() => setInput(transferable.decimalAmount.toString())}
fiatAmount={amount.localizedFiatAmount ?? ''}
newAmount={resulting.decimalAmount?.toLocaleString() ?? <CircularProgressIndicator size="1em" />}
newFiatAmount={resulting.localizedFiatAmount ?? <CircularProgressIndicator size="1em" />}
onConfirm={() => {
extrinsic.signAndSend(props.account.address).then(() => props.onRequestDismiss())
}}
inputSupportingText={error?.message}
onDismiss={props.onRequestDismiss}
/>
)
}

type MultiDelegateAddStakeDialogProps = {
account: Account
stake: Stake
onRequestDismiss: () => void
}

const MultiDelegateAddStakeDialog = (props: MultiDelegateAddStakeDialogProps) => {
const [delegate, setDelegate] = useState<string>()

return delegate === undefined ? (
<DelegatePickerDialog
title="Select a delegate to increase stake"
account={props.account}
onSelect={setDelegate}
onRequestDismiss={props.onRequestDismiss}
/>
) : (
<SubtensorAddStakeDialog
account={props.account}
stake={props.stake}
delegate={delegate}
onRequestDismiss={props.onRequestDismiss}
/>
)
}

type AddStakeDialogProps = {
account: Account
stake: Stake
onRequestDismiss: () => void
}

const AddStakeDialog = (props: AddStakeDialogProps) => {
if (props.stake.stakes?.length === 1)
return (
<SubtensorAddStakeDialog
account={props.account}
stake={props.stake}
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
delegate={props.stake.stakes.at(0)?.hotkey!}
onRequestDismiss={props.onRequestDismiss}
/>
)

if ((props.stake.stakes?.length ?? 0) > 1)
return (
<MultiDelegateAddStakeDialog
account={props.account}
stake={props.stake}
onRequestDismiss={props.onRequestDismiss}
/>
)

return null
}

export default AddStakeDialog
Loading

0 comments on commit 8aca9e0

Please sign in to comment.