Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(staking): [LW-8751] show pool rewards in activity #624

Merged
merged 35 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
78252f4
feat(staking): include pool rewards in wallet activity
refi93 Oct 9, 2023
083c445
feat(staking): expose reward amount breakdown in activity detail
refi93 Oct 16, 2023
aa09aa6
refactor(all): generalize "Transaction" name to "Activity"
refi93 Oct 17, 2023
a0dfca9
refactor(all): rename getActivityDetails to getActivityDetail
refi93 Oct 17, 2023
285358c
refactor(ui): clean up "Transactions" component naming - rename to Ac…
refi93 Oct 17, 2023
1e689fc
refactor(all): rename TransactionStatus to ActivityStatus
refi93 Oct 17, 2023
722cf39
refactor(all): rename TransformedTx to TransformedActivity
refi93 Oct 17, 2023
fecc785
refactor(all): split transaction from reward activity detail
refi93 Oct 17, 2023
6a98180
refactor(staking): split rendering logic of reward and tx details
refi93 Oct 18, 2023
0497b1e
fix(all): fix broken type import
refi93 Oct 18, 2023
f512ca3
refactor(all): rename _TransactionDetails to _TransactionDetailsProxy
refi93 Oct 19, 2023
5f0faff
fix(all): fix wallet activity unit tests
refi93 Oct 19, 2023
d0e79f5
Merge branch 'main' into feat/lw-8751-show-pool-rewards-in-activity
refi93 Oct 19, 2023
f36ac67
fix(all): code review fixes
refi93 Oct 20, 2023
0739235
fix(all): make rewards in RewardsDetails component non-optional
refi93 Oct 20, 2023
701c284
refactor(all): remove redundant props from Activity UI components
refi93 Oct 20, 2023
0537051
refactor(all): rename transaction key for activity details
refi93 Oct 20, 2023
e0ae082
refactor(all): change way of importing EpochNo type
refi93 Oct 20, 2023
959aac2
refactor(all): refactor transformTransactionStatus()
refi93 Oct 20, 2023
d6714d3
fix(all): lint fixes
refi93 Oct 20, 2023
28d1414
refactor(all): move TransactionDetailsProxy to separate file
refi93 Oct 20, 2023
b2710c8
refactor(all): remove redundant ts comment
refi93 Oct 20, 2023
0ef664e
test(all): add reward ui component test
refi93 Oct 20, 2023
5dc1216
fix(all): fix crashing build
refi93 Oct 20, 2023
734c46e
fix(all): fix build
refi93 Oct 20, 2023
edc882f
fix(all): fix type import
refi93 Oct 20, 2023
3accfcc
fix(all): fix ci build
refi93 Oct 20, 2023
6271c50
fix(all): fix ci build
refi93 Oct 20, 2023
5d6715f
fix(all): fix ci build
refi93 Oct 20, 2023
3349309
Merge branch 'main' into feat/lw-8751-show-pool-rewards-in-activity
refi93 Oct 24, 2023
f398c32
fix(all): add forgotten prop to RewardsDetails
refi93 Oct 24, 2023
8845a53
fix(all): make rewards details time non-optional
refi93 Oct 24, 2023
1a8da57
refactor(all): enforce spendable status for rewards details component
refi93 Oct 24, 2023
b24bd8c
Merge branch 'main' into feat/lw-8751-show-pool-rewards-in-activity
refi93 Oct 27, 2023
74a9b66
Merge branch 'main' into feat/lw-8751-show-pool-rewards-in-activity
refi93 Oct 27, 2023
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
6 changes: 3 additions & 3 deletions apps/browser-extension-wallet/src/api/transformers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import {
CoinOverview,
CardanoStakePool,
CardanoTxOut,
TransactionDetail,
CurrencyInfo
CurrencyInfo,
TransactionActivityDetail
} from '../types';
import { Wallet } from '@lace/cardano';
import { addEllipsis, getNumberWithUnit } from '@lace/common';
Expand Down Expand Up @@ -111,7 +111,7 @@ const isStakePool = (props: CardanoStakePool | Wallet.Cardano.SlotLeader): props
/**
* format block information
*/
export const blockTransformer = (block: Wallet.BlockInfo): TransactionDetail['blocks'] => ({
export const blockTransformer = (block: Wallet.BlockInfo): TransactionActivityDetail['blocks'] => ({
blockId: block.header.hash.toString(),
epoch: block.epoch.toString(),
block: block.header.blockNo.toString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { StateStatus, useWalletStore } from '@src/stores';
import { useFetchCoinPrice, useRedirection } from '@hooks';
import { Drawer, DrawerNavigation } from '@lace/common';
import { GroupedAssetActivityList } from '@lace/core';
import { TransactionDetail } from '@src/views/browser-view/features/activity';
import { ActivityDetail } from '@src/views/browser-view/features/activity';
import styles from './Activity.module.scss';
import { FundWalletBanner } from '@src/views/browser-view/components';
import { walletRoutePaths } from '@routes';
Expand All @@ -21,7 +21,7 @@ import { useWalletActivities } from '@hooks/useWalletActivities';
export const Activity = (): React.ReactElement => {
const { t } = useTranslation();
const { priceResult } = useFetchCoinPrice();
const { walletInfo, transactionDetail, resetTransactionState } = useWalletStore();
const { walletInfo, activityDetail, resetActivityState } = useWalletStore();
const layoutTitle = `${t('browserView.activity.title')}`;
const redirectToAssets = useRedirection(walletRoutePaths.assets);
const analytics = useAnalyticsContext();
Expand All @@ -43,21 +43,21 @@ export const Activity = (): React.ReactElement => {
return (
<ContentLayout title={layoutTitle} titleSideText={layoutSideText} isLoading={isLoading}>
<Drawer
visible={!!transactionDetail}
onClose={resetTransactionState}
visible={!!activityDetail}
onClose={resetActivityState}
navigation={
<DrawerNavigation
onArrowIconClick={resetTransactionState}
onArrowIconClick={resetActivityState}
onCloseIconClick={() => {
analytics.sendEventToPostHog(PostHogAction.ActivityActivityDetailXClick);
resetTransactionState();
resetActivityState();
redirectToAssets();
}}
/>
}
popupView
>
{transactionDetail && priceResult && <TransactionDetail price={priceResult} />}
{activityDetail && priceResult && <ActivityDetail price={priceResult} />}
</Drawer>
<div className={styles.activitiesContainer}>
{hasActivities ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
"from": "From",
"to": "To",
"multipleAddresses": "Multiple addresses",
"pools": "Pool(s)"
"pools": "Pool(s)",
"epoch": "Epoch"
},
"walletNameAndPasswordSetupStep": {
"title": "Let's set up your new wallet",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
stakePoolSearchSlice,
walletInfoSlice,
lockSlice,
transactionDetailSlice,
activityDetailSlice,
uiSlice,
blockchainProviderSlice
} from './slices';
Expand All @@ -34,7 +34,7 @@ export const createWalletStore = (
...networkSlice({ set, get }),
...stakePoolSearchSlice({ set, get }),
...lockSlice({ set, get }),
...transactionDetailSlice({ set, get }),
...activityDetailSlice({ set, get }),
...assetDetailsSlice({ set, get })
}));
};
Original file line number Diff line number Diff line change
@@ -1,50 +1,57 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { renderHook, act } from '@testing-library/react-hooks';
import { BlockchainProviderSlice, TransactionDetailSlice, WalletInfoSlice } from '../../types';
import { BlockchainProviderSlice, ActivityDetailSlice, WalletInfoSlice } from '../../types';
import { transactionMock } from '../../../utils/mocks/test-helpers';
import { transactionDetailSlice } from '../transaction-detail-slice';
import { activityDetailSlice } from '../activity-detail-slice';
import '@testing-library/jest-dom';
import create, { GetState, SetState } from 'zustand';
import { mockBlockchainProviders } from '@src/utils/mocks/blockchain-providers';
import { ActivityStatus } from '@lace/core';

const mockTransactionDetailSlice = (
set: SetState<TransactionDetailSlice>,
get: GetState<BlockchainProviderSlice & TransactionDetailSlice & WalletInfoSlice>
): TransactionDetailSlice => {
const mockActivityDetailSlice = (
set: SetState<ActivityDetailSlice>,
get: GetState<BlockchainProviderSlice & ActivityDetailSlice & WalletInfoSlice>
): ActivityDetailSlice => {
get = () =>
({ blockchainProvider: mockBlockchainProviders() } as BlockchainProviderSlice &
TransactionDetailSlice &
ActivityDetailSlice &
WalletInfoSlice);
return transactionDetailSlice({ set, get });
return activityDetailSlice({ set, get });
};

describe('Testing createStoreHook slice', () => {
test('should create store hook with transaction slices slice', () => {
const useTransactionsStore = create(mockTransactionDetailSlice);
const useTransactionsStore = create(mockActivityDetailSlice);
const { result } = renderHook(() => useTransactionsStore());
expect(result).toBeDefined();
});

test('should return transaction state and state handlers', () => {
const useTransactionsStore = create(mockTransactionDetailSlice);
const useTransactionsStore = create(mockActivityDetailSlice);
const { result } = renderHook(() => useTransactionsStore());
expect(result.current).toBeDefined();

expect(result.current.transactionDetail).not.toBeDefined();
expect(result.current.fetchingTransactionInfo).toBeDefined();
expect(result.current.getTransactionDetails).toBeDefined();
expect(result.current.resetTransactionState).toBeDefined();
expect(result.current.setTransactionDetail).toBeDefined();
expect(result.current.activityDetail).not.toBeDefined();
expect(result.current.fetchingActivityInfo).toBeDefined();
expect(result.current.getActivityDetail).toBeDefined();
expect(result.current.resetActivityState).toBeDefined();
expect(result.current.setTransactionActivityDetail).toBeDefined();
expect(result.current.setRewardsActivityDetail).toBeDefined();
});

test('should set transaction detail', () => {
const useTransactionsStore = create(mockTransactionDetailSlice);
const useTransactionsStore = create(mockActivityDetailSlice);
const { result, waitForValueToChange } = renderHook(() => useTransactionsStore());

act(() => {
result.current.setTransactionDetail(transactionMock.tx, transactionMock.direction);
result.current.setTransactionActivityDetail({
type: 'incoming',
status: ActivityStatus.SUCCESS,
activity: transactionMock.tx,
direction: transactionMock.direction
});
});
waitForValueToChange(() => result.current.transactionDetail);
expect(result.current.transactionDetail).toBeDefined();
waitForValueToChange(() => result.current.activityDetail);
expect(result.current.activityDetail).toBeDefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
StateStatus,
AssetDetailsSlice,
BlockchainProviderSlice,
TransactionDetailSlice,
ActivityDetailSlice,
UISlice,
WalletInfoSlice
} from '@stores/types';
Expand All @@ -21,7 +21,7 @@ const mockActivitiesSlice = (
get: GetState<
WalletInfoSlice &
WalletActivitiesSlice &
TransactionDetailSlice &
ActivityDetailSlice &
AssetDetailsSlice &
UISlice &
BlockchainProviderSlice
Expand All @@ -35,7 +35,7 @@ const mockActivitiesSlice = (
walletInfo: mockWalletInfoTestnet
} as WalletInfoSlice &
WalletActivitiesSlice &
TransactionDetailSlice &
ActivityDetailSlice &
AssetDetailsSlice &
UISlice &
BlockchainProviderSlice);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
/* eslint-disable complexity */
/* eslint-disable unicorn/no-array-reduce */
import isEmpty from 'lodash/isEmpty';
import {
TransactionDetailSlice,
ZustandHandlers,
BlockchainProviderSlice,
WalletInfoSlice,
SliceCreator
} from '../types';
import { CardanoTxOut, Transaction, TransactionDetail } from '../../types';
import { ActivityDetailSlice, ZustandHandlers, BlockchainProviderSlice, WalletInfoSlice, SliceCreator } from '../types';
import { CardanoTxOut, Transaction, ActivityDetail, TransactionActivityDetail } from '../../types';
import { blockTransformer, inputOutputTransformer } from '../../api/transformers';
import { Wallet } from '@lace/cardano';
import { getTransactionTotalOutput } from '../../utils/get-transaction-total-output';
import { inspectTxValues } from '@src/utils/tx-inspection';
import { firstValueFrom } from 'rxjs';
import { getAssetsInformation } from '@src/utils/get-assets-information';
import { MAX_POOLS_COUNT } from '@lace/staking';
import { TransactionType } from '@lace/core';
import { ActivityStatus, ActivityType } from '@lace/core';
import { formatDate, formatTime } from '@src/utils/format-date';

/**
* validates if the transaction is confirmed
Expand All @@ -40,11 +35,13 @@ const getTransactionAssetsId = (outputs: CardanoTxOut[]) => {
return assetIds;
};

const transactionMetadataTransformer = (metadata: Wallet.Cardano.TxMetadata): TransactionDetail['tx']['metadata'] =>
const transactionMetadataTransformer = (
metadata: Wallet.Cardano.TxMetadata
): TransactionActivityDetail['activity']['metadata'] =>
[...metadata.entries()].map(([key, value]) => ({ key: key.toString(), value: Wallet.cardanoMetadatumToObj(value) }));

const shouldIncludeFee = (
type: TransactionType,
type: ActivityType,
delegationInfo: Wallet.Cardano.StakeDelegationCertificate[] | undefined
) =>
!(
Expand All @@ -55,28 +52,83 @@ const shouldIncludeFee = (
(type === 'delegationDeregistration' && !!delegationInfo?.length)
);

const getPoolInfos = async (poolIds: Wallet.Cardano.PoolId[], stakePoolProvider: Wallet.StakePoolProvider) => {
const filters: Wallet.QueryStakePoolsArgs = {
filters: {
identifier: {
_condition: 'or',
values: poolIds.map((poolId) => ({ id: poolId }))
}
},
pagination: {
startAt: 0,
limit: MAX_POOLS_COUNT
}
};
const { pageResults: pools } = await stakePoolProvider.queryStakePools(filters);

return pools;
};

/**
* fetchs asset information
* fetches asset information
*/
const getTransactionDetail =
const buildGetActivityDetail =
({
set,
get
}: ZustandHandlers<
TransactionDetailSlice & BlockchainProviderSlice & WalletInfoSlice
>): TransactionDetailSlice['getTransactionDetails'] =>
ActivityDetailSlice & BlockchainProviderSlice & WalletInfoSlice
>): ActivityDetailSlice['getActivityDetail'] =>
// eslint-disable-next-line max-statements, sonarjs/cognitive-complexity
async ({ coinPrices, fiatCurrency }) => {
const {
blockchainProvider: { chainHistoryProvider, stakePoolProvider, assetProvider },
inMemoryWallet: wallet,
transactionDetail: { tx, status, direction, type },
activityDetail,
walletInfo
} = get();

if (activityDetail.type === 'rewards') {
const { activity, status, type } = activityDetail;
const poolInfos = await getPoolInfos(
activity.rewards.map(({ poolId }) => poolId),
stakePoolProvider
);

return {
activity: {
includedUtcDate: formatDate({ date: activity.spendableDate, format: 'MM/DD/YYYY', type: 'utc' }),
includedUtcTime: `${formatTime({ date: activity.spendableDate, type: 'utc' })} UTC`,
rewards: {
totalAmount: Wallet.util.lovelacesToAdaString(
Wallet.BigIntMath.sum(activity.rewards?.map(({ rewards }) => rewards) || []).toString()
),
spendableEpoch: activity.spendableEpoch,
rewards: activity.rewards.map((r) => {
const poolInfo = poolInfos.find((p) => p.id === r.poolId);
return {
amount: Wallet.util.lovelacesToAdaString(r.rewards.toString()),
pool: r.poolId
? {
id: r.poolId,
name: poolInfo?.metadata?.name || '-',
ticker: poolInfo?.metadata?.ticker || '-'
}
: undefined
};
})
}
},
status,
type
};
}

const { activity: tx, status, type, direction } = activityDetail;
const walletAssets = await firstValueFrom(wallet.assetInfo$);
const protocolParameters = await firstValueFrom(wallet.protocolParameters$);
set({ fetchingTransactionInfo: true });
set({ fetchingActivityInfo: true });

// Assets
const assetIds = getTransactionAssetsId(tx.body.outputs);
Expand Down Expand Up @@ -134,7 +186,7 @@ const getTransactionDetail =
(certificate) => certificate.__typename === 'StakeDelegationCertificate'
) as Wallet.Cardano.StakeDelegationCertificate[];

let transaction: TransactionDetail['tx'] = {
let transaction: ActivityDetail['activity'] = {
hash: tx.id.toString(),
totalOutput: totalOutputInAda,
fee: shouldIncludeFee(type, delegationInfo) ? feeInAda : undefined,
Expand All @@ -148,19 +200,10 @@ const getTransactionDetail =
};

if (type === 'delegation' && delegationInfo) {
const filters: Wallet.QueryStakePoolsArgs = {
filters: {
identifier: {
_condition: 'or',
values: delegationInfo.map((certificate) => ({ id: certificate.poolId }))
}
},
pagination: {
startAt: 0,
limit: MAX_POOLS_COUNT
}
};
const { pageResults: pools } = await stakePoolProvider.queryStakePools(filters);
const pools = await getPoolInfos(
delegationInfo.map(({ poolId }) => poolId),
stakePoolProvider
);

if (pools.length === 0) {
console.error('Stake pool was not found for delegation tx');
Expand All @@ -176,20 +219,23 @@ const getTransactionDetail =
}
}

set({ fetchingTransactionInfo: false });
return { tx: transaction, blocks, status, assetAmount, type };
set({ fetchingActivityInfo: false });
return { activity: transaction, blocks, status, assetAmount, type };
};

/**
* has all transactions search related actions and states
*/
export const transactionDetailSlice: SliceCreator<
TransactionDetailSlice & BlockchainProviderSlice & WalletInfoSlice,
TransactionDetailSlice
export const activityDetailSlice: SliceCreator<
ActivityDetailSlice & BlockchainProviderSlice & WalletInfoSlice,
ActivityDetailSlice
> = ({ set, get }) => ({
transactionDetail: undefined,
fetchingTransactionInfo: true,
getTransactionDetails: getTransactionDetail({ set, get }),
setTransactionDetail: (tx, direction, status, type) => set({ transactionDetail: { tx, direction, status, type } }),
resetTransactionState: () => set({ transactionDetail: undefined, fetchingTransactionInfo: false })
activityDetail: undefined,
fetchingActivityInfo: true,
getActivityDetail: buildGetActivityDetail({ set, get }),
setTransactionActivityDetail: ({ activity, direction, status, type }) =>
set({ activityDetail: { activity, direction, status, type } }),
setRewardsActivityDetail: ({ activity }) =>
set({ activityDetail: { activity, status: ActivityStatus.SPENDABLE, type: 'rewards' } }),
resetActivityState: () => set({ activityDetail: undefined, fetchingActivityInfo: false })
});
Loading
Loading