From e721d23e06dbfb0427d654434e7abd6446f5e6e2 Mon Sep 17 00:00:00 2001 From: Matthew Grainger <46547583+Matt561@users.noreply.github.com> Date: Thu, 24 Oct 2024 13:32:41 -0400 Subject: [PATCH] feat: STAKE-803 integrate unstake method from sdk (#11962) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR adds the ability to unstake natively within the MetaMask mobile app. ## **Related issues** Jira Ticket: [FE] [Unstake flow] Integrate unstake method from sdk - ([link](https://consensyssoftware.atlassian.net/browse/STAKE-803)) ## **Manual testing steps** prerequisite: if you don't have any Ethereum staked, you'll need to stake some in order to unstake. 1. Add `export MM_POOLED_STAKING_UI_ENABLED=true` to `.js.env` file. 2. Click on Ethereum from the asset list page 3. Click on "Unstake" 4. Enter a valid amount to unstake and click "Review" 5. Click "Continue" to initiate the transaction-controller flow. ## Figma Designs - Staking UX ([link](https://www.figma.com/design/1c0Y9jDJe6p0j82jydJDcs/Staking-UX?node-id=2979-19504&m=dev)) ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/bd1919d3-dcc6-4443-b08c-7944e183914c ### **After** https://github.com/user-attachments/assets/924905b1-405a-49f9-aa9e-eb0cbeaf872e ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- .../StakeConfirmationView.test.tsx | 6 + .../StakeConfirmationView.test.tsx.snap | 143 ++- .../StakeInputView.test.tsx.snap | 8 +- .../UnstakeConfirmationView.test.tsx | 6 + .../UnstakeConfirmationView.test.tsx.snap | 143 ++- app/components/UI/Stake/__mocks__/mockData.ts | 40 +- .../components/EstimatedAnnualRewardsCard.tsx | 7 +- .../AccountCard/AccountCard.test.tsx | 16 +- .../AccountCard/AccountCard.tsx | 23 +- .../__snapshots__/AccountCard.test.tsx.snap | 907 ++++++++++-------- .../ConfirmationFooter.test.tsx | 17 +- .../FooterButtonGroup.test.tsx | 60 +- .../FooterButtonGroup/FooterButtonGroup.tsx | 106 +- .../FooterButtonGroup.test.tsx.snap | 178 ++++ .../ConfirmationFooter.test.tsx.snap | 3 + .../ContractTag/ContractTag.test.tsx | 7 +- .../ContractTag/ContractTag.tsx | 28 +- .../ContractTag/ContractTag.types.ts | 2 + .../__snapshots__/ContractTag.test.tsx.snap | 140 ++- .../Stake/hooks/usePoolStakedUnstake/index.ts | 73 ++ .../usePoolStakedUnstake.test.tsx | 133 +++ .../UI/Stake/sdk/stakeSdkProvider.test.tsx | 42 +- .../UI/Stake/sdk/stakeSdkProvider.tsx | 25 +- 23 files changed, 1586 insertions(+), 527 deletions(-) create mode 100644 app/components/UI/Stake/hooks/usePoolStakedUnstake/index.ts create mode 100644 app/components/UI/Stake/hooks/usePoolStakedUnstake/usePoolStakedUnstake.test.tsx diff --git a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.test.tsx b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.test.tsx index 720b5242f61..90bdb658e9a 100644 --- a/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.test.tsx +++ b/app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.test.tsx @@ -7,6 +7,7 @@ import { backgroundState } from '../../../../../util/test/initial-root-state'; import configureMockStore from 'redux-mock-store'; import { Provider } from 'react-redux'; import { StakeConfirmationViewProps } from './StakeConfirmationView.types'; +import { MOCK_POOL_STAKING_SDK } from '../../__mocks__/mockData'; jest.mock('../../../../hooks/useIpfsGateway', () => jest.fn()); @@ -60,6 +61,11 @@ jest.mock('../../hooks/usePoolStakedDeposit', () => ({ }), })); +jest.mock('../../hooks/useStakeContext', () => ({ + __esModule: true, + useStakeContext: jest.fn(() => MOCK_POOL_STAKING_SDK), +})); + jest.mock('../../hooks/usePooledStakes', () => ({ __esModule: true, default: () => ({ diff --git a/app/components/UI/Stake/Views/StakeConfirmationView/__snapshots__/StakeConfirmationView.test.tsx.snap b/app/components/UI/Stake/Views/StakeConfirmationView/__snapshots__/StakeConfirmationView.test.tsx.snap index eedfba7fe99..ab1c216fd7a 100644 --- a/app/components/UI/Stake/Views/StakeConfirmationView/__snapshots__/StakeConfirmationView.test.tsx.snap +++ b/app/components/UI/Stake/Views/StakeConfirmationView/__snapshots__/StakeConfirmationView.test.tsx.snap @@ -638,18 +638,146 @@ exports[`StakeConfirmationView render matches snapshot 1`] = ` } } > - + > + + + + + + + + + + diff --git a/app/components/UI/Stake/Views/UnstakeConfirmationView/UnstakeConfirmationView.test.tsx b/app/components/UI/Stake/Views/UnstakeConfirmationView/UnstakeConfirmationView.test.tsx index ada7220b852..735c23b5be7 100644 --- a/app/components/UI/Stake/Views/UnstakeConfirmationView/UnstakeConfirmationView.test.tsx +++ b/app/components/UI/Stake/Views/UnstakeConfirmationView/UnstakeConfirmationView.test.tsx @@ -5,6 +5,7 @@ import { Image } from 'react-native'; import { createMockAccountsControllerState } from '../../../../../util/test/accountsControllerTestUtils'; import { backgroundState } from '../../../../../util/test/initial-root-state'; import { UnstakeConfirmationViewProps } from './UnstakeConfirmationView.types'; +import { MOCK_POOL_STAKING_SDK } from '../../__mocks__/mockData'; const MOCK_ADDRESS_1 = '0x0'; const MOCK_ADDRESS_2 = '0x1'; @@ -58,6 +59,11 @@ jest.mock('../../hooks/usePooledStakes', () => ({ }), })); +jest.mock('../../hooks/useStakeContext', () => ({ + __esModule: true, + useStakeContext: jest.fn(() => MOCK_POOL_STAKING_SDK), +})); + describe('UnstakeConfirmationView', () => { it('render matches snapshot', () => { const props: UnstakeConfirmationViewProps = { diff --git a/app/components/UI/Stake/Views/UnstakeConfirmationView/__snapshots__/UnstakeConfirmationView.test.tsx.snap b/app/components/UI/Stake/Views/UnstakeConfirmationView/__snapshots__/UnstakeConfirmationView.test.tsx.snap index c0b65def7af..2b82484bc70 100644 --- a/app/components/UI/Stake/Views/UnstakeConfirmationView/__snapshots__/UnstakeConfirmationView.test.tsx.snap +++ b/app/components/UI/Stake/Views/UnstakeConfirmationView/__snapshots__/UnstakeConfirmationView.test.tsx.snap @@ -863,18 +863,146 @@ exports[`UnstakeConfirmationView render matches snapshot 1`] = ` } } > - + > + + + + + + + + + + = { + fetchFromApi: jest.fn(), + getPooledStakes: jest.fn(), + getVaultData: jest.fn(), + getPooledStakingEligibility: jest.fn(), + baseUrl: 'https://staking.api.com', +}; + +const MOCK_POOLED_STAKING_CONTRACT_SERVICE = { + chainId: ChainId.ETHEREUM, + connectSignerOrProvider: jest.fn(), + contract: new Contract('0x0000000000000000000000000000000000000000', []), + convertToShares: jest.fn(), + encodeClaimExitedAssetsTransactionData: jest.fn(), + encodeDepositTransactionData: jest.fn(), + encodeEnterExitQueueTransactionData: jest.fn(), + encodeMulticallTransactionData: jest.fn(), + estimateClaimExitedAssetsGas: jest.fn(), + estimateDepositGas: jest.fn(), + estimateEnterExitQueueGas: jest.fn(), + estimateMulticallGas: jest.fn(), +}; + +export const MOCK_POOL_STAKING_SDK: Stake = { + stakingContract: MOCK_POOLED_STAKING_CONTRACT_SERVICE, + stakingApiService: MOCK_STAKING_API_SERVICE as StakingApiService, + sdkType: StakingType.POOLED, + setSdkType: jest.fn(), +}; diff --git a/app/components/UI/Stake/components/EstimatedAnnualRewardsCard.tsx b/app/components/UI/Stake/components/EstimatedAnnualRewardsCard.tsx index 35d39695c4f..7c2b306cef3 100644 --- a/app/components/UI/Stake/components/EstimatedAnnualRewardsCard.tsx +++ b/app/components/UI/Stake/components/EstimatedAnnualRewardsCard.tsx @@ -4,6 +4,7 @@ import { strings } from '../../../../../locales/i18n'; import Icon, { IconColor, IconName, + IconSize, } from '../../../../component-library/components/Icons/Icon'; import Text, { TextColor, @@ -66,7 +67,11 @@ const EstimatedAnnualRewardsCard = ({ onPress={onIconPress} accessibilityLabel="Learn More" > - + diff --git a/app/components/UI/Stake/components/StakingConfirmation/AccountCard/AccountCard.test.tsx b/app/components/UI/Stake/components/StakingConfirmation/AccountCard/AccountCard.test.tsx index b1e81d21976..4b7aab92d6c 100644 --- a/app/components/UI/Stake/components/StakingConfirmation/AccountCard/AccountCard.test.tsx +++ b/app/components/UI/Stake/components/StakingConfirmation/AccountCard/AccountCard.test.tsx @@ -3,10 +3,9 @@ import renderWithProvider from '../../../../../../util/test/renderWithProvider'; import AccountCard from './AccountCard'; import { strings } from '../../../../../../../locales/i18n'; import { createMockAccountsControllerState } from '../../../../../../util/test/accountsControllerTestUtils'; -import configureMockStore from 'redux-mock-store'; import { backgroundState } from '../../../../../../util/test/initial-root-state'; -import { Provider } from 'react-redux'; import { AccountCardProps } from './AccountCard.types'; +import { MOCK_POOL_STAKING_SDK } from '../../../__mocks__/mockData'; const MOCK_STAKING_CONTRACT_NAME = 'MM Pooled Staking'; @@ -18,8 +17,6 @@ const MOCK_ACCOUNTS_CONTROLLER_STATE = createMockAccountsControllerState([ MOCK_ADDRESS_2, ]); -const mockStore = configureMockStore(); - const mockInitialState = { settings: {}, engine: { @@ -29,7 +26,6 @@ const mockInitialState = { }, }, }; -const store = mockStore(mockInitialState); jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), @@ -50,6 +46,11 @@ jest.mock('@react-navigation/native', () => { }; }); +jest.mock('../../../hooks/useStakeContext', () => ({ + __esModule: true, + useStakeContext: jest.fn(() => MOCK_POOL_STAKING_SDK), +})); + describe('AccountCard', () => { it('render matches snapshot', () => { const props: AccountCardProps = { @@ -59,9 +60,8 @@ describe('AccountCard', () => { }; const { getByText, toJSON } = renderWithProvider( - - , - , + , + { state: mockInitialState }, ); expect(getByText(strings('stake.staking_from'))).toBeDefined(); diff --git a/app/components/UI/Stake/components/StakingConfirmation/AccountCard/AccountCard.tsx b/app/components/UI/Stake/components/StakingConfirmation/AccountCard/AccountCard.tsx index fdd805b94ea..41f5d883cff 100644 --- a/app/components/UI/Stake/components/StakingConfirmation/AccountCard/AccountCard.tsx +++ b/app/components/UI/Stake/components/StakingConfirmation/AccountCard/AccountCard.tsx @@ -16,9 +16,11 @@ import images from '../../../../../../images/image-icons'; import AccountTag from '../AccountTag/AccountTag'; import { selectNetworkName } from '../../../../../../selectors/networkInfos'; import { AccountCardProps } from './AccountCard.types'; +import { useStakeContext } from '../../../hooks/useStakeContext'; import ContractTag from '../ContractTag/ContractTag'; +import { RootState } from '../../../../BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal.test'; -const AccountHeaderCard = ({ +const AccountCard = ({ contractName, primaryLabel, secondaryLabel, @@ -29,6 +31,12 @@ const AccountHeaderCard = ({ const networkName = useSelector(selectNetworkName); + const useBlockieIcon = useSelector( + (state: RootState) => state.settings.useBlockieIcon, + ); + + const { stakingContract } = useStakeContext(); + return ( @@ -40,6 +48,7 @@ const AccountHeaderCard = ({ ), }} @@ -50,7 +59,15 @@ const AccountHeaderCard = ({ label: { text: secondaryLabel }, }} value={{ - label: , + label: ( + + ), }} /> @@ -75,4 +92,4 @@ const AccountHeaderCard = ({ ); }; -export default AccountHeaderCard; +export default AccountCard; diff --git a/app/components/UI/Stake/components/StakingConfirmation/AccountCard/__snapshots__/AccountCard.test.tsx.snap b/app/components/UI/Stake/components/StakingConfirmation/AccountCard/__snapshots__/AccountCard.test.tsx.snap index 6e485b602ef..696e3ad7e8b 100644 --- a/app/components/UI/Stake/components/StakingConfirmation/AccountCard/__snapshots__/AccountCard.test.tsx.snap +++ b/app/components/UI/Stake/components/StakingConfirmation/AccountCard/__snapshots__/AccountCard.test.tsx.snap @@ -1,43 +1,51 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AccountCard render matches snapshot 1`] = ` -[ - - + + @@ -46,42 +54,42 @@ exports[`AccountCard render matches snapshot 1`] = ` { "alignItems": "center", "flexDirection": "row", - "gap": 8, } } > - - - Staking from - - + Staking from + + + @@ -90,236 +98,236 @@ exports[`AccountCard render matches snapshot 1`] = ` { "alignItems": "center", "flexDirection": "row", - "gap": 8, } } > - - - - - - + + - - - + } + height={16} + matrix={ + [ + -0.458649554484315, + 0.8886172326549487, + -0.8886172326549487, + -0.458649554484315, + 14.2400072574634, + 19.300266514976617, + ] + } + propList={ + [ + "fill", + ] + } + width={16} + x={0} + y={0} + /> + + - + - + - Account 1 - - + } + testID="tagbase-text" + > + Account 1 + + + @@ -328,42 +336,42 @@ exports[`AccountCard render matches snapshot 1`] = ` { "alignItems": "center", "flexDirection": "row", - "gap": 8, } } > - - - Interacting with - - + Interacting with + + + @@ -372,127 +380,255 @@ exports[`AccountCard render matches snapshot 1`] = ` { "alignItems": "center", "flexDirection": "row", - "gap": 8, } } > - - - MM Pooled Staking - + + + + + + + + + + + MM Pooled Staking + - - + + + @@ -501,42 +637,42 @@ exports[`AccountCard render matches snapshot 1`] = ` { "alignItems": "center", "flexDirection": "row", - "gap": 8, } } > - - - Network - - + Network + + + @@ -545,7 +681,6 @@ exports[`AccountCard render matches snapshot 1`] = ` { "alignItems": "center", "flexDirection": "row", - "gap": 8, } } > @@ -554,6 +689,7 @@ exports[`AccountCard render matches snapshot 1`] = ` { "alignItems": "center", "flexDirection": "row", + "gap": 8, } } > @@ -561,63 +697,52 @@ exports[`AccountCard render matches snapshot 1`] = ` style={ { "alignItems": "center", - "flexDirection": "row", - "gap": 8, + "backgroundColor": "#ffffff", + "borderRadius": 8, + "height": 16, + "justifyContent": "center", + "overflow": "hidden", + "width": 16, } } > - - - - - Ethereum Main Network - + testID="network-avatar-image" + /> + + Ethereum Main Network + - - , - ",", -] + + + `; diff --git a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/ConfirmationFooter.test.tsx b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/ConfirmationFooter.test.tsx index f9a045f68bf..b7447a17bc9 100644 --- a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/ConfirmationFooter.test.tsx +++ b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/ConfirmationFooter.test.tsx @@ -5,6 +5,7 @@ import { ConfirmationFooterProps } from './ConfirmationFooter.types'; import { createMockAccountsControllerState } from '../../../../../../util/test/accountsControllerTestUtils'; import { backgroundState } from '../../../../../../util/test/initial-root-state'; import { FooterButtonGroupActions } from './FooterButtonGroup/FooterButtonGroup.types'; +import { MOCK_POOL_STAKING_SDK } from '../../../__mocks__/mockData'; const MOCK_ADDRESS_1 = '0x0'; const MOCK_ADDRESS_2 = '0x1'; @@ -34,20 +35,8 @@ jest.mock('@react-navigation/native', () => { }; }); -jest.mock('../../../hooks/usePoolStakedDeposit', () => ({ - __esModule: true, - default: () => ({ - poolStakingContract: {}, - estimateDepositGas: jest.fn(), - attemptDepositTransaction: jest.fn(), - }), -})); - -jest.mock('../../../hooks/usePooledStakes', () => ({ - __esModule: true, - default: () => ({ - refreshPooledStakes: jest.fn(), - }), +jest.mock('../../../hooks/useStakeContext', () => ({ + useStakeContext: () => MOCK_POOL_STAKING_SDK, })); describe('ConfirmationFooter', () => { diff --git a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.test.tsx b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.test.tsx index 509c2054dcb..b67a2a409e8 100644 --- a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.test.tsx +++ b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.test.tsx @@ -9,21 +9,25 @@ import { } from './FooterButtonGroup.types'; import { createMockAccountsControllerState } from '../../../../../../../util/test/accountsControllerTestUtils'; import { backgroundState } from '../../../../../../../util/test/initial-root-state'; +import { MOCK_POOL_STAKING_SDK } from '../../../../__mocks__/mockData'; const MOCK_ADDRESS_1 = '0x0'; -const MOCK_ADDRESS_2 = '0x1'; const MOCK_ACCOUNTS_CONTROLLER_STATE = createMockAccountsControllerState([ MOCK_ADDRESS_1, - MOCK_ADDRESS_2, ]); +const mockSubscribeOnceIf = jest.fn(); + const mockInitialState = { settings: {}, engine: { backgroundState: { ...backgroundState, AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE, + controllerMessenger: { + subscribeOnceIf: mockSubscribeOnceIf, + }, }, }, }; @@ -44,12 +48,24 @@ jest.mock('@react-navigation/native', () => { }; }); +jest.mock('../../../../hooks/useStakeContext', () => ({ + useStakeContext: () => MOCK_POOL_STAKING_SDK, +})); + +const mockAttemptDepositTransaction = jest.fn(); +const mockAttemptUnstakeTransaction = jest.fn(); + jest.mock('../../../../hooks/usePoolStakedDeposit', () => ({ __esModule: true, default: () => ({ - poolStakingContract: {}, - estimateDepositGas: jest.fn(), - attemptDepositTransaction: jest.fn(), + attemptDepositTransaction: mockAttemptDepositTransaction, + }), +})); + +jest.mock('../../../../hooks/usePoolStakedUnstake', () => ({ + __esModule: true, + default: () => ({ + attemptUnstakeTransaction: mockAttemptUnstakeTransaction, }), })); @@ -101,5 +117,37 @@ describe('FooterButtonGroup', () => { expect(toJSON()).toMatchSnapshot(); }); - it.todo('confirms stake when confirm button is pressed'); + it('attempts stake transaction on continue click', () => { + const props: FooterButtonGroupProps = { + valueWei: '3210000000000000', + action: FooterButtonGroupActions.STAKE, + }; + + const { getByText, toJSON } = renderWithProvider( + , + { state: mockInitialState }, + ); + + fireEvent.press(getByText(strings('stake.continue'))); + + expect(toJSON()).toMatchSnapshot(); + expect(mockAttemptDepositTransaction).toHaveBeenCalledTimes(1); + }); + + it('attempts unstake transaction on continue click', () => { + const props: FooterButtonGroupProps = { + valueWei: '3210000000000000', + action: FooterButtonGroupActions.UNSTAKE, + }; + + const { getByText, toJSON } = renderWithProvider( + , + { state: mockInitialState }, + ); + + fireEvent.press(getByText(strings('stake.continue'))); + + expect(toJSON()).toMatchSnapshot(); + expect(mockAttemptUnstakeTransaction).toHaveBeenCalledTimes(1); + }); }); diff --git a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.tsx b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.tsx index 5a98d66c48b..c60c393fb18 100644 --- a/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.tsx +++ b/app/components/UI/Stake/components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback, useState } from 'react'; import { useNavigation } from '@react-navigation/native'; import { View } from 'react-native'; import { strings } from '../../../../../../../../locales/i18n'; @@ -22,6 +22,7 @@ import { FooterButtonGroupProps, } from './FooterButtonGroup.types'; import Routes from '../../../../../../../constants/navigation/Routes'; +import usePoolStakedUnstake from '../../../../hooks/usePoolStakedUnstake'; import usePooledStakes from '../../../../hooks/usePooledStakes'; const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { @@ -35,36 +36,78 @@ const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { const { attemptDepositTransaction } = usePoolStakedDeposit(); const { refreshPooledStakes } = usePooledStakes(); - const handleStake = async () => { - if (!activeAccount?.address) return; - - const txRes = await attemptDepositTransaction( - valueWei, - activeAccount.address, - ); - - const transactionId = txRes?.transactionMeta?.id; - - // Listening for confirmation - Engine.controllerMessenger.subscribeOnceIf( - 'TransactionController:transactionSubmitted', - () => { - navigate(Routes.TRANSACTIONS_VIEW); - }, - ({ transactionMeta }) => transactionMeta.id === transactionId, - ); - - Engine.controllerMessenger.subscribeOnceIf( - 'TransactionController:transactionConfirmed', - () => { - refreshPooledStakes(); - }, - (transactionMeta) => transactionMeta.id === transactionId, - ); - }; + const { attemptUnstakeTransaction } = usePoolStakedUnstake(); + + const [didSubmitTransaction, setDidSubmitTransaction] = useState(false); + + const listenForTransactionEvents = useCallback( + (transactionId?: string) => { + if (!transactionId) return; + + Engine.controllerMessenger.subscribeOnceIf( + 'TransactionController:transactionSubmitted', + () => { + setDidSubmitTransaction(false); + navigate(Routes.TRANSACTIONS_VIEW); + }, + ({ transactionMeta }) => transactionMeta.id === transactionId, + ); + + Engine.controllerMessenger.subscribeOnceIf( + 'TransactionController:transactionFailed', + () => { + setDidSubmitTransaction(false); + }, + ({ transactionMeta }) => transactionMeta.id === transactionId, + ); + + Engine.controllerMessenger.subscribeOnceIf( + 'TransactionController:transactionRejected', + () => { + setDidSubmitTransaction(false); + }, + ({ transactionMeta }) => transactionMeta.id === transactionId, + ); + + Engine.controllerMessenger.subscribeOnceIf( + 'TransactionController:transactionConfirmed', + () => { + refreshPooledStakes(); + }, + (transactionMeta) => transactionMeta.id === transactionId, + ); + }, + [navigate, refreshPooledStakes], + ); + + const handleConfirmation = async () => { + try { + if (!activeAccount?.address) return; + + setDidSubmitTransaction(true); + + let transactionId: string | undefined; + + if (action === FooterButtonGroupActions.STAKE) { + const txRes = await attemptDepositTransaction( + valueWei, + activeAccount.address, + ); + transactionId = txRes?.transactionMeta?.id; + } + + if (action === FooterButtonGroupActions.UNSTAKE) { + const txRes = await attemptUnstakeTransaction( + valueWei, + activeAccount.address, + ); + transactionId = txRes?.transactionMeta?.id; + } - const handleConfirmation = () => { - if (action === FooterButtonGroupActions.STAKE) return handleStake(); + listenForTransactionEvents(transactionId); + } catch (e) { + setDidSubmitTransaction(false); + } }; return ( @@ -82,6 +125,7 @@ const FooterButtonGroup = ({ valueWei, action }: FooterButtonGroupProps) => { onPress={() => { navigation.goBack(); }} + disabled={didSubmitTransaction} />