Skip to content

Commit 24acef5

Browse files
authored
Merge pull request #2288 from oasisprotocol/mz/runtimeAccStaking
Show runtime account delegations
2 parents e004c3e + 06f5709 commit 24acef5

File tree

8 files changed

+426
-0
lines changed

8 files changed

+426
-0
lines changed

.changelog/2288.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Show runtime account delegations
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { FC, ReactNode } from 'react'
2+
import { Typography } from '@oasisprotocol/ui-library/src/components/typography'
3+
import StackedBarChartIcon from '@mui/icons-material/StackedBarChart'
4+
import { COLORS } from '../../../styles/theme/colors'
5+
6+
type AccountCardEmptyStateProps = {
7+
children?: ReactNode
8+
label: string
9+
}
10+
11+
export const AccountCardEmptyState: FC<AccountCardEmptyStateProps> = ({ children, label }) => {
12+
return (
13+
<div className="flex flex-col justify-center items-center gap-2 text-center pt-1 pb-2">
14+
<StackedBarChartIcon sx={{ color: COLORS.grayMedium, fontSize: 40, opacity: 0.5 }} />
15+
<Typography textColor="muted" className="font-semibold text-center">
16+
{label}
17+
</Typography>
18+
{children}
19+
</div>
20+
)
21+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { FC } from 'react'
2+
import { useTranslation } from 'react-i18next'
3+
import { Card, CardContent, CardHeader, CardTitle } from '@oasisprotocol/ui-library/src/components/cards'
4+
import Link from '@mui/material/Link'
5+
import { Skeleton } from '@oasisprotocol/ui-library/src/components/ui/skeleton'
6+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@oasisprotocol/ui-library/src/components/tabs'
7+
import {
8+
Runtime,
9+
RuntimeAccount,
10+
useGetRuntimeAccountsAddressDebondingDelegations,
11+
useGetRuntimeAccountsAddressDelegations,
12+
} from '../../../oasis-nexus/api'
13+
import { useRequiredScopeParam } from '../../../app/hooks/useScopeParam'
14+
import { AppErrors } from '../../../types/errors'
15+
import { NUMBER_OF_ITEMS_ON_DASHBOARD as PAGE_SIZE } from '../../../config'
16+
import { useSearchParamsPagination } from '../../components/Table/useSearchParamsPagination'
17+
import { Delegations } from '../../components/Delegations'
18+
import { dapps } from '../../utils/externalLinks'
19+
import { t } from 'i18next'
20+
import { AccountCardEmptyState } from './AccountCardEmptyState'
21+
import { Typography } from '@oasisprotocol/ui-library/src/components/typography'
22+
23+
type StakingProps = {
24+
account: RuntimeAccount | undefined
25+
isLoading: boolean
26+
}
27+
28+
export const Staking: FC<StakingProps> = ({ account, isLoading }) => {
29+
const { t } = useTranslation()
30+
31+
return (
32+
<Card variant="layout">
33+
<CardHeader>
34+
<CardTitle>
35+
<Typography variant="h3">{t('stakingOverview')}</Typography>
36+
</CardTitle>
37+
</CardHeader>
38+
<CardContent>
39+
<Tabs defaultValue="staked" aria-label={t('validator.delegations')}>
40+
<TabsList variant="layout" className="md:max-w-[400px] rounded-b-md mb-2">
41+
<TabsTrigger value="staked">{t('common.staked')}</TabsTrigger>
42+
<TabsTrigger value="debonding">{t('common.debonding')}</TabsTrigger>
43+
</TabsList>
44+
{isLoading && <Skeleton className="h-[200px] mt-8" />}
45+
{!isLoading && account && (
46+
<>
47+
<TabsContent value="staked" className="min-h-28">
48+
<ActiveDelegations address={account?.address} />
49+
</TabsContent>
50+
<TabsContent value="debonding" className="min-h-28">
51+
<DebondingDelegations address={account?.address} />
52+
</TabsContent>
53+
</>
54+
)}
55+
</Tabs>
56+
</CardContent>
57+
</Card>
58+
)
59+
}
60+
61+
type DelegationCardProps = {
62+
address: string
63+
}
64+
65+
const ActiveDelegations: FC<DelegationCardProps> = ({ address }) => {
66+
const pagination = useSearchParamsPagination('activeDelegations')
67+
const offset = (pagination.selectedPage - 1) * PAGE_SIZE
68+
const scope = useRequiredScopeParam()
69+
const { network } = scope
70+
const delegationsQuery = useGetRuntimeAccountsAddressDelegations(network, scope.layer as Runtime, address, {
71+
limit: PAGE_SIZE,
72+
offset,
73+
})
74+
const { isLoading, isFetched, data } = delegationsQuery
75+
if (isFetched && offset && !delegationsQuery.data?.data?.delegations?.length) {
76+
throw AppErrors.PageDoesNotExist
77+
}
78+
79+
if (isFetched && !delegationsQuery.data?.data.delegations.length) {
80+
return (
81+
<AccountCardEmptyState label={t('account.notStaking')}>
82+
<Link href={dapps.stake} rel="noopener noreferrer" target="_blank">
83+
{t('account.startStaking')}
84+
</Link>
85+
</AccountCardEmptyState>
86+
)
87+
}
88+
89+
return (
90+
<>
91+
{isFetched && (
92+
<Delegations
93+
delegations={delegationsQuery.data?.data.delegations}
94+
isLoading={isLoading}
95+
limit={PAGE_SIZE}
96+
linkType="validator"
97+
pagination={{
98+
className: 'mt-2',
99+
selectedPage: pagination.selectedPage,
100+
linkToPage: pagination.linkToPage,
101+
totalCount: data?.data.total_count,
102+
isTotalCountClipped: data?.data.is_total_count_clipped,
103+
rowsPerPage: PAGE_SIZE,
104+
}}
105+
/>
106+
)}
107+
</>
108+
)
109+
}
110+
111+
const DebondingDelegations: FC<DelegationCardProps> = ({ address }) => {
112+
const pagination = useSearchParamsPagination('debondingDelegations')
113+
const offset = (pagination.selectedPage - 1) * PAGE_SIZE
114+
const scope = useRequiredScopeParam()
115+
const { network } = scope
116+
const delegationsQuery = useGetRuntimeAccountsAddressDebondingDelegations(
117+
network,
118+
scope.layer as Runtime,
119+
address,
120+
{
121+
limit: PAGE_SIZE,
122+
offset,
123+
},
124+
)
125+
const { isLoading, isFetched, data } = delegationsQuery
126+
if (isFetched && offset && !delegationsQuery.data?.data?.debonding_delegations?.length) {
127+
throw AppErrors.PageDoesNotExist
128+
}
129+
130+
if (isFetched && !delegationsQuery.data?.data.debonding_delegations.length) {
131+
return <AccountCardEmptyState label={t('account.notDebonding')} />
132+
}
133+
134+
return (
135+
<>
136+
{isFetched && (
137+
<Delegations
138+
debonding
139+
delegations={delegationsQuery.data?.data.debonding_delegations}
140+
isLoading={isLoading}
141+
limit={PAGE_SIZE}
142+
linkType="validator"
143+
pagination={{
144+
className: 'mt-2',
145+
selectedPage: pagination.selectedPage,
146+
linkToPage: pagination.linkToPage,
147+
totalCount: data?.data.total_count,
148+
isTotalCountClipped: data?.data.is_total_count_clipped,
149+
rowsPerPage: PAGE_SIZE,
150+
}}
151+
/>
152+
)}
153+
</>
154+
)
155+
}

src/app/pages/RuntimeAccountDetailsPage/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
transfersContainerId,
2828
} from '../../utils/tabAnchors'
2929
import { ParamSetterFunction } from '../../hooks/useTypedSearchParam'
30+
import { Staking } from './Staking'
3031

3132
export type RuntimeAccountDetailsContext = {
3233
scope: RuntimeScope
@@ -85,6 +86,7 @@ export const RuntimeAccountDetailsPage: FC = () => {
8586
tokenPrices={tokenPrices}
8687
/>
8788
<DappBanner scope={scope} ethOrOasisAddress={address} />
89+
<Staking account={account} isLoading={isLoading} />
8890
<RouterTabs
8991
tabs={[
9092
{ label: t('common.transactions'), to: txLink },

src/app/utils/externalLinks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export const faucets = {
5656

5757
export const dapps = {
5858
wRose: 'https://rose.oasis.io/wrap',
59+
stake: 'https://rose.oasis.io/stake',
5960
sourcifyRoot: 'https://sourcify.dev/',
6061
abiPlayground: 'https://abi-playground.oasis.io/',
6162
}

src/locales/en/translation.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,7 @@
856856
"suggestSearchForRuntime": "Use search to find what you are looking for, i.e. <OptionalBreak><BlockLink>Block</BlockLink>, <TransactionLink>Transaction</TransactionLink>, <AccountLink>Address</AccountLink>, <TokenLink>Token</TokenLink></OptionalBreak>.",
857857
"suggestSearchForConsensus": "Use search to find what you are looking for, i.e. <OptionalBreak><BlockLink>Block</BlockLink>, <TransactionLink>Transaction</TransactionLink>, <AccountLink>Address</AccountLink></OptionalBreak>."
858858
},
859+
"stakingOverview": "Staking Overview",
859860
"tableSearch": {
860861
"error": {
861862
"tooShort": "Please enter at least 3 characters to perform a search.",

src/oasis-nexus/api.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1647,6 +1647,66 @@ export const useGetRuntimeRoflAppsIdInstancesRakTransactions: typeof generated.u
16471647
})
16481648
}
16491649

1650+
export const useGetRuntimeAccountsAddressDelegations: typeof generated.useGetRuntimeAccountsAddressDelegations =
1651+
(network, layer, address, params?, options?) => {
1652+
const ticker = getTokensForScope({ network, layer })[0].ticker
1653+
return generated.useGetRuntimeAccountsAddressDelegations(network, layer, address, params, {
1654+
...options,
1655+
request: {
1656+
...options?.request,
1657+
transformResponse: [
1658+
...arrayify(axios.defaults.transformResponse),
1659+
(data: generated.DelegationList, headers, status) => {
1660+
if (status !== 200) return data
1661+
return {
1662+
...data,
1663+
delegations: data.delegations.map(delegation => {
1664+
return {
1665+
...delegation,
1666+
amount: fromBaseUnits(delegation.amount, consensusDecimals),
1667+
layer: 'consensus',
1668+
network,
1669+
ticker,
1670+
}
1671+
}),
1672+
}
1673+
},
1674+
...arrayify(options?.request?.transformResponse),
1675+
],
1676+
},
1677+
})
1678+
}
1679+
1680+
export const useGetRuntimeAccountsAddressDebondingDelegations: typeof generated.useGetRuntimeAccountsAddressDebondingDelegations =
1681+
(network, layer, address, params?, options?) => {
1682+
const ticker = getTokensForScope({ network, layer })[0].ticker
1683+
return generated.useGetRuntimeAccountsAddressDebondingDelegations(network, layer, address, params, {
1684+
...options,
1685+
request: {
1686+
...options?.request,
1687+
transformResponse: [
1688+
...arrayify(axios.defaults.transformResponse),
1689+
(data: generated.DebondingDelegationList, headers, status) => {
1690+
if (status !== 200) return data
1691+
return {
1692+
...data,
1693+
debonding_delegations: data.debonding_delegations.map(delegation => {
1694+
return {
1695+
...delegation,
1696+
amount: fromBaseUnits(delegation.amount, consensusDecimals),
1697+
layer: 'consensus',
1698+
network,
1699+
ticker,
1700+
}
1701+
}),
1702+
}
1703+
},
1704+
...arrayify(options?.request?.transformResponse),
1705+
],
1706+
},
1707+
})
1708+
}
1709+
16501710
function transformRuntimeTransactionList(
16511711
data: generated.RuntimeTransactionList,
16521712
network: Network,

0 commit comments

Comments
 (0)