From 568cacdd2c63b9155851e7d9a9e26fd3aa6a7a59 Mon Sep 17 00:00:00 2001 From: Douglas Daniel Date: Thu, 7 Nov 2024 13:18:32 -0600 Subject: [PATCH] feat(wallet): ZCash Text Memo --- .../browser/brave_wallet_constants.h | 6 ++ .../slices/endpoints/transaction.endpoints.ts | 2 +- .../transaction_details_modal.tsx | 26 +++++ .../transaction-info.tsx | 12 +++ components/brave_wallet_ui/constants/types.ts | 1 + .../components/add_memo/add_memo.style.ts | 14 +++ .../send/components/add_memo/add_memo.tsx | 97 +++++++++++++++++++ .../page/screens/send/constants/magics.ts | 6 ++ .../screens/send/send_screen/send_screen.tsx | 49 +++++++++- components/brave_wallet_ui/stories/locale.ts | 8 ++ components/brave_wallet_ui/utils/tx-utils.ts | 7 +- components/resources/wallet_strings.grdp | 6 ++ 12 files changed, 227 insertions(+), 7 deletions(-) create mode 100644 components/brave_wallet_ui/page/screens/send/components/add_memo/add_memo.style.ts create mode 100644 components/brave_wallet_ui/page/screens/send/components/add_memo/add_memo.tsx create mode 100644 components/brave_wallet_ui/page/screens/send/constants/magics.ts diff --git a/components/brave_wallet/browser/brave_wallet_constants.h b/components/brave_wallet/browser/brave_wallet_constants.h index ebc6414f1367..9afae2d135d9 100644 --- a/components/brave_wallet/browser/brave_wallet_constants.h +++ b/components/brave_wallet/browser/brave_wallet_constants.h @@ -901,6 +901,12 @@ inline constexpr webui::LocalizedString kLocalizedStrings[] = { IDS_BRAVE_WALLET_TRANSACTION_DETAIL_HASH}, {"braveWalletTransactionDetailNetwork", IDS_BRAVE_WALLET_TRANSACTION_DETAIL_NETWORK}, + {"braveWalletMemo", IDS_BRAVE_WALLET_MEMO}, + {"braveWalletEnterAMessage", IDS_BRAVE_WALLET_ENTER_A_MESSAGE}, + {"braveWalletMessageOptional", IDS_BRAVE_WALLET_MESSAGE_OPTIONAL}, + {"braveWalletAddMemo", IDS_BRAVE_WALLET_ADD_MEMO}, + {"braveWalletRemoveMemo", IDS_BRAVE_WALLET_REMOVE_MEMO}, + {"braveWalletMemoLengthError", IDS_BRAVE_WALLET_MEMO_LENGTH_ERROR}, {"braveWalletTransactionPlaceholder", IDS_BRAVE_WALLET_TRANSACTION_PLACEHOLDER}, {"braveWalletTransactionApproveUnlimited", diff --git a/components/brave_wallet_ui/common/slices/endpoints/transaction.endpoints.ts b/components/brave_wallet_ui/common/slices/endpoints/transaction.endpoints.ts index 11d598e8f82b..dc0f9e6b842d 100644 --- a/components/brave_wallet_ui/common/slices/endpoints/transaction.endpoints.ts +++ b/components/brave_wallet_ui/common/slices/endpoints/transaction.endpoints.ts @@ -605,7 +605,7 @@ export const transactionEndpoints = ({ const zecTxData: BraveWallet.ZecTxData = { useShieldedPool: payload.useShieldedPool, to: payload.to, - memo: undefined, + memo: payload.memo, amount: BigInt(payload.value), fee: BigInt(0), inputs: [], diff --git a/components/brave_wallet_ui/components/desktop/popup-modals/transaction_details_modal/transaction_details_modal.tsx b/components/brave_wallet_ui/components/desktop/popup-modals/transaction_details_modal/transaction_details_modal.tsx index 8f4f1b50dadb..bb4bb980a4a2 100644 --- a/components/brave_wallet_ui/components/desktop/popup-modals/transaction_details_modal/transaction_details_modal.tsx +++ b/components/brave_wallet_ui/components/desktop/popup-modals/transaction_details_modal/transaction_details_modal.tsx @@ -401,6 +401,12 @@ export const TransactionDetailsModal = ({ onClose, transaction }: Props) => { accountInfosRegistry ) + const memoFromTransaction = transaction.txDataUnion.zecTxData?.memo + + const memoText = String.fromCharCode( + ...memoFromTransaction ?? [] + ) + // render return ( { + {memoText && ( + <> + + + {getLocale('braveWalletMemo')} + + + {memoText} + + + + + )} + {transaction.txHash && ( <> diff --git a/components/brave_wallet_ui/components/extension/confirm-transaction-panel/transaction-info.tsx b/components/brave_wallet_ui/components/extension/confirm-transaction-panel/transaction-info.tsx index e9c9ab1cf33f..9ae926399469 100644 --- a/components/brave_wallet_ui/components/extension/confirm-transaction-panel/transaction-info.tsx +++ b/components/brave_wallet_ui/components/extension/confirm-transaction-panel/transaction-info.tsx @@ -88,6 +88,10 @@ export const TransactionInfo = ({ ? 'braveWalletConfirmTransactionTransactionFee' : 'braveWalletConfirmTransactionGasFee' + const memoText = String.fromCharCode( + ...transactionDetails.zcashMemo ?? [] + ) + // render return ( <> @@ -263,6 +267,14 @@ export const TransactionInfo = ({ )} + + {memoText && ( + + + {getLocale('braveWalletMemo')} + {memoText} + + )} ) } diff --git a/components/brave_wallet_ui/constants/types.ts b/components/brave_wallet_ui/constants/types.ts index a833fd07604d..551daa0f9b04 100644 --- a/components/brave_wallet_ui/constants/types.ts +++ b/components/brave_wallet_ui/constants/types.ts @@ -328,6 +328,7 @@ export interface SendBtcTransactionParams extends BaseTransactionParams { export interface SendZecTransactionParams extends BaseTransactionParams { useShieldedPool: boolean + memo: number[] | undefined } /** diff --git a/components/brave_wallet_ui/page/screens/send/components/add_memo/add_memo.style.ts b/components/brave_wallet_ui/page/screens/send/components/add_memo/add_memo.style.ts new file mode 100644 index 000000000000..554ef5bc2674 --- /dev/null +++ b/components/brave_wallet_ui/page/screens/send/components/add_memo/add_memo.style.ts @@ -0,0 +1,14 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +import styled from 'styled-components' +import Input from '@brave/leo/react/input' + +export const MemoInput = styled(Input)` + display: flex; + flex-direction: column; + width: 100%; + gap: 4px; +` diff --git a/components/brave_wallet_ui/page/screens/send/components/add_memo/add_memo.tsx b/components/brave_wallet_ui/page/screens/send/components/add_memo/add_memo.tsx new file mode 100644 index 000000000000..b69a078541ce --- /dev/null +++ b/components/brave_wallet_ui/page/screens/send/components/add_memo/add_memo.tsx @@ -0,0 +1,97 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +import * as React from 'react' +import Button from '@brave/leo/react/button' +import Icon from '@brave/leo/react/icon' + +// Constants +import { MAX_ZCASH_MEMO_LENGTH } from '../../constants/magics' + +// Utils +import { getLocale } from '../../../../../../common/locale' + +// Styled Components +import { Column, Text } from '../../../../../components/shared/style' +import { MemoInput } from './add_memo.style' + +interface Props { + memoText: string + onUpdateMemoText: (value: string) => void +} + +export const AddMemo = (props: Props) => { + const { memoText, onUpdateMemoText } = props + + // State + const [showMemoTextInput, setShowMemoTextInput] = + React.useState(false) + + // Memos + const memoTextLength = React.useMemo(() => { + return memoText.length + }, [memoText]) + + // Methods + const onAddOrRemoveTextMemo = React.useCallback(() => { + if (showMemoTextInput) { + onUpdateMemoText('') + setShowMemoTextInput(false) + return + } + setShowMemoTextInput(true) + }, [showMemoTextInput, onUpdateMemoText]) + + return ( + + {showMemoTextInput && ( + onUpdateMemoText(e.value)} + placeholder={getLocale('braveWalletEnterAMessage')} + showErrors={memoText.length > MAX_ZCASH_MEMO_LENGTH} + > + + {getLocale('braveWalletMessageOptional')} + + + {memoTextLength}/{MAX_ZCASH_MEMO_LENGTH} + + + {getLocale('braveWalletMemoLengthError')} + + + )} + + + ) +} diff --git a/components/brave_wallet_ui/page/screens/send/constants/magics.ts b/components/brave_wallet_ui/page/screens/send/constants/magics.ts new file mode 100644 index 000000000000..e787b2ae57b8 --- /dev/null +++ b/components/brave_wallet_ui/page/screens/send/constants/magics.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +export const MAX_ZCASH_MEMO_LENGTH = 512 diff --git a/components/brave_wallet_ui/page/screens/send/send_screen/send_screen.tsx b/components/brave_wallet_ui/page/screens/send/send_screen/send_screen.tsx index af0f1630cbd0..3d830349568a 100644 --- a/components/brave_wallet_ui/page/screens/send/send_screen/send_screen.tsx +++ b/components/brave_wallet_ui/page/screens/send/send_screen/send_screen.tsx @@ -9,8 +9,11 @@ import { skipToken } from '@reduxjs/toolkit/query/react' import { useHistory, useLocation } from 'react-router' // Selectors -import { useSafeUISelector } from '../../../../common/hooks/use-safe-selector' -import { UISelectors } from '../../../../common/selectors' +import { + useSafeUISelector, + useSafeWalletSelector +} from '../../../../common/hooks/use-safe-selector' +import { UISelectors, WalletSelectors } from '../../../../common/selectors' // Types import { @@ -22,6 +25,9 @@ import { AmountValidationErrorType } from '../../../../constants/types' +// Constants +import { MAX_ZCASH_MEMO_LENGTH } from '../constants/magics' + // Utils import { getLocale } from '../../../../../common/locale' import Amount from '../../../../utils/amount' @@ -52,7 +58,8 @@ import { useSendSolTransactionMutation, useSendFilTransactionMutation, useSendBtcTransactionMutation, - useSendZecTransactionMutation + useSendZecTransactionMutation, + useValidateUnifiedAddressQuery } from '../../../../common/slices/api.slice' import { useAccountFromAddressQuery // @@ -86,6 +93,7 @@ import { import { SelectAddressButton // } from '../../composer_ui/select_address_button/select_address_button' +import { AddMemo } from '../components/add_memo/add_memo' interface Props { isAndroid?: boolean @@ -125,9 +133,13 @@ export const SendScreen = React.memo((props: Props) => { React.useState('') const [isWarningAcknowledged, setIsWarningAcknowledged] = React.useState(false) + const [memoText, setMemoText] = React.useState('') // Selectors const isPanel = useSafeUISelector(UISelectors.isPanel) + const isZCashShieldedTransactionsEnabled = useSafeWalletSelector( + WalletSelectors.isZCashShieldedTransactionsEnabled + ) // Mutations const [sendSPLTransfer] = useSendSPLTransferMutation() @@ -147,6 +159,20 @@ export const SendScreen = React.memo((props: Props) => { }) }) + const { + data: zecAddressValidationResult = BraveWallet.ZCashAddressValidationResult + .Unknown + } = useValidateUnifiedAddressQuery( + networkFromParams?.coin === BraveWallet.CoinType.ZEC && + isZCashShieldedTransactionsEnabled && + toAddressOrUrl + ? { + address: toAddressOrUrl, + testnet: networkFromParams.chainId === BraveWallet.Z_CASH_TESTNET + } + : skipToken + ) + const tokenFromParams = React.useMemo(() => { if (!networkFromParams) { return @@ -416,6 +442,8 @@ export const SendScreen = React.memo((props: Props) => { } case BraveWallet.CoinType.ZEC: { + const memoArray = + memoText !== '' ? new TextEncoder().encode(memoText) : undefined await sendZecTransaction({ useShieldedPool: tokenFromParams.isShielded, network: networkFromParams, @@ -423,7 +451,8 @@ export const SendScreen = React.memo((props: Props) => { to: toAddress, value: new Amount(sendAmount) .multiplyByDecimals(tokenFromParams.decimals) - .toHex() + .toHex(), + memo: memoArray ? Array.from(memoArray) : undefined }) resetSendFields() } @@ -437,6 +466,7 @@ export const SendScreen = React.memo((props: Props) => { sendingMaxAmount, sendAmount, resolvedDomainAddress, + memoText, resetSendFields, sendEvmTransaction, sendERC20Transfer, @@ -557,12 +587,23 @@ export const SendScreen = React.memo((props: Props) => { onChange={setIsWarningAcknowledged} /> )} + {isZCashShieldedTransactionsEnabled && + tokenFromParams?.coin === BraveWallet.CoinType.ZEC && + toAddressOrUrl && + zecAddressValidationResult === + BraveWallet.ZCashAddressValidationResult.ValidShielded && ( + + )} MAX_ZCASH_MEMO_LENGTH || !toAddressOrUrl || insufficientFundsError || sendAmount === '' || diff --git a/components/brave_wallet_ui/stories/locale.ts b/components/brave_wallet_ui/stories/locale.ts index ae15f77b0bb3..0eed4f9f1f9b 100644 --- a/components/brave_wallet_ui/stories/locale.ts +++ b/components/brave_wallet_ui/stories/locale.ts @@ -1120,6 +1120,14 @@ provideStrings({ braveWalletTransactionDetailHash: 'Transaction hash', braveWalletTransactionDetailNetwork: 'Network', + // Transaction Memo + braveWalletMemo: 'Memo', + braveWalletEnterAMessage: 'You can enter a message...', + braveWalletMessageOptional: 'Message (Optional)', + braveWalletAddMemo: 'Add memo', + braveWalletRemoveMemo: 'Remove memo', + braveWalletMemoLengthError: 'Memo must be less than 512 characters long.', + // Transactions Status braveWalletTransactionStatusUnapproved: 'Unapproved', braveWalletTransactionStatusApproved: 'Approved', diff --git a/components/brave_wallet_ui/utils/tx-utils.ts b/components/brave_wallet_ui/utils/tx-utils.ts index d9a91ccaa087..3d825bbb2f8e 100644 --- a/components/brave_wallet_ui/utils/tx-utils.ts +++ b/components/brave_wallet_ui/utils/tx-utils.ts @@ -139,6 +139,7 @@ export interface ParsedTransaction // Solana Specific isAssociatedTokenAccountCreation: boolean hasSystemProgramAssignInstruction: boolean + zcashMemo: number[] | undefined } export type ParsedTransactionWithoutFiatValues = Omit< @@ -1745,7 +1746,8 @@ export const parseTransactionWithoutPrices = ({ weiTransferredValue, formattedSendCurrencyTotal, isAssociatedTokenAccountCreation: isAssociatedTokenAccountCreationTx(tx), - hasSystemProgramAssignInstruction: hasSystemProgramAssignInstruction(tx) + hasSystemProgramAssignInstruction: hasSystemProgramAssignInstruction(tx), + zcashMemo: tx.txDataUnion.zecTxData?.memo ?? undefined } } @@ -1809,7 +1811,8 @@ export const parseTransactionWithPrices = ({ token, txNetwork: transactionNetwork, transferredValueWei: weiTransferredValue - }) + }), + zcashMemo: tx.txDataUnion.zecTxData?.memo ?? undefined } } diff --git a/components/resources/wallet_strings.grdp b/components/resources/wallet_strings.grdp index b9149c96314d..64706b65efe9 100644 --- a/components/resources/wallet_strings.grdp +++ b/components/resources/wallet_strings.grdp @@ -481,6 +481,12 @@ Recent transactions Transaction hash Network + Memo + You can enter a message... + Message (Optional) + Add memo + Remove memo + Memo must be less than 512 characters long. Max priority fee Edit gas While not a guarantee, miners will likely prioritize your transaction if you pay a higher fee.