Skip to content

Commit

Permalink
feat(suite-native): send address checksum
Browse files Browse the repository at this point in the history
  • Loading branch information
PeKne committed Nov 4, 2024
1 parent df9ac7a commit c584547
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 62 deletions.
8 changes: 8 additions & 0 deletions suite-native/intl/src/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,14 @@ export const en = {
recipients: {
title: 'Amount & recipients',
addressLabel: 'Recipient address',
checksum: {
label: 'We’ve adjusted the casing of your address to match checksum format. <link>Learn more</link>',
alert: {
title: 'This address needs to be converted to checksum format.',
body: 'This will adjust the casing of your address to match checksum format and allow us to properly validate your address. <link>Learn more</link>',
primaryButton: 'Convert',
},
},
addressQrLabel: 'Scan recipient address',
amountLabel: 'Amount to be sent',
maxButton: 'Send max',
Expand Down
4 changes: 3 additions & 1 deletion suite-native/module-send/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@suite-native/settings": "workspace:*",
"@suite-native/tokens": "workspace:*",
"@trezor/blockchain-link-types": "workspace:*",
"@trezor/connect": "workspace:*",
"@trezor/react-utils": "workspace:*",
"@trezor/styles": "workspace:*",
"@trezor/theme": "workspace:*",
Expand All @@ -53,6 +54,7 @@
"react-native": "0.75.2",
"react-native-reanimated": "3.16.1",
"react-native-svg": "15.6.0",
"react-redux": "8.0.7"
"react-redux": "8.0.7",
"web3-utils": "^4.3.1"
}
}
28 changes: 28 additions & 0 deletions suite-native/module-send/src/components/AddressChecksumMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Link } from '@suite-native/link';
import { HStack, Text } from '@suite-native/atoms';
import { Icon } from '@suite-native/icons';
import { Translation } from '@suite-native/intl';

const LINK_URL = 'https://trezor.io/learn/a/evm-address-checksum-in-trezor-suite';

export const AddressChecksumMessage = () => (
<HStack>
<Icon name="info" size="medium" color="iconSubdued" />
<Text variant="label" color="textSubdued">
<Translation
id="moduleSend.outputs.recipients.checksum.label"
values={{
link: linkChunk => (
<Link
href={LINK_URL}
label={linkChunk}
textVariant="label"
isUnderlined
textColor="textSubdued"
/>
),
}}
/>
</Text>
</HStack>
);
7 changes: 5 additions & 2 deletions suite-native/module-send/src/components/AddressInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import { isDebugEnv } from '@suite-native/config';
import { QrCodeBottomSheetIcon } from './QrCodeBottomSheetIcon';
import { getOutputFieldName } from '../utils';
import { SendOutputsFormValues } from '../sendOutputsFormSchema';
import { useTokenOfNetworkAlert } from '../hooks/useTokenOfNetworkAlert';
import { useAddressValidationAlerts } from '../hooks/useAddressValidationAlerts';
import { AddressChecksumMessage } from './AddressChecksumMessage';

type AddressInputProps = {
index: number;
Expand All @@ -32,12 +33,13 @@ export const AddressInput = ({ index, accountKey }: AddressInputProps) => {
const networkSymbol = useSelector((state: AccountsRootState) =>
selectAccountNetworkSymbol(state, accountKey),
);

const freshAccountAddress = useSelector(
(state: NativeAccountsRootState & TransactionsRootState) =>
selectFreshAccountAddress(state, accountKey),
);

useTokenOfNetworkAlert({ inputIndex: index });
const { wasAddressChecksummed } = useAddressValidationAlerts({ inputIndex: index });

const handleScanAddressQRCode = (qrCodeData: string) => {
setValue(addressFieldName, qrCodeData, { shouldValidate: true });
Expand Down Expand Up @@ -79,6 +81,7 @@ export const AddressInput = ({ index, accountKey }: AddressInputProps) => {
accessibilityLabel="address input"
rightIcon={<QrCodeBottomSheetIcon onCodeScanned={handleScanAddressQRCode} />}
/>
{wasAddressChecksummed && <AddressChecksumMessage />}
</VStack>
);
};
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
import { ReactNode, useEffect, useRef } from 'react';
import { ReactNode } from 'react';
import { useSelector } from 'react-redux';

import { useRoute, RouteProp } from '@react-navigation/native';

import { getNetwork } from '@suite-common/wallet-config';
import { Box, VStack, Text, AlertBox } from '@suite-native/atoms';
import { SendStackParamList, SendStackRoutes } from '@suite-native/navigation';
import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
import { Translation } from '@suite-native/intl';
import { selectAccountTokenSymbol, TokensRootState } from '@suite-native/tokens';
import { CryptoIcon } from '@suite-native/icons';
import { useAlert } from '@suite-native/alerts';
import { useFormContext } from '@suite-native/forms';
import { isAddressValid } from '@suite-common/wallet-utils';
import { AccountsRootState, selectAccountNetworkSymbol } from '@suite-common/wallet-core';
import { AccountKey, TokenAddress } from '@suite-common/wallet-types';

import { getOutputFieldName } from '../utils';

type UseTokenOfNetworkAlertArgs = {
inputIndex: number;
};
import { VStack, Box, AlertBox, Text } from '@suite-native/atoms';
import { CryptoIcon } from '@suite-native/icons';
import { Translation } from '@suite-native/intl';
import { TokensRootState, selectAccountTokenSymbol } from '@suite-native/tokens';
import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';

const iconWrapperStyle = prepareNativeStyle(() => ({
overflow: 'visible',
Expand All @@ -30,12 +18,12 @@ const iconWrapperStyle = prepareNativeStyle(() => ({

const networkIconWrapperStyle = prepareNativeStyle(utils => ({
position: 'absolute',
backgroundColor: utils.colors.backgroundSurfaceElevation1,
padding: 3,
borderRadius: utils.borders.radii.round,
right: 0,
bottom: 0,
padding: 3,
overflow: 'visible',
backgroundColor: utils.colors.backgroundSurfaceElevation1,
borderRadius: utils.borders.radii.round,
}));

type ParagraphProps = {
Expand All @@ -50,7 +38,7 @@ const Paragraph = ({ header, body }: ParagraphProps) => (
</VStack>
);

const TokenOfNetworkAlertBody = ({
export const TokenOfNetworkAlertBody = ({
accountKey,
tokenContract,
}: {
Expand Down Expand Up @@ -117,40 +105,3 @@ const TokenOfNetworkAlertBody = ({
</VStack>
);
};

export const useTokenOfNetworkAlert = ({ inputIndex }: UseTokenOfNetworkAlertArgs) => {
const wasAlertShown = useRef(false);
const { showAlert } = useAlert();
const {
params: { tokenContract, accountKey },
} = useRoute<RouteProp<SendStackParamList, SendStackRoutes.SendOutputs>>();

const tokenSymbol = useSelector((state: TokensRootState) =>
selectAccountTokenSymbol(state, accountKey, tokenContract),
);
const networkSymbol = useSelector((state: AccountsRootState) =>
selectAccountNetworkSymbol(state, accountKey),
);

const { watch } = useFormContext();

const addressValue = watch(getOutputFieldName(inputIndex, 'address'));

const isFilledValidAddress =
addressValue && networkSymbol && isAddressValid(addressValue, networkSymbol);

useEffect(() => {
if (tokenContract && isFilledValidAddress && !wasAlertShown.current) {
showAlert({
appendix: (
<TokenOfNetworkAlertBody
accountKey={accountKey}
tokenContract={tokenContract}
/>
),
primaryButtonTitle: <Translation id="generic.buttons.gotIt" />,
});
wasAlertShown.current = true;
}
}, [isFilledValidAddress, showAlert, tokenContract, tokenSymbol, accountKey]);
};
138 changes: 138 additions & 0 deletions suite-native/module-send/src/hooks/useAddressValidationAlerts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { useCallback, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';

import { useRoute, RouteProp } from '@react-navigation/native';
import { checkAddressCheckSum, toChecksumAddress } from 'web3-utils';
import { G } from '@mobily/ts-belt';

import { SendStackParamList, SendStackRoutes } from '@suite-native/navigation';
import { Translation } from '@suite-native/intl';
import { selectAccountTokenSymbol, TokensRootState } from '@suite-native/tokens';
import { useAlert } from '@suite-native/alerts';
import { useFormContext } from '@suite-native/forms';
import { isAddressValid } from '@suite-common/wallet-utils';
import { AccountsRootState, selectAccountNetworkSymbol } from '@suite-common/wallet-core';
import TrezorConnect from '@trezor/connect';
import { Link } from '@suite-native/link';

import { getOutputFieldName } from '../utils';
import { TokenOfNetworkAlertBody } from '../components/TokenOfNetworkAlertContent';

type UseAddressValidationAlertsArgs = {
inputIndex: number;
};

const CHECKSUM_LINK_URL = 'https://trezor.io/learn/a/evm-address-checksum-in-trezor-suite';

export const useAddressValidationAlerts = ({ inputIndex }: UseAddressValidationAlertsArgs) => {
const {
params: { tokenContract, accountKey },
} = useRoute<RouteProp<SendStackParamList, SendStackRoutes.SendOutputs>>();
const [wasAddressChecksummed, setWasAddressChecksummed] = useState(false);
const [wasTokenAlertDisplayed, setWasTokenAlertDisplayed] = useState(
G.isNullable(tokenContract),
);
const { showAlert } = useAlert();

const tokenSymbol = useSelector((state: TokensRootState) =>
selectAccountTokenSymbol(state, accountKey, tokenContract),
);
const networkSymbol = useSelector((state: AccountsRootState) =>
selectAccountNetworkSymbol(state, accountKey),
);

const { watch, setValue } = useFormContext();

const addressFieldName = getOutputFieldName(inputIndex, 'address');
const addressValue = watch(addressFieldName);

const isFilledValidAddress =
addressValue && networkSymbol && isAddressValid(addressValue, networkSymbol);

const convertAddressToChecksum = useCallback(() => {
setValue(addressFieldName, toChecksumAddress(addressValue), {
shouldValidate: true,
});
setWasAddressChecksummed(true);
}, [addressFieldName, addressValue, setValue]);

const handleAddressChecksum = useCallback(async () => {
if (isFilledValidAddress && !checkAddressCheckSum(addressValue)) {
const params = {
descriptor: addressValue,
coin: networkSymbol,
};

const addressInfo = await TrezorConnect.getAccountInfo(params);

if (addressInfo.success) {
// Already used addresses are checksumed without displaying the alert.
const isUsedAddress = addressInfo.payload.history.total !== 0;
if (isUsedAddress) {
convertAddressToChecksum();

return;
}
}

showAlert({
title: <Translation id="moduleSend.outputs.recipients.checksum.alert.title" />,
description: (
<Translation
id="moduleSend.outputs.recipients.checksum.alert.body"
values={{
link: linkChunk => (
<Link
href={CHECKSUM_LINK_URL}
label={linkChunk}
isUnderlined
textColor="textSubdued"
/>
),
}}
/>
),
primaryButtonTitle: (
<Translation id="moduleSend.outputs.recipients.checksum.alert.primaryButton" />
),
onPressPrimaryButton: convertAddressToChecksum,
});
}
}, [addressValue, isFilledValidAddress, networkSymbol, showAlert, convertAddressToChecksum]);

useEffect(() => {
const shouldShowTokenAlert =
tokenContract && isFilledValidAddress && !wasTokenAlertDisplayed;
const shouldChecksumAddress =
!wasAddressChecksummed && isFilledValidAddress && wasTokenAlertDisplayed;

if (shouldShowTokenAlert) {
showAlert({
appendix: (
<TokenOfNetworkAlertBody
accountKey={accountKey}
tokenContract={tokenContract}
/>
),
primaryButtonTitle: <Translation id="generic.buttons.gotIt" />,
onPressPrimaryButton: () => setWasTokenAlertDisplayed(true),
});
} else if (shouldChecksumAddress) handleAddressChecksum();
// TODO: add path for contract address alert: https://github.com/trezor/trezor-suite/issues/14936.
else if (!isFilledValidAddress) {
setWasTokenAlertDisplayed(false);
setWasAddressChecksummed(false);
}
}, [
isFilledValidAddress,
showAlert,
tokenContract,
tokenSymbol,
accountKey,
wasAddressChecksummed,
handleAddressChecksum,
wasTokenAlertDisplayed,
]);

return { wasAddressChecksummed };
};
1 change: 1 addition & 0 deletions suite-native/module-send/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
{
"path": "../../packages/blockchain-link-types"
},
{ "path": "../../packages/connect" },
{ "path": "../../packages/react-utils" },
{ "path": "../../packages/styles" },
{ "path": "../../packages/theme" },
Expand Down
2 changes: 2 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10248,6 +10248,7 @@ __metadata:
"@suite-native/settings": "workspace:*"
"@suite-native/tokens": "workspace:*"
"@trezor/blockchain-link-types": "workspace:*"
"@trezor/connect": "workspace:*"
"@trezor/react-utils": "workspace:*"
"@trezor/styles": "workspace:*"
"@trezor/theme": "workspace:*"
Expand All @@ -10261,6 +10262,7 @@ __metadata:
react-native-reanimated: "npm:3.16.1"
react-native-svg: "npm:15.6.0"
react-redux: "npm:8.0.7"
web3-utils: "npm:^4.3.1"
languageName: unknown
linkType: soft

Expand Down

0 comments on commit c584547

Please sign in to comment.