Skip to content

Commit

Permalink
feat(suite-native): send token drafts
Browse files Browse the repository at this point in the history
  • Loading branch information
PeKne committed Nov 1, 2024
1 parent 61bb366 commit edb743a
Show file tree
Hide file tree
Showing 15 changed files with 89 additions and 47 deletions.
4 changes: 2 additions & 2 deletions packages/suite/src/actions/wallet/send/sendFormThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
selectSendFormDrafts,
signTransactionThunk,
sendFormActions,
selectSendFormDraftByAccountKey,
selectSendFormDraftByKey,
} from '@suite-common/wallet-core';
import { isCardanoTx, isRbfTransaction } from '@suite-common/wallet-utils';
import { MetadataAddPayload } from '@suite-common/metadata-types';
Expand Down Expand Up @@ -140,7 +140,7 @@ const applySendFormMetadataLabelsThunk = createThunk(

if (!metadata.enabled) return;

const formDraft = selectSendFormDraftByAccountKey(getState(), selectedAccount.key);
const formDraft = selectSendFormDraftByKey(getState(), selectedAccount.key);

const outputsPermutation = isCardanoTx(selectedAccount, precomposedTransaction)
? precomposedTransaction?.outputs.map((_o, i) => i) // cardano preserves order of outputs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Deferred } from '@trezor/utils';
import {
DeviceRootState,
selectDevice,
selectSendFormDraftByAccountKey,
selectSendFormDraftByKey,
selectSendFormReviewButtonRequestsCount,
selectStakePrecomposedForm,
StakeState,
Expand Down Expand Up @@ -69,7 +69,7 @@ export const TransactionReviewModalContent = ({
const precomposedForm = useSelector(state =>
isStakeState(txInfoState)
? selectStakePrecomposedForm(state)
: selectSendFormDraftByAccountKey(state, account?.key),
: selectSendFormDraftByKey(state, account?.key),
);

const isRbfAction = precomposedTx !== undefined && isRbfTransaction(precomposedTx);
Expand Down
3 changes: 2 additions & 1 deletion suite-common/wallet-core/src/send/sendFormActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
FormState,
AccountKey,
GeneralPrecomposedTransactionFinal,
TokenAddress,
} from '@suite-common/wallet-types';
import { BlockbookTransaction } from '@trezor/blockchain-link-types';

Expand All @@ -12,7 +13,7 @@ import { SerializedTx } from './sendFormTypes';

const storeDraft = createAction(
`${SEND_MODULE_PREFIX}/store-draft`,
(payload: { accountKey: AccountKey; formState: FormState }) => ({
(payload: { accountKey: AccountKey; formState: FormState; tokenContract?: TokenAddress }) => ({
payload,
}),
);
Expand Down
32 changes: 21 additions & 11 deletions suite-common/wallet-core/src/send/sendFormReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import {
FormState,
GeneralPrecomposedTransactionFinal,
Output,
SendFormDraftKey,
TokenAddress,
} from '@suite-common/wallet-types';
import { cloneObject } from '@trezor/utils';
import { createReducerWithExtraDeps } from '@suite-common/redux-utils';
import { BlockbookTransaction } from '@trezor/blockchain-link-types';
import { NetworkSymbol, networks } from '@suite-common/wallet-config';
import { DeviceModelInternal } from '@trezor/connect';
import { getSendFormDraftKey } from '@suite-common/wallet-utils';

import { sendFormActions } from './sendFormActions';
import { accountsActions } from '../accounts/accountsActions';
Expand All @@ -23,7 +26,7 @@ import { SerializedTx } from './sendFormTypes';

export type SendState = {
drafts: {
[key: AccountKey]: FormState;
[key: SendFormDraftKey]: FormState;
};
sendRaw?: boolean;
precomposedTx?: GeneralPrecomposedTransactionFinal;
Expand All @@ -46,13 +49,18 @@ export type SendRootState = {

export const prepareSendFormReducer = createReducerWithExtraDeps(initialState, (builder, extra) => {
builder
.addCase(sendFormActions.storeDraft, (state, { payload: { accountKey, formState } }) => {
// 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 '#<Object>'
// This might not be necessary in the future when the dependencies are upgraded.
state.drafts[accountKey] = cloneObject(formState);
})
.addCase(
sendFormActions.storeDraft,
(state, { payload: { accountKey, tokenContract, formState } }) => {
const draftKey = getSendFormDraftKey(accountKey, tokenContract);

// 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 '#<Object>'
// This might not be necessary in the future when the dependencies are upgraded.
state.drafts[draftKey] = cloneObject(formState);
},
)
.addCase(sendFormActions.removeDraft, (state, { payload: { accountKey } }) => {
delete state.drafts[accountKey];
})
Expand Down Expand Up @@ -96,22 +104,24 @@ export const selectSendSerializedTx = (state: SendRootState) => state.wallet.sen
export const selectSendSignedTx = (state: SendRootState) => state.wallet.send.signedTx;
export const selectSendFormDrafts = (state: SendRootState) => state.wallet.send.drafts;

export const selectSendFormDraftByAccountKey = (
export const selectSendFormDraftByKey = (
state: SendRootState,
accountKey?: AccountKey,
tokenContract?: TokenAddress,
): FormState | null => {
if (G.isUndefined(accountKey)) return null;

return state.wallet.send.drafts[accountKey] ?? null;
return state.wallet.send.drafts[getSendFormDraftKey(accountKey, tokenContract)] ?? null;
};

export const selectSendFormDraftOutputsByAccountKey = (
state: SendRootState,
accountKey?: AccountKey,
tokenContract?: TokenAddress,
): Output[] | null => {
if (G.isUndefined(accountKey)) return null;

const draft = selectSendFormDraftByAccountKey(state, accountKey);
const draft = selectSendFormDraftByKey(state, accountKey, tokenContract);

return draft?.outputs ?? null;
};
Expand Down
1 change: 1 addition & 0 deletions suite-common/wallet-types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './selectedAccount';
export * from './transaction';
export * from './stake';
export * from './stakeForm';
export * from './send';
5 changes: 5 additions & 0 deletions suite-common/wallet-types/src/send.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { AccountKey, TokenAddress } from './account';

export type SendFormDraftKey =
| AccountKey
| (`${AccountKey}-${TokenAddress}` & { __type: 'SendFormDraftKey' });
10 changes: 10 additions & 0 deletions suite-common/wallet-utils/src/sendFormUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ import type {
CurrencyOption,
ExcludedUtxos,
GeneralPrecomposedTransactionFinal,
AccountKey,
TokenAddress,
SendFormDraftKey,
} from '@suite-common/wallet-types';

import { amountToSatoshi, getUtxoOutpoint, networkAmountToSatoshi } from './accountUtils';
Expand Down Expand Up @@ -461,6 +464,13 @@ export const getExcludedUtxos = ({
return excludedUtxos;
};

export const getSendFormDraftKey = (
accountKey: AccountKey,
tokenAddress?: TokenAddress,
): SendFormDraftKey => {
return tokenAddress ? `${accountKey}-${tokenAddress}` : accountKey;
};

// SOL Specific

export const getLamportsFromSol = (amountInSol: string) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const AddressReviewStepList = () => {

const isAddressConfirmed = useSelector(
(state: AccountsRootState & DeviceRootState & SendRootState) =>
selectIsFirstTransactionAddressConfirmed(state, accountKey),
selectIsFirstTransactionAddressConfirmed(state, accountKey, tokenContract),
);

useEffect(() => {
Expand Down Expand Up @@ -104,6 +104,7 @@ export const AddressReviewStepList = () => {
const response = await dispatch(
signTransactionThunk({
accountKey,
tokenContract,
feeLevel: transaction,
}),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { isFulfilled } from '@reduxjs/toolkit';
import {
AccountsRootState,
selectAccountByKey,
selectSendFormDraftByAccountKey,
selectSendFormDraftByKey,
SendRootState,
} from '@suite-common/wallet-core';
import { AccountKey, TokenAddress } from '@suite-common/wallet-types';
Expand Down Expand Up @@ -82,7 +82,7 @@ export const OutputsReviewFooter = ({
const isTransactionAlreadySigned = useSelector(selectIsTransactionAlreadySigned);

const formValues = useSelector((state: SendRootState) =>
selectSendFormDraftByAccountKey(state, accountKey),
selectSendFormDraftByKey(state, accountKey, tokenContract),
);

{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ export const ReviewOutputItemList = ({ accountKey, tokenContract }: ReviewOutput

const reviewOutputs = useSelector(
(state: AccountsRootState & DeviceRootState & SendRootState) =>
selectTransactionReviewOutputs(state, accountKey),
selectTransactionReviewOutputs(state, accountKey, tokenContract),
);
const isTransactionAlreadySigned = useSelector(selectIsTransactionAlreadySigned);
const activeStep = useSelector((state: AccountsRootState & DeviceRootState & SendRootState) =>
selectTransactionReviewActiveStepIndex(state, accountKey),
selectTransactionReviewActiveStepIndex(state, accountKey, tokenContract),
);

const [childHeights, setChildHeights] = useState<number[]>([]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const ReviewOutputSummaryItem = ({
const { translate } = useTranslate();
const summaryOutput = useSelector(
(state: AccountsRootState & DeviceRootState & SendRootState) =>
selectReviewSummaryOutput(state, accountKey),
selectReviewSummaryOutput(state, accountKey, tokenContract),
);

if (!summaryOutput) return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ export const SendAddressReviewScreen = ({

const isAddressConfirmed = useSelector(
(state: AccountsRootState & DeviceRootState & SendRootState) =>
selectIsFirstTransactionAddressConfirmed(state, accountKey),
selectIsFirstTransactionAddressConfirmed(state, accountKey, tokenContract),
);

const isReviewInProgress = useSelector(
(state: AccountsRootState & DeviceRootState & SendRootState) =>
selectIsOutputsReviewInProgress(state, accountKey),
selectIsOutputsReviewInProgress(state, accountKey, tokenContract),
);

useEffect(() => {
Expand Down
7 changes: 3 additions & 4 deletions suite-native/module-send/src/screens/SendOutputsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
composeSendFormTransactionFeeLevelsThunk,
selectAccountByKey,
selectNetworkFeeInfo,
selectSendFormDraftByAccountKey,
selectSendFormDraftByKey,
sendFormActions,
updateFeeInfoThunk,
} from '@suite-common/wallet-core';
Expand Down Expand Up @@ -91,7 +91,7 @@ export const SendOutputsScreen = ({
selectNetworkFeeInfo(state, account?.symbol),
);
const sendFormDraft = useSelector((state: SendRootState) =>
selectSendFormDraftByAccountKey(state, accountKey),
selectSendFormDraftByKey(state, accountKey, tokenContract),
);

const network = account ? getNetwork(account.symbol) : null;
Expand Down Expand Up @@ -125,6 +125,7 @@ export const SendOutputsScreen = ({
dispatch(
sendFormActions.storeDraft({
accountKey,
tokenContract,
formState: constructFormDraft({ formValues: getValues(), tokenContract }),
}),
);
Expand All @@ -144,8 +145,6 @@ export const SendOutputsScreen = ({
}, [getValues, accountKey, dispatch]);

useEffect(() => {
// TODO: Store token draft to separate object so it do not override mainnet draft.
// https://github.com/trezor/trezor-suite/issues/15078
const prefillValuesFromStoredDraft = async () => {
if (sendFormDraft?.outputs) {
setValue('outputs', sendFormDraft.outputs);
Expand Down
32 changes: 19 additions & 13 deletions suite-native/module-send/src/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
selectAccountByKey,
selectDevice,
selectSendPrecomposedTx,
selectSendFormDraftByAccountKey,
selectSendFormDraftByKey,
selectSendFormReviewButtonRequestsCount,
selectSendSerializedTx,
} from '@suite-common/wallet-core';
Expand All @@ -17,15 +17,16 @@ import {
getTransactionReviewOutputState,
isRbfTransaction,
} from '@suite-common/wallet-utils';
import { ReviewOutputState } from '@suite-common/wallet-types';
import { AccountKey, ReviewOutputState, TokenAddress } from '@suite-common/wallet-types';

import { StatefulReviewOutput } from './types';

export const selectTransactionReviewOutputs = (
state: SendRootState & AccountsRootState & DeviceRootState,
accountKey: string,
accountKey: AccountKey,
tokenContract?: TokenAddress,
): StatefulReviewOutput[] | null => {
const precomposedForm = selectSendFormDraftByAccountKey(state, accountKey);
const precomposedForm = selectSendFormDraftByKey(state, accountKey, tokenContract);
const precomposedTx = selectSendPrecomposedTx(state);

const decreaseOutputId =
Expand Down Expand Up @@ -66,18 +67,20 @@ export const selectTransactionReviewOutputs = (

export const selectIsOutputsReviewInProgress = (
state: SendRootState & AccountsRootState & DeviceRootState,
accountKey: string,
accountKey: AccountKey,
tokenContract?: TokenAddress,
): boolean => {
const outputs = selectTransactionReviewOutputs(state, accountKey);
const outputs = selectTransactionReviewOutputs(state, accountKey, tokenContract);

return G.isNotNullable(outputs) && A.isNotEmpty(outputs);
};

export const selectIsFirstTransactionAddressConfirmed = (
state: SendRootState & AccountsRootState & DeviceRootState,
accountKey: string,
tokenContract?: TokenAddress,
): boolean => {
const outputs = selectTransactionReviewOutputs(state, accountKey);
const outputs = selectTransactionReviewOutputs(state, accountKey, tokenContract);

return outputs?.[0].state === 'success';
};
Expand All @@ -90,15 +93,16 @@ export const selectIsTransactionAlreadySigned = (state: SendRootState) => {

export const selectReviewSummaryOutputState = (
state: SendRootState & AccountsRootState & DeviceRootState,
accountKey: string,
accountKey: AccountKey,
tokenContract?: TokenAddress,
): ReviewOutputState => {
const isTransactionAlreadySigned = selectIsTransactionAlreadySigned(state);

if (isTransactionAlreadySigned) {
return 'success';
}

const reviewOutputs = selectTransactionReviewOutputs(state, accountKey);
const reviewOutputs = selectTransactionReviewOutputs(state, accountKey, tokenContract);

if (reviewOutputs && A.all(reviewOutputs, output => output.state === 'success')) {
return 'active';
Expand All @@ -109,15 +113,16 @@ export const selectReviewSummaryOutputState = (

export const selectReviewSummaryOutput = (
state: AccountsRootState & DeviceRootState & SendRootState,
accountKey: string,
accountKey: AccountKey,
tokenContract?: TokenAddress,
) => {
const precomposedTx = selectSendPrecomposedTx(state);

if (!precomposedTx) return null;

const { totalSpent, fee } = precomposedTx;

const outputState = selectReviewSummaryOutputState(state, accountKey);
const outputState = selectReviewSummaryOutputState(state, accountKey, tokenContract);

return {
state: outputState,
Expand All @@ -128,9 +133,10 @@ export const selectReviewSummaryOutput = (

export const selectTransactionReviewActiveStepIndex = (
state: AccountsRootState & DeviceRootState & SendRootState,
accountKey: string,
accountKey: AccountKey,
tokenContract?: TokenAddress,
) => {
const reviewOutputs = selectTransactionReviewOutputs(state, accountKey);
const reviewOutputs = selectTransactionReviewOutputs(state, accountKey, tokenContract);

if (!reviewOutputs) return 0;

Expand Down
Loading

0 comments on commit edb743a

Please sign in to comment.