Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature(wallet-mobile): new tx review for withdraw staking rewards #3707

Merged
merged 7 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is not a good idea to have objects that are not serializable in the state, (eg. if we decide to leverage persisted state this will fail), and context also can quickly become a problem

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. It is a temporal solution until the common error screens are implemented

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
Loading