From 373ae4ccc2adf8074153b51b3add3894c4105aaf Mon Sep 17 00:00:00 2001 From: Petr Knetl Date: Tue, 14 May 2024 15:01:00 +0200 Subject: [PATCH] refactor(suite): split pushSendFormTransactionThunk into sub-thunks --- .../actions/wallet/moveLabelsForRbfActions.ts | 20 +- .../src/actions/wallet/send/sendFormThunks.ts | 170 +++++++++++++-- .../wallet-core/src/send/sendFormThunks.ts | 206 ++++++------------ .../src/transactions/transactionsThunks.ts | 1 - suite-common/wallet-types/src/sendForm.ts | 11 +- 5 files changed, 239 insertions(+), 169 deletions(-) diff --git a/packages/suite/src/actions/wallet/moveLabelsForRbfActions.ts b/packages/suite/src/actions/wallet/moveLabelsForRbfActions.ts index 842274183e0e..adf4643adbdf 100644 --- a/packages/suite/src/actions/wallet/moveLabelsForRbfActions.ts +++ b/packages/suite/src/actions/wallet/moveLabelsForRbfActions.ts @@ -3,7 +3,11 @@ import { findChainedTransactions, findTransactions } from '@suite-common/wallet- import { Dispatch, GetState } from 'src/types/suite'; import * as metadataLabelingActions from 'src/actions/suite/metadataLabelingActions'; import { AccountLabels, AccountOutputLabels } from '@suite-common/metadata-types'; -import { AccountKey, WalletAccountTransaction } from '@suite-common/wallet-types'; +import { + AccountKey, + RbfLabelsToBeUpdated, + WalletAccountTransaction, +} from '@suite-common/wallet-types'; type DeleteAllOutputLabelsParams = { labels: AccountLabels['outputLabels']['labels']; @@ -65,17 +69,9 @@ type FindLabelsToBeMovedOrDeletedParams = { prevTxid: string; }; -export type LabelsToBeMovedOrDeleted = Record< - AccountKey, - { - toBeMoved: WalletAccountTransaction; - toBeDeleted: WalletAccountTransaction[]; - } ->; - export const findLabelsToBeMovedOrDeleted = ({ prevTxid }: FindLabelsToBeMovedOrDeletedParams) => - (_dispatch: Dispatch, getState: GetState): LabelsToBeMovedOrDeleted => { + (_dispatch: Dispatch, getState: GetState): RbfLabelsToBeUpdated => { const accountTransactions = findTransactions( prevTxid, getState().wallet.transactions.transactions, @@ -100,12 +96,12 @@ export const findLabelsToBeMovedOrDeleted = }; return result; - }, {} as LabelsToBeMovedOrDeleted); + }, {} as RbfLabelsToBeUpdated); }; type MoveLabelsForRbfParams = { newTxid: string; - toBeMovedOrDeletedList: LabelsToBeMovedOrDeleted; + toBeMovedOrDeletedList: RbfLabelsToBeUpdated; }; export const moveLabelsForRbfAction = diff --git a/packages/suite/src/actions/wallet/send/sendFormThunks.ts b/packages/suite/src/actions/wallet/send/sendFormThunks.ts index 5712955f4f5b..7be80cebc01c 100644 --- a/packages/suite/src/actions/wallet/send/sendFormThunks.ts +++ b/packages/suite/src/actions/wallet/send/sendFormThunks.ts @@ -1,29 +1,37 @@ import { G } from '@mobily/ts-belt'; +import { isRejected } from '@reduxjs/toolkit'; import { createThunk } from '@suite-common/redux-utils'; import { Account, FormState, + GeneralPrecomposedTransactionFinal, PrecomposedTransactionFinal, - PrecomposedTransactionFinalCardano, + RbfLabelsToBeUpdated, } from '@suite-common/wallet-types'; - -import * as modalActions from 'src/actions/suite/modalActions'; -import { - selectSelectedAccountKey, - selectIsSelectedAccountLoaded, -} from 'src/reducers/wallet/selectedAccountReducer'; - import { enhancePrecomposedTransactionThunk, pushSendFormTransactionThunk, + replaceTransactionThunk, selectDevice, + selectPrecomposedSendForm, selectSendFormDrafts, signTransactionThunk, + sendFormActions, } from '@suite-common/wallet-core'; -import { sendFormActions } from '@suite-common/wallet-core'; -import { isRejected } from '@reduxjs/toolkit'; +import { isCardanoTx } from '@suite-common/wallet-utils'; +import { MetadataAddPayload } from '@suite-common/metadata-types'; import { Unsuccessful } from '@trezor/connect'; +import { getSynchronize } from '@trezor/utils'; + +import { + selectSelectedAccountKey, + selectIsSelectedAccountLoaded, +} from 'src/reducers/wallet/selectedAccountReducer'; +import { findLabelsToBeMovedOrDeleted } from '../moveLabelsForRbfActions'; +import { selectMetadata } from 'src/reducers/suite/metadataReducer'; +import * as metadataLabelingActions from 'src/actions/suite/metadataLabelingActions'; +import * as modalActions from 'src/actions/suite/modalActions'; export const MODULE_PREFIX = '@send'; @@ -82,6 +90,108 @@ export const importSendFormRequestThunk = createThunk( (_, { dispatch }) => dispatch(modalActions.openDeferredModal({ type: 'import-transaction' })), ); +const updateRbfLabelsThunk = createThunk( + `${MODULE_PREFIX}/updateReplacedTransactionThunk`, + ( + { + labelsToBeEdited, + precomposedTx, + txid, + }: { + labelsToBeEdited: RbfLabelsToBeUpdated; + precomposedTx: PrecomposedTransactionFinal; + txid: string; + }, + { dispatch, extra }, + ) => { + const { + thunks: { moveLabelsForRbfAction }, + } = extra; + + dispatch( + moveLabelsForRbfAction({ + toBeMovedOrDeletedList: labelsToBeEdited, + newTxid: txid, + }), + ); + + // notification from the backend may be delayed. + // modify affected transaction(s) in the reducer until the real account update occurs. + // this will update transaction details (like time, fee etc.) + dispatch( + replaceTransactionThunk({ + precomposedTx, + newTxid: txid, + }), + ); + }, +); + +const applySendFormMetadataLabelsThunk = createThunk( + `${MODULE_PREFIX}/applyMetadataLabelsThunk`, + ( + { + selectedAccount, + precomposedTx, + txid, + }: { + selectedAccount: Account; + precomposedTx: GeneralPrecomposedTransactionFinal; + txid: string; + }, + { dispatch, getState }, + ) => { + const metadata = selectMetadata(getState()); + if (metadata.enabled) { + const precomposedForm = selectPrecomposedSendForm(getState()); + let outputsPermutation: number[]; + if (isCardanoTx(selectedAccount, precomposedTx)) { + // cardano preserves order of outputs + outputsPermutation = precomposedTx?.outputs.map((_o, i) => i); + } else { + outputsPermutation = precomposedTx?.outputsPermutation; + } + + const synchronize = getSynchronize(); + + precomposedForm?.outputs + // create array of metadata objects + .map((formOutput, index) => { + const { label } = formOutput; + // final ordering of outputs differs from order in send form + // outputsPermutation contains mapping from @trezor/utxo-lib outputs to send form outputs + // mapping goes like this: Array<@trezor/utxo-lib index : send form index> + const outputIndex = outputsPermutation.findIndex(p => p === index); + const outputMetadata: Extract = { + type: 'outputLabel', + entityKey: selectedAccount.key, + txid, + outputIndex, + value: label, + defaultValue: '', + }; + + return outputMetadata; + }) + // filter out empty values AFTER creating metadata objects (see outputs mapping above) + .filter(output => output.value) + // propagate metadata to reducers and persistent storage + .forEach((output, index, arr) => { + const isLast = index === arr.length - 1; + + synchronize(() => + dispatch( + metadataLabelingActions.addAccountMetadata({ + ...output, + skipSave: !isLast, + }), + ), + ); + }); + } + }, +); + export const signAndPushSendFormTransactionThunk = createThunk( `${MODULE_PREFIX}/signSendFormTransactionThunk`, async ( @@ -91,9 +201,7 @@ export const signAndPushSendFormTransactionThunk = createThunk( selectedAccount, }: { formValues: FormState; - precomposedTransaction: - | PrecomposedTransactionFinal - | PrecomposedTransactionFinalCardano; + precomposedTransaction: GeneralPrecomposedTransactionFinal; selectedAccount?: Account; }, { dispatch, getState }, @@ -130,10 +238,19 @@ export const signAndPushSendFormTransactionThunk = createThunk( } // Open a deferred modal and get the decision - const decision = await dispatch( + const isPushConfirmed = await dispatch( modalActions.openDeferredModal({ type: 'review-transaction' }), ); - if (decision) { + if (isPushConfirmed) { + const isRbf = precomposedTransaction.prevTxid !== undefined; + + // This has to be executed prior to pushing the transaction! + const rbfLabelsToBeEdited = isRbf + ? dispatch( + findLabelsToBeMovedOrDeleted({ prevTxid: precomposedTransaction.prevTxid }), + ) + : null; + // push tx to the network const pushResponse = await dispatch( pushSendFormTransactionThunk({ @@ -145,7 +262,28 @@ export const signAndPushSendFormTransactionThunk = createThunk( return pushResponse.payload as Unsuccessful; } - return pushResponse.payload; + const result = pushResponse.payload; + const { txid } = result.payload; + + if (isRbf && rbfLabelsToBeEdited) { + await dispatch( + updateRbfLabelsThunk({ + labelsToBeEdited: rbfLabelsToBeEdited, + precomposedTx: precomposedTransaction, + txid, + }), + ); + } + + dispatch( + applySendFormMetadataLabelsThunk({ + selectedAccount, + precomposedTx: precomposedTransaction, + txid, + }), + ); + + return result; } }, ); diff --git a/suite-common/wallet-core/src/send/sendFormThunks.ts b/suite-common/wallet-core/src/send/sendFormThunks.ts index 81f870653b17..c5e9f233caeb 100644 --- a/suite-common/wallet-core/src/send/sendFormThunks.ts +++ b/suite-common/wallet-core/src/send/sendFormThunks.ts @@ -7,10 +7,10 @@ import { AccountKey, ComposeActionContext, FormState, + GeneralPrecomposedTransactionFinal, PrecomposedTransactionFinal, PrecomposedTransactionFinalCardano, } from '@suite-common/wallet-types'; -import { MetadataAddPayload } from '@suite-common/metadata-types'; import { notificationsActions } from '@suite-common/toast-notifications'; import { NetworkSymbol } from '@suite-common/wallet-config'; import { @@ -26,13 +26,12 @@ import { tryGetAccountIdentity, } from '@suite-common/wallet-utils'; import TrezorConnect from '@trezor/connect'; -import { cloneObject, getSynchronize } from '@trezor/utils'; +import { cloneObject } from '@trezor/utils'; import { BlockbookTransaction } from '@trezor/blockchain-link-types'; import { addFakePendingCardanoTxThunk, addFakePendingTxThunk, - replaceTransactionThunk, } from '../transactions/transactionsThunks'; import { accountsActions } from '../accounts/accountsActions'; import { selectAccounts } from '../accounts/accountsReducer'; @@ -42,7 +41,6 @@ import { selectSendFormDrafts, selectSendSerializedTx, selectSendPrecomposedTx, - selectPrecomposedSendForm, } from './sendFormReducer'; import { sendFormActions } from './sendFormActions'; import { @@ -178,6 +176,55 @@ export const cancelSignSendFormTransactionThunk = createThunk( }, ); +const synchronizeSentTransactionThunk = createThunk( + `${SEND_MODULE_PREFIX}/synchronizePendingTransactionsThunk`, + ( + { + selectedAccount, + precomposedTx, + txid, + }: { + selectedAccount: Account; + precomposedTx: GeneralPrecomposedTransactionFinal; + txid: string; + }, + { dispatch }, + ) => { + // notification from the backend may be delayed. + // modify affected account balance. + // TODO: make it work with ETH accounts + if (isCardanoTx(selectedAccount, precomposedTx)) { + const pendingAccount = getPendingAccount({ + account: selectedAccount, + tx: precomposedTx, + txid, + }); + if (pendingAccount) { + // manually add fake pending tx as we don't have the data about mempool txs + dispatch( + addFakePendingCardanoTxThunk({ + precomposedTx, + txid, + account: selectedAccount, + }), + ); + dispatch(accountsActions.updateAccount(pendingAccount)); + } + } else if (selectedAccount.networkType === 'bitcoin') { + dispatch( + addFakePendingTxThunk({ + precomposedTx, + account: selectedAccount, + }), + ); + } else { + // there is no point in fetching account data right after tx submit + // as the account will update only after the tx is confirmed + dispatch(syncAccountsWithBlockchainThunk(selectedAccount.symbol)); + } + }, +); + export const pushSendFormTransactionThunk = createThunk( `${SEND_MODULE_PREFIX}/pushSendFormTransactionThunk`, async ( @@ -190,23 +237,15 @@ export const pushSendFormTransactionThunk = createThunk( ) => { const { actions: { onModalCancel }, - selectors: { selectMetadata, selectBitcoinAmountUnit }, - thunks: { findLabelsToBeMovedOrDeleted, moveLabelsForRbfAction, addAccountMetadata }, + selectors: { selectBitcoinAmountUnit }, } = extra; const precomposedTx = selectSendPrecomposedTx(getState()); const serializedTx = selectSendSerializedTx(getState()); const device = selectDevice(getState()); const bitcoinAmountUnit = selectBitcoinAmountUnit(getState()); - const metadata = selectMetadata(getState()); if (!serializedTx || !precomposedTx) return rejectWithValue('Transaction not found.'); - const isRbf = precomposedTx.prevTxid !== undefined; - - const toBeMovedOrDeletedList = isRbf - ? dispatch(findLabelsToBeMovedOrDeleted({ prevTxid: precomposedTx.prevTxid })) - : undefined; - const pushTxResponse = await TrezorConnect.pushTransaction({ ...serializedTx, identity: tryGetAccountIdentity(selectedAccount), @@ -243,129 +282,22 @@ export const pushSendFormTransactionThunk = createThunk( }), ); - if (isRbf) { - if (toBeMovedOrDeletedList !== undefined) { - await dispatch( - moveLabelsForRbfAction({ - toBeMovedOrDeletedList, - newTxid: txid, - }), - ); - } - - // notification from the backend may be delayed. - // modify affected transaction(s) in the reducer until the real account update occurs. - // this will update transaction details (like time, fee etc.) - dispatch( - replaceTransactionThunk({ - precomposedTx, - newTxid: txid, - }), - ); - } - - // notification from the backend may be delayed. - // modify affected account balance. - // TODO: make it work with ETH accounts - if (selectedAccount.networkType === 'cardano') { - const pendingAccount = getPendingAccount({ - account: selectedAccount, - tx: precomposedTx, - txid, - }); - if (pendingAccount) { - // manually add fake pending tx as we don't have the data about mempool txs - dispatch( - addFakePendingCardanoTxThunk({ - precomposedTx, - txid, - account: selectedAccount, - }), - ); - dispatch(accountsActions.updateAccount(pendingAccount)); - } - } - - if ( - selectedAccount.networkType === 'bitcoin' && - !isCardanoTx(selectedAccount, precomposedTx) - ) { - dispatch( - addFakePendingTxThunk({ - precomposedTx, - account: selectedAccount, - }), - ); - } - - if ( - selectedAccount.networkType !== 'bitcoin' && - selectedAccount.networkType !== 'cardano' - ) { - // there is no point in fetching account data right after tx submit - // as the account will update only after the tx is confirmed - dispatch(syncAccountsWithBlockchainThunk(selectedAccount.symbol)); - } - - // handle metadata (labeling) from send form - if (metadata.enabled) { - const precomposedForm = selectPrecomposedSendForm(getState()); - let outputsPermutation: number[]; - if (isCardanoTx(selectedAccount, precomposedTx)) { - // cardano preserves order of outputs - outputsPermutation = precomposedTx?.outputs.map((_o, i) => i); - } else { - outputsPermutation = precomposedTx?.outputsPermutation; - } - - const synchronize = getSynchronize(); - - precomposedForm?.outputs - // create array of metadata objects - .map((formOutput, index) => { - const { label } = formOutput; - // final ordering of outputs differs from order in send form - // outputsPermutation contains mapping from @trezor/utxo-lib outputs to send form outputs - // mapping goes like this: Array<@trezor/utxo-lib index : send form index> - const outputIndex = outputsPermutation.findIndex(p => p === index); - const outputMetadata: Extract = - { - type: 'outputLabel', - entityKey: selectedAccount.key, - txid, // txid becomes available, use it - outputIndex, - value: label, - defaultValue: '', - }; - - return outputMetadata; - }) - // filter out empty values AFTER creating metadata objects (see outputs mapping above) - .filter(output => output.value) - // propagate metadata to reducers and persistent storage - .forEach((output, index, arr) => { - const isLast = index === arr.length - 1; - - synchronize(() => - dispatch(addAccountMetadata({ ...output, skipSave: !isLast })), - ); - }); - } - - dispatch(cancelSignSendFormTransactionThunk()); - - return fulfillWithValue(pushTxResponse); + dispatch(synchronizeSentTransactionThunk({ selectedAccount, precomposedTx, txid })); + } else { + dispatch( + notificationsActions.addToast({ + type: 'sign-tx-error', + error: pushTxResponse.payload.error, + }), + ); } - dispatch( - notificationsActions.addToast({ - type: 'sign-tx-error', - error: pushTxResponse.payload.error, - }), - ); + // cleanup send form state and close review modal dispatch(cancelSignSendFormTransactionThunk()); - return rejectWithValue(pushTxResponse); + return pushTxResponse.success + ? fulfillWithValue(pushTxResponse) + : rejectWithValue(pushTxResponse); }, ); @@ -497,9 +429,7 @@ export const enhancePrecomposedTransactionThunk = createThunk( selectedAccount, }: { transactionFormValues: FormState; - precomposedTransaction: - | PrecomposedTransactionFinal - | PrecomposedTransactionFinalCardano; + precomposedTransaction: GeneralPrecomposedTransactionFinal; selectedAccount: Account; }, { getState, dispatch, rejectWithValue }, @@ -529,14 +459,12 @@ export const enhancePrecomposedTransactionThunk = createThunk( (!hasDecreasedOutput && nativeRbfAvailable) || (hasDecreasedOutput && decreaseOutputAvailable); - const enhancedPrecomposedTx: - | PrecomposedTransactionFinal - | PrecomposedTransactionFinalCardano = { + const enhancedPrecomposedTx: GeneralPrecomposedTransactionFinal = { ...precomposedTransaction, - rbf: formValues.options.includes('bitcoinRBF'), }; if (formValues.rbfParams && !isCardanoTx(selectedAccount, enhancedPrecomposedTx)) { + enhancedPrecomposedTx.rbf = formValues.options.includes('bitcoinRBF'); enhancedPrecomposedTx.prevTxid = formValues.rbfParams.txid; enhancedPrecomposedTx.feeDifference = new BigNumber(precomposedTransaction.fee) .minus(formValues.rbfParams.baseFee) diff --git a/suite-common/wallet-core/src/transactions/transactionsThunks.ts b/suite-common/wallet-core/src/transactions/transactionsThunks.ts index a011ec6d1b5a..931cc4b1f058 100644 --- a/suite-common/wallet-core/src/transactions/transactionsThunks.ts +++ b/suite-common/wallet-core/src/transactions/transactionsThunks.ts @@ -32,7 +32,6 @@ import { TRANSACTIONS_MODULE_PREFIX, transactionsActions } from './transactionsA import { selectAccountByKey, selectAccounts } from '../accounts/accountsReducer'; import { selectBlockchainHeightBySymbol } from '../blockchain/blockchainReducer'; import { selectHistoricFiatRates } from '../fiat-rates/fiatRatesSelectors'; -import { selectNetworkTokenDefinitions } from '../token-definitions/tokenDefinitionsSelectors'; import { selectSendSignedTx } from '../send/sendFormReducer'; /** diff --git a/suite-common/wallet-types/src/sendForm.ts b/suite-common/wallet-types/src/sendForm.ts index f53ddc117d7d..e6dcfeb1f464 100644 --- a/suite-common/wallet-types/src/sendForm.ts +++ b/suite-common/wallet-types/src/sendForm.ts @@ -4,7 +4,7 @@ import { FieldPath, UseFormReturn } from 'react-hook-form'; import { Network, NetworkSymbol } from '@suite-common/wallet-config'; import { AccountUtxo, FeeLevel, PROTO } from '@trezor/connect'; -import { Account } from './account'; +import { Account, AccountKey } from './account'; import { CurrencyOption, FeeInfo, @@ -12,6 +12,7 @@ import { PrecomposedLevels, PrecomposedLevelsCardano, RbfTransactionParams, + WalletAccountTransaction, } from './transaction'; import { Rate } from './fiatRates'; @@ -128,3 +129,11 @@ export type SendContextValues = // UTXO selection utxoSelection: UtxoSelectionContext; }; + +export type RbfLabelsToBeUpdated = Record< + AccountKey, + { + toBeMoved: WalletAccountTransaction; + toBeDeleted: WalletAccountTransaction[]; + } +>;