Skip to content

Commit c584547

Browse files
committed
feat(suite-native): send address checksum
1 parent df9ac7a commit c584547

8 files changed

+195
-62
lines changed

suite-native/intl/src/en.ts

+8
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,14 @@ export const en = {
962962
recipients: {
963963
title: 'Amount & recipients',
964964
addressLabel: 'Recipient address',
965+
checksum: {
966+
label: 'We’ve adjusted the casing of your address to match checksum format. <link>Learn more</link>',
967+
alert: {
968+
title: 'This address needs to be converted to checksum format.',
969+
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>',
970+
primaryButton: 'Convert',
971+
},
972+
},
965973
addressQrLabel: 'Scan recipient address',
966974
amountLabel: 'Amount to be sent',
967975
maxButton: 'Send max',

suite-native/module-send/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"@suite-native/settings": "workspace:*",
4242
"@suite-native/tokens": "workspace:*",
4343
"@trezor/blockchain-link-types": "workspace:*",
44+
"@trezor/connect": "workspace:*",
4445
"@trezor/react-utils": "workspace:*",
4546
"@trezor/styles": "workspace:*",
4647
"@trezor/theme": "workspace:*",
@@ -53,6 +54,7 @@
5354
"react-native": "0.75.2",
5455
"react-native-reanimated": "3.16.1",
5556
"react-native-svg": "15.6.0",
56-
"react-redux": "8.0.7"
57+
"react-redux": "8.0.7",
58+
"web3-utils": "^4.3.1"
5759
}
5860
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Link } from '@suite-native/link';
2+
import { HStack, Text } from '@suite-native/atoms';
3+
import { Icon } from '@suite-native/icons';
4+
import { Translation } from '@suite-native/intl';
5+
6+
const LINK_URL = 'https://trezor.io/learn/a/evm-address-checksum-in-trezor-suite';
7+
8+
export const AddressChecksumMessage = () => (
9+
<HStack>
10+
<Icon name="info" size="medium" color="iconSubdued" />
11+
<Text variant="label" color="textSubdued">
12+
<Translation
13+
id="moduleSend.outputs.recipients.checksum.label"
14+
values={{
15+
link: linkChunk => (
16+
<Link
17+
href={LINK_URL}
18+
label={linkChunk}
19+
textVariant="label"
20+
isUnderlined
21+
textColor="textSubdued"
22+
/>
23+
),
24+
}}
25+
/>
26+
</Text>
27+
</HStack>
28+
);

suite-native/module-send/src/components/AddressInput.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import { isDebugEnv } from '@suite-native/config';
1919
import { QrCodeBottomSheetIcon } from './QrCodeBottomSheetIcon';
2020
import { getOutputFieldName } from '../utils';
2121
import { SendOutputsFormValues } from '../sendOutputsFormSchema';
22-
import { useTokenOfNetworkAlert } from '../hooks/useTokenOfNetworkAlert';
22+
import { useAddressValidationAlerts } from '../hooks/useAddressValidationAlerts';
23+
import { AddressChecksumMessage } from './AddressChecksumMessage';
2324

2425
type AddressInputProps = {
2526
index: number;
@@ -32,12 +33,13 @@ export const AddressInput = ({ index, accountKey }: AddressInputProps) => {
3233
const networkSymbol = useSelector((state: AccountsRootState) =>
3334
selectAccountNetworkSymbol(state, accountKey),
3435
);
36+
3537
const freshAccountAddress = useSelector(
3638
(state: NativeAccountsRootState & TransactionsRootState) =>
3739
selectFreshAccountAddress(state, accountKey),
3840
);
3941

40-
useTokenOfNetworkAlert({ inputIndex: index });
42+
const { wasAddressChecksummed } = useAddressValidationAlerts({ inputIndex: index });
4143

4244
const handleScanAddressQRCode = (qrCodeData: string) => {
4345
setValue(addressFieldName, qrCodeData, { shouldValidate: true });
@@ -79,6 +81,7 @@ export const AddressInput = ({ index, accountKey }: AddressInputProps) => {
7981
accessibilityLabel="address input"
8082
rightIcon={<QrCodeBottomSheetIcon onCodeScanned={handleScanAddressQRCode} />}
8183
/>
84+
{wasAddressChecksummed && <AddressChecksumMessage />}
8285
</VStack>
8386
);
8487
};

suite-native/module-send/src/hooks/useTokenOfNetworkAlert.tsx suite-native/module-send/src/components/TokenOfNetworkAlertContent.tsx

+10-59
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,14 @@
1-
import { ReactNode, useEffect, useRef } from 'react';
1+
import { ReactNode } from 'react';
22
import { useSelector } from 'react-redux';
33

4-
import { useRoute, RouteProp } from '@react-navigation/native';
5-
64
import { getNetwork } from '@suite-common/wallet-config';
7-
import { Box, VStack, Text, AlertBox } from '@suite-native/atoms';
8-
import { SendStackParamList, SendStackRoutes } from '@suite-native/navigation';
9-
import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
10-
import { Translation } from '@suite-native/intl';
11-
import { selectAccountTokenSymbol, TokensRootState } from '@suite-native/tokens';
12-
import { CryptoIcon } from '@suite-native/icons';
13-
import { useAlert } from '@suite-native/alerts';
14-
import { useFormContext } from '@suite-native/forms';
15-
import { isAddressValid } from '@suite-common/wallet-utils';
165
import { AccountsRootState, selectAccountNetworkSymbol } from '@suite-common/wallet-core';
176
import { AccountKey, TokenAddress } from '@suite-common/wallet-types';
18-
19-
import { getOutputFieldName } from '../utils';
20-
21-
type UseTokenOfNetworkAlertArgs = {
22-
inputIndex: number;
23-
};
7+
import { VStack, Box, AlertBox, Text } from '@suite-native/atoms';
8+
import { CryptoIcon } from '@suite-native/icons';
9+
import { Translation } from '@suite-native/intl';
10+
import { TokensRootState, selectAccountTokenSymbol } from '@suite-native/tokens';
11+
import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';
2412

2513
const iconWrapperStyle = prepareNativeStyle(() => ({
2614
overflow: 'visible',
@@ -30,12 +18,12 @@ const iconWrapperStyle = prepareNativeStyle(() => ({
3018

3119
const networkIconWrapperStyle = prepareNativeStyle(utils => ({
3220
position: 'absolute',
33-
backgroundColor: utils.colors.backgroundSurfaceElevation1,
34-
padding: 3,
35-
borderRadius: utils.borders.radii.round,
3621
right: 0,
3722
bottom: 0,
23+
padding: 3,
3824
overflow: 'visible',
25+
backgroundColor: utils.colors.backgroundSurfaceElevation1,
26+
borderRadius: utils.borders.radii.round,
3927
}));
4028

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

53-
const TokenOfNetworkAlertBody = ({
41+
export const TokenOfNetworkAlertBody = ({
5442
accountKey,
5543
tokenContract,
5644
}: {
@@ -117,40 +105,3 @@ const TokenOfNetworkAlertBody = ({
117105
</VStack>
118106
);
119107
};
120-
121-
export const useTokenOfNetworkAlert = ({ inputIndex }: UseTokenOfNetworkAlertArgs) => {
122-
const wasAlertShown = useRef(false);
123-
const { showAlert } = useAlert();
124-
const {
125-
params: { tokenContract, accountKey },
126-
} = useRoute<RouteProp<SendStackParamList, SendStackRoutes.SendOutputs>>();
127-
128-
const tokenSymbol = useSelector((state: TokensRootState) =>
129-
selectAccountTokenSymbol(state, accountKey, tokenContract),
130-
);
131-
const networkSymbol = useSelector((state: AccountsRootState) =>
132-
selectAccountNetworkSymbol(state, accountKey),
133-
);
134-
135-
const { watch } = useFormContext();
136-
137-
const addressValue = watch(getOutputFieldName(inputIndex, 'address'));
138-
139-
const isFilledValidAddress =
140-
addressValue && networkSymbol && isAddressValid(addressValue, networkSymbol);
141-
142-
useEffect(() => {
143-
if (tokenContract && isFilledValidAddress && !wasAlertShown.current) {
144-
showAlert({
145-
appendix: (
146-
<TokenOfNetworkAlertBody
147-
accountKey={accountKey}
148-
tokenContract={tokenContract}
149-
/>
150-
),
151-
primaryButtonTitle: <Translation id="generic.buttons.gotIt" />,
152-
});
153-
wasAlertShown.current = true;
154-
}
155-
}, [isFilledValidAddress, showAlert, tokenContract, tokenSymbol, accountKey]);
156-
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { useCallback, useEffect, useState } from 'react';
2+
import { useSelector } from 'react-redux';
3+
4+
import { useRoute, RouteProp } from '@react-navigation/native';
5+
import { checkAddressCheckSum, toChecksumAddress } from 'web3-utils';
6+
import { G } from '@mobily/ts-belt';
7+
8+
import { SendStackParamList, SendStackRoutes } from '@suite-native/navigation';
9+
import { Translation } from '@suite-native/intl';
10+
import { selectAccountTokenSymbol, TokensRootState } from '@suite-native/tokens';
11+
import { useAlert } from '@suite-native/alerts';
12+
import { useFormContext } from '@suite-native/forms';
13+
import { isAddressValid } from '@suite-common/wallet-utils';
14+
import { AccountsRootState, selectAccountNetworkSymbol } from '@suite-common/wallet-core';
15+
import TrezorConnect from '@trezor/connect';
16+
import { Link } from '@suite-native/link';
17+
18+
import { getOutputFieldName } from '../utils';
19+
import { TokenOfNetworkAlertBody } from '../components/TokenOfNetworkAlertContent';
20+
21+
type UseAddressValidationAlertsArgs = {
22+
inputIndex: number;
23+
};
24+
25+
const CHECKSUM_LINK_URL = 'https://trezor.io/learn/a/evm-address-checksum-in-trezor-suite';
26+
27+
export const useAddressValidationAlerts = ({ inputIndex }: UseAddressValidationAlertsArgs) => {
28+
const {
29+
params: { tokenContract, accountKey },
30+
} = useRoute<RouteProp<SendStackParamList, SendStackRoutes.SendOutputs>>();
31+
const [wasAddressChecksummed, setWasAddressChecksummed] = useState(false);
32+
const [wasTokenAlertDisplayed, setWasTokenAlertDisplayed] = useState(
33+
G.isNullable(tokenContract),
34+
);
35+
const { showAlert } = useAlert();
36+
37+
const tokenSymbol = useSelector((state: TokensRootState) =>
38+
selectAccountTokenSymbol(state, accountKey, tokenContract),
39+
);
40+
const networkSymbol = useSelector((state: AccountsRootState) =>
41+
selectAccountNetworkSymbol(state, accountKey),
42+
);
43+
44+
const { watch, setValue } = useFormContext();
45+
46+
const addressFieldName = getOutputFieldName(inputIndex, 'address');
47+
const addressValue = watch(addressFieldName);
48+
49+
const isFilledValidAddress =
50+
addressValue && networkSymbol && isAddressValid(addressValue, networkSymbol);
51+
52+
const convertAddressToChecksum = useCallback(() => {
53+
setValue(addressFieldName, toChecksumAddress(addressValue), {
54+
shouldValidate: true,
55+
});
56+
setWasAddressChecksummed(true);
57+
}, [addressFieldName, addressValue, setValue]);
58+
59+
const handleAddressChecksum = useCallback(async () => {
60+
if (isFilledValidAddress && !checkAddressCheckSum(addressValue)) {
61+
const params = {
62+
descriptor: addressValue,
63+
coin: networkSymbol,
64+
};
65+
66+
const addressInfo = await TrezorConnect.getAccountInfo(params);
67+
68+
if (addressInfo.success) {
69+
// Already used addresses are checksumed without displaying the alert.
70+
const isUsedAddress = addressInfo.payload.history.total !== 0;
71+
if (isUsedAddress) {
72+
convertAddressToChecksum();
73+
74+
return;
75+
}
76+
}
77+
78+
showAlert({
79+
title: <Translation id="moduleSend.outputs.recipients.checksum.alert.title" />,
80+
description: (
81+
<Translation
82+
id="moduleSend.outputs.recipients.checksum.alert.body"
83+
values={{
84+
link: linkChunk => (
85+
<Link
86+
href={CHECKSUM_LINK_URL}
87+
label={linkChunk}
88+
isUnderlined
89+
textColor="textSubdued"
90+
/>
91+
),
92+
}}
93+
/>
94+
),
95+
primaryButtonTitle: (
96+
<Translation id="moduleSend.outputs.recipients.checksum.alert.primaryButton" />
97+
),
98+
onPressPrimaryButton: convertAddressToChecksum,
99+
});
100+
}
101+
}, [addressValue, isFilledValidAddress, networkSymbol, showAlert, convertAddressToChecksum]);
102+
103+
useEffect(() => {
104+
const shouldShowTokenAlert =
105+
tokenContract && isFilledValidAddress && !wasTokenAlertDisplayed;
106+
const shouldChecksumAddress =
107+
!wasAddressChecksummed && isFilledValidAddress && wasTokenAlertDisplayed;
108+
109+
if (shouldShowTokenAlert) {
110+
showAlert({
111+
appendix: (
112+
<TokenOfNetworkAlertBody
113+
accountKey={accountKey}
114+
tokenContract={tokenContract}
115+
/>
116+
),
117+
primaryButtonTitle: <Translation id="generic.buttons.gotIt" />,
118+
onPressPrimaryButton: () => setWasTokenAlertDisplayed(true),
119+
});
120+
} else if (shouldChecksumAddress) handleAddressChecksum();
121+
// TODO: add path for contract address alert: https://github.com/trezor/trezor-suite/issues/14936.
122+
else if (!isFilledValidAddress) {
123+
setWasTokenAlertDisplayed(false);
124+
setWasAddressChecksummed(false);
125+
}
126+
}, [
127+
isFilledValidAddress,
128+
showAlert,
129+
tokenContract,
130+
tokenSymbol,
131+
accountKey,
132+
wasAddressChecksummed,
133+
handleAddressChecksum,
134+
wasTokenAlertDisplayed,
135+
]);
136+
137+
return { wasAddressChecksummed };
138+
};

suite-native/module-send/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
{
4747
"path": "../../packages/blockchain-link-types"
4848
},
49+
{ "path": "../../packages/connect" },
4950
{ "path": "../../packages/react-utils" },
5051
{ "path": "../../packages/styles" },
5152
{ "path": "../../packages/theme" },

yarn.lock

+2
Original file line numberDiff line numberDiff line change
@@ -10248,6 +10248,7 @@ __metadata:
1024810248
"@suite-native/settings": "workspace:*"
1024910249
"@suite-native/tokens": "workspace:*"
1025010250
"@trezor/blockchain-link-types": "workspace:*"
10251+
"@trezor/connect": "workspace:*"
1025110252
"@trezor/react-utils": "workspace:*"
1025210253
"@trezor/styles": "workspace:*"
1025310254
"@trezor/theme": "workspace:*"
@@ -10261,6 +10262,7 @@ __metadata:
1026110262
react-native-reanimated: "npm:3.16.1"
1026210263
react-native-svg: "npm:15.6.0"
1026310264
react-redux: "npm:8.0.7"
10265+
web3-utils: "npm:^4.3.1"
1026410266
languageName: unknown
1026510267
linkType: soft
1026610268

0 commit comments

Comments
 (0)