Skip to content

Commit

Permalink
feat(suite): add support for Solana staking rewards
Browse files Browse the repository at this point in the history
  • Loading branch information
dev-pvl authored and tomasklim committed Feb 7, 2025
1 parent 72c506f commit 52a34dd
Show file tree
Hide file tree
Showing 10 changed files with 389 additions and 5 deletions.
1 change: 1 addition & 0 deletions packages/suite-desktop-core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const allowedDomains = [
'eth-api-b2c-stage.everstake.one', // staking endpoint for Holesky testnet, works only with VPN
'eth-api-b2c.everstake.one', // staking endpoint for Ethereum mainnet
'dashboard-api.everstake.one', // staking enpoint for Solana
'stake-sync-api.everstake.one', // staking rewards enpoint for Solana
];

export const cspRules = [
Expand Down
87 changes: 87 additions & 0 deletions packages/suite/src/hooks/wallet/useSolanaRewards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import {
EverstakeRewardsEndpointType,
StakeAccountRewards,
StakeRootState,
fetchEverstakeRewards,
selectStakingRewards,
} from '@suite-common/wallet-core';
import { useDebounce } from '@trezor/react-utils';

import { Account } from 'src/types/wallet';

const PAGE_SIZE_DEFAULT = 10;

export const useSolanaRewards = (account: Account) => {
const { data, isLoading } =
useSelector((state: StakeRootState) => selectStakingRewards(state, account.symbol)) || {};

const { rewards } = data ?? {};
const selectedAccountRewards = rewards?.[account.descriptor];

const dispatch = useDispatch();
const debounce = useDebounce();

const itemsPerPage = PAGE_SIZE_DEFAULT;
const startPage = 1;

const [currentPage, setSelectedPage] = useState(startPage);
const [slicedRewards, setSlicedRewards] = useState<StakeAccountRewards[]>([]);

const startIndex = (currentPage - 1) * itemsPerPage;
const stopIndex = startIndex + itemsPerPage;

const fetchRewards = useCallback(
async ({ symbol, descriptor }: Account) => {
const controller = new AbortController();
await debounce(() => {
if (symbol !== 'sol') return;
dispatch(
fetchEverstakeRewards({
symbol,
endpointType: EverstakeRewardsEndpointType.GetRewards,
address: descriptor,
signal: controller.signal,
}),
);
});

return () => controller.abort();
},
[dispatch, debounce],
);

useEffect(() => {
fetchRewards(account);
}, [account, fetchRewards]);

useEffect(() => {
if (selectedAccountRewards) {
const slicedRewards = selectedAccountRewards?.slice(startIndex, stopIndex);
setSlicedRewards(slicedRewards);
}
}, [currentPage, selectedAccountRewards, startIndex, stopIndex]);

useEffect(() => {
// reset page on account change
setSelectedPage(startPage);
}, [account.descriptor, account.symbol, startPage]);

const totalItems = selectedAccountRewards?.length ?? 0;
const showPagination = totalItems > itemsPerPage;
const isLastPage = stopIndex >= totalItems;

return {
slicedRewards,
isLoading,
currentPage,
setSelectedPage,
totalItems,
itemsPerPage,
showPagination,
isLastPage,
fetchRewards,
};
};
17 changes: 17 additions & 0 deletions packages/suite/src/support/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5214,6 +5214,14 @@ export default defineMessages({
id: 'TR_MY_PORTFOLIO',
defaultMessage: 'Portfolio',
},
TR_REWARD: {
id: 'TR_REWARD',
defaultMessage: 'Reward',
},
TR_REWARDS: {
id: 'TR_REWARDS',
defaultMessage: 'Rewards',
},
TR_ALL_TRANSACTIONS: {
id: 'TR_ALL_TRANSACTIONS',
defaultMessage: 'Transactions',
Expand Down Expand Up @@ -8697,6 +8705,15 @@ export default defineMessages({
id: 'TR_STAKE_RESTAKED_BADGE',
defaultMessage: 'Restaked',
},
TR_STAKE_REWARDS_BAGE: {
id: 'TR_STAKE_REWARDS_BAGE',
defaultMessage: 'Epoch number {count}',
},
TR_STAKE_REWARDS_TOOLTIP: {
id: 'TR_STAKE_REWARDS_TOOLTIP',
defaultMessage:
'An epoch in Solana is approximately {count, plural, one {# day} other {# days}} long.',
},
TR_STAKE_ETH_CARD_TITLE: {
id: 'TR_STAKE_ETH_CARD_TITLE',
defaultMessage: 'The easiest way to earn {symbol}',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { DashboardSection } from 'src/components/dashboard';
import { Translation } from 'src/components/suite';

import { StakingDashboard } from '../StakingDashboard/StakingDashboard';
import { RewardsList } from './components/RewardsList';
import { ApyCard } from '../StakingDashboard/components/ApyCard';
import { ClaimCard } from '../StakingDashboard/components/ClaimCard';
import { PayoutCard } from '../StakingDashboard/components/PayoutCard';
Expand Down Expand Up @@ -65,6 +66,7 @@ export const SolStakingDashboard = ({ selectedAccount }: SolStakingDashboardProp
/>
</Column>
</DashboardSection>
<RewardsList account={account} />
</Column>
}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import React, { useRef } from 'react';

import { SOLANA_EPOCH_DAYS } from '@suite-common/wallet-constants';
import { formatNetworkAmount } from '@suite-common/wallet-utils';
import {
Badge,
Card,
Column,
Icon,
IconButton,
Row,
SkeletonStack,
Text,
Tooltip,
} from '@trezor/components';
import { spacings } from '@trezor/theme';

import { DashboardSection } from 'src/components/dashboard';
import {
CoinBalance,
FiatValue,
FormattedDate,
HiddenPlaceholder,
Translation,
} from 'src/components/suite';
import { Pagination } from 'src/components/wallet';
import { useSolanaRewards } from 'src/hooks/wallet/useSolanaRewards';
import { Account } from 'src/types/wallet';
import SkeletonTransactionItem from 'src/views/wallet/transactions/TransactionList/SkeletonTransactionItem';
import { ColDate } from 'src/views/wallet/transactions/TransactionList/TransactionsGroup/CommonComponents';

import { RewardsEmpty } from './RewardsEmpty';

interface RewardsListProps {
account: Account;
}

export const RewardsList = ({ account }: RewardsListProps) => {
const sectionRef = useRef<HTMLDivElement>(null);

const {
slicedRewards,
isLoading,
currentPage,
setSelectedPage,
totalItems,
itemsPerPage,
showPagination,
isLastPage,
fetchRewards,
} = useSolanaRewards(account);

const isSolanaMainnet = account.symbol === 'sol';

const onPageSelected = (page: number) => {
setSelectedPage(page);
if (sectionRef.current) {
sectionRef.current.scrollIntoView();
}
};

return (
<DashboardSection
ref={sectionRef}
heading={<Translation id="TR_REWARDS" />}
data-testid="@wallet/accounts/rewards-list"
>
{isLoading ? (
<SkeletonStack $col $childMargin="0px 0px 16px 0px">
<SkeletonTransactionItem />
<SkeletonTransactionItem />
<SkeletonTransactionItem />
</SkeletonStack>
) : (
<>
{slicedRewards?.map(reward => (
<React.Fragment key={reward.epoch}>
<Row>
<ColDate>
<FormattedDate
value={reward?.time ?? undefined}
day="numeric"
month="long"
year="numeric"
/>
</ColDate>
</Row>
<Card>
<Row
justifyContent="space-between"
margin={{ horizontal: spacings.xs, bottom: spacings.xs }}
>
<Row gap={spacings.xs}>
<Icon name="arrowLineDown" variant="tertiary" />
<Column>
<Text typographyStyle="body" variant="tertiary">
<Translation id="TR_REWARD" />
</Text>
<Tooltip
maxWidth={250}
content={
<Translation
id="TR_STAKE_REWARDS_TOOLTIP"
values={{ count: SOLANA_EPOCH_DAYS }}
/>
}
>
<Badge size="small">
<Row gap={spacings.xxs} alignItems="center">
<Translation
id="TR_STAKE_REWARDS_BAGE"
values={{ count: reward.epoch }}
/>
<Icon name="info" size="small" />
</Row>
</Badge>
</Tooltip>
</Column>
</Row>
{reward?.amount && (
<Column alignItems="end">
<HiddenPlaceholder>
<CoinBalance
value={formatNetworkAmount(
reward?.amount,
account.symbol,
)}
symbol={account.symbol}
/>
</HiddenPlaceholder>
<HiddenPlaceholder>
<Text typographyStyle="hint" variant="tertiary">
<FiatValue
amount={formatNetworkAmount(
reward?.amount,
account.symbol,
)}
symbol={account.symbol}
/>
</Text>
</HiddenPlaceholder>
</Column>
)}
</Row>
</Card>
</React.Fragment>
))}
</>
)}

{showPagination && (
<Pagination
hasPages={true}
currentPage={currentPage}
isLastPage={isLastPage}
perPage={itemsPerPage}
totalItems={totalItems}
onPageSelected={onPageSelected}
/>
)}
</DashboardSection>
);
};
3 changes: 3 additions & 0 deletions suite-common/wallet-core/src/stake/stakeConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ export const EVERSTAKE_ENDPOINT_PREFIX: Record<
sol: 'https://dashboard-api.everstake.one',
dsol: 'https://dashboard-api.everstake.one',
};

export const EVERSTAKE_REWARDS_SOLANA_ENPOINT =
'https://stake-sync-api.everstake.one/solana/rewards';
Loading

0 comments on commit 52a34dd

Please sign in to comment.