Skip to content

Commit 6b276dc

Browse files
authored
fix: [lw-11876]: upgrade to lace in send flow if locked rewards available (#1570)
* fix: [lw-11876]: show upgrade to lace in send flow in locked rewards available * fix: [lw-11830]: check for retired drep status to get locked rewards * fix: adjust debounce timing * fix: resolve pr comment * fix: resolve eslint issues
1 parent 242e0ff commit 6b276dc

File tree

10 files changed

+366
-49
lines changed

10 files changed

+366
-49
lines changed

apps/browser-extension-wallet/src/popup.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useEffect, useState } from 'react';
22
import * as ReactDOM from 'react-dom';
33
import { HashRouter } from 'react-router-dom';
4-
import { PopupView } from '@routes';
4+
import { PopupView, walletRoutePaths } from '@routes';
55
import { StoreProvider } from '@stores';
66
import { CurrencyStoreProvider } from '@providers/currency';
77
import { AppSettingsProvider, DatabaseProvider, ThemeProvider, AnalyticsProvider } from '@providers';
@@ -34,8 +34,8 @@ const App = (): React.ReactElement => {
3434
const newModeValue = changes.BACKGROUND_STORAGE?.newValue?.namiMigration;
3535
if (oldModeValue?.mode !== newModeValue?.mode) {
3636
setMode(newModeValue);
37-
// Force back to original routing
38-
window.location.hash = '#';
37+
// Force back to original routing unless it is staking route (see LW-11876)
38+
if (window.location.hash.split('#')[1] !== walletRoutePaths.earn) window.location.hash = '#';
3939
}
4040
});
4141

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/* eslint-disable no-magic-numbers */
2+
/* eslint-disable import/imports-first */
3+
const mockUseWalletStore = jest.fn();
4+
import { renderHook } from '@testing-library/react-hooks';
5+
import { useRewardAccountsData } from '../hooks';
6+
import { act } from 'react-dom/test-utils';
7+
import { BehaviorSubject } from 'rxjs';
8+
import * as Stores from '@src/stores';
9+
import { Wallet } from '@lace/cardano';
10+
11+
const rewardAccounts$ = new BehaviorSubject([]);
12+
13+
const inMemoryWallet = {
14+
delegation: {
15+
rewardAccounts$
16+
}
17+
};
18+
19+
jest.mock('@src/stores', (): typeof Stores => ({
20+
...jest.requireActual<typeof Stores>('@src/stores'),
21+
useWalletStore: mockUseWalletStore
22+
}));
23+
24+
describe('Testing useRewardAccountsData hook', () => {
25+
test('should return proper rewards accounts hook state', async () => {
26+
mockUseWalletStore.mockReset();
27+
mockUseWalletStore.mockImplementation(() => ({
28+
inMemoryWallet
29+
}));
30+
31+
const hook = renderHook(() => useRewardAccountsData());
32+
expect(hook.result.current.areAllRegisteredStakeKeysWithoutVotingDelegation).toEqual(false);
33+
expect(hook.result.current.poolIdToRewardAccountsMap).toEqual(new Map());
34+
expect(hook.result.current.lockedStakeRewards).toEqual(BigInt(0));
35+
36+
act(() => {
37+
rewardAccounts$.next([{ credentialStatus: Wallet.Cardano.StakeCredentialStatus.Unregistered }]);
38+
});
39+
expect(hook.result.current.areAllRegisteredStakeKeysWithoutVotingDelegation).toEqual(false);
40+
expect(hook.result.current.poolIdToRewardAccountsMap).toEqual(new Map());
41+
expect(hook.result.current.lockedStakeRewards).toEqual(BigInt(0));
42+
43+
act(() => {
44+
rewardAccounts$.next([
45+
{ credentialStatus: Wallet.Cardano.StakeCredentialStatus.Registered, rewardBalance: BigInt(0) }
46+
]);
47+
});
48+
expect(hook.result.current.areAllRegisteredStakeKeysWithoutVotingDelegation).toEqual(true);
49+
expect(hook.result.current.poolIdToRewardAccountsMap).toEqual(new Map());
50+
expect(hook.result.current.lockedStakeRewards).toEqual(BigInt(0));
51+
52+
act(() => {
53+
rewardAccounts$.next([
54+
{
55+
credentialStatus: Wallet.Cardano.StakeCredentialStatus.Registered,
56+
dRepDelegatee: { delegateRepresentative: { active: true } },
57+
rewardBalance: BigInt(1_000_000)
58+
}
59+
]);
60+
});
61+
expect(hook.result.current.areAllRegisteredStakeKeysWithoutVotingDelegation).toEqual(false);
62+
expect(hook.result.current.poolIdToRewardAccountsMap).toEqual(new Map());
63+
expect(hook.result.current.lockedStakeRewards).toEqual(BigInt(0));
64+
65+
act(() => {
66+
rewardAccounts$.next([
67+
{
68+
credentialStatus: Wallet.Cardano.StakeCredentialStatus.Unregistered,
69+
rewardBalance: BigInt(1_000_000)
70+
},
71+
{
72+
credentialStatus: Wallet.Cardano.StakeCredentialStatus.Unregistered,
73+
dRepDelegatee: { delegateRepresentative: { active: false } },
74+
rewardBalance: BigInt(1_000_000)
75+
},
76+
{
77+
credentialStatus: Wallet.Cardano.StakeCredentialStatus.Unregistered,
78+
dRepDelegatee: { delegateRepresentative: { active: true } },
79+
rewardBalance: BigInt(1_000_000)
80+
},
81+
{
82+
credentialStatus: Wallet.Cardano.StakeCredentialStatus.Registered,
83+
rewardBalance: BigInt(1_000_000)
84+
},
85+
{
86+
credentialStatus: Wallet.Cardano.StakeCredentialStatus.Registered,
87+
dRepDelegatee: { delegateRepresentative: { active: true } },
88+
rewardBalance: BigInt(1_000_000)
89+
},
90+
{
91+
credentialStatus: Wallet.Cardano.StakeCredentialStatus.Registered,
92+
dRepDelegatee: { delegateRepresentative: { active: false } },
93+
rewardBalance: BigInt(1_000_000)
94+
}
95+
]);
96+
});
97+
expect(hook.result.current.areAllRegisteredStakeKeysWithoutVotingDelegation).toEqual(false);
98+
expect(hook.result.current.poolIdToRewardAccountsMap).toEqual(new Map());
99+
expect(hook.result.current.lockedStakeRewards).toEqual(BigInt(2_000_000));
100+
101+
const poolId = 'poolId';
102+
const rewardAccount = {
103+
credentialStatus: Wallet.Cardano.StakeCredentialStatus.Registered,
104+
dRepDelegatee: { delegateRepresentative: {} },
105+
delegatee: { nextNextEpoch: { id: poolId } },
106+
rewardBalance: BigInt(0)
107+
};
108+
act(() => {
109+
rewardAccounts$.next([rewardAccount]);
110+
});
111+
expect(hook.result.current.areAllRegisteredStakeKeysWithoutVotingDelegation).toEqual(false);
112+
expect(hook.result.current.poolIdToRewardAccountsMap.size).toEqual(1);
113+
expect(hook.result.current.poolIdToRewardAccountsMap.get(poolId)).toEqual([rewardAccount]);
114+
});
115+
});

apps/browser-extension-wallet/src/views/browser-view/features/staking/hooks.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1-
import { useCallback } from 'react';
1+
import { useCallback, useMemo } from 'react';
22
import { useDelegationStore } from '@src/features/delegation/stores';
33
import { useWalletStore } from '@stores';
44
import { withSignTxConfirmation } from '@lib/wallet-api-ui';
55
import { useSecrets } from '@lace/core';
6+
import { useObservable } from '@lace/common';
7+
import { Wallet } from '@lace/cardano';
8+
import groupBy from 'lodash/groupBy';
9+
10+
interface UseRewardAccountsDataType {
11+
areAllRegisteredStakeKeysWithoutVotingDelegation: boolean;
12+
poolIdToRewardAccountsMap: Map<string, Wallet.Cardano.RewardAccountInfo[]>;
13+
lockedStakeRewards: bigint;
14+
}
615

716
export const useDelegationTransaction = (): { signAndSubmitTransaction: () => Promise<void> } => {
817
const { password, clearSecrets } = useSecrets();
@@ -17,3 +26,69 @@ export const useDelegationTransaction = (): { signAndSubmitTransaction: () => Pr
1726

1827
return { signAndSubmitTransaction };
1928
};
29+
30+
export const getPoolIdToRewardAccountsMap = (
31+
rewardAccounts: Wallet.Cardano.RewardAccountInfo[]
32+
): UseRewardAccountsDataType['poolIdToRewardAccountsMap'] =>
33+
new Map(
34+
Object.entries(
35+
groupBy(rewardAccounts, ({ delegatee }) => {
36+
const delagationInfo = delegatee?.nextNextEpoch || delegatee?.nextEpoch || delegatee?.currentEpoch;
37+
return delagationInfo?.id.toString() ?? '';
38+
})
39+
).filter(([poolId]) => !!poolId)
40+
);
41+
42+
export const useRewardAccountsData = (): UseRewardAccountsDataType => {
43+
const { inMemoryWallet } = useWalletStore();
44+
const rewardAccounts = useObservable(inMemoryWallet.delegation.rewardAccounts$);
45+
const accountsWithRegisteredStakeCreds = useMemo(
46+
() =>
47+
rewardAccounts?.filter(
48+
({ credentialStatus }) => Wallet.Cardano.StakeCredentialStatus.Registered === credentialStatus
49+
) ?? [],
50+
[rewardAccounts]
51+
);
52+
53+
const areAllRegisteredStakeKeysWithoutVotingDelegation = useMemo(
54+
() =>
55+
accountsWithRegisteredStakeCreds.length > 0 &&
56+
!accountsWithRegisteredStakeCreds.some(({ dRepDelegatee }) => dRepDelegatee),
57+
[accountsWithRegisteredStakeCreds]
58+
);
59+
60+
const accountsWithRegisteredStakeCredsWithoutVotingDelegation = useMemo(
61+
() =>
62+
accountsWithRegisteredStakeCreds.filter(
63+
({ dRepDelegatee }) =>
64+
!dRepDelegatee ||
65+
(dRepDelegatee &&
66+
'active' in dRepDelegatee.delegateRepresentative &&
67+
!dRepDelegatee.delegateRepresentative.active)
68+
),
69+
[accountsWithRegisteredStakeCreds]
70+
);
71+
72+
const lockedStakeRewards = useMemo(
73+
() =>
74+
BigInt(
75+
accountsWithRegisteredStakeCredsWithoutVotingDelegation
76+
? Wallet.BigIntMath.sum(
77+
accountsWithRegisteredStakeCredsWithoutVotingDelegation.map(({ rewardBalance }) => rewardBalance)
78+
)
79+
: 0
80+
),
81+
[accountsWithRegisteredStakeCredsWithoutVotingDelegation]
82+
);
83+
84+
const poolIdToRewardAccountsMap = useMemo(
85+
() => getPoolIdToRewardAccountsMap(accountsWithRegisteredStakeCreds),
86+
[accountsWithRegisteredStakeCreds]
87+
);
88+
89+
return {
90+
areAllRegisteredStakeKeysWithoutVotingDelegation,
91+
lockedStakeRewards,
92+
poolIdToRewardAccountsMap
93+
};
94+
};

apps/browser-extension-wallet/src/views/nami-mode/NamiView.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,16 @@ import {
1818
useWalletManager,
1919
useBuildDelegation,
2020
useBalances,
21-
useHandleResolver
21+
useHandleResolver,
22+
useRedirection
2223
} from '@hooks';
2324
import { walletManager, withSignTxConfirmation } from '@lib/wallet-api-ui';
2425
import { useAnalytics } from './hooks';
2526
import { useDappContext, withDappContext } from '@src/features/dapp/context';
2627
import { localDappService } from '../browser-view/features/dapp/components/DappList/localDappService';
2728
import { isValidURL } from '@src/utils/is-valid-url';
2829
import { CARDANO_COIN_SYMBOL } from './constants';
29-
import { useDelegationTransaction } from '../browser-view/features/staking/hooks';
30+
import { useDelegationTransaction, useRewardAccountsData } from '../browser-view/features/staking/hooks';
3031
import { useSecrets } from '@lace/core';
3132
import { useDelegationStore } from '@src/features/delegation/stores';
3233
import { useStakePoolDetails } from '@src/features/stake-pool-details/store';
@@ -43,6 +44,7 @@ import { BackgroundStorage } from '@lib/scripts/types';
4344
import { getWalletAccountsQtyString } from '@src/utils/get-wallet-count-string';
4445
import { useNetworkError } from '@hooks/useNetworkError';
4546
import { createHistoricalOwnInputResolver } from '@src/utils/own-input-resolver';
47+
import { walletRoutePaths } from '@routes';
4648

4749
const { AVAILABLE_CHAINS, DEFAULT_SUBMIT_API } = config();
4850

@@ -179,6 +181,10 @@ export const NamiView = withDappContext((): React.ReactElement => {
179181
setDeletingWallet(false);
180182
}, [analytics, deleteWallet, setDeletingWallet, walletRepository]);
181183

184+
const { lockedStakeRewards } = useRewardAccountsData();
185+
186+
const redirectToStaking = useRedirection(walletRoutePaths.earn);
187+
182188
return (
183189
<OutsideHandlesProvider
184190
{...{
@@ -239,7 +245,9 @@ export const NamiView = withDappContext((): React.ReactElement => {
239245
chainHistoryProvider,
240246
protocolParameters: walletState?.protocolParameters,
241247
assetInfo: walletState?.assetInfo,
242-
createHistoricalOwnInputResolver
248+
createHistoricalOwnInputResolver,
249+
lockedStakeRewards,
250+
redirectToStaking
243251
}}
244252
>
245253
<CommonOutsideHandlesProvider

packages/nami/src/features/outside-handles-provider/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,6 @@ export interface OutsideHandlesContextValue {
131131
addresses: Wallet.WalletAddress[];
132132
}>,
133133
) => Wallet.Cardano.InputResolver;
134+
lockedStakeRewards: bigint;
135+
redirectToStaking: () => void;
134136
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import React from 'react';
2+
3+
import { Box, Button, Text, Link, useColorModeValue } from '@chakra-ui/react';
4+
import { AnimatePresence, motion } from 'framer-motion';
5+
6+
import { useOutsideHandles } from '../../../features/outside-handles-provider';
7+
8+
export const UpgradeToLaceBanner = ({
9+
showSwitchToLaceBanner,
10+
}: Readonly<{ showSwitchToLaceBanner: boolean }>) => {
11+
const warningBackground = useColorModeValue('#fcf5e3', '#fcf5e3');
12+
const { openExternalLink, switchWalletMode, redirectToStaking } =
13+
useOutsideHandles();
14+
15+
return (
16+
<AnimatePresence>
17+
{showSwitchToLaceBanner && (
18+
<motion.div
19+
key="splashScreen"
20+
initial={{
21+
y: '-224px',
22+
height: '0px',
23+
marginBottom: 0,
24+
}}
25+
animate={{
26+
y: '0px',
27+
height: '224px',
28+
marginBottom: '1.25rem',
29+
}}
30+
transition={{
31+
all: { duration: 5, ease: 'easeInOut' },
32+
}}
33+
exit={{
34+
y: '-224px',
35+
height: '0px',
36+
marginBottom: 0,
37+
}}
38+
>
39+
<Box
40+
display="flex"
41+
alignItems="center"
42+
justifyContent="flex-end"
43+
flexDirection="column"
44+
background={warningBackground}
45+
rounded="xl"
46+
padding="18"
47+
gridGap="8px"
48+
mb="4"
49+
overflow="hidden"
50+
>
51+
<Text
52+
color="gray.800"
53+
fontSize="14"
54+
fontWeight="500"
55+
lineHeight="24px"
56+
>
57+
Your ADA balance includes Locked Stake Rewards that can only be
58+
withdrawn or transacted after registering your voting power.
59+
Upgrade to Lace to continue. For more information, visit our{' '}
60+
<Link
61+
isExternal
62+
textDecoration="underline"
63+
onClick={() => {
64+
openExternalLink('https://www.lace.io/faq');
65+
}}
66+
>
67+
FAQs.
68+
</Link>
69+
</Text>
70+
<Button
71+
height="36px"
72+
width="100%"
73+
colorScheme="teal"
74+
onClick={async () => {
75+
redirectToStaking();
76+
await switchWalletMode();
77+
}}
78+
>
79+
Upgrade to Lace
80+
</Button>
81+
</Box>
82+
</motion.div>
83+
)}
84+
</AnimatePresence>
85+
);
86+
};

0 commit comments

Comments
 (0)