From 03fe1552b225632a59d911963244d467f096dcc2 Mon Sep 17 00:00:00 2001 From: Petr Knetl Date: Fri, 22 Nov 2024 13:42:38 +0100 Subject: [PATCH] feat(suite-native): advanced recipient address validation --- suite-native/forms/src/hooks/useField.ts | 3 ++- .../src/components/AddressInput.tsx | 5 +++- .../src/components/SendMaxButton.tsx | 24 ++++++++--------- .../src/screens/SendOutputsScreen.tsx | 4 +++ .../module-send/src/sendOutputsFormSchema.ts | 27 +++++++++++++++---- 5 files changed, 43 insertions(+), 20 deletions(-) diff --git a/suite-native/forms/src/hooks/useField.ts b/suite-native/forms/src/hooks/useField.ts index 28302f56818..f88eccd0ad9 100644 --- a/suite-native/forms/src/hooks/useField.ts +++ b/suite-native/forms/src/hooks/useField.ts @@ -28,7 +28,7 @@ export const useField = ({ const { field: { onBlur, onChange, value }, - fieldState: { error, isDirty }, + fieldState: { error, isDirty, isTouched }, } = useController({ name, control, @@ -47,6 +47,7 @@ export const useField = ({ errorMessage, hasError, isDirty, + isTouched, value: transformedValue, onBlur, onChange, diff --git a/suite-native/module-send/src/components/AddressInput.tsx b/suite-native/module-send/src/components/AddressInput.tsx index 02607614c1d..7f830eef7eb 100644 --- a/suite-native/module-send/src/components/AddressInput.tsx +++ b/suite-native/module-send/src/components/AddressInput.tsx @@ -57,7 +57,10 @@ export const AddressInput = ({ index, accountKey }: AddressInputProps) => { // Debug helper to fill opened account address. const fillSelfAddress = () => { if (freshAccountAddress) - setValue(addressFieldName, freshAccountAddress.address, { shouldValidate: true }); + setValue(addressFieldName, freshAccountAddress.address, { + shouldValidate: true, + shouldTouch: true, + }); }; return ( diff --git a/suite-native/module-send/src/components/SendMaxButton.tsx b/suite-native/module-send/src/components/SendMaxButton.tsx index 8699e68abc0..b5515d6ec28 100644 --- a/suite-native/module-send/src/components/SendMaxButton.tsx +++ b/suite-native/module-send/src/components/SendMaxButton.tsx @@ -9,9 +9,8 @@ import { useCryptoFiatConverters } from '@suite-native/formatters'; import { AccountsRootState, selectAccountNetworkSymbol } from '@suite-common/wallet-core'; import { Button } from '@suite-native/atoms'; import { AccountKey, TokenAddress } from '@suite-common/wallet-types'; -import { useFormContext } from '@suite-native/forms'; +import { useField, useFormContext } from '@suite-native/forms'; import { useDebounce } from '@trezor/react-utils'; -import { isAddressValid } from '@suite-common/wallet-utils'; import { Translation } from '@suite-native/intl'; import { selectAccountTokenBalance, TokensRootState } from '@suite-native/tokens'; @@ -28,6 +27,7 @@ type SendMaxButtonProps = { export const SendMaxButton = ({ outputIndex, accountKey, tokenContract }: SendMaxButtonProps) => { const dispatch = useDispatch(); const debounce = useDebounce(); + const networkSymbol = useSelector((state: AccountsRootState) => selectAccountNetworkSymbol(state, accountKey), ); @@ -36,19 +36,21 @@ export const SendMaxButton = ({ outputIndex, accountKey, tokenContract }: SendMa selectAccountTokenBalance(state, accountKey, tokenContract), ); + const { hasError: hasAddressError, isTouched: isAddressTouched } = useField({ + name: getOutputFieldName(outputIndex, 'address'), + }); + const [maxAmountValue, setMaxAmountValue] = useState(); const converters = useCryptoFiatConverters({ networkSymbol: networkSymbol!, tokenContract }); const { setValue, watch } = useFormContext(); const formValues = watch(); - const addressValue = formValues.outputs[outputIndex]?.address; - const hasOutputValidAddress = - addressValue && networkSymbol && isAddressValid(addressValue, networkSymbol); + const isAddressValid = isAddressTouched && !hasAddressError; const isMainnetSendMaxAvailable = - !tokenContract && formValues.outputs.length === 1 && hasOutputValidAddress; + !tokenContract && formValues.outputs.length === 1 && isAddressValid; const isSendMaxAvailable = tokenContract || isMainnetSendMaxAvailable; const calculateFeeLevelsMaxAmount = useCallback(async () => { @@ -90,14 +92,10 @@ export const SendMaxButton = ({ outputIndex, accountKey, tokenContract }: SendMa }; return ( - isSendMaxAvailable && ( + isSendMaxAvailable && + maxAmountValue && ( - diff --git a/suite-native/module-send/src/screens/SendOutputsScreen.tsx b/suite-native/module-send/src/screens/SendOutputsScreen.tsx index a01d9872051..da0d6fe52d0 100644 --- a/suite-native/module-send/src/screens/SendOutputsScreen.tsx +++ b/suite-native/module-send/src/screens/SendOutputsScreen.tsx @@ -14,6 +14,7 @@ import { SendRootState, composeSendFormTransactionFeeLevelsThunk, selectAccountByKey, + selectDeviceUnavailableCapabilities, selectNetworkFeeInfo, selectSendFormDraftByKey, sendFormActions, @@ -96,6 +97,8 @@ export const SendOutputsScreen = ({ selectSendFormDraftByKey(state, accountKey, tokenContract), ); + const deviceUnavailableCapabilities = useSelector(selectDeviceUnavailableCapabilities); + const network = account ? getNetwork(account.symbol) : null; const form = useForm({ @@ -111,6 +114,7 @@ export const SendOutputsScreen = ({ isValueInSats: isAmountInSats, feeLevelsMaxAmount, decimals: tokenInfo?.decimals ?? network?.decimals, + isTaprootAvailable: !deviceUnavailableCapabilities?.taproot, }, defaultValues: getDefaultValues({ tokenContract }), }); diff --git a/suite-native/module-send/src/sendOutputsFormSchema.ts b/suite-native/module-send/src/sendOutputsFormSchema.ts index 8f25ea11e00..b75811ebe10 100644 --- a/suite-native/module-send/src/sendOutputsFormSchema.ts +++ b/suite-native/module-send/src/sendOutputsFormSchema.ts @@ -2,7 +2,14 @@ import { G } from '@mobily/ts-belt'; import { BigNumber } from '@trezor/utils'; import { getNetworkType, NetworkSymbol } from '@suite-common/wallet-config'; -import { formatNetworkAmount, isAddressValid, isDecimalsValid } from '@suite-common/wallet-utils'; +import { + formatNetworkAmount, + isAddressDeprecated, + isAddressValid, + isBech32AddressUppercase, + isDecimalsValid, + isTaprootAddress, +} from '@suite-common/wallet-utils'; import { FeeInfo } from '@suite-common/wallet-types'; import { yup } from '@suite-common/validators'; import { U_INT_32 } from '@suite-common/wallet-constants'; @@ -18,6 +25,7 @@ export type SendFormFormContext = { feeLevelsMaxAmount?: FeeLevelsMaxAmount; decimals?: number; accountDescriptor?: string; + isTaprootAvailable?: boolean; }; const isAmountDust = (amount: string, context?: SendFormFormContext) => { @@ -89,12 +97,21 @@ export const sendOutputsFormValidationSchema = yup.object({ 'is-invalid-address', 'The address format is incorrect.', (value, { options: { context } }: yup.TestContext) => { - const networkSymbol = context?.networkSymbol; + if (!value || !context) { + return false; + } + const { networkSymbol, isTaprootAvailable } = context; + + if (!networkSymbol) return false; + + const isTaprootValid = + isTaprootAvailable || !isTaprootAddress(value, networkSymbol); return ( - G.isNotNullable(value) && - G.isNotNullable(networkSymbol) && - isAddressValid(value, networkSymbol) + isAddressValid(value, networkSymbol) && + !isAddressDeprecated(value, networkSymbol) && + !isBech32AddressUppercase(value) && // bech32 addresses are valid as uppercase but are not accepted by Trezor + isTaprootValid // bech32m/Taproot addresses are valid but may not be supported by older FW ); }, )