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
…3707)

Co-authored-by: jorbuedo <[email protected]>
  • Loading branch information
banklesss and jorbuedo authored Oct 24, 2024
1 parent b1df6f7 commit 5a8dcc7
Show file tree
Hide file tree
Showing 19 changed files with 511 additions and 349 deletions.
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
@@ -1,4 +1,3 @@
// import {CredKind} from '@emurgo/csl-mobile-bridge'
import {CredKind} from '@emurgo/cross-csl-core'
import {isNonNullable} from '@yoroi/common'
import {infoExtractName} from '@yoroi/portfolio'
Expand All @@ -14,6 +13,7 @@ import {asQuantity} from '../../../../yoroi-wallets/utils/utils'
import {usePortfolioTokenInfos} from '../../../Portfolio/common/hooks/usePortfolioTokenInfos'
import {useSelectedWallet} from '../../../WalletManager/common/hooks/useSelectedWallet'
import {
FormattedCertificate,
FormattedFee,
FormattedInputs,
FormattedOutputs,
Expand Down Expand Up @@ -50,12 +50,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)

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

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

const formatCertificates = (certificates: TransactionBody['certs']) => {
return (
certificates?.map((cert) => {
const [type, certificate] = Object.entries(cert)[0]
return {type, certificate} as unknown as FormattedCertificate
}) ?? null
)
}

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
108 changes: 84 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 {CertificateType, FormattedTx} 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,58 @@ export const DelegateVotingToDrepOperation = ({drepID}: {drepID: string}) => {
)
}

export const useOperations = (certificates: FormattedTx['certificates']) => {
if (certificates === null) return []

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

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

case CertificateType.StakeDelegation: {
const poolKeyHash = certificate.value.pool_keyhash ?? null
if (poolKeyHash == null) return acc
return [...acc, <StakeDelegateOperation key={index} poolId={poolKeyHash} />]
}

case CertificateType.VoteDelegation: {
const drep = certificate.value.drep

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

const drepId = ('KeyHash' in drep ? 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 5a8dcc7

Please sign in to comment.