From ef6f94fbbf493bc3dc5cc40eb10777ec61ed4720 Mon Sep 17 00:00:00 2001 From: Vladimir Volek Date: Wed, 11 Dec 2024 03:27:48 +0100 Subject: [PATCH] feat: cardano review modal with governance fix: modal heading fix: text Update packages/suite/src/components/suite/modals/ReduxModal/CardanoWithdrawModal.tsx Co-authored-by: Marek Mahut review fix: new modal fix: modal fix: migration fix(suite): Cardano withdraw modal UI fix(suite): Cardano withdraw modal UI - buttons --- .../blockchain-link-types/src/blockfrost.ts | 16 ++++ packages/blockchain-link-types/src/common.ts | 8 ++ .../actions/wallet/cardanoStakingActions.ts | 64 +++++++++----- .../constants/cardanoStakingConstants.ts | 2 +- .../ReduxModal/CardanoWithdrawModal.tsx | 61 ++++++++++++++ .../UserContextModal/UserContextModal.tsx | 3 + .../src/hooks/wallet/useCardanoStaking.ts | 83 +++++++++++++++---- .../__tests__/cardanoStakingReducer.test.ts | 8 +- .../reducers/wallet/cardanoStakingReducer.ts | 23 ++++- .../suite/src/storage/migrations/index.ts | 8 ++ .../suite/src/support/extraDependencies.ts | 2 +- packages/suite/src/support/messages.ts | 20 +++++ .../suite/src/types/wallet/cardanoStaking.ts | 1 + .../staking/components/CardanoRewards.tsx | 15 +++- packages/urls/src/urls.ts | 4 + .../redux-utils/src/extraDependenciesType.ts | 2 +- suite-common/suite-types/src/modal.ts | 3 + .../test-utils/src/extraDependenciesMock.ts | 2 +- .../src/blockchain/blockchainMiddleware.ts | 4 +- suite-common/wallet-types/src/account.ts | 8 ++ .../wallet-types/src/cardanoStaking.ts | 22 ++++- .../src/__fixtures__/cardanoUtils.ts | 4 +- suite-common/wallet-utils/src/accountUtils.ts | 1 + suite-common/wallet-utils/src/cardanoUtils.ts | 10 ++- suite-common/wallet-utils/src/index.ts | 2 +- 25 files changed, 317 insertions(+), 59 deletions(-) create mode 100644 packages/suite/src/components/suite/modals/ReduxModal/CardanoWithdrawModal.tsx diff --git a/packages/blockchain-link-types/src/blockfrost.ts b/packages/blockchain-link-types/src/blockfrost.ts index afcb214d94e..4ee80dcd594 100644 --- a/packages/blockchain-link-types/src/blockfrost.ts +++ b/packages/blockchain-link-types/src/blockfrost.ts @@ -169,6 +169,22 @@ export interface BlockfrostAccountInfo { total: number; index: number; }; + misc: { + staking: { + address: string; + isActive: boolean; + rewards: string; + poolId: string | null; + drep: { + drep_id: string; + hex: string; + amount: string; + active: boolean; + active_epoch: number | null; + has_script: boolean; + } | null; + }; + }; } export interface ParseAssetResult { diff --git a/packages/blockchain-link-types/src/common.ts b/packages/blockchain-link-types/src/common.ts index 0748dad8a70..9b0015d21f4 100644 --- a/packages/blockchain-link-types/src/common.ts +++ b/packages/blockchain-link-types/src/common.ts @@ -239,6 +239,14 @@ export interface AccountInfo { isActive: boolean; rewards: string; poolId: string | null; + drep: { + drep_id: string; + hex: string; + amount: string; + active: boolean; + active_epoch: number | null; + has_script: boolean; + } | null; }; // SOL owner?: string; // The Solana program owning the account diff --git a/packages/suite/src/actions/wallet/cardanoStakingActions.ts b/packages/suite/src/actions/wallet/cardanoStakingActions.ts index 2eb4b13990d..6c66d8888eb 100644 --- a/packages/suite/src/actions/wallet/cardanoStakingActions.ts +++ b/packages/suite/src/actions/wallet/cardanoStakingActions.ts @@ -1,14 +1,24 @@ import { getUnixTime } from 'date-fns'; import { BlockchainBlock } from '@trezor/connect'; -import { CARDANO_STAKE_POOL_PREVIEW_URL, CARDANO_STAKE_POOL_MAINNET_URL } from '@trezor/urls'; -import { isPending, getAccountTransactions } from '@suite-common/wallet-utils'; +import { + CARDANO_STAKE_POOL_PREVIEW_URL, + CARDANO_STAKE_POOL_MAINNET_URL, + CARDANO_MAINNET_DREP, + CARDANO_PREVIEW_DREP, +} from '@trezor/urls'; +import { isPending, getAccountTransactions, getNetworkName } from '@suite-common/wallet-utils'; import { CARDANO_DEFAULT_TTL_OFFSET } from '@suite-common/wallet-constants'; import { transactionsActions } from '@suite-common/wallet-core'; import { getNetworkOptional } from '@suite-common/wallet-config'; import { CARDANO_STAKING } from 'src/actions/wallet/constants'; -import { PendingStakeTx, PoolsResponse, CardanoNetwork } from 'src/types/wallet/cardanoStaking'; +import { + PendingStakeTx, + PoolsResponse, + CardanoNetwork, + DRepResponse, +} from 'src/types/wallet/cardanoStaking'; import { Account, WalletAccountTransaction } from 'src/types/wallet'; import { Dispatch, GetState } from 'src/types/suite'; @@ -16,8 +26,9 @@ export type CardanoStakingAction = | { type: typeof CARDANO_STAKING.ADD_PENDING_STAKE_TX; pendingStakeTx: PendingStakeTx } | { type: typeof CARDANO_STAKING.REMOVE_PENDING_STAKE_TX; accountKey: string } | { - type: typeof CARDANO_STAKING.SET_TREZOR_POOLS; + type: typeof CARDANO_STAKING.SET_TREZOR_DATA; trezorPools: PoolsResponse; + trezorDRep: DRepResponse; network: CardanoNetwork; } | { type: typeof CARDANO_STAKING.SET_FETCH_ERROR; error: boolean; network: CardanoNetwork } @@ -102,8 +113,8 @@ export const validatePendingStakeTxOnTx = } }; -export const fetchTrezorPools = (network: 'ADA' | 'tADA') => async (dispatch: Dispatch) => { - const cardanoNetwork = network === 'ADA' ? 'mainnet' : 'preview'; +export const fetchTrezorData = (network: 'ADA' | 'tADA') => async (dispatch: Dispatch) => { + const cardanoNetwork = getNetworkName(network); dispatch({ type: CARDANO_STAKING.SET_FETCH_LOADING, @@ -111,25 +122,38 @@ export const fetchTrezorPools = (network: 'ADA' | 'tADA') => async (dispatch: Di network: cardanoNetwork, }); - // Fetch ID of Trezor stake pool that will be used in delegation transaction - const url = - cardanoNetwork === 'mainnet' - ? CARDANO_STAKE_POOL_MAINNET_URL - : CARDANO_STAKE_POOL_PREVIEW_URL; - try { - const response = await fetch(url, { credentials: 'same-origin' }); - const responseJson = await response.json(); - - if (!responseJson || !('next' in responseJson) || !('pools' in responseJson)) { - // todo: even if this happens, error will be overridden by this bug - // https://github.com/trezor/trezor-suite/issues/5485 + // Fetch ID of Trezor stake pool that will be used in delegation transaction + const urlPools = + cardanoNetwork === 'mainnet' + ? CARDANO_STAKE_POOL_MAINNET_URL + : CARDANO_STAKE_POOL_PREVIEW_URL; + + const responsePools = await fetch(urlPools, { credentials: 'same-origin' }); + const responsePoolsJson = await responsePools.json(); + + if ( + !responsePoolsJson || + !('next' in responsePoolsJson) || + !('pools' in responsePoolsJson) + ) { throw new Error('Cardano: fetchTrezorPools: Invalid data format'); } + // Fetch DRep for transaction withdrawal + const urlDRep = cardanoNetwork === 'mainnet' ? CARDANO_MAINNET_DREP : CARDANO_PREVIEW_DREP; + + const responseDRep = await fetch(urlDRep, { credentials: 'same-origin' }); + const responseDRepJson = await responseDRep.json(); + + if (!responseDRepJson || !('drep' in responseDRepJson)) { + throw new Error('Cardano: fetchTrezorDRep: Invalid data format'); + } + dispatch({ - type: CARDANO_STAKING.SET_TREZOR_POOLS, - trezorPools: responseJson as PoolsResponse, + type: CARDANO_STAKING.SET_TREZOR_DATA, + trezorPools: responsePoolsJson as PoolsResponse, + trezorDRep: responseDRepJson as DRepResponse, network: cardanoNetwork, }); } catch { diff --git a/packages/suite/src/actions/wallet/constants/cardanoStakingConstants.ts b/packages/suite/src/actions/wallet/constants/cardanoStakingConstants.ts index 3a68120c898..1c1bad7d2c0 100644 --- a/packages/suite/src/actions/wallet/constants/cardanoStakingConstants.ts +++ b/packages/suite/src/actions/wallet/constants/cardanoStakingConstants.ts @@ -1,6 +1,6 @@ export const ADD_PENDING_STAKE_TX = '@cardano-staking/set-pending-stake-tx'; export const REMOVE_PENDING_STAKE_TX = '@cardano-staking/remove-pending-stake-tx'; export const IS_LOADING = '@cardano-staking/is-loading'; -export const SET_TREZOR_POOLS = '@cardano-staking/set-trezor-pools'; +export const SET_TREZOR_DATA = '@cardano-staking/set-trezor-data'; export const SET_FETCH_LOADING = '@cardano-staking/set-fetch-loading'; export const SET_FETCH_ERROR = '@cardano-staking/set-fetch-error'; diff --git a/packages/suite/src/components/suite/modals/ReduxModal/CardanoWithdrawModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/CardanoWithdrawModal.tsx new file mode 100644 index 00000000000..b95512c34f2 --- /dev/null +++ b/packages/suite/src/components/suite/modals/ReduxModal/CardanoWithdrawModal.tsx @@ -0,0 +1,61 @@ +import { Icon, Link, NewModal, Row, Column, Text, Card, Paragraph } from '@trezor/components'; +import { spacings } from '@trezor/theme'; +import { getNetworkName } from '@suite-common/wallet-utils'; + +import { useSelector } from 'src/hooks/suite/useSelector'; +import { useCardanoStaking } from 'src/hooks/wallet/useCardanoStaking'; +import { Translation } from 'src/components/suite'; +import { selectSelectedAccount } from 'src/reducers/wallet/selectedAccountReducer'; + +export const CardanoWithdrawModal = ({ onCancel }: { onCancel: () => void }) => { + const { voteAbstain, voteDelegate } = useCardanoStaking(); + const account = useSelector(state => selectSelectedAccount(state)); + + if (!account || account.networkType !== 'cardano') { + throw Error( + 'CardanoWithdrawModal used for other network or account in selectedAccount is undefined', + ); + } + + const cardanoNetwork = getNetworkName(account.symbol); + const { trezorDRep } = useSelector(state => state.wallet.cardanoStaking[cardanoNetwork]); + const trezorDRepBech32 = trezorDRep?.drep.bech32; + + return ( + } + bottomContent={ + <> + voteDelegate()}> + + + voteAbstain()} variant="tertiary"> + + + + } + > + + + + + + + + + + + {trezorDRepBech32} + + + + + + + + + + + ); +}; diff --git a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UserContextModal.tsx b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UserContextModal.tsx index 215e70cb0a6..fedd3135c84 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UserContextModal.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/UserContextModal/UserContextModal.tsx @@ -50,6 +50,7 @@ import type { ReduxModalProps } from '../ReduxModal'; import { EverstakeModal } from './UnstakeModal/EverstakeModal'; import { PassphraseMismatchModal } from './PassphraseMismatchModal'; import { FirmwareRevisionOptOutModal } from './FirmwareRevisionOptOutModal'; +import { CardanoWithdrawModal } from '../CardanoWithdrawModal'; /** Modals opened as a result of user action */ export const UserContextModal = ({ @@ -105,6 +106,8 @@ export const UserContextModal = ({ ); case 'review-transaction': return ; + case 'cardano-withdraw-modal': + return ; case 'coinmarket-buy-terms': { return ( { throw Error('useCardanoStaking used for other network'); } + const alreadyVoted = account.misc?.staking?.drep !== null; const device = useSelector(selectSelectedDevice); + const cardanoNetwork = getNetworkName(account.symbol); + const { trezorDRep } = useSelector(state => state.wallet.cardanoStaking[cardanoNetwork]); const isDeviceLocked = useSelector(selectIsDeviceLocked); const cardanoStaking = useSelector(state => state.wallet.cardanoStaking); const dispatch = useDispatch(); @@ -80,6 +86,7 @@ export const useCardanoStaking = (): CardanoStaking => { >({ status: false, }); + const [error, setError] = useState(undefined); const stakingPath = getStakingPath(account); const pendingStakeTx = cardanoStaking.pendingTx.find(tx => tx.accountKey === account.key); @@ -89,9 +96,9 @@ export const useCardanoStaking = (): CardanoStaking => { address: stakeAddress, poolId: registeredPoolId, isActive: isStakingActive, + drep: accountDRep, } = account.misc.staking; - const cardanoNetwork = account.symbol === 'ada' ? 'mainnet' : 'preview'; const { trezorPools, isFetchLoading, isFetchError } = cardanoStaking[cardanoNetwork]; const currentPool = registeredPoolId && trezorPools @@ -99,8 +106,9 @@ export const useCardanoStaking = (): CardanoStaking => { : null; const isStakingOnTrezorPool = !isFetchLoading && !isFetchError ? !!currentPool : true; // fallback to true to prevent flickering in UI while we fetch the data const isCurrentPoolOversaturated = currentPool ? isPoolOverSaturated(currentPool) : false; + const prepareTxPlan = useCallback( - async (action: 'delegate' | 'withdrawal') => { + async (action: CardanoAction) => { const changeAddress = getUnusedChangeAddress(account); if (!changeAddress || !account.utxo || !account.addresses) return null; @@ -110,10 +118,26 @@ export const useCardanoStaking = (): CardanoStaking => { ? getStakePoolForDelegation(trezorPools, account.balance).hex : ''; - const certificates = + let certificates = action === 'delegate' ? getDelegationCertificates(stakingPath, pool, !isStakingActive) : []; + + if (action === 'voteAbstain') { + const dRep = { type: PROTO.CardanoDRepType.ABSTAIN }; + + certificates = getVotingCertificates(stakingPath, dRep); + } + + if (action === 'voteDelegate') { + const dRep = { + type: PROTO.CardanoDRepType.KEY_HASH, + hex: trezorDRep?.drep?.hex, + }; + + certificates = getVotingCertificates(stakingPath, dRep); + } + const withdrawals = action === 'withdrawal' ? [ @@ -142,11 +166,19 @@ export const useCardanoStaking = (): CardanoStaking => { return { txPlan: response.payload[0], certificates, withdrawals }; }, - [account, stakingPath, isStakingActive, rewardsAmount, stakeAddress, trezorPools], + [ + trezorDRep, + account, + trezorPools, + stakingPath, + isStakingActive, + rewardsAmount, + stakeAddress, + ], ); const calculateFeeAndDeposit = useCallback( - async (action: 'delegate' | 'withdrawal') => { + async (action: CardanoAction) => { setLoading(true); try { const composeRes = await prepareTxPlan(action); @@ -186,8 +218,9 @@ export const useCardanoStaking = (): CardanoStaking => { ); const signAndPushTransaction = useCallback( - async (action: 'delegate' | 'withdrawal') => { + async (action: CardanoAction) => { const composeRes = await prepareTxPlan(action); + if (!composeRes) return; const { txPlan, certificates, withdrawals } = composeRes; @@ -257,18 +290,33 @@ export const useCardanoStaking = (): CardanoStaking => { ); const action = useCallback( - async (action: 'delegate' | 'withdrawal') => { + async (actionType: CardanoAction) => { setError(undefined); setLoading(true); try { - await signAndPushTransaction(action); - } catch (error) { - if (error.message === 'UTXO_BALANCE_INSUFFICIENT') { + switch (actionType) { + case 'withdrawal': + await signAndPushTransaction('withdrawal'); + break; + case 'delegate': + await signAndPushTransaction('delegate'); + break; + case 'voteAbstain': + await signAndPushTransaction('voteAbstain'); + break; + case 'voteDelegate': + await signAndPushTransaction('voteDelegate'); + break; + default: + break; + } + } catch (err: any) { + if (err.message === 'UTXO_BALANCE_INSUFFICIENT') { setError('AMOUNT_IS_NOT_ENOUGH'); dispatch( notificationsActions.addToast({ type: - action === 'delegate' + actionType === 'delegate' ? 'cardano-delegate-error' : 'cardano-withdrawal-error', error: 'UTXO_BALANCE_INSUFFICIENT', @@ -278,7 +326,7 @@ export const useCardanoStaking = (): CardanoStaking => { dispatch( notificationsActions.addToast({ type: 'sign-tx-error', - error: error.message, + error: err.message, }), ); } @@ -289,7 +337,9 @@ export const useCardanoStaking = (): CardanoStaking => { ); const delegate = useCallback(() => action('delegate'), [action]); - const withdraw = useCallback(() => action('withdrawal'), [action]); + const withdrawal = useCallback(() => action('withdrawal'), [action]); + const voteAbstain = useCallback(() => action('voteAbstain'), [action]); + const voteDelegate = useCallback(() => action('voteDelegate'), [action]); return { deposit, @@ -298,6 +348,7 @@ export const useCardanoStaking = (): CardanoStaking => { pendingStakeTx, deviceAvailable: getDeviceAvailability(device, isDeviceLocked), delegatingAvailable, + alreadyVoted, withdrawingAvailable, registeredPoolId, isActive: isStakingActive, @@ -307,7 +358,11 @@ export const useCardanoStaking = (): CardanoStaking => { isStakingOnTrezorPool, isCurrentPoolOversaturated, delegate, - withdraw, + withdrawal, + voteAbstain, + voteDelegate, + trezorDRep, + accountDRepHex: accountDRep?.hex, calculateFeeAndDeposit, trezorPools, error, diff --git a/packages/suite/src/reducers/wallet/__tests__/cardanoStakingReducer.test.ts b/packages/suite/src/reducers/wallet/__tests__/cardanoStakingReducer.test.ts index bb2ba9b7156..90518c3b7de 100644 --- a/packages/suite/src/reducers/wallet/__tests__/cardanoStakingReducer.test.ts +++ b/packages/suite/src/reducers/wallet/__tests__/cardanoStakingReducer.test.ts @@ -58,10 +58,12 @@ describe('cardanoStakingReducer reducer', () => { pools: [], next: { hex: 'a', bech32: 'b', live_stake: 'a', saturation: 'a' }, }, + trezorDRep: undefined, isFetchError: false, isFetchLoading: false, }, preview: { + trezorDRep: undefined, trezorPools: { pools: [], next: { hex: 'a', bech32: 'b', live_stake: 'a', saturation: 'a' }, @@ -162,10 +164,10 @@ describe('cardanoStakingReducer reducer', () => { }); }); - it('CARDANO_STAKING.SET_TREZOR_POOLS mainnet', () => { + it('CARDANO_STAKING.SET_TREZOR_DATA mainnet', () => { expect( reducer(undefined, { - type: CARDANO_STAKING.SET_TREZOR_POOLS, + type: CARDANO_STAKING.SET_TREZOR_DATA, network: 'mainnet', trezorPools: { next: { @@ -229,7 +231,7 @@ describe('cardanoStakingReducer reducer', () => { it('CARDANO_STAKING.SET_TREZOR_POOLS preview', () => { expect( reducer(undefined, { - type: CARDANO_STAKING.SET_TREZOR_POOLS, + type: CARDANO_STAKING.SET_TREZOR_DATA, network: 'preview', trezorPools: { next: { diff --git a/packages/suite/src/reducers/wallet/cardanoStakingReducer.ts b/packages/suite/src/reducers/wallet/cardanoStakingReducer.ts index 921aede68e8..cdd200d54d0 100644 --- a/packages/suite/src/reducers/wallet/cardanoStakingReducer.ts +++ b/packages/suite/src/reducers/wallet/cardanoStakingReducer.ts @@ -4,17 +4,24 @@ import { BigNumber } from '@trezor/utils/src/bigNumber'; import { CARDANO_STAKING } from 'src/actions/wallet/constants'; import { WalletAction } from 'src/types/wallet'; -import { CardanoNetwork, PendingStakeTx, PoolsResponse } from 'src/types/wallet/cardanoStaking'; +import { + CardanoNetwork, + DRepResponse, + PendingStakeTx, + PoolsResponse, +} from 'src/types/wallet/cardanoStaking'; export interface State { pendingTx: PendingStakeTx[]; mainnet: { trezorPools: PoolsResponse | undefined; + trezorDRep: DRepResponse | undefined; isFetchLoading: boolean; isFetchError: boolean; }; preview: { trezorPools: PoolsResponse | undefined; + trezorDRep: DRepResponse | undefined; isFetchLoading: boolean; isFetchError: boolean; }; @@ -24,11 +31,13 @@ export const initialState: State = { pendingTx: [], mainnet: { trezorPools: undefined, + trezorDRep: undefined, isFetchLoading: false, isFetchError: false, }, preview: { trezorPools: undefined, + trezorDRep: undefined, isFetchLoading: false, isFetchError: false, }, @@ -43,10 +52,16 @@ const remove = (state: State, accountKey: string) => { state.pendingTx.splice(index, 1); }; -const setTrezorPools = (state: State, trezorPools: PoolsResponse, network: CardanoNetwork) => { +const setTrezorData = ( + state: State, + trezorPools: PoolsResponse, + trezorDRep: DRepResponse, + network: CardanoNetwork, +) => { // sorted from least saturated to most trezorPools.pools.sort((a, b) => new BigNumber(a.live_stake).comparedTo(b.live_stake)); state[network].trezorPools = trezorPools; + state[network].trezorDRep = trezorDRep; }; const setLoading = (state: State, isLoading: boolean, network: CardanoNetwork) => { @@ -64,8 +79,8 @@ const cardanoStakingReducer = (state: State = initialState, action: WalletAction return add(draft, action.pendingStakeTx); case CARDANO_STAKING.REMOVE_PENDING_STAKE_TX: return remove(draft, action.accountKey); - case CARDANO_STAKING.SET_TREZOR_POOLS: - return setTrezorPools(draft, action.trezorPools, action.network); + case CARDANO_STAKING.SET_TREZOR_DATA: + return setTrezorData(draft, action.trezorPools, action.trezorDRep, action.network); case CARDANO_STAKING.SET_FETCH_LOADING: return setLoading(draft, action.loading, action.network); case CARDANO_STAKING.SET_FETCH_ERROR: diff --git a/packages/suite/src/storage/migrations/index.ts b/packages/suite/src/storage/migrations/index.ts index b4bf5f81d02..66c21faeb98 100644 --- a/packages/suite/src/storage/migrations/index.ts +++ b/packages/suite/src/storage/migrations/index.ts @@ -1157,6 +1157,14 @@ export const migrate: OnUpgradeFunc = async ( } if (oldVersion < 51) { + await updateAll(transaction, 'accounts', account => { + if (account.networkType === 'cardano') { + account.misc.staking.drep = null; + + return account; + } + }); + await updateAll(transaction, 'accounts', account => { if (account.networkType === 'ethereum' && account.symbol !== 'eth') { const { chainId } = getNetwork(account.symbol); diff --git a/packages/suite/src/support/extraDependencies.ts b/packages/suite/src/support/extraDependencies.ts index 3f30bddb60f..257598af77f 100644 --- a/packages/suite/src/support/extraDependencies.ts +++ b/packages/suite/src/support/extraDependencies.ts @@ -60,7 +60,7 @@ const connectInitSettings = { export const extraDependencies: ExtraDependencies = { thunks: { cardanoValidatePendingTxOnBlock: cardanoStakingActions.validatePendingTxOnBlock, - cardanoFetchTrezorPools: cardanoStakingActions.fetchTrezorPools, + cardanoFetchTrezorData: cardanoStakingActions.fetchTrezorData, initMetadata: metadataLabelingActions.init, fetchAndSaveMetadata: metadataLabelingActions.fetchAndSaveMetadata, addAccountMetadata: metadataLabelingActions.addAccountMetadata, diff --git a/packages/suite/src/support/messages.ts b/packages/suite/src/support/messages.ts index 6126ce18a92..7441b159792 100644 --- a/packages/suite/src/support/messages.ts +++ b/packages/suite/src/support/messages.ts @@ -7609,6 +7609,26 @@ export default defineMessages({ id: 'TR_CARDANO_FINGERPRINT_HEADLINE', defaultMessage: 'Fingerprint', }, + TR_CARDANO_WITHDRAW_MODAL_TITLE: { + id: 'TR_CARDANO_WITHDRAW_MODAL_TITLE', + defaultMessage: 'Delegate voting rights', + }, + TR_CARDANO_WITHDRAW_MODAL_TITLE_DESCRIPTION: { + id: 'TR_CARDANO_WITHDRAW_MODAL_TITLE_DESCRIPTION', + defaultMessage: `When withdrawing your rewards, you can choose to support the Cardano ecosystem by delegating your community voting rights. Your votes will be delegated to Five Binaries, the operators of Cardano staking in Trezor Suite. This helps strengthen the network's resilience, sustainability, and community-driven governance. If you prefer, you can easily opt out of governance.`, + }, + TR_CARDANO_WITHDRAW_MODAL_SUB_TITLE: { + id: 'TR_CARDANO_WITHDRAW_MODAL_SUB_TITLE', + defaultMessage: 'Delegate Representative (DRep)', + }, + TR_CARDANO_WITHDRAW_MODAL_BUTTON_ABSTAIN: { + id: 'TR_CARDANO_WITHDRAW_MODAL_BUTTON_ABSTAIN', + defaultMessage: 'Opt Out', + }, + TR_CARDANO_WITHDRAW_MODAL_BUTTON_DELEGATE: { + id: 'TR_CARDANO_WITHDRAW_MODAL_BUTTON_DELEGATE', + defaultMessage: 'Delegate', + }, TR_EXCEEDS_MAX: { id: 'TR_EXCEEDS_MAX', defaultMessage: 'Exceeds max length', diff --git a/packages/suite/src/types/wallet/cardanoStaking.ts b/packages/suite/src/types/wallet/cardanoStaking.ts index bc7a81d0d53..9dcb80220b5 100644 --- a/packages/suite/src/types/wallet/cardanoStaking.ts +++ b/packages/suite/src/types/wallet/cardanoStaking.ts @@ -5,5 +5,6 @@ export type { StakePool, PoolsResponse, ActionAvailability, + DRepResponse, CardanoStaking, } from '@suite-common/wallet-types'; diff --git a/packages/suite/src/views/wallet/staking/components/CardanoRewards.tsx b/packages/suite/src/views/wallet/staking/components/CardanoRewards.tsx index 4514cb76ce1..834ed4e8cae 100644 --- a/packages/suite/src/views/wallet/staking/components/CardanoRewards.tsx +++ b/packages/suite/src/views/wallet/staking/components/CardanoRewards.tsx @@ -6,10 +6,12 @@ import { DeviceModelInternal } from '@trezor/connect'; import { spacings } from '@trezor/theme'; import { getNetworkDisplaySymbol } from '@suite-common/wallet-config'; +import { useDispatch } from 'src/hooks/suite'; import { getReasonForDisabledAction, useCardanoStaking } from 'src/hooks/wallet/useCardanoStaking'; import { Translation } from 'src/components/suite/Translation'; import { Account } from 'src/types/wallet'; import { HiddenPlaceholder } from 'src/components/suite/HiddenPlaceholder'; +import { openModal } from 'src/actions/suite/modalActions'; import { DeviceButton } from './DeviceButton'; import { @@ -34,14 +36,17 @@ export const CardanoRewards = ({ account, deviceModel }: CardanoRewardsProps) => const { address, rewards, - withdraw, calculateFeeAndDeposit, loading, + withdrawal, withdrawingAvailable, deviceAvailable, + alreadyVoted, pendingStakeTx, } = useCardanoStaking(); + const dispatch = useDispatch(); + useEffect(() => { calculateFeeAndDeposit('withdrawal'); }, [calculateFeeAndDeposit]); @@ -103,7 +108,13 @@ export const CardanoRewards = ({ account, deviceModel }: CardanoRewardsProps) => isLoading={loading} isDisabled={isRewardsWithdrawDisabled} deviceModelInternal={deviceModel} - onClick={withdraw} + onClick={() => { + if (alreadyVoted) { + withdrawal(); + } else { + dispatch(openModal({ type: 'cardano-withdraw-modal' })); + } + }} tooltipContent={ !reasonMessageId || (deviceAvailable.status && withdrawingAvailable.status) ? undefined : ( diff --git a/packages/urls/src/urls.ts b/packages/urls/src/urls.ts index 308cbddf7d6..06942a5374a 100644 --- a/packages/urls/src/urls.ts +++ b/packages/urls/src/urls.ts @@ -124,6 +124,10 @@ export const CARDANO_STAKE_POOL_MAINNET_URL: Url = 'https://trezor-cardano-mainnet.blockfrost.io/api/v0/pools/'; export const CARDANO_STAKE_POOL_PREVIEW_URL: Url = 'https://trezor-cardano-preview.blockfrost.io/api/v0/pools/'; +export const CARDANO_MAINNET_DREP: Url = + 'https://trezor-cardano-mainnet.blockfrost.io/api/v0/dreps/'; +export const CARDANO_PREVIEW_DREP: Url = + 'https://trezor-cardano-preview.blockfrost.io/api/v0/dreps/'; export const CHROME_URL: Url = 'https://www.google.com/chrome/'; export const CHROME_UPDATE_URL: Url = 'https://support.google.com/chrome/answer/95414'; diff --git a/suite-common/redux-utils/src/extraDependenciesType.ts b/suite-common/redux-utils/src/extraDependenciesType.ts index b388c6362ff..de596477c12 100644 --- a/suite-common/redux-utils/src/extraDependenciesType.ts +++ b/suite-common/redux-utils/src/extraDependenciesType.ts @@ -42,7 +42,7 @@ export type ExtraDependencies = { block: BlockchainBlock; timestamp: number; }>; - cardanoFetchTrezorPools: SuiteCompatibleThunk<'tADA' | 'ADA'>; + cardanoFetchTrezorData: SuiteCompatibleThunk<'tADA' | 'ADA'>; initMetadata: SuiteCompatibleThunk; fetchAndSaveMetadata: SuiteCompatibleThunk; addAccountMetadata: SuiteCompatibleThunk< diff --git a/suite-common/suite-types/src/modal.ts b/suite-common/suite-types/src/modal.ts index a8f2e0a47b2..3302ebb8873 100644 --- a/suite-common/suite-types/src/modal.ts +++ b/suite-common/suite-types/src/modal.ts @@ -185,6 +185,9 @@ export type UserContextPayload = | { type: 'passphrase-mismatch-warning'; } + | { + type: 'cardano-withdraw-modal'; + } | { type: 'connect-popup'; onConfirm: () => void; diff --git a/suite-common/test-utils/src/extraDependenciesMock.ts b/suite-common/test-utils/src/extraDependenciesMock.ts index e140e87bba6..d5e5c65750b 100644 --- a/suite-common/test-utils/src/extraDependenciesMock.ts +++ b/suite-common/test-utils/src/extraDependenciesMock.ts @@ -60,7 +60,7 @@ export const mockReducer = (name: string) => (state: any, action: any) => { export const extraDependenciesMock: ExtraDependencies = { thunks: { cardanoValidatePendingTxOnBlock: mockThunk('validatePendingTxOnBlock'), - cardanoFetchTrezorPools: mockThunk('fetchTrezorPools'), + cardanoFetchTrezorData: mockThunk('fetchTrezorData'), fetchAndSaveMetadata: mockThunk('fetchAndSaveMetadata'), initMetadata: mockThunk('initMetadata'), addAccountMetadata: mockThunk('addAccountMetadata'), diff --git a/suite-common/wallet-core/src/blockchain/blockchainMiddleware.ts b/suite-common/wallet-core/src/blockchain/blockchainMiddleware.ts index db493842a48..8d2b7a0a479 100644 --- a/suite-common/wallet-core/src/blockchain/blockchainMiddleware.ts +++ b/suite-common/wallet-core/src/blockchain/blockchainMiddleware.ts @@ -16,7 +16,7 @@ export const prepareBlockchainMiddleware = createMiddlewareWithExtraDeps( // propagate action to reducers next(action); - const { cardanoValidatePendingTxOnBlock, cardanoFetchTrezorPools } = extra.thunks; + const { cardanoValidatePendingTxOnBlock, cardanoFetchTrezorData } = extra.thunks; switch (action.type) { case TREZOR_CONNECT_BLOCKCHAIN_ACTIONS.CONNECT: @@ -26,7 +26,7 @@ export const prepareBlockchainMiddleware = createMiddlewareWithExtraDeps( // for cardano staking if applicable if (['ADA', 'tADA'].includes(action.payload.coin.shortcut)) { dispatch( - cardanoFetchTrezorPools(action.payload.coin.shortcut as 'ADA' | 'tADA'), + cardanoFetchTrezorData(action.payload.coin.shortcut as 'ADA' | 'tADA'), ); } break; diff --git a/suite-common/wallet-types/src/account.ts b/suite-common/wallet-types/src/account.ts index 03f7989f742..6de26a6b53e 100644 --- a/suite-common/wallet-types/src/account.ts +++ b/suite-common/wallet-types/src/account.ts @@ -43,6 +43,14 @@ type AccountNetworkSpecific = isActive: boolean; rewards: string; poolId: string | null; + drep: { + drep_id: string; + hex: string; + amount: string; + active: boolean; + active_epoch: number | null; + has_script: boolean; + } | null; }; }; page: AccountInfo['page']; diff --git a/suite-common/wallet-types/src/cardanoStaking.ts b/suite-common/wallet-types/src/cardanoStaking.ts index b59f92c93df..8258ef8c925 100644 --- a/suite-common/wallet-types/src/cardanoStaking.ts +++ b/suite-common/wallet-types/src/cardanoStaking.ts @@ -16,6 +16,17 @@ export type PoolsResponse = { pools: StakePool[]; }; +interface DRep { + hex: string; + bech32: string; +} + +export interface DRepResponse { + [key: string]: DRep; +} + +export type CardanoAction = 'delegate' | 'withdrawal' | 'voteDelegate' | 'voteAbstain'; + export type ActionAvailability = | { status: true; reason?: undefined } | { status: false; reason: 'POOL_ID_FETCH_FAIL' | 'TX_NOT_FINAL' | 'UTXO_BALANCE_INSUFFICIENT' } @@ -38,10 +49,15 @@ export type CardanoStaking = { isFetchError: boolean; isCurrentPoolOversaturated: boolean; trezorPools: PoolsResponse | undefined; + trezorDRep?: DRepResponse; + accountDRepHex?: string; isActive: boolean; + alreadyVoted: boolean; rewards: string; - delegate(): void; - withdraw(): void; - calculateFeeAndDeposit(action: 'delegate' | 'withdrawal'): void; + delegate: () => void; + withdrawal: () => void; + voteDelegate: () => void; + voteAbstain: () => void; + calculateFeeAndDeposit: (action: CardanoAction) => Promise; error?: string; }; diff --git a/suite-common/wallet-utils/src/__fixtures__/cardanoUtils.ts b/suite-common/wallet-utils/src/__fixtures__/cardanoUtils.ts index 7abfd7a64e3..3ae4a3c515a 100644 --- a/suite-common/wallet-utils/src/__fixtures__/cardanoUtils.ts +++ b/suite-common/wallet-utils/src/__fixtures__/cardanoUtils.ts @@ -630,7 +630,7 @@ export const getVotingCertificates = [ stakingPath: 'path', dRep: { type: 0, //keyHash - keyHash: 'hex', + hex: 'hex', }, result: [ { @@ -648,7 +648,7 @@ export const getVotingCertificates = [ stakingPath: 'path', dRep: { type: 1, // scriptHash - keyHash: 'hex', + hex: 'hex', }, result: [ { diff --git a/suite-common/wallet-utils/src/accountUtils.ts b/suite-common/wallet-utils/src/accountUtils.ts index e7ba77a39a3..c5983acc92b 100644 --- a/suite-common/wallet-utils/src/accountUtils.ts +++ b/suite-common/wallet-utils/src/accountUtils.ts @@ -858,6 +858,7 @@ export const getAccountSpecific = (accountInfo: Partial, networkTyp isActive: misc && misc.staking ? misc.staking.isActive : false, address: misc && misc.staking ? misc.staking.address : '', poolId: misc && misc.staking ? misc.staking.poolId : null, + drep: misc && misc.staking ? misc.staking.drep : null, }, }, marker: undefined, diff --git a/suite-common/wallet-utils/src/cardanoUtils.ts b/suite-common/wallet-utils/src/cardanoUtils.ts index 30559a27515..cf9e96f0424 100644 --- a/suite-common/wallet-utils/src/cardanoUtils.ts +++ b/suite-common/wallet-utils/src/cardanoUtils.ts @@ -41,6 +41,9 @@ export const getAddressType = () => PROTO.CardanoAddressType.BASE; export const getNetworkId = (accountSymbol: Account['symbol']) => accountSymbol === 'ada' ? CARDANO.NETWORK_IDS.mainnet : CARDANO.NETWORK_IDS.testnet; +export const getNetworkName = (accountSymbol: string): 'preview' | 'mainnet' => + accountSymbol.toLowerCase() === 'ada' ? 'mainnet' : 'preview'; + export const getUnusedChangeAddress = (account: Pick) => { if (!account.addresses) return; @@ -135,16 +138,15 @@ export const getDelegationCertificates = ( export const getVotingCertificates = ( stakingPath: string, - dRep: { keyHash?: string; type: PROTO.CardanoDRepType }, + dRep: { hex?: string; type: PROTO.CardanoDRepType }, ) => { const result: CardanoCertificate[] = [ { type: PROTO.CardanoCertificateType.VOTE_DELEGATION, path: stakingPath, dRep: { - keyHash: dRep.type === PROTO.CardanoDRepType.KEY_HASH ? dRep.keyHash : undefined, - scriptHash: - dRep.type === PROTO.CardanoDRepType.SCRIPT_HASH ? dRep.keyHash : undefined, + keyHash: dRep.type === PROTO.CardanoDRepType.KEY_HASH ? dRep.hex : undefined, + scriptHash: dRep.type === PROTO.CardanoDRepType.SCRIPT_HASH ? dRep.hex : undefined, type: dRep.type, }, }, diff --git a/suite-common/wallet-utils/src/index.ts b/suite-common/wallet-utils/src/index.ts index b211ee9a62f..c92c0ebd9b3 100644 --- a/suite-common/wallet-utils/src/index.ts +++ b/suite-common/wallet-utils/src/index.ts @@ -1,7 +1,7 @@ export * from './accountUtils'; +export * from './cardanoUtils'; export * from './backendUtils'; export * from './balanceUtils'; -export * from './cardanoUtils'; export * from './csvParserUtils'; export * from './discreetModeUtils'; export * from './ethUtils';