Skip to content

Commit

Permalink
feat(earn): prepare transactions and use on confirmation screen (#6192)
Browse files Browse the repository at this point in the history
### Description

Handles the preparation of all earn transactions via a shared hook
that's used on both `EarnEnterAmount.tsx` &
`EarnConfirmationScreen.tsx`.

### Test plan

- [x] Tested locally on iOS
- [x] Tested locally on Andorid
- [x] Unit Tests updated

### Related issues

- Part of ACT-1389

### Backwards compatibility

Yes

### Network scalability

N/A

---------

Co-authored-by: Finnian Jacobson-Schulte <[email protected]>
Co-authored-by: Jacob Waterman <[email protected]>
  • Loading branch information
3 people authored Nov 8, 2024
1 parent d7a0abb commit 40d6606
Show file tree
Hide file tree
Showing 14 changed files with 770 additions and 145 deletions.
97 changes: 78 additions & 19 deletions src/earn/EarnConfirmationScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { Provider } from 'react-redux'
import AppAnalytics from 'src/analytics/AppAnalytics'
import { EarnEvents } from 'src/analytics/Events'
import EarnConfirmationScreen from 'src/earn/EarnConfirmationScreen'
import { prepareWithdrawAndClaimTransactions } from 'src/earn/prepareTransactions'
import {
prepareClaimTransactions,
prepareWithdrawAndClaimTransactions,
prepareWithdrawTransactions,
} from 'src/earn/prepareTransactions'
import { withdrawStart } from 'src/earn/slice'
import { isGasSubsidizedForNetwork } from 'src/earn/utils'
import { navigate } from 'src/navigator/NavigationService'
Expand Down Expand Up @@ -92,8 +96,9 @@ const mockPreparedTransaction: PreparedTransactionsPossible = {
describe('EarnConfirmationScreen', () => {
beforeEach(() => {
jest.clearAllMocks()

jest.mocked(prepareWithdrawAndClaimTransactions).mockResolvedValue(mockPreparedTransaction)
jest.mocked(prepareClaimTransactions).mockResolvedValue(mockPreparedTransaction)
jest.mocked(prepareWithdrawTransactions).mockResolvedValue(mockPreparedTransaction)
jest
.mocked(getFeatureGate)
.mockImplementation(
Expand All @@ -103,14 +108,15 @@ describe('EarnConfirmationScreen', () => {
store.clearActions()
})

it('renders total balance, rewards and gas after fetching rewards and preparing tx for exit', async () => {
it('renders total balance, rewards and gas after fetching rewards and preparing tx', async () => {
const { getByText, getByTestId, queryByTestId } = render(
<Provider store={store}>
<MockedNavigator
component={EarnConfirmationScreen}
params={{
pool: { ...mockEarnPositions[0], balance: '10.75' },
mode: 'Exit',
mode: 'exit',
useMax: true,
}}
/>
</Provider>
Expand All @@ -124,6 +130,7 @@ describe('EarnConfirmationScreen', () => {
expect(getByTestId(`EarnConfirmation/${mockArbUsdcTokenId}/FiatAmount`)).toHaveTextContent(
'₱15.73'
)

expect(getByTestId('EarnConfirmation/GasLoading')).toBeTruthy()
expect(getByTestId('EarnConfirmationScreen/CTA')).toBeDisabled()

Expand All @@ -138,6 +145,7 @@ describe('EarnConfirmationScreen', () => {
await waitFor(() => {
expect(queryByTestId('EarnConfirmation/GasLoading')).toBeFalsy()
})

expect(getByTestId('EarnConfirmation/GasFeeCryptoAmount')).toHaveTextContent('0.06 ETH')
expect(getByTestId('EarnConfirmation/GasFeeFiatAmount')).toHaveTextContent('₱119.70')
expect(queryByTestId('EarnConfirmation/GasSubsidized')).toBeFalsy()
Expand All @@ -149,19 +157,22 @@ describe('EarnConfirmationScreen', () => {
walletAddress: mockAccount.toLowerCase(),
hooksApiUrl: 'https://api.alfajores.valora.xyz/hooks-api',
amount: '10.75',
useMax: true,
})
expect(store.getActions()).toEqual([])
})

it('renders total balance and gas after fetching rewards and preparing tx for partial withdrawal', async () => {
it('renders total balance, rewards and gas after fetching rewards and preparing tx for partial withdrawal', async () => {
const inputAmount = (10.75 * +mockEarnPositions[0].pricePerShare) / 2 // Input amount is half of the balance
const txAmount = '5.37500000000000045455' // inputAmount divided by pricePerShare but with more precision
const { getByText, getByTestId, queryByTestId, queryByText } = render(
<Provider store={store}>
<MockedNavigator
component={EarnConfirmationScreen}
params={{
pool: { ...mockEarnPositions[0], balance: '10.75' },
mode: 'withdraw',
inputAmount: (10.75 * +mockEarnPositions[0].pricePerShare) / 2, // Input amount is half of the balance
inputAmount,
}}
/>
</Provider>
Expand All @@ -175,8 +186,6 @@ describe('EarnConfirmationScreen', () => {
expect(getByTestId(`EarnConfirmation/${mockArbUsdcTokenId}/FiatAmount`)).toHaveTextContent(
'₱7.86'
)
expect(getByTestId('EarnConfirmation/GasLoading')).toBeTruthy()
expect(getByTestId('EarnConfirmationScreen/CTA')).toBeDisabled()

expect(queryByText('earnFlow.collect.reward')).toBeFalsy()

Expand All @@ -187,14 +196,60 @@ describe('EarnConfirmationScreen', () => {
expect(getByTestId('EarnConfirmation/GasFeeFiatAmount')).toHaveTextContent('₱119.70')
expect(queryByTestId('EarnConfirmation/GasSubsidized')).toBeFalsy()
expect(getByTestId('EarnConfirmationScreen/CTA')).toBeEnabled()
// TODO(act-1389): update this test to make sure that reward positions are not included in partial withdrawals.
expect(prepareWithdrawAndClaimTransactions).toHaveBeenCalledWith({
expect(prepareWithdrawTransactions).toHaveBeenCalledWith({
feeCurrencies: mockStoreBalancesToTokenBalances([mockTokenBalances[mockArbEthTokenId]]),
pool: { ...mockEarnPositions[0], balance: '10.75' },
rewardsPositions: [mockRewardsPositions[1]],
walletAddress: mockAccount.toLowerCase(),
hooksApiUrl: 'https://api.alfajores.valora.xyz/hooks-api',
amount: '5.37500000000000045455',
amount: txAmount,
})
expect(queryByText('earnFlow.collect.reward')).toBeFalsy()
expect(store.getActions()).toEqual([])
})

it('renders rewards and gas after fetching rewards and preparing tx for claim rewards', async () => {
const { getByText, getByTestId, queryByTestId } = render(
<Provider store={store}>
<MockedNavigator
component={EarnConfirmationScreen}
params={{
pool: { ...mockEarnPositions[0], balance: '10.75' },
mode: 'claim-rewards',
useMax: true,
}}
/>
</Provider>
)

expect(getByText('earnFlow.collect.titleClaim')).toBeTruthy()
expect(getByTestId('EarnConfirmation/GasLoading')).toBeTruthy()
expect(getByTestId('EarnConfirmationScreen/CTA')).toBeDisabled()

expect(getByText('earnFlow.collect.reward')).toBeTruthy()
expect(getByTestId(`EarnConfirmation/${mockArbArbTokenId}/CryptoAmount`)).toHaveTextContent(
'0.01 ARB'
)
expect(getByTestId(`EarnConfirmation/${mockArbArbTokenId}/FiatAmount`)).toHaveTextContent(
'₱0.016'
)

await waitFor(() => {
expect(queryByTestId('EarnConfirmation/GasLoading')).toBeFalsy()
})

expect(getByTestId('EarnConfirmation/GasFeeCryptoAmount')).toHaveTextContent('0.06 ETH')
expect(getByTestId('EarnConfirmation/GasFeeFiatAmount')).toHaveTextContent('₱119.70')
expect(queryByTestId('EarnConfirmation/GasSubsidized')).toBeFalsy()
expect(getByTestId('EarnConfirmationScreen/CTA')).toBeEnabled()
expect(prepareClaimTransactions).toHaveBeenCalledWith({
feeCurrencies: mockStoreBalancesToTokenBalances([mockTokenBalances[mockArbEthTokenId]]),
pool: { ...mockEarnPositions[0], balance: '10.75' },
walletAddress: mockAccount.toLowerCase(),
hooksApiUrl: 'https://api.alfajores.valora.xyz/hooks-api',
amount: '10.75',
useMax: true,
rewardsPositions: [mockRewardsPositions[1]],
})
expect(store.getActions()).toEqual([])
})
Expand All @@ -218,6 +273,7 @@ describe('EarnConfirmationScreen', () => {
params={{
pool: { ...mockEarnPositions[0], balance: '10.75' },
mode: 'withdraw',
useMax: true,
}}
/>
</Provider>
Expand Down Expand Up @@ -245,24 +301,22 @@ describe('EarnConfirmationScreen', () => {
})

it('shows error and keeps cta disabled if prepare tx fails', async () => {
jest
.mocked(prepareWithdrawAndClaimTransactions)
.mockRejectedValue(new Error('Failed to prepare'))
jest.mocked(prepareWithdrawTransactions).mockRejectedValue(new Error('Failed to prepare'))
const { getByText, getByTestId, queryByTestId } = render(
<Provider store={store}>
<MockedNavigator
component={EarnConfirmationScreen}
params={{
pool: mockEarnPositions[0],
mode: 'withdraw',
useMax: true,
}}
/>
</Provider>
)

expect(getByText('earnFlow.collect.titleWithdraw')).toBeTruthy()
expect(getByText('earnFlow.collect.total')).toBeTruthy()
expect(getByTestId('EarnConfirmation/GasLoading')).toBeTruthy()
expect(getByTestId('EarnConfirmationScreen/CTA')).toBeDisabled()

await waitFor(() => {
Expand All @@ -274,25 +328,26 @@ describe('EarnConfirmationScreen', () => {
})

it('disables cta if not enough balance for gas', async () => {
jest.mocked(prepareWithdrawAndClaimTransactions).mockResolvedValue({
jest.mocked(prepareWithdrawTransactions).mockResolvedValue({
type: 'not-enough-balance-for-gas',
feeCurrencies: [mockPreparedTransaction.feeCurrency],
})

const { getByText, getByTestId, queryByTestId } = render(
<Provider store={store}>
<MockedNavigator
component={EarnConfirmationScreen}
params={{
pool: mockEarnPositions[0],
mode: 'withdraw',
useMax: true,
}}
/>
</Provider>
)

expect(getByText('earnFlow.collect.titleWithdraw')).toBeTruthy()
expect(getByText('earnFlow.collect.total')).toBeTruthy()
expect(getByTestId('EarnConfirmation/GasLoading')).toBeTruthy()
expect(getByTestId('EarnConfirmationScreen/CTA')).toBeDisabled()

await waitFor(() => {
Expand All @@ -310,6 +365,7 @@ describe('EarnConfirmationScreen', () => {
params={{
pool: { ...mockEarnPositions[0], balance: '10.75' },
mode: 'withdraw',
useMax: true,
}}
/>
</Provider>
Expand Down Expand Up @@ -360,6 +416,7 @@ describe('EarnConfirmationScreen', () => {
params={{
pool: mockEarnPositions[0],
mode: 'withdraw',
useMax: true,
}}
/>
</Provider>
Expand All @@ -376,18 +433,18 @@ describe('EarnConfirmationScreen', () => {
})

it('navigate and fire analytics on no gas CTA press', async () => {
jest.mocked(prepareWithdrawAndClaimTransactions).mockResolvedValue({
jest.mocked(prepareWithdrawTransactions).mockResolvedValue({
type: 'not-enough-balance-for-gas',
feeCurrencies: [mockPreparedTransaction.feeCurrency],
})

const { getByText, queryByTestId } = render(
<Provider store={store}>
<MockedNavigator
component={EarnConfirmationScreen}
params={{
pool: mockEarnPositions[0],
mode: 'withdraw',
useMax: true,
}}
/>
</Provider>
Expand Down Expand Up @@ -428,6 +485,7 @@ describe('EarnConfirmationScreen', () => {
params={{
pool: mockEarnPositions[0],
mode: 'withdraw',
useMax: true,
}}
/>
</Provider>
Expand All @@ -447,6 +505,7 @@ describe('EarnConfirmationScreen', () => {
params={{
pool: mockEarnPositions[0],
mode,
useMax: true,
}}
/>
</Provider>
Expand Down
5 changes: 2 additions & 3 deletions src/earn/EarnConfirmationScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Button, { BtnSizes } from 'src/components/Button'
import InLineNotification, { NotificationVariant } from 'src/components/InLineNotification'
import TokenDisplay from 'src/components/TokenDisplay'
import TokenIcon, { IconSize } from 'src/components/TokenIcon'
import { usePrepareWithdrawAndClaimTransactions } from 'src/earn/hooks'
import { usePrepareEarnConfirmationScreenTransactions } from 'src/earn/hooks'
import { withdrawStatusSelector } from 'src/earn/selectors'
import { withdrawStart } from 'src/earn/slice'
import { EarnActiveMode } from 'src/earn/types'
Expand Down Expand Up @@ -77,12 +77,11 @@ export default function EarnConfirmationScreen({ route }: Props) {
[withdrawToken, pool.pricePerShare, inputAmount]
)

// Will need this to handle preparing a claim transaction, a withdrawal transaction and a withdrawal and claim transaction
const {
result: prepareTransactionsResult,
loading: isPreparingTransactions,
error: prepareTransactionError,
} = usePrepareWithdrawAndClaimTransactions({
} = usePrepareEarnConfirmationScreenTransactions(mode, {
amount: withdrawAmountInDepositToken.dividedBy(pool.pricePerShare[0]).toString(),
pool,
walletAddress,
Expand Down
Loading

0 comments on commit 40d6606

Please sign in to comment.