diff --git a/CHANGELOG.md b/CHANGELOG.md index a93be492443..d60082ebc06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - added: Logbox disable option to env.json - added: Reverse-resolve recipient addresses to ENS / Unstoppable Domains / ZNS names in the send flow, address modal, and transaction history. +- changed: Prevent sending to the same wallet's own address for EVM assets. ## 4.49.0 (staging) diff --git a/eslint.config.mjs b/eslint.config.mjs index 4ed016e51dd..993f03b252d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -472,7 +472,7 @@ export default [ 'src/util/crypto.ts', 'src/util/CryptoAmount.ts', 'src/util/cryptoTextUtils.ts', - 'src/util/CurrencyInfoHelpers.ts', + 'src/util/CurrencyWalletHelpers.ts', 'src/util/exchangeRates.ts', diff --git a/src/components/modals/PendingTxModal.tsx b/src/components/modals/PendingTxModal.tsx index 91b020264bb..53161c5ac4a 100644 --- a/src/components/modals/PendingTxModal.tsx +++ b/src/components/modals/PendingTxModal.tsx @@ -6,11 +6,11 @@ import type { import * as React from 'react' import type { AirshipBridge } from 'react-native-airship' -import { getSpecialCurrencyInfo } from '../../constants/WalletAndCurrencyConstants' import { useAsyncEffect } from '../../hooks/useAsyncEffect' import { useHandler } from '../../hooks/useHandler' import { lstrings } from '../../locales/strings' import type { NavigationBase } from '../../types/routerTypes' +import { isEvmWallet } from '../../util/CurrencyInfoHelpers' import { ButtonsView } from '../buttons/ButtonsView' import { Airship, showError, showToast } from '../services/AirshipInstance' import { Paragraph } from '../themed/EdgeText' @@ -25,16 +25,6 @@ interface PendingTxModalProps { navigation: NavigationBase } -/** - * Checks if a wallet is EVM-based by looking at its WalletConnect v2 chain ID namespace. - * EVM chains use the 'eip155' namespace. - */ -function isEvmWallet(wallet: EdgeCurrencyWallet): boolean { - const { pluginId } = wallet.currencyInfo - const specialInfo = getSpecialCurrencyInfo(pluginId) - return specialInfo.walletConnectV2ChainId?.namespace === 'eip155' -} - const PendingTxModal = (props: PendingTxModalProps): React.ReactElement => { const { bridge, wallet, navigation, tokenId } = props const [pendingTransaction, setPendingTransaction] = diff --git a/src/components/scenes/SendScene2.tsx b/src/components/scenes/SendScene2.tsx index 58aa9578ff5..474131da54a 100644 --- a/src/components/scenes/SendScene2.tsx +++ b/src/components/scenes/SendScene2.tsx @@ -49,7 +49,7 @@ import { useState } from '../../types/reactHooks' import { useDispatch, useSelector } from '../../types/reactRedux' import type { EdgeAppSceneProps, NavigationBase } from '../../types/routerTypes' import type { FioRequest } from '../../types/types' -import { getCurrencyCode } from '../../util/CurrencyInfoHelpers' +import { getCurrencyCode, isEvmWallet } from '../../util/CurrencyInfoHelpers' import { getWalletName } from '../../util/CurrencyWalletHelpers' import { addToFioAddressCache, @@ -182,16 +182,6 @@ const MULTI_OUT_DIFF_PERCENT = '0.005' const PIN_MAX_LENGTH = 4 const INFINITY_STRING = '999999999999999999999999999999999999999' -/** - * Checks if a wallet is EVM-based by looking at its WalletConnect v2 chain ID - * namespace. EVM chains use the 'eip155' namespace. - */ -const isEvmWallet = (wallet: EdgeCurrencyWallet): boolean => { - const { pluginId } = wallet.currencyInfo - const specialInfo = getSpecialCurrencyInfo(pluginId) - return specialInfo.walletConnectV2ChainId?.namespace === 'eip155' -} - const SendComponent: React.FC = props => { const { route, navigation } = props const dispatch = useDispatch() diff --git a/src/components/tiles/AddressTile2.tsx b/src/components/tiles/AddressTile2.tsx index 4ce6c84385a..e3df9cb97ec 100644 --- a/src/components/tiles/AddressTile2.tsx +++ b/src/components/tiles/AddressTile2.tsx @@ -21,7 +21,7 @@ import { lstrings } from '../../locales/strings' import { PaymentProtoError } from '../../types/PaymentProtoError' import { useSelector } from '../../types/reactRedux' import type { NavigationBase } from '../../types/routerTypes' -import { getCurrencyCode } from '../../util/CurrencyInfoHelpers' +import { getCurrencyCode, isEvmWallet } from '../../util/CurrencyInfoHelpers' import { parseDeepLink } from '../../util/DeepLinkParser' import { checkPubAddress } from '../../util/FioAddressUtils' import { type NameService, reverseLookupName } from '../../util/nameServices' @@ -310,6 +310,24 @@ export const AddressTile2 = React.forwardRef( return } + // Prevent sending to the same wallet's own address. EVM wallets use a + // single static address across all EVM chains, so a self-send is + // always a mistake. (Cross-wallet "self transfer" to a *different* + // wallet via `handleSelfTransfer` is unaffected.) Compare + // case-insensitively since EVM addresses are checksummed hex. + if (isEvmWallet(coreWallet)) { + const ownReceiveAddress = await coreWallet.getReceiveAddress({ + tokenId: null + }) + if ( + parsedUri.publicAddress.toLowerCase() === + ownReceiveAddress.publicAddress.toLowerCase() + ) { + showError(lstrings.send_to_self_error_message) + return + } + } + // If we don't already have a resolved name from a forward-typed // domain, attempt a reverse lookup against the parsed public // address. The dispatcher caches per (pluginId, address) so this diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 6b289adf405..1c5a29a3848 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -248,6 +248,7 @@ const strings = { fragment_required: 'Required', scan_invalid_address_error_title: 'Invalid Address', scan_invalid_address_error_description: 'Not a valid public address', + send_to_self_error_message: 'You cannot send to the same wallet', fragment_send_subtitle: 'Send', fragment_send_myself: 'Myself', fragment_send_from_label: 'From', diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index deedd2840cd..638d7ec9e3e 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -154,6 +154,7 @@ "fragment_required": "Required", "scan_invalid_address_error_title": "Invalid Address", "scan_invalid_address_error_description": "Not a valid public address", + "send_to_self_error_message": "You cannot send to the same wallet", "fragment_send_subtitle": "Send", "fragment_send_myself": "Myself", "fragment_send_from_label": "From", diff --git a/src/util/CurrencyInfoHelpers.ts b/src/util/CurrencyInfoHelpers.ts index 24e8eb13d9b..732c073e227 100644 --- a/src/util/CurrencyInfoHelpers.ts +++ b/src/util/CurrencyInfoHelpers.ts @@ -10,7 +10,10 @@ import type { } from 'edge-core-js' import { showError } from '../components/services/AirshipInstance' -import { SPECIAL_CURRENCY_INFO } from '../constants/WalletAndCurrencyConstants' +import { + getSpecialCurrencyInfo, + SPECIAL_CURRENCY_INFO +} from '../constants/WalletAndCurrencyConstants' import { ENV } from '../env' import type { EdgeAsset } from '../types/types' import { asMaybeContractLocation } from './cleaners' @@ -24,6 +27,16 @@ export function isKeysOnlyPlugin(pluginId: string): boolean { return keysOnlyMode || ENV.KEYS_ONLY_PLUGINS[pluginId] } +/** + * Checks if a wallet is EVM-based by looking at its WalletConnect v2 chain ID + * namespace. EVM chains use the 'eip155' namespace. + */ +export function isEvmWallet(wallet: EdgeCurrencyWallet): boolean { + const { pluginId } = wallet.currencyInfo + const specialInfo = getSpecialCurrencyInfo(pluginId) + return specialInfo.walletConnectV2ChainId?.namespace === 'eip155' +} + export type FindTokenParams = | { account: EdgeAccount