From 337fabb4cc48a3ef0c3dbc3b970468b3127a162e Mon Sep 17 00:00:00 2001 From: Petr Knetl Date: Wed, 15 May 2024 13:15:56 +0200 Subject: [PATCH] refactor(suite): redundant precomposedForm state removed from sendForm reducer --- .../src/actions/wallet/send/sendFormThunks.ts | 183 +++++++++--------- .../suite/src/actions/wallet/stakeActions.ts | 2 +- .../TransactionReviewModalContent.tsx | 26 ++- .../suite/src/hooks/wallet/form/useCompose.ts | 6 +- .../src/hooks/wallet/useCardanoStaking.ts | 6 +- .../wallet/useCoinmarketRecomposeAndSign.ts | 12 +- .../suite/src/hooks/wallet/useSendForm.ts | 6 +- .../wallet/__tests__/sendFormReducer.test.ts | 9 +- .../wallet-core/src/send/sendFormActions.ts | 3 +- .../wallet-core/src/send/sendFormReducer.ts | 21 +- .../wallet-core/src/send/sendFormThunks.ts | 111 ++++++----- .../src/transactions/transactionsThunks.ts | 42 ++-- 12 files changed, 231 insertions(+), 196 deletions(-) diff --git a/packages/suite/src/actions/wallet/send/sendFormThunks.ts b/packages/suite/src/actions/wallet/send/sendFormThunks.ts index 7be80cebc01..6b05d0b8904 100644 --- a/packages/suite/src/actions/wallet/send/sendFormThunks.ts +++ b/packages/suite/src/actions/wallet/send/sendFormThunks.ts @@ -14,10 +14,10 @@ import { pushSendFormTransactionThunk, replaceTransactionThunk, selectDevice, - selectPrecomposedSendForm, selectSendFormDrafts, signTransactionThunk, sendFormActions, + selectSendFormDraftByAccountKey, } from '@suite-common/wallet-core'; import { isCardanoTx } from '@suite-common/wallet-utils'; import { MetadataAddPayload } from '@suite-common/metadata-types'; @@ -28,7 +28,7 @@ import { selectSelectedAccountKey, selectIsSelectedAccountLoaded, } from 'src/reducers/wallet/selectedAccountReducer'; -import { findLabelsToBeMovedOrDeleted } from '../moveLabelsForRbfActions'; +import { findLabelsToBeMovedOrDeleted, moveLabelsForRbfAction } 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'; @@ -95,19 +95,15 @@ const updateRbfLabelsThunk = createThunk( ( { labelsToBeEdited, - precomposedTx, + precomposedTransaction, txid, }: { labelsToBeEdited: RbfLabelsToBeUpdated; - precomposedTx: PrecomposedTransactionFinal; + precomposedTransaction: PrecomposedTransactionFinal; txid: string; }, - { dispatch, extra }, + { dispatch }, ) => { - const { - thunks: { moveLabelsForRbfAction }, - } = extra; - dispatch( moveLabelsForRbfAction({ toBeMovedOrDeletedList: labelsToBeEdited, @@ -120,7 +116,7 @@ const updateRbfLabelsThunk = createThunk( // this will update transaction details (like time, fee etc.) dispatch( replaceTransactionThunk({ - precomposedTx, + precomposedTransaction, newTxid: txid, }), ); @@ -132,63 +128,61 @@ const applySendFormMetadataLabelsThunk = createThunk( ( { selectedAccount, - precomposedTx, + precomposedTransaction, txid, }: { selectedAccount: Account; - precomposedTx: GeneralPrecomposedTransactionFinal; + precomposedTransaction: 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, - }), - ), - ); - }); - } + + if (!metadata.enabled) return; + + const formDraft = selectSendFormDraftByAccountKey(getState(), selectedAccount.key); + + const outputsPermutation = isCardanoTx(selectedAccount, precomposedTransaction) + ? precomposedTransaction?.outputs.map((_o, i) => i) // cardano preserves order of outputs + : precomposedTransaction?.outputsPermutation; + + const synchronize = getSynchronize(); + + formDraft?.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, + }), + ), + ); + }); }, ); @@ -241,49 +235,50 @@ export const signAndPushSendFormTransactionThunk = createThunk( const isPushConfirmed = await dispatch( modalActions.openDeferredModal({ type: 'review-transaction' }), ); - 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({ - selectedAccount, - }), - ); - if (isRejected(pushResponse)) { - return pushResponse.payload as Unsuccessful; - } + if (!isPushConfirmed) { + return; + } - const result = pushResponse.payload; - const { txid } = result.payload; + const isRbf = precomposedTransaction.prevTxid !== undefined; - if (isRbf && rbfLabelsToBeEdited) { - await dispatch( - updateRbfLabelsThunk({ - labelsToBeEdited: rbfLabelsToBeEdited, - precomposedTx: precomposedTransaction, - txid, - }), - ); - } + // 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({ + selectedAccount, + }), + ); + + if (isRejected(pushResponse)) { + return pushResponse.payload as Unsuccessful; + } + + const result = pushResponse.payload; + const { txid } = result.payload; + if (isRbf && rbfLabelsToBeEdited) { dispatch( - applySendFormMetadataLabelsThunk({ - selectedAccount, - precomposedTx: precomposedTransaction, + updateRbfLabelsThunk({ + labelsToBeEdited: rbfLabelsToBeEdited, + precomposedTransaction, txid, }), ); - - return result; } + + dispatch( + applySendFormMetadataLabelsThunk({ + selectedAccount, + precomposedTransaction, + txid, + }), + ); + + return result; }, ); diff --git a/packages/suite/src/actions/wallet/stakeActions.ts b/packages/suite/src/actions/wallet/stakeActions.ts index 6c9808467c6..d8e22df8895 100644 --- a/packages/suite/src/actions/wallet/stakeActions.ts +++ b/packages/suite/src/actions/wallet/stakeActions.ts @@ -117,7 +117,7 @@ const pushTransaction = // this will update transaction details (like time, fee etc.) dispatch( replaceTransactionThunk({ - precomposedTx, + precomposedTransaction: precomposedTx, newTxid: txid, }), ); diff --git a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModalContent.tsx b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModalContent.tsx index 3cd26b01b09..67e40390ad2 100644 --- a/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModalContent.tsx +++ b/packages/suite/src/components/suite/modals/ReduxModal/TransactionReviewModal/TransactionReviewModalContent.tsx @@ -3,6 +3,7 @@ import styled from 'styled-components'; import { ConfirmOnDevice, variables } from '@trezor/components'; import { Deferred } from '@trezor/utils'; import { selectDevice, StakeState } from '@suite-common/wallet-core'; +import { FormState, StakeFormState } from '@suite-common/wallet-types'; import { isCardanoTx } from '@suite-common/wallet-utils'; import { SendState } from '@suite-common/wallet-core'; import { useSelector } from 'src/hooks/suite'; @@ -28,6 +29,14 @@ const StyledModal = styled(Modal)` } `; +const isStakeState = (state: SendState | StakeState): state is StakeState => { + return 'data' in state; +}; + +const isStakeForm = (form: FormState | StakeFormState): form is StakeFormState => { + return 'ethereumStakeType' in form; +}; + interface TransactionReviewModalContentProps { decision: Deferred | undefined; txInfoState: SendState | StakeState; @@ -48,13 +57,21 @@ export const TransactionReviewModalContent = ({ const deviceModelInternal = device?.features?.internal_model; - const { precomposedTx, precomposedForm, serializedTx } = txInfoState; + const { account } = selectedAccount; + const { precomposedTx, serializedTx } = txInfoState; + + if (!account) { + return null; + } + + const precomposedForm: FormState | StakeFormState | undefined = isStakeState(txInfoState) + ? txInfoState.precomposedForm + : txInfoState.drafts[account.key]; if (selectedAccount.status !== 'loaded' || !device || !precomposedTx || !precomposedForm) { return null; } - const { account } = selectedAccount; const { networkType } = account; const isCardano = isCardanoTx(account, precomposedTx); const isEthereum = networkType === 'ethereum'; @@ -71,8 +88,9 @@ export const TransactionReviewModalContent = ({ precomposedTx, }); - const ethereumStakeType = - 'ethereumStakeType' in precomposedForm ? precomposedForm.ethereumStakeType : null; + const ethereumStakeType = isStakeForm(precomposedForm) + ? precomposedForm.ethereumStakeType + : null; // omit other button requests (like passphrase) const buttonRequests = device.buttonRequests.filter( diff --git a/packages/suite/src/hooks/wallet/form/useCompose.ts b/packages/suite/src/hooks/wallet/form/useCompose.ts index 888d8260343..03b75eef5d3 100644 --- a/packages/suite/src/hooks/wallet/form/useCompose.ts +++ b/packages/suite/src/hooks/wallet/form/useCompose.ts @@ -231,16 +231,16 @@ export const useCompose = ({ // called from the UI, triggers signing process const sign = async () => { const values = getValues(); - const composedTx = composedLevels + const precomposedTransaction = composedLevels ? composedLevels[values.selectedFee || 'normal'] : undefined; - if (composedTx && composedTx.type === 'final') { + if (precomposedTransaction && precomposedTransaction.type === 'final') { // sign workflow in Actions: // signSendFormTransactionThunk > sign[COIN]TransactionThunk > sendFormActions.storeSignedTransaction (modal with promise decision) const result = await dispatch( signAndPushSendFormTransactionThunk({ formValues: values, - precomposedTransaction: composedTx, + precomposedTransaction, selectedAccount, }), ).unwrap(); diff --git a/packages/suite/src/hooks/wallet/useCardanoStaking.ts b/packages/suite/src/hooks/wallet/useCardanoStaking.ts index 911e70f3773..6c94c855fc8 100644 --- a/packages/suite/src/hooks/wallet/useCardanoStaking.ts +++ b/packages/suite/src/hooks/wallet/useCardanoStaking.ts @@ -235,7 +235,11 @@ export const useCardanoStaking = (): CardanoStaking => { }), ); dispatch( - addFakePendingCardanoTxThunk({ precomposedTx: txPlan, txid, account }), + addFakePendingCardanoTxThunk({ + precomposedTransaction: txPlan, + txid, + account, + }), ); dispatch(setPendingStakeTx(account, txid)); } else { diff --git a/packages/suite/src/hooks/wallet/useCoinmarketRecomposeAndSign.ts b/packages/suite/src/hooks/wallet/useCoinmarketRecomposeAndSign.ts index 42015bbb816..67a3800163e 100644 --- a/packages/suite/src/hooks/wallet/useCoinmarketRecomposeAndSign.ts +++ b/packages/suite/src/hooks/wallet/useCoinmarketRecomposeAndSign.ts @@ -123,14 +123,14 @@ export const useCoinmarketRecomposeAndSign = () => { return; } - const composedToSign = composedLevels[selectedFee]; + const precomposedToSign = composedLevels[selectedFee]; - if (!composedToSign || composedToSign.type !== 'final') { + if (!precomposedToSign || precomposedToSign.type !== 'final') { let errorMessage: string | undefined; - if (composedToSign?.type === 'error' && composedToSign.errorMessage) { + if (precomposedToSign?.type === 'error' && precomposedToSign.errorMessage) { errorMessage = translationString( - composedToSign.errorMessage.id, - composedToSign.errorMessage.values as { [key: string]: any }, + precomposedToSign.errorMessage.id, + precomposedToSign.errorMessage.values as { [key: string]: any }, ); } if (!errorMessage) { @@ -149,7 +149,7 @@ export const useCoinmarketRecomposeAndSign = () => { return dispatch( signAndPushSendFormTransactionThunk({ formValues, - precomposedTransaction: composedToSign, + precomposedTransaction: precomposedToSign, selectedAccount: account, }), ).unwrap(); diff --git a/packages/suite/src/hooks/wallet/useSendForm.ts b/packages/suite/src/hooks/wallet/useSendForm.ts index 8a5979bc660..c0413584f00 100644 --- a/packages/suite/src/hooks/wallet/useSendForm.ts +++ b/packages/suite/src/hooks/wallet/useSendForm.ts @@ -258,17 +258,17 @@ export const useSendForm = (props: UseSendFormProps): SendContextValues => { // get response from TransactionReviewModal const sign = useCallback(async () => { const values = getValues(); - const composedTx = composedLevels + const precomposedTransaction = composedLevels ? composedLevels[values.selectedFee || 'normal'] : undefined; - if (composedTx && composedTx.type === 'final') { + if (precomposedTransaction && precomposedTransaction.type === 'final') { // sign workflow in Actions: // signSendFormTransactionThunk > sign[COIN]SendFormTransactionThunk > sendFormActions.storeSignedTransaction (modal with promise decision) setLoading(true); const result = await dispatch( signAndPushSendFormTransactionThunk({ formValues: values, - precomposedTransaction: composedTx, + precomposedTransaction, selectedAccount: props.selectedAccount.account, }), ).unwrap(); diff --git a/packages/suite/src/reducers/wallet/__tests__/sendFormReducer.test.ts b/packages/suite/src/reducers/wallet/__tests__/sendFormReducer.test.ts index 437bb2ad448..e999250d037 100644 --- a/packages/suite/src/reducers/wallet/__tests__/sendFormReducer.test.ts +++ b/packages/suite/src/reducers/wallet/__tests__/sendFormReducer.test.ts @@ -65,13 +65,14 @@ describe('sendFormReducer', () => { it('SEND.REQUEST_SIGN_TRANSACTION - save', () => { const action: Action = sendFormActions.storePrecomposedTransaction({ - formState: formStateMock, + accountKey: 'key1', + enhancedFormDraft: formStateMock, precomposedTransaction: precomposedTxMock, }); const state = prepareSendFormReducer(extraDependencies)(initialState, action); expect(state.precomposedTx).toEqual(precomposedTxMock); - expect(state.precomposedForm).toEqual(formStateMock); + expect(state.drafts['key1']).toEqual(formStateMock); }); it('SEND.REQUEST_PUSH_TRANSACTION - save', () => { @@ -93,14 +94,12 @@ describe('sendFormReducer', () => { { ...initialState, serializedTx: formSignedTxMock, - precomposedForm: formStateMock, precomposedTx: precomposedTxMock, }, action, ); expect(state.serializedTx).toBeUndefined(); expect(state.precomposedTx).toBeUndefined(); - expect(state.precomposedForm).toBeUndefined(); }); it('SEND.SEND_RAW', () => { @@ -121,14 +120,12 @@ describe('sendFormReducer', () => { ...initialState, sendRaw: true, precomposedTx: precomposedTxMock, - precomposedForm: formStateMock, serializedTx: formSignedTxMock, }, action, ); expect(state.sendRaw).toBeUndefined(); expect(state.precomposedTx).toBeUndefined(); - expect(state.precomposedForm).toBeUndefined(); expect(state.serializedTx).toBeUndefined(); }); }); diff --git a/suite-common/wallet-core/src/send/sendFormActions.ts b/suite-common/wallet-core/src/send/sendFormActions.ts index 25253a92199..a6a11b931d9 100644 --- a/suite-common/wallet-core/src/send/sendFormActions.ts +++ b/suite-common/wallet-core/src/send/sendFormActions.ts @@ -27,7 +27,8 @@ const removeDraft = createAction( const storePrecomposedTransaction = createAction( `${SEND_MODULE_PREFIX}/store-precomposed-transaction`, (payload: { - formState: FormState; + accountKey: AccountKey; + enhancedFormDraft: FormState; precomposedTransaction: GeneralPrecomposedTransactionFinal; }) => ({ payload, diff --git a/suite-common/wallet-core/src/send/sendFormReducer.ts b/suite-common/wallet-core/src/send/sendFormReducer.ts index 9495551f194..28e27f6160f 100644 --- a/suite-common/wallet-core/src/send/sendFormReducer.ts +++ b/suite-common/wallet-core/src/send/sendFormReducer.ts @@ -1,4 +1,8 @@ -import { FormState, GeneralPrecomposedTransactionFinal } from '@suite-common/wallet-types'; +import { + AccountKey, + FormState, + GeneralPrecomposedTransactionFinal, +} from '@suite-common/wallet-types'; import { cloneObject } from '@trezor/utils'; import { SerializedTx } from '@suite-common/wallet-types'; import { createReducerWithExtraDeps } from '@suite-common/redux-utils'; @@ -9,13 +13,12 @@ import { accountsActions } from '../accounts/accountsActions'; export type SendState = { drafts: { - [key: string]: FormState; // Key: account key + [key: AccountKey]: FormState; }; sendRaw?: boolean; precomposedTx?: GeneralPrecomposedTransactionFinal; - precomposedForm?: FormState; signedTx?: BlockbookTransaction; - serializedTx?: SerializedTx; // hex representation of signed transaction (payload for TrezorConnect.pushTransaction) + serializedTx?: SerializedTx; // hexadecimal representation of signed transaction (payload for TrezorConnect.pushTransaction) }; export const initialState: SendState = { @@ -45,13 +48,13 @@ export const prepareSendFormReducer = createReducerWithExtraDeps(initialState, ( }) .addCase( sendFormActions.storePrecomposedTransaction, - (state, { payload: { precomposedTransaction, formState } }) => { + (state, { payload: { precomposedTransaction, accountKey, enhancedFormDraft } }) => { state.precomposedTx = precomposedTransaction; // Deep-cloning to prevent buggy interaction between react-hook-form and immer, see https://github.com/orgs/react-hook-form/discussions/3715#discussioncomment-2151458 // Otherwise, whenever the outputs fieldArray is updated after the form draft or precomposedForm is saved, there is na error: // TypeError: Cannot assign to read only property of object '#' // This might not be necessary in the future when the dependencies are upgraded. - state.precomposedForm = cloneObject(formState); + state.drafts[accountKey] = cloneObject(enhancedFormDraft); }, ) .addCase( @@ -63,7 +66,6 @@ export const prepareSendFormReducer = createReducerWithExtraDeps(initialState, ( ) .addCase(sendFormActions.discardTransaction, state => { delete state.precomposedTx; - delete state.precomposedForm; delete state.serializedTx; delete state.signedTx; }) @@ -73,7 +75,6 @@ export const prepareSendFormReducer = createReducerWithExtraDeps(initialState, ( .addCase(sendFormActions.dispose, state => { delete state.sendRaw; delete state.precomposedTx; - delete state.precomposedForm; delete state.serializedTx; delete state.signedTx; }) @@ -88,6 +89,6 @@ export const prepareSendFormReducer = createReducerWithExtraDeps(initialState, ( export const selectSendPrecomposedTx = (state: SendRootState) => state.wallet.send.precomposedTx; export const selectSendSerializedTx = (state: SendRootState) => state.wallet.send.serializedTx; export const selectSendSignedTx = (state: SendRootState) => state.wallet.send.signedTx; -export const selectPrecomposedSendForm = (state: SendRootState) => - state.wallet.send.precomposedForm; export const selectSendFormDrafts = (state: SendRootState) => state.wallet.send.drafts; +export const selectSendFormDraftByAccountKey = (state: SendRootState, accountKey: AccountKey) => + state.wallet.send.drafts[accountKey]; diff --git a/suite-common/wallet-core/src/send/sendFormThunks.ts b/suite-common/wallet-core/src/send/sendFormThunks.ts index 7aa3ab31cf9..9a9938058d5 100644 --- a/suite-common/wallet-core/src/send/sendFormThunks.ts +++ b/suite-common/wallet-core/src/send/sendFormThunks.ts @@ -1,4 +1,4 @@ -import { G, A } from '@mobily/ts-belt'; +import { G } from '@mobily/ts-belt'; import { BigNumber } from '@trezor/utils/src/bigNumber'; import { createThunk } from '@suite-common/redux-utils'; @@ -25,7 +25,7 @@ import { getNetwork, tryGetAccountIdentity, } from '@suite-common/wallet-utils'; -import TrezorConnect from '@trezor/connect'; +import TrezorConnect, { Success, Unsuccessful } from '@trezor/connect'; import { cloneObject } from '@trezor/utils'; import { BlockbookTransaction } from '@trezor/blockchain-link-types'; @@ -180,11 +180,11 @@ const synchronizeSentTransactionThunk = createThunk( ( { selectedAccount, - precomposedTx, + precomposedTransaction, txid, }: { selectedAccount: Account; - precomposedTx: GeneralPrecomposedTransactionFinal; + precomposedTransaction: GeneralPrecomposedTransactionFinal; txid: string; }, { dispatch }, @@ -192,17 +192,17 @@ const synchronizeSentTransactionThunk = createThunk( // notification from the backend may be delayed. // modify affected account balance. // TODO: make it work with ETH accounts - if (isCardanoTx(selectedAccount, precomposedTx)) { + if (isCardanoTx(selectedAccount, precomposedTransaction)) { const pendingAccount = getPendingAccount({ account: selectedAccount, - tx: precomposedTx, + tx: precomposedTransaction, txid, }); if (pendingAccount) { // manually add fake pending tx as we don't have the data about mempool txs dispatch( addFakePendingCardanoTxThunk({ - precomposedTx, + precomposedTransaction, txid, account: selectedAccount, }), @@ -212,7 +212,7 @@ const synchronizeSentTransactionThunk = createThunk( } else if (selectedAccount.networkType === 'bitcoin') { dispatch( addFakePendingTxThunk({ - precomposedTx, + precomposedTransaction, account: selectedAccount, }), ); @@ -224,26 +224,27 @@ const synchronizeSentTransactionThunk = createThunk( }, ); -export const pushSendFormTransactionThunk = createThunk( +export const pushSendFormTransactionThunk = createThunk< + Success<{ txid: string }>, + { selectedAccount: Account }, + { rejectValue: string | Unsuccessful } +>( `${SEND_MODULE_PREFIX}/pushSendFormTransactionThunk`, async ( - { - selectedAccount, - }: { - selectedAccount: Account; - }, + { selectedAccount }, { dispatch, getState, extra, rejectWithValue, fulfillWithValue }, ) => { const { actions: { onModalCancel }, selectors: { selectBitcoinAmountUnit }, } = extra; - const precomposedTx = selectSendPrecomposedTx(getState()); + const precomposedTransaction = selectSendPrecomposedTx(getState()); const serializedTx = selectSendSerializedTx(getState()); const device = selectDevice(getState()); const bitcoinAmountUnit = selectBitcoinAmountUnit(getState()); - if (!serializedTx || !precomposedTx) return rejectWithValue('Transaction not found.'); + if (!serializedTx || !precomposedTransaction) + return rejectWithValue('Transaction not found.'); const pushTxResponse = await TrezorConnect.pushTransaction({ ...serializedTx, @@ -253,9 +254,11 @@ export const pushSendFormTransactionThunk = createThunk( // close modal regardless result dispatch(onModalCancel()); - const { token } = precomposedTx; + const { token } = precomposedTransaction; const spentWithoutFee = !token - ? new BigNumber(precomposedTx.totalSpent).minus(precomposedTx.fee).toString() + ? new BigNumber(precomposedTransaction.totalSpent) + .minus(precomposedTransaction.fee) + .toString() : '0'; const areSatoshisUsed = getAreSatoshisUsed(bitcoinAmountUnit, selectedAccount); @@ -263,7 +266,7 @@ export const pushSendFormTransactionThunk = createThunk( // get total amount without fee OR token amount const formattedAmount = token ? `${formatAmount( - precomposedTx.totalSpent, + precomposedTransaction.totalSpent, token.decimals, )} ${token.symbol!.toUpperCase()}` : formatNetworkAmount(spentWithoutFee, selectedAccount.symbol, true, areSatoshisUsed); @@ -281,7 +284,13 @@ export const pushSendFormTransactionThunk = createThunk( }), ); - dispatch(synchronizeSentTransactionThunk({ selectedAccount, precomposedTx, txid })); + dispatch( + synchronizeSentTransactionThunk({ + selectedAccount, + precomposedTransaction, + txid, + }), + ); } else { dispatch( notificationsActions.addToast({ @@ -419,23 +428,22 @@ export const signTransactionThunk = createThunk( }, ); -export const enhancePrecomposedTransactionThunk = createThunk( - `${SEND_MODULE_PREFIX}/prepareTransactionForSigningThunk`, +export const enhancePrecomposedTransactionThunk = createThunk< + GeneralPrecomposedTransactionFinal, + { + transactionFormValues: FormState; + precomposedTransaction: GeneralPrecomposedTransactionFinal; + selectedAccount: Account; + }, + { rejectValue: string } +>( + `${SEND_MODULE_PREFIX}/enhancePrecomposedTransactionThunk`, async ( - { - transactionFormValues: formValues, - precomposedTransaction, - selectedAccount, - }: { - transactionFormValues: FormState; - precomposedTransaction: GeneralPrecomposedTransactionFinal; - selectedAccount: Account; - }, + { transactionFormValues: formValues, precomposedTransaction, selectedAccount }, { getState, dispatch, rejectWithValue }, ) => { const device = selectDevice(getState()); const selectedAccountNetwork = getNetwork(selectedAccount.symbol); - if (!device) return rejectWithValue('Device not found'); // native RBF is available since FW 1.9.4/2.3.5 @@ -458,46 +466,51 @@ export const enhancePrecomposedTransactionThunk = createThunk( (!hasDecreasedOutput && nativeRbfAvailable) || (hasDecreasedOutput && decreaseOutputAvailable); - const enhancedPrecomposedTx: GeneralPrecomposedTransactionFinal = { + const enhancedPrecomposedTransaction: GeneralPrecomposedTransactionFinal = { ...precomposedTransaction, }; - 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) - .toFixed(); - enhancedPrecomposedTx.useNativeRbf = useNativeRbf; - enhancedPrecomposedTx.useDecreaseOutput = hasDecreasedOutput; + if (!isCardanoTx(selectedAccount, enhancedPrecomposedTransaction)) { + enhancedPrecomposedTransaction.rbf = formValues.options.includes('bitcoinRBF'); + + if (formValues.rbfParams) { + enhancedPrecomposedTransaction.prevTxid = formValues.rbfParams.txid; + enhancedPrecomposedTransaction.feeDifference = new BigNumber( + precomposedTransaction.fee, + ) + .minus(formValues.rbfParams.baseFee) + .toFixed(); + enhancedPrecomposedTransaction.useNativeRbf = useNativeRbf; + enhancedPrecomposedTransaction.useDecreaseOutput = hasDecreasedOutput; + } } if ( - !isCardanoTx(selectedAccount, enhancedPrecomposedTx) && + !isCardanoTx(selectedAccount, enhancedPrecomposedTransaction) && selectedAccount.networkType === 'ethereum' && - enhancedPrecomposedTx.token?.contract && + enhancedPrecomposedTransaction.token?.contract && selectedAccountNetwork?.chainId ) { const isTokenKnown = await fetch( `https://data.trezor.io/firmware/eth-definitions/chain-id/${ selectedAccountNetwork.chainId - }/token-${enhancedPrecomposedTx.token.contract.substring(2).toLowerCase()}.dat`, + }/token-${enhancedPrecomposedTransaction.token.contract.substring(2).toLowerCase()}.dat`, { method: 'HEAD' }, ) .then(response => response.ok) .catch(() => false); - enhancedPrecomposedTx.isTokenKnown = isTokenKnown; + enhancedPrecomposedTransaction.isTokenKnown = isTokenKnown; } - // store formValues and transactionInfo in send reducer to be used by TransactionReviewModal dispatch( sendFormActions.storePrecomposedTransaction({ - formState: formValues, - precomposedTransaction: enhancedPrecomposedTx, + accountKey: selectedAccount.key, + enhancedFormDraft: formValues, + precomposedTransaction: enhancedPrecomposedTransaction, }), ); - return enhancedPrecomposedTx; + return enhancedPrecomposedTransaction; }, ); diff --git a/suite-common/wallet-core/src/transactions/transactionsThunks.ts b/suite-common/wallet-core/src/transactions/transactionsThunks.ts index f342b083176..45242967536 100644 --- a/suite-common/wallet-core/src/transactions/transactionsThunks.ts +++ b/suite-common/wallet-core/src/transactions/transactionsThunks.ts @@ -41,20 +41,26 @@ import { selectSendSignedTx } from '../send/sendFormReducer'; */ interface ReplaceTransactionThunkParams { // transaction input parameters. It has to be passed as argument rather than obtained form send-form state, because this thunk is used also by eth-staking module that uses different redux state. - precomposedTx: PrecomposedTransactionFinal; + precomposedTransaction: PrecomposedTransactionFinal; newTxid: string; } export const replaceTransactionThunk = createThunk( `${TRANSACTIONS_MODULE_PREFIX}/replaceTransactionThunk`, - ({ precomposedTx, newTxid }: ReplaceTransactionThunkParams, { getState, dispatch }) => { - if (!precomposedTx.prevTxid) return; // ignore if it's not a replacement tx + ( + { precomposedTransaction, newTxid }: ReplaceTransactionThunkParams, + { getState, dispatch }, + ) => { + if (!precomposedTransaction.prevTxid) return; // ignore if it's not a replacement tx const walletTransactions = selectTransactions(getState()); const signedTransaction = selectSendSignedTx(getState()); // find all transactions to replace, they may be related to another account - const origTransactions = findTransactions(precomposedTx.prevTxid, walletTransactions); + const origTransactions = findTransactions( + precomposedTransaction.prevTxid, + walletTransactions, + ); // prepare replace actions for txs const actions = origTransactions.flatMap(origTx => { @@ -76,17 +82,17 @@ export const replaceTransactionThunk = createThunk( newTx = { ...origTx.tx, txid: newTxid, - fee: precomposedTx.fee, - rbf: !!precomposedTx.rbf, + fee: precomposedTransaction.fee, + rbf: !!precomposedTransaction.rbf, blockTime: Math.round(new Date().getTime() / 1000), // TODO: details: {}, is it worth it? }; // update ethereumSpecific values - newTx.ethereumSpecific = replaceEthereumSpecific(newTx, precomposedTx); + newTx.ethereumSpecific = replaceEthereumSpecific(newTx, precomposedTransaction); // finalized and recv tx shouldn't have rbfParams - if (!precomposedTx.rbf || origTx.tx.type === 'recv') { + if (!precomposedTransaction.rbf || origTx.tx.type === 'recv') { delete newTx.rbfParams; } else { // update tx rbfParams @@ -96,7 +102,7 @@ export const replaceTransactionThunk = createThunk( return transactionsActions.replaceTransaction({ key: origTx.key, - txid: precomposedTx.prevTxid, + txid: precomposedTransaction.prevTxid, tx: newTx, }); }); @@ -107,14 +113,14 @@ export const replaceTransactionThunk = createThunk( ); interface AddFakePendingTransactionParams { - precomposedTx: PrecomposedTransactionFinal; + precomposedTransaction: PrecomposedTransactionFinal; account: Account; } export const addFakePendingTxThunk = createThunk( `${TRANSACTIONS_MODULE_PREFIX}/addFakePendingTransaction`, ( - { precomposedTx, account }: AddFakePendingTransactionParams, + { precomposedTransaction, account }: AddFakePendingTransactionParams, { dispatch, getState, rejectWithValue }, ) => { const blockHeight = selectBlockchainHeightBySymbol(getState(), account.symbol); @@ -148,7 +154,7 @@ export const addFakePendingTxThunk = createThunk( Object.keys(affectedAccounts).forEach(key => { const affectedAccount = affectedAccounts[key]; - if (!precomposedTx.prevTxid) { + if (!precomposedTransaction.prevTxid) { // create and profile pending transaction for affected account if it's not a replacement tx const affectedAccountTransaction = blockbookUtils.transformTransaction( signedTransaction, @@ -170,7 +176,7 @@ export const addFakePendingTxThunk = createThunk( const pendingAccount = getPendingAccount({ account: affectedAccount, - tx: precomposedTx, + tx: precomposedTransaction, txid: signedTransaction.txid, receivingAccount: account.key !== affectedAccount.key, }); @@ -186,11 +192,11 @@ export const addFakePendingCardanoTxThunk = createThunk( `${TRANSACTIONS_MODULE_PREFIX}/addFakePendingTransaction`, ( { - precomposedTx, + precomposedTransaction, txid, account, }: { - precomposedTx: Pick; + precomposedTransaction: Pick; txid: string; account: Account; }, @@ -206,10 +212,10 @@ export const addFakePendingCardanoTxThunk = createThunk( blockTime: Math.floor(new Date().getTime() / 1000), blockHash: undefined, // amounts (as most of props below) don't matter much since it is temp fake anyway - amount: precomposedTx.totalSpent, - fee: precomposedTx.fee, + amount: precomposedTransaction.totalSpent, + fee: precomposedTransaction.fee, feeRate: '0', - totalSpent: precomposedTx.totalSpent, + totalSpent: precomposedTransaction.totalSpent, targets: [], tokens: [], internalTransfers: [],