Skip to content

Commit

Permalink
feature(wallet-mobile): new tx review for withdraw staking rewards
Browse files Browse the repository at this point in the history
  • Loading branch information
banklesss committed Oct 24, 2024
1 parent ffcc4d8 commit 28bbd8f
Show file tree
Hide file tree
Showing 21 changed files with 530 additions and 351 deletions.
1 change: 0 additions & 1 deletion apps/wallet-mobile/.storybook/storybook.requires.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ export const ReviewTxProvider = ({
dispatch({type: ReviewTxActionType.OnSuccessChanged, onSuccess}),
onErrorChanged: (onError: ReviewTxState['onError']) => dispatch({type: ReviewTxActionType.OnErrorChanged, onError}),
onNotSupportedCIP1694Changed: (onNotSupportedCIP1694: ReviewTxState['onNotSupportedCIP1694']) =>
dispatch({type: ReviewTxActionType.onNotSupportedCIP1694Changed, onNotSupportedCIP1694}),
dispatch({type: ReviewTxActionType.OnNotSupportedCIP1694Changed, onNotSupportedCIP1694}),
onCIP36SupportChangeChanged: (onCIP36SupportChange: ReviewTxState['onCIP36SupportChange']) =>
dispatch({type: ReviewTxActionType.onCIP36SupportChangeChanged, onCIP36SupportChange}),
dispatch({type: ReviewTxActionType.OnCIP36SupportChangeChanged, onCIP36SupportChange}),
reset: () => dispatch({type: ReviewTxActionType.Reset}),
}).current

const context = React.useMemo(
Expand Down Expand Up @@ -78,14 +79,26 @@ const reviewTxReducer = (state: ReviewTxState, action: ReviewTxAction) => {
draft.onError = action.onError
break

case ReviewTxActionType.onNotSupportedCIP1694Changed:
case ReviewTxActionType.OnNotSupportedCIP1694Changed:
draft.onNotSupportedCIP1694 = action.onNotSupportedCIP1694
break

case ReviewTxActionType.onCIP36SupportChangeChanged:
case ReviewTxActionType.OnCIP36SupportChangeChanged:
draft.onCIP36SupportChange = action.onCIP36SupportChange
break

case ReviewTxActionType.Reset:
draft.unsignedTx = castDraft(defaultState.unsignedTx)
draft.cbor = defaultState.cbor
draft.operations = defaultState.operations
draft.customReceiverTitle = defaultState.customReceiverTitle
draft.details = defaultState.details
draft.onSuccess = defaultState.onSuccess
draft.onError = defaultState.onError
draft.onNotSupportedCIP1694 = defaultState.onNotSupportedCIP1694
draft.onCIP36SupportChange = defaultState.onCIP36SupportChange
break

default:
throw new Error('[ReviewTxContext] invalid action')
}
Expand Down Expand Up @@ -122,13 +135,16 @@ type ReviewTxAction =
onError: ReviewTxState['onError']
}
| {
type: ReviewTxActionType.onNotSupportedCIP1694Changed
type: ReviewTxActionType.OnNotSupportedCIP1694Changed
onNotSupportedCIP1694: ReviewTxState['onNotSupportedCIP1694']
}
| {
type: ReviewTxActionType.onCIP36SupportChangeChanged
type: ReviewTxActionType.OnCIP36SupportChangeChanged
onCIP36SupportChange: ReviewTxState['onCIP36SupportChange']
}
| {
type: ReviewTxActionType.Reset
}

export type ReviewTxState = {
unsignedTx: YoroiUnsignedTx | null
Expand All @@ -152,6 +168,7 @@ type ReviewTxActions = {
onErrorChanged: (onError: ReviewTxState['onError']) => void
onNotSupportedCIP1694Changed: (onNotSupportedCIP1694: ReviewTxState['onNotSupportedCIP1694']) => void
onCIP36SupportChangeChanged: (onCIP36SupportChange: ReviewTxState['onCIP36SupportChange']) => void
reset: () => void
}

const defaultState: ReviewTxState = Object.freeze({
Expand Down Expand Up @@ -181,6 +198,7 @@ const initialReviewTxContext: ReviewTxContext = {
onErrorChanged: missingInit,
onNotSupportedCIP1694Changed: missingInit,
onCIP36SupportChangeChanged: missingInit,
reset: missingInit,
}

enum ReviewTxActionType {
Expand All @@ -191,8 +209,9 @@ enum ReviewTxActionType {
DetailsChanged = 'detailsChanged',
OnSuccessChanged = 'onSuccessChanged',
OnErrorChanged = 'onErrorChanged',
onNotSupportedCIP1694Changed = 'onNotSupportedCIP1694Changed',
onCIP36SupportChangeChanged = 'onCIP36SupportChangeChanged',
OnNotSupportedCIP1694Changed = 'onNotSupportedCIP1694Changed',
OnCIP36SupportChangeChanged = 'onCIP36SupportChangeChanged',
Reset = 'reset',
}

type ReviewTxContext = ReviewTxState & ReviewTxActions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {asQuantity} from '../../../../yoroi-wallets/utils/utils'
import {usePortfolioTokenInfos} from '../../../Portfolio/common/hooks/usePortfolioTokenInfos'
import {useSelectedWallet} from '../../../WalletManager/common/hooks/useSelectedWallet'
import {
Certificates,
FormattedCertificates,
FormattedFee,
FormattedInputs,
FormattedOutputs,
Expand Down Expand Up @@ -50,12 +52,13 @@ export const useFormattedTx = (data: TransactionBody): FormattedTx => {
const formattedInputs = useFormattedInputs(wallet, inputs, portfolioTokenInfos)
const formattedOutputs = useFormattedOutputs(wallet, outputs, portfolioTokenInfos)
const formattedFee = formatFee(wallet, data)
const formattedCertificates = formatCertificates(data.certs as Certificates)

return {
inputs: formattedInputs,
outputs: formattedOutputs,
fee: formattedFee,
certificates: data.certs ?? null,
certificates: formattedCertificates,
}
}

Expand Down Expand Up @@ -220,6 +223,10 @@ export const formatFee = (wallet: YoroiWallet, data: TransactionBody): Formatted
}
}

const formatCertificates = (certificates: Certificates) => {
return certificates.flatMap(Object.entries) as FormattedCertificates
}

const deriveAddress = async (address: string, chainId: number) => {
try {
return await deriveRewardAddressFromAddress(address, chainId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {ConfirmTxWithSpendingPasswordModal} from '../../../../components/Confirm
import {useModal} from '../../../../components/Modal/ModalContext'
import {YoroiSignedTx, YoroiUnsignedTx} from '../../../../yoroi-wallets/types/yoroi'
import {useSelectedWallet} from '../../../WalletManager/common/hooks/useSelectedWallet'
import {useReviewTx} from '../ReviewTxProvider'
import {useStrings} from './useStrings'

// TODO: make it compatible with CBOR signing
Expand All @@ -28,6 +29,16 @@ export const useOnConfirm = ({
const {meta} = useSelectedWallet()
const {openModal, closeModal} = useModal()
const strings = useStrings()
const {reset} = useReviewTx()

const handleOnSuccess = (signedTx: YoroiSignedTx) => {
onSuccess?.(signedTx)
reset()
}
const handleOnError = () => {
onError?.()
reset()
}

const onConfirm = () => {
if (meta.isHW) {
Expand All @@ -36,7 +47,7 @@ export const useOnConfirm = ({
<ConfirmTxWithHwModal
onCancel={closeModal}
unsignedTx={unsignedTx}
onSuccess={(signedTx) => onSuccess?.(signedTx)}
onSuccess={handleOnSuccess}
onNotSupportedCIP1694={() => {
if (onNotSupportedCIP1694) {
closeModal()
Expand All @@ -55,8 +66,8 @@ export const useOnConfirm = ({
strings.signTransaction,
<ConfirmTxWithSpendingPasswordModal
unsignedTx={unsignedTx}
onSuccess={(signedTx) => onSuccess?.(signedTx)}
onError={onError ?? undefined}
onSuccess={handleOnSuccess}
onError={handleOnError}
/>,
)
return
Expand All @@ -65,11 +76,7 @@ export const useOnConfirm = ({
if (!meta.isHW && meta.isEasyConfirmationEnabled) {
openModal(
strings.signTransaction,
<ConfirmTxWithOsModal
unsignedTx={unsignedTx}
onSuccess={(signedTx) => onSuccess?.(signedTx)}
onError={onError ?? undefined}
/>,
<ConfirmTxWithOsModal unsignedTx={unsignedTx} onSuccess={handleOnSuccess} onError={handleOnError} />,
)
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export const useStrings = () => {
selectNoConfidence: intl.formatMessage(messages.selectNoConfidence),
delegateVotingToDRep: intl.formatMessage(messages.delegateVotingToDRep),
delegateStake: intl.formatMessage(messages.delegateStake),
deregisterStakingKey: intl.formatMessage(messages.deregisterStakingKey),
rewardsWithdrawalLabel: intl.formatMessage(messages.rewardsWithdrawalLabel),
rewardsWithdrawalText: intl.formatMessage(messages.rewardsWithdrawalText),
}
}

Expand Down Expand Up @@ -160,6 +163,18 @@ const messages = defineMessages({
id: 'txReview.operations.registerStakingKey',
defaultMessage: '!!!Register staking key deposit',
},
deregisterStakingKey: {
id: 'txReview.operations.deregisterStakingKey',
defaultMessage: '!!!Deregister staking key',
},
rewardsWithdrawalLabel: {
id: 'txReview.operations.rewardsWithdrawal.label',
defaultMessage: '!!!Staking',
},
rewardsWithdrawalText: {
id: 'txReview.operations.rewardsWithdrawal.text',
defaultMessage: '!!!Rewards withdrawal',
},
selectAbstain: {
id: 'txReview.operations.selectAbstain',
defaultMessage: '!!!Select abstain',
Expand Down
109 changes: 85 additions & 24 deletions apps/wallet-mobile/src/features/ReviewTx/common/operations.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import {PoolInfoApi} from '@emurgo/yoroi-lib'
import {useBech32DRepID} from '@yoroi/staking'
import {useTheme} from '@yoroi/theme'
import * as React from 'react'
import {Linking, StyleSheet, Text, View} from 'react-native'
import {TouchableOpacity} from 'react-native-gesture-handler'
import {useQuery} from 'react-query'

import {Space} from '../../../components/Space/Space'
import {getPoolBech32Id} from '../../../yoroi-wallets/cardano/delegationUtils'
import {wrappedCsl} from '../../../yoroi-wallets/cardano/wrappedCsl'
import {usePoolInfo} from '../../../yoroi-wallets/hooks'
import {formatTokenWithText} from '../../../yoroi-wallets/utils/format'
import {asQuantity} from '../../../yoroi-wallets/utils/utils'
import {useSelectedNetwork} from '../../WalletManager/common/hooks/useSelectedNetwork'
import {useSelectedWallet} from '../../WalletManager/common/hooks/useSelectedWallet'
import {useStrings} from './hooks/useStrings'
import {Certificate, CertificateTypes, FormattedCertificates} from './types'

export const RegisterStakingKeyOperation = () => {
export const StakeRegistrationOperation = () => {
const {styles} = useStyles()
const strings = useStrings()
const {wallet} = useSelectedWallet()
Expand All @@ -31,7 +31,32 @@ export const RegisterStakingKeyOperation = () => {
</View>
)
}
export const DelegateStakeOperation = ({poolId}: {poolId: string}) => {

export const StakeDeregistrationOperation = () => {
const {styles} = useStyles()
const strings = useStrings()

return (
<View style={styles.operation}>
<Text style={styles.operationLabel}>{strings.deregisterStakingKey}</Text>
</View>
)
}

export const StakeRewardsWithdrawalOperation = () => {
const {styles} = useStyles()
const strings = useStrings()

return (
<View style={styles.operation}>
<Text style={styles.operationLabel}>{strings.rewardsWithdrawalLabel}</Text>

<Text style={styles.operationValue}>{strings.rewardsWithdrawalText}</Text>
</View>
)
}

export const StakeDelegateOperation = ({poolId}: {poolId: string}) => {
const {styles} = useStyles()
const strings = useStrings()
const poolInfo = usePoolInfo({poolId})
Expand All @@ -55,23 +80,6 @@ export const DelegateStakeOperation = ({poolId}: {poolId: string}) => {
)
}

export const usePoolInfo = ({poolId}: {poolId: string}) => {
const {networkManager} = useSelectedNetwork()
const poolInfoApi = React.useMemo(
() => new PoolInfoApi(networkManager.legacyApiBaseUrl),
[networkManager.legacyApiBaseUrl],
)
const poolInfo = useQuery({
queryKey: ['usePoolInfoStakeOperation', poolId],
queryFn: async () => {
const poolBech32Id = await getPoolBech32Id(poolId)
return poolInfoApi.getSingleExplorerPoolInfo(poolBech32Id)
},
})

return poolInfo?.data ?? null
}

export const AbstainOperation = () => {
const {styles} = useStyles()
const strings = useStrings()
Expand All @@ -94,11 +102,11 @@ export const NoConfidenceOperation = () => {
)
}

export const DelegateVotingToDrepOperation = ({drepID}: {drepID: string}) => {
export const VoteDelegationOperation = ({drepID}: {drepID: string}) => {
const {styles} = useStyles()
const strings = useStrings()

const {data: bech32DrepId} = useBech32DRepID(drepID)
const bech32DrepId = useDrepBech32Id(drepID)

return (
<View style={styles.operation}>
Expand All @@ -111,6 +119,59 @@ export const DelegateVotingToDrepOperation = ({drepID}: {drepID: string}) => {
)
}

export const useOperations = (certificates: FormattedCertificates | null) => {
if (certificates === null) return []

return certificates.reduce<React.ReactNode[]>((acc, [certificateKind, CertificateData], index) => {
switch (certificateKind) {
case CertificateTypes.StakeRegistration:
return [...acc, <StakeRegistrationOperation key={index} />]

case CertificateTypes.StakeDeregistration:
return [...acc, <StakeDeregistrationOperation key={index} />]

case CertificateTypes.StakeDelegation: {
const poolKeyHash = (CertificateData as Certificate[CertificateTypes.StakeDelegation]).pool_keyhash ?? null
if (poolKeyHash == null) return acc
return [...acc, <StakeDelegateOperation key={index} poolId={poolKeyHash} />]
}

case CertificateTypes.VoteDelegation: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const drep: any = (CertificateData as Certificate[CertificateTypes.VoteDelegation]).drep

if (drep === 'AlwaysAbstain') return [...acc, <AbstainOperation key={index} />]
if (drep === 'AlwaysNoConfidence') return [...acc, <NoConfidenceOperation key={index} />]

const drepId = drep.KeyHash ?? drep.ScriptHash ?? ''
return [...acc, <VoteDelegationOperation key={index} drepID={drepId} />]
}

default:
return acc
}
}, [])
}

export const getDrepBech32Id = async (poolId: string) => {
const {csl, release} = wrappedCsl()
try {
const keyHash = await csl.Ed25519KeyHash.fromHex(poolId)
return keyHash.toBech32('drep')
} finally {
release()
}
}

export const useDrepBech32Id = (poolId: string) => {
const query = useQuery({
queryKey: ['drepBech32', poolId],
queryFn: () => getDrepBech32Id(poolId),
})

return query?.data ?? null
}

const useStyles = () => {
const {color, atoms} = useTheme()

Expand Down
Loading

0 comments on commit 28bbd8f

Please sign in to comment.