Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/2288.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Show runtime account delegations
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { FC, ReactNode } from 'react'
import { Typography } from '@oasisprotocol/ui-library/src/components/typography'
import StackedBarChartIcon from '@mui/icons-material/StackedBarChart'
import { COLORS } from '../../../styles/theme/colors'

type AccountCardEmptyStateProps = {
children?: ReactNode
label: string
}

export const AccountCardEmptyState: FC<AccountCardEmptyStateProps> = ({ children, label }) => {
return (
<div className="flex flex-col justify-center items-center gap-2 text-center pt-1 pb-2">
<StackedBarChartIcon sx={{ color: COLORS.grayMedium, fontSize: 40, opacity: 0.5 }} />
<Typography textColor="muted" className="font-semibold text-center">
{label}
</Typography>
{children}
</div>
)
}
155 changes: 155 additions & 0 deletions src/app/pages/RuntimeAccountDetailsPage/Staking.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { Card, CardContent, CardHeader, CardTitle } from '@oasisprotocol/ui-library/src/components/cards'
import Link from '@mui/material/Link'
import { Skeleton } from '@oasisprotocol/ui-library/src/components/ui/skeleton'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@oasisprotocol/ui-library/src/components/tabs'
import {
Runtime,
RuntimeAccount,
useGetRuntimeAccountsAddressDebondingDelegations,
useGetRuntimeAccountsAddressDelegations,
} from '../../../oasis-nexus/api'
import { useRequiredScopeParam } from '../../../app/hooks/useScopeParam'
import { AppErrors } from '../../../types/errors'
import { NUMBER_OF_ITEMS_ON_DASHBOARD as PAGE_SIZE } from '../../../config'
import { useSearchParamsPagination } from '../../components/Table/useSearchParamsPagination'
import { Delegations } from '../../components/Delegations'
import { dapps } from '../../utils/externalLinks'
import { t } from 'i18next'
import { AccountCardEmptyState } from './AccountCardEmptyState'
import { Typography } from '@oasisprotocol/ui-library/src/components/typography'

type StakingProps = {
account: RuntimeAccount | undefined
isLoading: boolean
}

export const Staking: FC<StakingProps> = ({ account, isLoading }) => {
const { t } = useTranslation()

return (
<Card variant="layout">
<CardHeader>
<CardTitle>
<Typography variant="h3">{t('stakingOverview')}</Typography>
</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="staked" aria-label={t('validator.delegations')}>
<TabsList variant="layout" className="md:max-w-[400px] rounded-b-md mb-2">
<TabsTrigger value="staked">{t('common.staked')}</TabsTrigger>
<TabsTrigger value="debonding">{t('common.debonding')}</TabsTrigger>
</TabsList>
{isLoading && <Skeleton className="h-[200px] mt-8" />}
{!isLoading && account && (
<>
<TabsContent value="staked" className="min-h-28">
<ActiveDelegations address={account?.address} />
</TabsContent>
<TabsContent value="debonding" className="min-h-28">
<DebondingDelegations address={account?.address} />
</TabsContent>
</>
)}
</Tabs>
</CardContent>
</Card>
)
}

type DelegationCardProps = {
address: string
}

const ActiveDelegations: FC<DelegationCardProps> = ({ address }) => {
const pagination = useSearchParamsPagination('activeDelegations')
const offset = (pagination.selectedPage - 1) * PAGE_SIZE
const scope = useRequiredScopeParam()
const { network } = scope
const delegationsQuery = useGetRuntimeAccountsAddressDelegations(network, scope.layer as Runtime, address, {
limit: PAGE_SIZE,
offset,
})
const { isLoading, isFetched, data } = delegationsQuery
if (isFetched && offset && !delegationsQuery.data?.data?.delegations?.length) {
throw AppErrors.PageDoesNotExist
}

if (isFetched && !delegationsQuery.data?.data.delegations.length) {
return (
<AccountCardEmptyState label={t('account.notStaking')}>
<Link href={dapps.stake} rel="noopener noreferrer" target="_blank">
{t('account.startStaking')}
</Link>
</AccountCardEmptyState>
)
}

return (
<>
{isFetched && (
<Delegations
delegations={delegationsQuery.data?.data.delegations}
isLoading={isLoading}
limit={PAGE_SIZE}
linkType="validator"
pagination={{
className: 'mt-2',
selectedPage: pagination.selectedPage,
linkToPage: pagination.linkToPage,
totalCount: data?.data.total_count,
isTotalCountClipped: data?.data.is_total_count_clipped,
rowsPerPage: PAGE_SIZE,
}}
/>
)}
</>
)
}

const DebondingDelegations: FC<DelegationCardProps> = ({ address }) => {
const pagination = useSearchParamsPagination('debondingDelegations')
const offset = (pagination.selectedPage - 1) * PAGE_SIZE
const scope = useRequiredScopeParam()
const { network } = scope
const delegationsQuery = useGetRuntimeAccountsAddressDebondingDelegations(
network,
scope.layer as Runtime,
address,
{
limit: PAGE_SIZE,
offset,
},
)
const { isLoading, isFetched, data } = delegationsQuery
if (isFetched && offset && !delegationsQuery.data?.data?.debonding_delegations?.length) {
throw AppErrors.PageDoesNotExist
}

if (isFetched && !delegationsQuery.data?.data.debonding_delegations.length) {
return <AccountCardEmptyState label={t('account.notDebonding')} />
}

return (
<>
{isFetched && (
<Delegations
debonding
delegations={delegationsQuery.data?.data.debonding_delegations}
isLoading={isLoading}
limit={PAGE_SIZE}
linkType="validator"
pagination={{
className: 'mt-2',
selectedPage: pagination.selectedPage,
linkToPage: pagination.linkToPage,
totalCount: data?.data.total_count,
isTotalCountClipped: data?.data.is_total_count_clipped,
rowsPerPage: PAGE_SIZE,
}}
/>
)}
</>
)
}
2 changes: 2 additions & 0 deletions src/app/pages/RuntimeAccountDetailsPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
transfersContainerId,
} from '../../utils/tabAnchors'
import { ParamSetterFunction } from '../../hooks/useTypedSearchParam'
import { Staking } from './Staking'

export type RuntimeAccountDetailsContext = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Total delegated/undelegated balance are not shown. Is that planned as a separate PR, or did some commits get lost?
image

Copy link
Contributor Author

@buberdds buberdds Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scope: RuntimeScope
Expand Down Expand Up @@ -85,6 +86,7 @@ export const RuntimeAccountDetailsPage: FC = () => {
tokenPrices={tokenPrices}
/>
<DappBanner scope={scope} ethOrOasisAddress={address} />
<Staking account={account} isLoading={isLoading} />
<RouterTabs
tabs={[
{ label: t('common.transactions'), to: txLink },
Expand Down
1 change: 1 addition & 0 deletions src/app/utils/externalLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const faucets = {

export const dapps = {
wRose: 'https://rose.oasis.io/wrap',
stake: 'https://rose.oasis.io/stake',
sourcifyRoot: 'https://sourcify.dev/',
abiPlayground: 'https://abi-playground.oasis.io/',
}
Expand Down
1 change: 1 addition & 0 deletions src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,7 @@
"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>.",
"suggestSearchForConsensus": "Use search to find what you are looking for, i.e. <OptionalBreak><BlockLink>Block</BlockLink>, <TransactionLink>Transaction</TransactionLink>, <AccountLink>Address</AccountLink></OptionalBreak>."
},
"stakingOverview": "Staking Overview",
"tableSearch": {
"error": {
"tooShort": "Please enter at least 3 characters to perform a search.",
Expand Down
60 changes: 60 additions & 0 deletions src/oasis-nexus/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1647,6 +1647,66 @@ export const useGetRuntimeRoflAppsIdInstancesRakTransactions: typeof generated.u
})
}

export const useGetRuntimeAccountsAddressDelegations: typeof generated.useGetRuntimeAccountsAddressDelegations =
(network, layer, address, params?, options?) => {
const ticker = getTokensForScope({ network, layer })[0].ticker
return generated.useGetRuntimeAccountsAddressDelegations(network, layer, address, params, {
...options,
request: {
...options?.request,
transformResponse: [
...arrayify(axios.defaults.transformResponse),
(data: generated.DelegationList, headers, status) => {
if (status !== 200) return data
return {
...data,
delegations: data.delegations.map(delegation => {
return {
...delegation,
amount: fromBaseUnits(delegation.amount, consensusDecimals),
layer: 'consensus',
network,
ticker,
}
}),
}
},
...arrayify(options?.request?.transformResponse),
],
},
})
}

export const useGetRuntimeAccountsAddressDebondingDelegations: typeof generated.useGetRuntimeAccountsAddressDebondingDelegations =
(network, layer, address, params?, options?) => {
const ticker = getTokensForScope({ network, layer })[0].ticker
return generated.useGetRuntimeAccountsAddressDebondingDelegations(network, layer, address, params, {
...options,
request: {
...options?.request,
transformResponse: [
...arrayify(axios.defaults.transformResponse),
(data: generated.DebondingDelegationList, headers, status) => {
if (status !== 200) return data
return {
...data,
debonding_delegations: data.debonding_delegations.map(delegation => {
return {
...delegation,
amount: fromBaseUnits(delegation.amount, consensusDecimals),
layer: 'consensus',
network,
ticker,
}
}),
}
},
...arrayify(options?.request?.transformResponse),
],
},
})
}

function transformRuntimeTransactionList(
data: generated.RuntimeTransactionList,
network: Network,
Expand Down
Loading
Loading