diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d9eedf316c..f4242412746 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -335,3 +335,16 @@ jobs: echo "All jobs passed step skipped. Block PR." exit 1 fi + + log-merge-group-failure: + name: Log merge group failure + # Only run this job if the merge group event fails, skip on forks + if: ${{ github.event_name == 'merge_group' && failure() && !github.event.repository.fork }} + needs: + - check-all-jobs-pass + uses: metamask/github-tools/.github/workflows/log-merge-group-failure.yml@6bbad335a01fce1a9ec1eabd9515542c225d46c0 + secrets: + GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }} + GOOGLE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_SERVICE_ACCOUNT }} + SPREADSHEET_ID: ${{ secrets.GOOGLE_MERGE_QUEUE_SPREADSHEET_ID }} + SHEET_NAME: ${{ secrets.GOOGLE_MERGE_QUEUE_SHEET_NAME }} diff --git a/.js.env.example b/.js.env.example index 38daeab2a2c..0d1d2764861 100644 --- a/.js.env.example +++ b/.js.env.example @@ -66,6 +66,9 @@ export SEGMENT_FLUSH_INTERVAL="1" # example for flush when 1 event is queued export SEGMENT_FLUSH_EVENT_LIMIT="1" +# URL of the decoding API used to provide additional data from signature requests +export DECODING_API_URL: 'https://signature-insights.api.cx.metamask.io/v1' + # URL of security alerts API used to validate dApp requests. export SECURITY_ALERTS_API_URL="https://security-alerts.api.cx.metamask.io" diff --git a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.types.tsx b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.types.tsx index de90049af66..b6ba4f94932 100644 --- a/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.types.tsx +++ b/app/components/UI/AccountFromToInfoCard/AccountFromToInfoCard.types.tsx @@ -1,4 +1,4 @@ -import { InternalAccount } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; interface SelectedAsset { isETH: boolean; diff --git a/app/components/UI/AccountSelectorList/AccountSelectorList.tsx b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx index 499a3eacb96..c672eb7dcde 100644 --- a/app/components/UI/AccountSelectorList/AccountSelectorList.tsx +++ b/app/components/UI/AccountSelectorList/AccountSelectorList.tsx @@ -11,7 +11,7 @@ import { selectInternalAccounts } from '../../../selectors/accountsController'; import Cell, { CellVariant, } from '../../../component-library/components/Cells/Cell'; -import { InternalAccount } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import { useStyles } from '../../../component-library/hooks'; import { TextColor } from '../../../component-library/components/Texts/Text'; import SensitiveText, { diff --git a/app/components/UI/Ramp/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap b/app/components/UI/Ramp/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap index 52e3ff03e42..1abf3c09e47 100644 --- a/app/components/UI/Ramp/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap +++ b/app/components/UI/Ramp/Views/BuildQuote/__snapshots__/BuildQuote.test.tsx.snap @@ -2604,7 +2604,6 @@ exports[`BuildQuote View Crypto Currency Data renders the loading page when cryp "padding": 0, }, undefined, - undefined, ] } > @@ -2794,7 +2793,6 @@ exports[`BuildQuote View Crypto Currency Data renders the loading page when cryp "padding": 0, }, undefined, - undefined, ] } > @@ -4194,7 +4192,6 @@ exports[`BuildQuote View Fiat Currency Data renders the loading page when fiats "padding": 0, }, undefined, - undefined, ] } > @@ -4384,7 +4381,6 @@ exports[`BuildQuote View Fiat Currency Data renders the loading page when fiats "padding": 0, }, undefined, - undefined, ] } > @@ -5784,7 +5780,6 @@ exports[`BuildQuote View Payment Method Data renders the loading page when payme "padding": 0, }, undefined, - undefined, ] } > @@ -5974,7 +5969,6 @@ exports[`BuildQuote View Payment Method Data renders the loading page when payme "padding": 0, }, undefined, - undefined, ] } > @@ -7374,7 +7368,6 @@ exports[`BuildQuote View Regions data renders the loading page when regions are "padding": 0, }, undefined, - undefined, ] } > @@ -7564,7 +7557,6 @@ exports[`BuildQuote View Regions data renders the loading page when regions are "padding": 0, }, undefined, - undefined, ] } > @@ -8511,7 +8503,6 @@ exports[`BuildQuote View renders correctly 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -8842,7 +8833,6 @@ exports[`BuildQuote View renders correctly 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -9137,7 +9127,6 @@ exports[`BuildQuote View renders correctly 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -11620,7 +11609,6 @@ exports[`BuildQuote View renders correctly 2`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -11951,7 +11939,6 @@ exports[`BuildQuote View renders correctly 2`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -12094,7 +12081,6 @@ exports[`BuildQuote View renders correctly 2`] = ` "padding": 0, }, undefined, - undefined, ] } > diff --git a/app/components/UI/Ramp/Views/OrderDetails/__snapshots__/OrderDetails.test.tsx.snap b/app/components/UI/Ramp/Views/OrderDetails/__snapshots__/OrderDetails.test.tsx.snap index 88387a8f98c..6ab703e40da 100644 --- a/app/components/UI/Ramp/Views/OrderDetails/__snapshots__/OrderDetails.test.tsx.snap +++ b/app/components/UI/Ramp/Views/OrderDetails/__snapshots__/OrderDetails.test.tsx.snap @@ -585,7 +585,6 @@ exports[`OrderDetails renders a cancelled order 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -2083,7 +2082,6 @@ exports[`OrderDetails renders a completed order 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -3595,7 +3593,6 @@ exports[`OrderDetails renders a created order 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -5023,7 +5020,6 @@ exports[`OrderDetails renders a failed order 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -6535,7 +6531,6 @@ exports[`OrderDetails renders a pending order 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -9070,7 +9065,6 @@ exports[`OrderDetails renders non-transacted orders 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -10609,7 +10603,6 @@ exports[`OrderDetails renders the support links if the provider has them 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -12141,7 +12134,6 @@ exports[`OrderDetails renders transacted orders that do not have timeDescription "padding": 0, }, undefined, - undefined, ] } > @@ -13616,7 +13608,6 @@ exports[`OrderDetails renders transacted orders that have timeDescriptionPending "padding": 0, }, undefined, - undefined, ] } > diff --git a/app/components/UI/Ramp/Views/PaymentMethods/__snapshots__/PaymentMethods.test.tsx.snap b/app/components/UI/Ramp/Views/PaymentMethods/__snapshots__/PaymentMethods.test.tsx.snap index e5a9d3b334f..2e25a8d696d 100644 --- a/app/components/UI/Ramp/Views/PaymentMethods/__snapshots__/PaymentMethods.test.tsx.snap +++ b/app/components/UI/Ramp/Views/PaymentMethods/__snapshots__/PaymentMethods.test.tsx.snap @@ -495,7 +495,6 @@ exports[`PaymentMethods View renders correctly 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -781,7 +780,6 @@ exports[`PaymentMethods View renders correctly 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -1084,7 +1082,6 @@ exports[`PaymentMethods View renders correctly 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -1945,7 +1942,6 @@ exports[`PaymentMethods View renders correctly for sell 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -2231,7 +2227,6 @@ exports[`PaymentMethods View renders correctly for sell 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -2534,7 +2529,6 @@ exports[`PaymentMethods View renders correctly for sell 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -3391,7 +3385,6 @@ exports[`PaymentMethods View renders correctly while loading 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -3641,7 +3634,6 @@ exports[`PaymentMethods View renders correctly while loading 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -3893,7 +3885,6 @@ exports[`PaymentMethods View renders correctly while loading 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -6591,7 +6582,6 @@ exports[`PaymentMethods View renders correctly with null data 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -6841,7 +6831,6 @@ exports[`PaymentMethods View renders correctly with null data 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -7093,7 +7082,6 @@ exports[`PaymentMethods View renders correctly with null data 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -7823,7 +7811,6 @@ exports[`PaymentMethods View renders correctly with payment method with disclaim "padding": 0, }, undefined, - undefined, ] } > @@ -8109,7 +8096,6 @@ exports[`PaymentMethods View renders correctly with payment method with disclaim "padding": 0, }, undefined, - undefined, ] } > @@ -8412,7 +8398,6 @@ exports[`PaymentMethods View renders correctly with payment method with disclaim "padding": 0, }, undefined, - undefined, ] } > @@ -9964,7 +9949,6 @@ exports[`PaymentMethods View renders correctly with show back button false 1`] = "padding": 0, }, undefined, - undefined, ] } > @@ -10250,7 +10234,6 @@ exports[`PaymentMethods View renders correctly with show back button false 1`] = "padding": 0, }, undefined, - undefined, ] } > @@ -10553,7 +10536,6 @@ exports[`PaymentMethods View renders correctly with show back button false 1`] = "padding": 0, }, undefined, - undefined, ] } > diff --git a/app/components/UI/Ramp/Views/Quotes/__snapshots__/Quotes.test.tsx.snap b/app/components/UI/Ramp/Views/Quotes/__snapshots__/Quotes.test.tsx.snap index 97f6fd0365a..3ad1e4f304e 100644 --- a/app/components/UI/Ramp/Views/Quotes/__snapshots__/Quotes.test.tsx.snap +++ b/app/components/UI/Ramp/Views/Quotes/__snapshots__/Quotes.test.tsx.snap @@ -35,7 +35,6 @@ exports[`LoadingQuotes component renders correctly 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -277,7 +276,6 @@ exports[`LoadingQuotes component renders correctly 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -416,7 +414,6 @@ exports[`LoadingQuotes component renders correctly 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -3546,7 +3543,6 @@ exports[`Quotes renders correctly after animation with quotes 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -3819,7 +3815,6 @@ exports[`Quotes renders correctly after animation with quotes 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -5023,7 +5018,6 @@ exports[`Quotes renders correctly after animation with quotes and expanded 2`] = "padding": 0, }, undefined, - undefined, ] } > @@ -5296,7 +5290,6 @@ exports[`Quotes renders correctly after animation with quotes and expanded 2`] = "padding": 0, }, undefined, - undefined, ] } > @@ -5596,7 +5589,6 @@ exports[`Quotes renders correctly after animation with quotes and expanded 2`] = "padding": 0, }, undefined, - undefined, ] } > diff --git a/app/components/UI/Ramp/Views/Regions/__snapshots__/Regions.test.tsx.snap b/app/components/UI/Ramp/Views/Regions/__snapshots__/Regions.test.tsx.snap index cffaf29508d..d181f8b8542 100644 --- a/app/components/UI/Ramp/Views/Regions/__snapshots__/Regions.test.tsx.snap +++ b/app/components/UI/Ramp/Views/Regions/__snapshots__/Regions.test.tsx.snap @@ -566,7 +566,6 @@ exports[`Regions View renders correctly 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -1346,7 +1345,6 @@ exports[`Regions View renders correctly while loading 1`] = ` undefined, undefined, undefined, - undefined, ] } > @@ -2575,7 +2573,6 @@ exports[`Regions View renders correctly with no data 1`] = ` undefined, undefined, undefined, - undefined, ] } > @@ -3825,7 +3822,6 @@ exports[`Regions View renders correctly with selectedRegion 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -4624,7 +4620,6 @@ exports[`Regions View renders correctly with unsupportedRegion 1`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -4949,7 +4944,6 @@ exports[`Regions View renders correctly with unsupportedRegion 1`] = ` undefined, undefined, undefined, - undefined, { "backgroundColor": "#ffffff", "borderWidth": 0, @@ -5799,7 +5793,6 @@ exports[`Regions View renders correctly with unsupportedRegion 2`] = ` "padding": 0, }, undefined, - undefined, ] } > @@ -6124,7 +6117,6 @@ exports[`Regions View renders correctly with unsupportedRegion 2`] = ` undefined, undefined, undefined, - undefined, { "backgroundColor": "#ffffff", "borderWidth": 0, @@ -6974,7 +6966,6 @@ exports[`Regions View renders regions modal when pressing select button 1`] = ` "padding": 0, }, undefined, - undefined, ] } > diff --git a/app/components/UI/Ramp/components/Box.tsx b/app/components/UI/Ramp/components/Box.tsx index 54eb1b9cd0e..41974701101 100644 --- a/app/components/UI/Ramp/components/Box.tsx +++ b/app/components/UI/Ramp/components/Box.tsx @@ -21,9 +21,6 @@ const createStyles = (colors: Colors) => label: { marginVertical: 8, }, - noBorder: { - borderWidth: 0, - }, highlighted: { borderColor: colors.primary.default, }, @@ -41,7 +38,6 @@ interface Props { style?: StyleProp; thin?: boolean; activeOpacity?: number; - noBorder?: boolean; // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any onPress?: () => any; @@ -60,7 +56,6 @@ const Box: React.FC = ({ accessible, accessibilityLabel, compact, - noBorder, ...props }: Props) => { const { colors } = useTheme(); @@ -82,7 +77,6 @@ const Box: React.FC = ({ thin && styles.thin, highlighted && styles.highlighted, compact && styles.compact, - noBorder && styles.noBorder, style, ]} {...props} diff --git a/app/components/UI/SimulationDetails/formatAmount.ts b/app/components/UI/SimulationDetails/formatAmount.ts index 8aabfc4e553..8b5ebe3a8d4 100644 --- a/app/components/UI/SimulationDetails/formatAmount.ts +++ b/app/components/UI/SimulationDetails/formatAmount.ts @@ -1,7 +1,10 @@ import { BigNumber } from 'bignumber.js'; const MIN_AMOUNT = new BigNumber('0.000001'); -const PRECISION = 6; + +// The default precision for displaying currency values. +// It set to the number of decimal places in the minimum amount. +export const DEFAULT_PRECISION = new BigNumber(MIN_AMOUNT).decimalPlaces(); // The number of significant decimals places to show for amounts less than 1. const MAX_SIGNIFICANT_DECIMAL_PLACES = 3; @@ -12,11 +15,21 @@ export function formatAmountMaxPrecision( locale: string, num: number | BigNumber, ): string { - return new Intl.NumberFormat(locale, { - minimumSignificantDigits: 1, - }).format(new BigNumber(num.toString()).toNumber()); + const bigNumberValue = new BigNumber(num); + const numberOfDecimals = bigNumberValue.decimalPlaces(); + const formattedValue = bigNumberValue.toFixed(numberOfDecimals ?? 0); + + const [integerPart, fractionalPart] = formattedValue.split('.'); + const formattedIntegerPart = new Intl.NumberFormat(locale).format( + integerPart as unknown as number, + ); + + return fractionalPart + ? `${formattedIntegerPart}.${fractionalPart}` + : formattedIntegerPart; } + /** * Formats the a token amount with variable precision and significant * digits. @@ -65,7 +78,7 @@ export function formatAmount(locale: string, amount: BigNumber): string { return new Intl.NumberFormat(locale, { maximumSignificantDigits: MAX_SIGNIFICANT_DECIMAL_PLACES, } as Intl.NumberFormatOptions).format( - amount.decimalPlaces(PRECISION).toNumber(), + amount.dp(DEFAULT_PRECISION ?? 0).toNumber(), ); } @@ -73,7 +86,7 @@ export function formatAmount(locale: string, amount: BigNumber): string { // Cap the digits right of the decimal point: The more digits present // on the left side of the decimal point, the less decimal places // we show on the right side. - const digitsLeftOfDecimal = amount.abs().toFixed(0).length; + const digitsLeftOfDecimal = amount.abs().dp(0).toString().length; const maximumFractionDigits = Math.max( 0, MAX_SIGNIFICANT_DECIMAL_PLACES - digitsLeftOfDecimal + 1, @@ -81,5 +94,10 @@ export function formatAmount(locale: string, amount: BigNumber): string { return new Intl.NumberFormat(locale, { maximumFractionDigits, - } as Intl.NumberFormatOptions).format(amount.toNumber()); + } as Intl.NumberFormatOptions).format( + // string is valid parameter for format function + // for some reason it gives TS issue + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/format#number + amount.toFixed(maximumFractionDigits) as unknown as number, + ); } diff --git a/app/components/UI/Tokens/TokenList/PortfolioBalance/index.tsx b/app/components/UI/Tokens/TokenList/PortfolioBalance/index.tsx index 250e675b460..97dcf6dfbcb 100644 --- a/app/components/UI/Tokens/TokenList/PortfolioBalance/index.tsx +++ b/app/components/UI/Tokens/TokenList/PortfolioBalance/index.tsx @@ -52,7 +52,7 @@ import { TotalFiatBalancesCrossChains, useGetTotalFiatBalanceCrossChains, } from '../../../../hooks/useGetTotalFiatBalanceCrossChains'; -import { InternalAccount } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import { getChainIdsToPoll } from '../../../../../selectors/tokensController'; import AggregatedPercentageCrossChains from '../../../../../component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentageCrossChains'; diff --git a/app/components/Views/AccountActions/AccountActions.tsx b/app/components/Views/AccountActions/AccountActions.tsx index 720c9a53214..2841bf87722 100644 --- a/app/components/Views/AccountActions/AccountActions.tsx +++ b/app/components/Views/AccountActions/AccountActions.tsx @@ -11,7 +11,7 @@ import { useDispatch, useSelector } from 'react-redux'; import Share from 'react-native-share'; // External dependencies -import { InternalAccount } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import BottomSheet, { BottomSheetRef, } from '../../../component-library/components/BottomSheets/BottomSheet'; @@ -397,7 +397,9 @@ const AccountActions = () => { actionTitle={strings('accounts.remove_hardware_account')} iconName={IconName.Close} onPress={showRemoveHWAlert} - testID={AccountActionsBottomSheetSelectorsIDs.REMOVE_HARDWARE_ACCOUNT} + testID={ + AccountActionsBottomSheetSelectorsIDs.REMOVE_HARDWARE_ACCOUNT + } /> )} { diff --git a/app/components/Views/AddAccountActions/AddAccountActions.tsx b/app/components/Views/AddAccountActions/AddAccountActions.tsx index 7cda0b6bdc0..80c2d8021c7 100644 --- a/app/components/Views/AddAccountActions/AddAccountActions.tsx +++ b/app/components/Views/AddAccountActions/AddAccountActions.tsx @@ -20,7 +20,7 @@ import { useMetrics } from '../../../components/hooks/useMetrics'; ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) import { CaipChainId } from '@metamask/utils'; -import { KeyringClient } from '@metamask/keyring-api'; +import { KeyringClient } from '@metamask/keyring-snap-client'; import { BitcoinWalletSnapSender } from '../../../core/SnapKeyring/BitcoinWalletSnap'; import { MultichainNetworks } from '../../../core/Multichain/constants'; import { useSelector } from 'react-redux'; diff --git a/app/components/Views/EditAccountName/EditAccountName.tsx b/app/components/Views/EditAccountName/EditAccountName.tsx index 0f69a2b7734..e1946deae14 100644 --- a/app/components/Views/EditAccountName/EditAccountName.tsx +++ b/app/components/Views/EditAccountName/EditAccountName.tsx @@ -10,7 +10,7 @@ import { useSelector } from 'react-redux'; import { SafeAreaView } from 'react-native'; // External dependencies -import { InternalAccount } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import Text from '../../../component-library/components/Texts/Text/Text'; import { View } from 'react-native-animatable'; import { TextVariant } from '../../../component-library/components/Texts/Text'; diff --git a/app/components/Views/RevealPrivateCredential/RevealPrivateCredential.tsx b/app/components/Views/RevealPrivateCredential/RevealPrivateCredential.tsx index b3e921d6366..d06c2fb4d25 100644 --- a/app/components/Views/RevealPrivateCredential/RevealPrivateCredential.tsx +++ b/app/components/Views/RevealPrivateCredential/RevealPrivateCredential.tsx @@ -11,7 +11,7 @@ import { } from 'react-native'; import { useDispatch, useSelector } from 'react-redux'; import { wordlist } from '@metamask/scure-bip39/dist/wordlists/english'; -import { InternalAccount } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import QRCode from 'react-native-qrcode-svg'; import { RouteProp, ParamListBase } from '@react-navigation/native'; import ScrollableTabView, { diff --git a/app/components/Views/RevealPrivateCredential/index.test.tsx b/app/components/Views/RevealPrivateCredential/index.test.tsx index 6e9d90b7916..bbf92a7743f 100644 --- a/app/components/Views/RevealPrivateCredential/index.test.tsx +++ b/app/components/Views/RevealPrivateCredential/index.test.tsx @@ -2,11 +2,13 @@ import React from 'react'; import { render, fireEvent, waitFor } from '@testing-library/react-native'; import configureMockStore from 'redux-mock-store'; import { Provider } from 'react-redux'; -import { InternalAccount } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import { backgroundState } from '../../../util/test/initial-root-state'; import { RevealPrivateCredential } from './'; import { ThemeContext, mockTheme } from '../../../util/theme'; import { RevealSeedViewSelectorsIDs } from '../../../../e2e/selectors/Settings/SecurityAndPrivacy/RevealSeedView.selectors'; +import { EthAccountType, EthMethod, EthScopes } from '@metamask/keyring-api'; +import { KeyringTypes } from '@metamask/keyring-controller'; jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), @@ -147,24 +149,26 @@ describe('RevealPrivateCredential', () => { it('renders with a custom selectedAddress', async () => { const mockInternalAccount: InternalAccount = { - type: 'eip155:eoa', + type: EthAccountType.Eoa, id: 'unique-account-id-1', address: '0x1234567890123456789012345678901234567890', options: { someOption: 'optionValue', anotherOption: 42, }, + scopes: [EthScopes.Namespace], methods: [ - 'personal_sign', - 'eth_sign', - 'eth_signTransaction', - 'eth_sendTransaction', + EthMethod.PersonalSign, + EthMethod.SignTransaction, + EthMethod.SignTypedDataV1, + EthMethod.SignTypedDataV3, + EthMethod.SignTypedDataV4, ], metadata: { name: 'Test Account', importTime: Date.now(), keyring: { - type: 'HD Key Tree', + type: KeyringTypes.hd, }, nameLastUpdatedAt: Date.now(), snap: { diff --git a/app/components/Views/Snaps/KeyringSnapRemovalWarning/KeyringSnapRemovalWarning.tsx b/app/components/Views/Snaps/KeyringSnapRemovalWarning/KeyringSnapRemovalWarning.tsx index b2ec560b5dd..cd8b66c42a5 100644 --- a/app/components/Views/Snaps/KeyringSnapRemovalWarning/KeyringSnapRemovalWarning.tsx +++ b/app/components/Views/Snaps/KeyringSnapRemovalWarning/KeyringSnapRemovalWarning.tsx @@ -15,7 +15,7 @@ import BottomSheet, { import Text, { TextVariant, } from '../../../../component-library/components/Texts/Text'; -import { InternalAccount } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import BannerAlert from '../../../../component-library/components/Banners/Banner/variants/BannerAlert'; import { BannerAlertSeverity } from '../../../../component-library/components/Banners/Banner'; import BottomSheetHeader from '../../../../component-library/components/BottomSheets/BottomSheetHeader'; @@ -170,56 +170,56 @@ export default function KeyringSnapRemovalWarning({ 'app_settings.snaps.snap_settings.remove_account_snap_warning.banner_title', )} /> - {showConfirmation ? ( - <> - - {`${strings( - 'app_settings.snaps.snap_settings.remove_account_snap_warning.remove_account_snap_alert_description_1', - )} `} - - {snap.manifest.proposedName} - - {` ${strings( - 'app_settings.snaps.snap_settings.remove_account_snap_warning.remove_account_snap_alert_description_2', - )}`} + {showConfirmation ? ( + <> + + {`${strings( + 'app_settings.snaps.snap_settings.remove_account_snap_warning.remove_account_snap_alert_description_1', + )} `} + + {snap.manifest.proposedName} - - {error && ( - - {strings( - 'app_settings.snaps.snap_settings.remove_account_snap_warning.remove_snap_error', - { - snapName: snap.manifest.proposedName, - }, - )} - - )} - - ) : ( - <> - + {` ${strings( + 'app_settings.snaps.snap_settings.remove_account_snap_warning.remove_account_snap_alert_description_2', + )}`} + + + {error && ( + {strings( - 'app_settings.snaps.snap_settings.remove_account_snap_warning.description', + 'app_settings.snaps.snap_settings.remove_account_snap_warning.remove_snap_error', + { + snapName: snap.manifest.proposedName, + }, )} - + )} + + ) : ( + <> + + {strings( + 'app_settings.snaps.snap_settings.remove_account_snap_warning.description', + )} + + - {accountListItems} - - - - )} - - + {accountListItems} + + + + )} + + ); } diff --git a/app/components/Views/Snaps/SnapSettings/SnapSettings.tsx b/app/components/Views/Snaps/SnapSettings/SnapSettings.tsx index d56c1d0b200..e23a429e20f 100644 --- a/app/components/Views/Snaps/SnapSettings/SnapSettings.tsx +++ b/app/components/Views/Snaps/SnapSettings/SnapSettings.tsx @@ -31,7 +31,7 @@ import { selectPermissionControllerState } from '../../../../selectors/snaps/per import KeyringSnapRemovalWarning from '../KeyringSnapRemovalWarning/KeyringSnapRemovalWarning'; import { getAccountsBySnapId } from '../../../../core/SnapKeyring/utils/getAccountsBySnapId'; import { selectInternalAccounts } from '../../../../selectors/accountsController'; -import { InternalAccount } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import Logger from '../../../../util/Logger'; interface SnapSettingsProps { snap: Snap; @@ -100,7 +100,6 @@ const SnapSettings = () => { setIsShowingSnapKeyringRemoveWarning(false); }, []); - const removeSnap = useCallback(async () => { const { SnapController } = Engine.context; await SnapController.removeSnap(snap.id); @@ -110,8 +109,11 @@ const SnapSettings = () => { for (const keyringAccount of keyringAccounts) { await Engine.removeAccount(keyringAccount.address); } - } catch(error) { - Logger.error(error as Error, 'SnapSettings: failed to remove snap accounts when calling Engine.removeAccount'); + } catch (error) { + Logger.error( + error as Error, + 'SnapSettings: failed to remove snap accounts when calling Engine.removeAccount', + ); } } navigation.goBack(); @@ -125,7 +127,6 @@ const SnapSettings = () => { } }, [isKeyringSnap, keyringAccounts.length, removeSnap]); - const handleRemoveSnapKeyring = useCallback(() => { try { setIsShowingSnapKeyringRemoveWarning(true); diff --git a/app/components/Views/Snaps/components/KeyringAccountListItem/KeyringAccountListItem.tsx b/app/components/Views/Snaps/components/KeyringAccountListItem/KeyringAccountListItem.tsx index f0935d131b8..7afe2fc148f 100644 --- a/app/components/Views/Snaps/components/KeyringAccountListItem/KeyringAccountListItem.tsx +++ b/app/components/Views/Snaps/components/KeyringAccountListItem/KeyringAccountListItem.tsx @@ -1,6 +1,6 @@ ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) import React, { useCallback } from 'react'; -import { InternalAccount } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import { toChecksumHexAddress } from '@metamask/controller-utils'; import ButtonIcon, { ButtonIconSizes, diff --git a/app/components/Views/confirmations/Confirm/Confirm.test.tsx b/app/components/Views/confirmations/Confirm/Confirm.test.tsx index 8007a4c9866..6f6da173c48 100644 --- a/app/components/Views/confirmations/Confirm/Confirm.test.tsx +++ b/app/components/Views/confirmations/Confirm/Confirm.test.tsx @@ -57,7 +57,7 @@ describe('Confirm', () => { expect(getByText('Estimated changes')).toBeDefined(); expect( getByText( - 'You’re signing into a site and there are no predicted changes to your account.', + "You're signing into a site and there are no predicted changes to your account.", ), ).toBeDefined(); expect(getByText('Request from')).toBeDefined(); diff --git a/app/components/Views/confirmations/Confirm/Confirm.tsx b/app/components/Views/confirmations/Confirm/Confirm.tsx index a13ec947e8d..4a18a99d7d1 100644 --- a/app/components/Views/confirmations/Confirm/Confirm.tsx +++ b/app/components/Views/confirmations/Confirm/Confirm.tsx @@ -8,7 +8,7 @@ import Footer from '../components/Confirm/Footer'; import Info from '../components/Confirm/Info'; import SignatureBlockaidBanner from '../components/Confirm/SignatureBlockaidBanner'; import Title from '../components/Confirm/Title'; -import useConfirmationRedesignEnabled from '../hooks/useConfirmationRedesignEnabled'; +import { useConfirmationRedesignEnabled } from '../hooks/useConfirmationRedesignEnabled'; import styleSheet from './Confirm.styles'; const Confirm = () => { diff --git a/app/components/Views/confirmations/components/ApproveTransactionReview/AddNickname/types.ts b/app/components/Views/confirmations/components/ApproveTransactionReview/AddNickname/types.ts index beb9d1c0469..32ce99fcbdb 100644 --- a/app/components/Views/confirmations/components/ApproveTransactionReview/AddNickname/types.ts +++ b/app/components/Views/confirmations/components/ApproveTransactionReview/AddNickname/types.ts @@ -1,6 +1,6 @@ import { AddressBookControllerState } from '@metamask/address-book-controller'; import { NetworkType } from '@metamask/controller-utils'; -import { InternalAccount } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import type { NetworkState } from '@metamask/network-controller'; import { Hex } from '@metamask/utils'; diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV1/TypedSignV1.test.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV1/TypedSignV1.test.tsx index 0c533f651c9..31af8bd8955 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV1/TypedSignV1.test.tsx +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV1/TypedSignV1.test.tsx @@ -12,7 +12,7 @@ describe('TypedSignV1', () => { expect(getByText('Estimated changes')).toBeDefined(); expect( getByText( - 'You’re signing into a site and there are no predicted changes to your account.', + "You're signing into a site and there are no predicted changes to your account.", ), ).toBeDefined(); expect(getByText('Request from')).toBeDefined(); diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Simulation.test.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Simulation.test.tsx new file mode 100644 index 00000000000..6f015ab9b16 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Simulation.test.tsx @@ -0,0 +1,142 @@ +import React from 'react'; +import cloneDeep from 'lodash/cloneDeep'; +import { + DecodingData, + DecodingDataChangeType, + DecodingDataStateChanges, + SignatureRequest, +} from '@metamask/signature-controller'; +import useGetTokenStandardAndDetails from '../../../../../hooks/useGetTokenStandardAndDetails'; +import { typedSignV4ConfirmationState } from '../../../../../../../../util/test/confirm-data-helpers'; +import renderWithProvider from '../../../../../../../../util/test/renderWithProvider'; +import { memoizedGetTokenStandardAndDetails } from '../../../../../utils/token'; +import TypedSignV3V4Simulation from './Simulation'; + +jest.mock('../../../../../hooks/useGetTokenStandardAndDetails'); + +jest.mock('../../../../../../../../core/Engine', () => ({ + context: { + NetworkController: { + findNetworkClientIdByChainId: () => 'mainnet', + }, + }, +})); + +const stateChangesApprove = [ + { + assetType: 'ERC20', + changeType: DecodingDataChangeType.Approve, + address: '0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad', + amount: '12345', + contractAddress: '0x6b175474e89094c44da98b954eedeac495271d0f', + }, +]; + +const mockState = ( + mockStateChanges: DecodingDataStateChanges, + { + mockDecodingDataProps, + stubDecodingLoading = false, + }: { + mockDecodingDataProps?: Partial; + stubDecodingLoading?: boolean; + } = { + mockDecodingDataProps: {}, + stubDecodingLoading: false, + }, +) => { + const clonedMockState = cloneDeep(typedSignV4ConfirmationState); + const request = clonedMockState.engine.backgroundState.SignatureController + .signatureRequests[ + 'fb2029e1-b0ab-11ef-9227-05a11087c334' + ] as SignatureRequest; + + request.decodingLoading = stubDecodingLoading; + request.decodingData = { + ...mockDecodingDataProps, + stateChanges: mockStateChanges, + }; + + return clonedMockState; +}; + +describe('PermitSimulation', () => { + afterEach(() => { + jest.clearAllMocks(); + + /** Reset memoized function using getTokenStandardAndDetails for each test */ + memoizedGetTokenStandardAndDetails?.cache?.clear?.(); + }); + + it('renders DecodedSimulation loader if decodingLoading is true', async () => { + const { queryByTestId } = renderWithProvider(, { + state: mockState(stateChangesApprove, { + stubDecodingLoading: true, + }), + }); + + expect(await queryByTestId('confirm-v3v4-simulation-loader')).toBeDefined(); + }); + + it('renders DecodingSimulation with "Unavailable" if decoding data is empty', async () => { + const { getByText } = renderWithProvider(, { + state: mockState([]), + }); + + expect(await getByText('Estimated changes')).toBeDefined(); + expect(await getByText('Unavailable')).toBeDefined(); + }); + + it('renders DecodingSimulation for permits', async () => { + ( + useGetTokenStandardAndDetails as jest.MockedFn< + typeof useGetTokenStandardAndDetails + > + ).mockReturnValue({ + symbol: 'TST', + decimals: '4', + balance: undefined, + standard: 'ERC20', + decimalsNumber: 4, + }); + + const { getByText } = renderWithProvider(, { + state: mockState(stateChangesApprove), + }); + + expect(await getByText('Estimated changes')).toBeDefined(); + expect(await getByText('Spending cap')).toBeDefined(); + expect(await getByText('1.235')).toBeDefined(); + }); + + it('renders PermitSimulation if decoding api returns error', async () => { + ( + useGetTokenStandardAndDetails as jest.MockedFn< + typeof useGetTokenStandardAndDetails + > + ).mockReturnValue({ + symbol: 'TST', + decimals: '2', + balance: undefined, + standard: 'ERC20', + decimalsNumber: 4, + }); + + const { getByText } = renderWithProvider(, { + state: mockState([], { + mockDecodingDataProps: { + error: { message: 'some error', type: 'SOME_ERROR' }, + } as Partial, + }), + }); + + expect(await getByText('Estimated changes')).toBeDefined(); + expect(await getByText('Spending cap')).toBeDefined(); + expect(await getByText('0.3')).toBeDefined(); + expect( + await getByText( + "You're giving the spender permission to spend this many tokens from your account.", + ), + ).toBeDefined(); + }); +}); diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Simulation.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Simulation.tsx new file mode 100644 index 00000000000..37265d0d0cd --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Simulation.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +import { useTypedSignSimulationEnabled } from '../../../../../hooks/useTypedSignSimulationEnabled'; +import { isRecognizedPermit } from '../../../../../utils/signature'; +import { useSignatureRequest } from '../../../../../hooks/useSignatureRequest'; +import DecodedSimulation from './TypedSignDecoded'; +import PermitSimulation from './TypedSignPermit'; + +const TypedSignV3V4Simulation: React.FC = () => { + const signatureRequest = useSignatureRequest(); + const isPermit = signatureRequest && isRecognizedPermit(signatureRequest); + const isSimulationSupported = useTypedSignSimulationEnabled(); + + if (!isSimulationSupported || !signatureRequest) { + return null; + } + + const { decodingData, decodingLoading } = signatureRequest; + const hasValidDecodingData = !( + (!decodingLoading && decodingData === undefined) || + decodingData?.error + ); + + if (!hasValidDecodingData && isPermit) { + return ; + } + + return ; +}; + +export default TypedSignV3V4Simulation; diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Static/Static.test.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Static/Static.test.tsx new file mode 100644 index 00000000000..237ceffd226 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Static/Static.test.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Text } from 'react-native'; +import { render } from '@testing-library/react-native'; +import StaticSimulation from './Static'; + +const mockProps = { + title: 'Test Title', + titleTooltip: 'Test Tooltip', + description: 'Test Description', + simulationElements: <>, +}; + +describe('StaticSimulation', () => { + it('renders correctly with basic props', () => { + const { getByText } = render(); + + expect(getByText('Test Title')).toBeDefined(); + expect(getByText('Test Description')).toBeDefined(); + }); + + it('shows loader when isLoading is true', () => { + const { queryByTestId } = render( + , + ); + + expect(queryByTestId('confirm-v3v4-simulation-loader')).toBeDefined(); + }); + + it('shows simulation elements when not loading', () => { + const simulationElements = Test Simulation; + const { getByText } = render( + , + ); + + expect(getByText('Test Simulation')).toBeDefined(); + }); +}); diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Static/Static.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Static/Static.tsx new file mode 100644 index 00000000000..febb2b71115 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Static/Static.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { StyleSheet, View } from 'react-native'; + +import { useStyles } from '../../../../../../../../../component-library/hooks'; +import InfoRow from '../../../../../UI/InfoRow'; +import InfoSection from '../../../../../UI/InfoRow/InfoSection'; +import Loader from '../../../../../../../../../component-library/components-temp/Loader'; + +const styleSheet = () => StyleSheet.create({ + base: { + display: 'flex', + justifyContent: 'space-between', + }, + loaderContainer: { + display: 'flex', + justifyContent: 'center', + }, +}); + +const StaticSimulation: React.FC<{ + title: string; + titleTooltip: string; + description?: string; + simulationElements: React.ReactNode; + isLoading?: boolean; + isCollapsed?: boolean; +}> = ({ + title, + titleTooltip, + description, + simulationElements, + isLoading, + isCollapsed = false, +}) => { + const { styles } = useStyles(styleSheet, {}); + + return( + + + + {description} + + {isLoading ? ( + + + + ) : ( + simulationElements + )} + + + ); +}; + +export default StaticSimulation; diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Static/index.ts b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Static/index.ts new file mode 100644 index 00000000000..58015012827 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/Static/index.ts @@ -0,0 +1 @@ +export { default } from './Static'; diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignDecoded/TypedSignDecoded.test.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignDecoded/TypedSignDecoded.test.tsx new file mode 100644 index 00000000000..cc9da83b409 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignDecoded/TypedSignDecoded.test.tsx @@ -0,0 +1,200 @@ +import React from 'react'; +import cloneDeep from 'lodash/cloneDeep'; +import { + DecodingDataChangeType, + DecodingDataStateChanges, + SignatureRequest, +} from '@metamask/signature-controller'; + +import { strings } from '../../../../../../../../../../locales/i18n'; +import { typedSignV4ConfirmationState } from '../../../../../../../../../util/test/confirm-data-helpers'; +import renderWithProvider from '../../../../../../../../../util/test/renderWithProvider'; +import TypedSignDecoded, { + getStateChangeToolip, + getStateChangeType, + StateChangeType, +} from './TypedSignDecoded'; + +const stateChangesApprove = [ + { + assetType: 'ERC20', + changeType: DecodingDataChangeType.Approve, + address: '0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad', + amount: '12345', + contractAddress: '0x6b175474e89094c44da98b954eedeac495271d0f', + }, +]; + +const stateChangesListingERC1155: DecodingDataStateChanges = [ + { + assetType: 'NATIVE', + changeType: DecodingDataChangeType.Receive, + address: '', + amount: '900000000000000000', + contractAddress: '', + }, + { + assetType: 'ERC1155', + changeType: DecodingDataChangeType.Listing, + address: '', + amount: '', + contractAddress: '0xafd4896984CA60d2feF66136e57f958dCe9482d5', + tokenID: '77789', + }, +]; + +const stateChangesNftListing: DecodingDataStateChanges = [ + { + assetType: 'ERC721', + changeType: DecodingDataChangeType.Listing, + address: '', + amount: '', + contractAddress: '0x922dC160f2ab743312A6bB19DD5152C1D3Ecca33', + tokenID: '22222', + }, + { + assetType: 'ERC20', + changeType: DecodingDataChangeType.Receive, + address: '', + amount: '950000000000000000', + contractAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + }, +]; + +const stateChangesNftBidding: DecodingDataStateChanges = [ + { + assetType: 'ERC20', + changeType: DecodingDataChangeType.Bidding, + address: '', + amount: '', + contractAddress: '0x922dC160f2ab743312A6bB19DD5152C1D3Ecca33', + tokenID: '189', + }, + { + assetType: 'ERC721', + changeType: DecodingDataChangeType.Receive, + address: '', + amount: '950000000000000000', + contractAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + }, +]; + +const mockState = ( + mockStateChanges: DecodingDataStateChanges, + stubDecodingLoading: boolean = false, +) => { + const clonedMockState = cloneDeep(typedSignV4ConfirmationState); + const request = clonedMockState.engine.backgroundState.SignatureController + .signatureRequests[ + 'fb2029e1-b0ab-11ef-9227-05a11087c334' + ] as SignatureRequest; + request.decodingLoading = stubDecodingLoading; + request.decodingData = { + stateChanges: mockStateChanges, + }; + + return clonedMockState; +}; + +describe('DecodedSimulation', () => { + it('renders for ERC20 approval', async () => { + const { getByText } = renderWithProvider(, { + state: mockState(stateChangesApprove), + }); + + expect(await getByText('Estimated changes')).toBeDefined(); + expect(await getByText('Spending cap')).toBeDefined(); + expect(await getByText('12,345')).toBeDefined(); + }); + + it('renders "Unlimited" for large values', async () => { + const { getByText } = renderWithProvider(, { + state: mockState([{ + ...stateChangesApprove[0], + amount: '1461501637330902918203684832716283019655932542975', + }]), + }); + + expect(await getByText('Estimated changes')).toBeDefined(); + expect(await getByText('Spending cap')).toBeDefined(); + expect(await getByText('Unlimited')).toBeDefined(); + }); + + it('renders for ERC712 token', async () => { + const { getByText } = renderWithProvider(, { + state: mockState(stateChangesNftListing), + }); + + expect(await getByText('Estimated changes')).toBeDefined(); + expect(await getByText('Listing price')).toBeDefined(); + expect(await getByText('You list')).toBeDefined(); + expect(await getByText('#22222')).toBeDefined(); + }); + + it('renders for ERC1155 token', async () => { + const { getByText } = renderWithProvider(, { + state: mockState(stateChangesListingERC1155), + }); + + expect(await getByText('Estimated changes')).toBeDefined(); + expect(await getByText('You receive')).toBeDefined(); + expect(await getByText('You list')).toBeDefined(); + expect(await getByText('#77789')).toBeDefined(); + }); + + it('renders label only once if there are multiple state changes of same changeType', async () => { + const { getAllByText } = renderWithProvider(, { + state: mockState([ + stateChangesApprove[0], + stateChangesApprove[0], + stateChangesApprove[0], + ]), + }); + + expect(await getAllByText('12,345')).toHaveLength(3); + expect(await getAllByText('Spending cap')).toHaveLength(1); + }); + + it('renders unavailable message if no state change is returned', async () => { + const { getByText } = renderWithProvider(, { + state: mockState([]), + }); + + expect(await getByText('Estimated changes')).toBeDefined(); + expect(await getByText('Unavailable')).toBeDefined(); + }); + + describe('getStateChangeToolip', () => { + it('return correct tooltip when permit is for listing NFT', () => { + const tooltip = getStateChangeToolip(StateChangeType.NFTListingReceive); + expect(tooltip).toBe( + strings('confirm.simulation.decoded_tooltip_list_nft'), + ); + }); + + it('return correct tooltip when permit is for bidding NFT', () => { + const tooltip = getStateChangeToolip(StateChangeType.NFTBiddingReceive); + expect(tooltip).toBe( + strings('confirm.simulation.decoded_tooltip_bid_nft'), + ); + }); + }); + + describe('getStateChangeType', () => { + it('return correct state change type for NFT listing receive', () => { + const stateChange = getStateChangeType( + stateChangesNftListing, + stateChangesNftListing[1], + ); + expect(stateChange).toBe(StateChangeType.NFTListingReceive); + }); + + it('return correct state change type for NFT bidding receive', () => { + const stateChange = getStateChangeType( + stateChangesNftBidding, + stateChangesNftBidding[1], + ); + expect(stateChange).toBe(StateChangeType.NFTBiddingReceive); + }); + }); +}); diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignDecoded/TypedSignDecoded.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignDecoded/TypedSignDecoded.tsx new file mode 100644 index 00000000000..2adc5d40fec --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignDecoded/TypedSignDecoded.tsx @@ -0,0 +1,212 @@ +import React, { useMemo } from 'react'; +import { View } from 'react-native'; +import { + DecodingDataChangeType, + DecodingDataStateChange, + DecodingDataStateChanges, +} from '@metamask/signature-controller'; +import { Hex } from '@metamask/utils'; + +import { TokenStandard } from '../../../../../../../../UI/SimulationDetails/types'; +import Text from '../../../../../../../../../component-library/components/Texts/Text'; +import { strings } from '../../../../../../../../../../locales/i18n'; +import { useSignatureRequest } from '../../../../../../hooks/useSignatureRequest'; +import InfoRow from '../../../../../UI/InfoRow'; +import NativeValueDisplay from '../components/NativeValueDisplay'; +import SimulationValueDisplay from '../components/ValueDisplay'; +import StaticSimulation from '../Static'; + +const styles = { + unavailableContainer: { + paddingHorizontal: 8, + paddingBottom: 8, + }, +}; + +export enum StateChangeType { + NFTListingReceive = 'NFTListingReceive', + NFTBiddingReceive = 'NFTBiddingReceive', +} + +export const getStateChangeType = ( + stateChangeList: DecodingDataStateChanges | null, + stateChange: DecodingDataStateChange, +): StateChangeType | undefined => { + if (stateChange.changeType === DecodingDataChangeType.Receive) { + if ( + stateChangeList?.some( + (change) => + change.changeType === DecodingDataChangeType.Listing && + change.assetType === TokenStandard.ERC721, + ) + ) { + return StateChangeType.NFTListingReceive; + } + if ( + stateChange.assetType === TokenStandard.ERC721 && + stateChangeList?.some( + (change) => change.changeType === DecodingDataChangeType.Bidding, + ) + ) { + return StateChangeType.NFTBiddingReceive; + } + } + return undefined; +}; + +export const getStateChangeToolip = ( + nftTransactionType: StateChangeType | undefined, +): string | undefined => { + if (nftTransactionType === StateChangeType.NFTListingReceive) { + return strings('confirm.simulation.decoded_tooltip_list_nft'); + } else if (nftTransactionType === StateChangeType.NFTBiddingReceive) { + return strings('confirm.simulation.decoded_tooltip_bid_nft'); + } + return undefined; +}; + +const stateChangeOrder = { + [DecodingDataChangeType.Transfer]: 1, + [DecodingDataChangeType.Listing]: 2, + [DecodingDataChangeType.Approve]: 3, + [DecodingDataChangeType.Revoke]: 4, + [DecodingDataChangeType.Bidding]: 5, + [DecodingDataChangeType.Receive]: 6, +}; + +const getStateChangeLabelMap = ( + changeType: string, + stateChangeType?: StateChangeType, +) => ({ + [DecodingDataChangeType.Transfer]: strings('confirm.simulation.label_change_type_transfer'), + [DecodingDataChangeType.Receive]: + stateChangeType === StateChangeType.NFTListingReceive + ? strings('confirm.simulation.label_change_type_nft_listing') + : strings('confirm.simulation.label_change_type_receive'), + [DecodingDataChangeType.Approve]: strings('confirm.simulation.label_change_type_permit'), + [DecodingDataChangeType.Revoke]: strings('confirm.simulation.label_change_type_permit'), + [DecodingDataChangeType.Bidding]: strings('confirm.simulation.label_change_type_bidding'), + [DecodingDataChangeType.Listing]: strings('confirm.simulation.label_change_type_listing'), + }[changeType]); + +const StateChangeRow = ({ + stateChangeList, + stateChange, + chainId, + shouldDisplayLabel, +}: { + stateChangeList: DecodingDataStateChanges | null; + stateChange: DecodingDataStateChange; + chainId: Hex; + shouldDisplayLabel: boolean; +}) => { + const { assetType, changeType, amount, contractAddress, tokenID } = + stateChange; + const nftTransactionType = getStateChangeType(stateChangeList, stateChange); + const tooltip = shouldDisplayLabel ? getStateChangeToolip(nftTransactionType) : undefined; + + const canDisplayValueAsUnlimited = + assetType === TokenStandard.ERC20 && + (changeType === DecodingDataChangeType.Approve || + changeType === DecodingDataChangeType.Revoke); + + const changeLabel = shouldDisplayLabel + ? getStateChangeLabelMap(changeType, nftTransactionType) + : ''; + + return ( + + {(assetType === TokenStandard.ERC20 || + assetType === TokenStandard.ERC721 || + assetType === TokenStandard.ERC1155) && ( + + )} + {assetType === 'NATIVE' && ( + + )} + + ); +}; + +const DecodedSimulation: React.FC = () => { + const signatureRequest = useSignatureRequest(); + + const chainId = signatureRequest?.chainId as Hex; + const { decodingLoading, decodingData } = signatureRequest ?? {}; + + const stateChangeFragment = useMemo(() => { + const orderedStateChanges = [...(decodingData?.stateChanges ?? [])].sort((c1, c2) => + stateChangeOrder[c1.changeType] > stateChangeOrder[c2.changeType] + ? 1 + : -1, + ); + const stateChangesGrouped: Record = ( + orderedStateChanges ?? [] + ).reduce>( + (result, stateChange) => { + result[stateChange.changeType] = [ + ...(result[stateChange.changeType] ?? []), + stateChange, + ]; + return result; + }, + {}, + ); + + return Object.entries(stateChangesGrouped).flatMap(([_, changeList]) => + changeList.map((change: DecodingDataStateChange, index: number) => ( + + )), + ); + }, [chainId, decodingData?.stateChanges]); + + return ( + + {strings('confirm.simulation.unavailable')} + + ) + } + isLoading={decodingLoading} + isCollapsed={decodingLoading || !stateChangeFragment.length} + /> + ); +}; + +export default DecodedSimulation; diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignDecoded/index.ts b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignDecoded/index.ts new file mode 100644 index 00000000000..f4bd7dcc026 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignDecoded/index.ts @@ -0,0 +1 @@ +export { default } from './TypedSignDecoded'; diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignPermit/TypedSignPermit.test.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignPermit/TypedSignPermit.test.tsx index 2b814f9629b..e7ff8ddf91d 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignPermit/TypedSignPermit.test.tsx +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/TypedSignPermit/TypedSignPermit.test.tsx @@ -19,7 +19,11 @@ describe('PermitSimulation', () => { }); expect(getByText('Estimated changes')).toBeDefined(); - expect(getByText('You’re giving the spender permission to spend this many tokens from your account.')).toBeDefined(); + expect( + getByText( + "You're giving the spender permission to spend this many tokens from your account.", + ), + ).toBeDefined(); expect(getByText('Spending cap')).toBeDefined(); expect(getByText('3,000')).toBeDefined(); expect(getByText('0xCcCCc...ccccC')).toBeDefined(); diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/NativeValueDisplay/NativeValueDisplay.test.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/NativeValueDisplay/NativeValueDisplay.test.tsx new file mode 100644 index 00000000000..f0ab367e2ee --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/NativeValueDisplay/NativeValueDisplay.test.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import NativeValueDisplay from './NativeValueDisplay'; +import { backgroundState } from '../../../../../../../../../../util/test/initial-root-state'; +import renderWithProvider from '../../../../../../../../../../util/test/renderWithProvider'; +import { fireEvent } from '@testing-library/react-native'; + +const mockInitialState = { + engine: { + backgroundState, + }, +}; + +describe('NativeValueDisplay', () => { + it('renders component correctly', async () => { + const { findByText } = renderWithProvider( + , + { state: mockInitialState }, + ); + + expect(await findByText('< 0.000001')).toBeDefined(); + expect(await findByText('ETH')).toBeDefined(); + }); + + it('displays modal when clicking on the value', async () => { + const { findByText } = renderWithProvider( + , + { state: mockInitialState }, + ); + + const button = await findByText('< 0.000001'); + fireEvent.press(button); + + expect(await findByText('Spending Cap')).toBeDefined(); + }); +}); diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/NativeValueDisplay/NativeValueDisplay.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/NativeValueDisplay/NativeValueDisplay.tsx new file mode 100644 index 00000000000..54ad3cd28e8 --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/NativeValueDisplay/NativeValueDisplay.tsx @@ -0,0 +1,153 @@ +import React, { useState } from 'react'; +import { Text,TouchableOpacity, View } from 'react-native'; +import { useSelector } from 'react-redux'; +import { Hex } from '@metamask/utils'; +import { BigNumber } from 'bignumber.js'; + +import { RootState } from '../../../../../../../../../../reducers'; +import { selectConversionRateByChainId } from '../../../../../../../../../../selectors/currencyRateController'; +import { useTheme } from '../../../../../../../../../../util/theme'; + +import ButtonPill from '../../../../../../../../../../component-library/components-temp/Buttons/ButtonPill/ButtonPill'; +import { ButtonIconSizes } from '../../../../../../../../../../component-library/components/Buttons/ButtonIcon/ButtonIcon.types'; +import ButtonIcon from '../../../../../../../../../../component-library/components/Buttons/ButtonIcon/ButtonIcon'; +import { IconName , IconColor } from '../../../../../../../../../../component-library/components/Icons/Icon'; + +import AssetPill from '../../../../../../../../../UI/SimulationDetails/AssetPill/AssetPill'; +import { IndividualFiatDisplay } from '../../../../../../../../../UI/SimulationDetails/FiatDisplay/FiatDisplay'; +import { + formatAmount, + formatAmountMaxPrecision, +} from '../../../../../../../../../UI/SimulationDetails/formatAmount'; +import { AssetType } from '../../../../../../../../../UI/SimulationDetails/types'; +import { shortenString } from '../../../../../../../../../../util/notifications/methods/common'; +import { isNumberValue } from '../../../../../../../../../../util/number'; +import { calcTokenAmount } from '../../../../../../../../../../util/transactions'; +import BottomModal from '../../../../../../UI/BottomModal'; + +/** + * Reusing ValueDisplay styles for now. See issue to handle abstracting UI + * @see {@link https://github.com/MetaMask/metamask-mobile/issues/12974} + */ +import styleSheet from '../ValueDisplay/ValueDisplay.styles'; + +const NATIVE_DECIMALS = 18; + +interface PermitSimulationValueDisplayParams { + /** ID of the associated chain. */ + chainId: Hex; + + /** Change type to be displayed in value tooltip */ + labelChangeType: string; + + /** The token amount */ + value: number | string; + + /** True if value is being credited to wallet */ + credit?: boolean; + + /** True if value is being debited to wallet */ + debit?: boolean; +} + +const NativeValueDisplay: React.FC = ({ + chainId, + credit, + debit, + labelChangeType, + value, +}) => { + const [hasValueModalOpen, setHasValueModalOpen] = useState(false); + + const { colors } = useTheme(); + const styles = styleSheet(colors); + + const conversionRate = useSelector((state: RootState) => + selectConversionRateByChainId(state, chainId), + ); + + const tokenAmount = isNumberValue(value) ? calcTokenAmount(value, NATIVE_DECIMALS) : null; + const isValidTokenAmount = tokenAmount !== null && tokenAmount !== undefined && tokenAmount instanceof BigNumber; + + const fiatValue = isValidTokenAmount && conversionRate + ? tokenAmount.times(String(conversionRate)).toNumber() + : undefined; + + const tokenValue = isValidTokenAmount ? formatAmount('en-US', tokenAmount) : null; + const tokenValueMaxPrecision = isValidTokenAmount ? formatAmountMaxPrecision('en-US', tokenAmount) : null; + + function handlePressTokenValue() { + setHasValueModalOpen(true); + } + + return ( + + + + + + {credit && '+ '} + {debit && '- '} + {tokenValue !== null && + shortenString(tokenValue || '', { + truncatedCharLimit: 15, + truncatedStartChars: 15, + truncatedEndChars: 0, + skipCharacterInEnd: true, + })} + + + + + + + + + {/** + TODO - add fiat shorten prop after tooltip logic has been updated + {@see {@link https://github.com/MetaMask/metamask-mobile/issues/12656} + */} + {fiatValue !== undefined && ( + + )} + + {hasValueModalOpen && ( + /** + * TODO replace BottomModal instances with BottomSheet + * {@see {@link https://github.com/MetaMask/metamask-mobile/issues/12656}} + */ + setHasValueModalOpen(false)}> + setHasValueModalOpen(false)} + > + + + setHasValueModalOpen(false)} + iconName={IconName.ArrowLeft} + /> + + {labelChangeType} + + + + {tokenValueMaxPrecision} + + + + + )} + + ); +}; + +export default NativeValueDisplay; diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/NativeValueDisplay/index.ts b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/NativeValueDisplay/index.ts new file mode 100644 index 00000000000..1e6534cfb1c --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/NativeValueDisplay/index.ts @@ -0,0 +1 @@ +export { default } from './NativeValueDisplay'; diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/ValueDisplay.styles.ts b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/ValueDisplay.styles.ts index 6414e76e0ba..9a1da0579cc 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/ValueDisplay.styles.ts +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/ValueDisplay.styles.ts @@ -12,7 +12,9 @@ const styleSheet = (colors: Theme['colors']) => borderWidth: 0, padding: 0, }, - + fiatDisplay: { + paddingEnd: 8, + }, flexRowTokenValueAndAddress: { display: 'flex', flexDirection: 'row', @@ -22,7 +24,7 @@ const styleSheet = (colors: Theme['colors']) => borderWidth: 0, padding: 0, }, - tokenAddress: { + marginStart4: { marginStart: 4, }, tokenValueTooltipContent: { @@ -35,7 +37,6 @@ const styleSheet = (colors: Theme['colors']) => valueAndAddress: { paddingVertical: 4, paddingLeft: 8, - paddingRight: 8, gap: 5, flexDirection: 'row', alignItems: 'center', diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/ValueDisplay.test.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/ValueDisplay.test.tsx index 8decf038ea1..8cf784909dc 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/ValueDisplay.test.tsx +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/ValueDisplay.test.tsx @@ -1,4 +1,3 @@ - import React from 'react'; import { act } from '@testing-library/react-native'; import SimulationValueDisplay from './ValueDisplay'; @@ -23,10 +22,11 @@ const mockTrackEvent = jest.fn(); jest.mock('../../../../../../../../../hooks/useMetrics'); jest.mock('../../../../../../../hooks/useGetTokenStandardAndDetails'); - jest.mock('../../../../../../../../../../util/address', () => ({ getTokenDetails: jest.fn(), - renderShortAddress: jest.requireActual('../../../../../../../../../../util/address').renderShortAddress + renderShortAddress: jest.requireActual( + '../../../../../../../../../../util/address', + ).renderShortAddress, })); describe('SimulationValueDisplay', () => { @@ -54,7 +54,11 @@ describe('SimulationValueDisplay', () => { }); it('renders component correctly', async () => { - (useGetTokenStandardAndDetails as jest.MockedFn).mockReturnValue({ + ( + useGetTokenStandardAndDetails as jest.MockedFn< + typeof useGetTokenStandardAndDetails + > + ).mockReturnValue({ symbol: 'TST', decimals: '4', balance: undefined, @@ -72,15 +76,38 @@ describe('SimulationValueDisplay', () => { { state: mockInitialState }, ); - await act(async () => { - await Promise.resolve(); + expect(await findByText('0.432')).toBeDefined(); + }); + + it('renders "Unlimited" for large values when canDisplayValueAsUnlimited is true', async () => { + (useGetTokenStandardAndDetails as jest.MockedFn).mockReturnValue({ + symbol: 'TST', + decimals: '4', + balance: undefined, + standard: TokenStandard.ERC20, + decimalsNumber: 4, }); - expect(await findByText('0.432')).toBeDefined(); + const { findByText } = renderWithProvider( + , + { state: mockInitialState }, + ); + + expect(await findByText('Unlimited')).toBeDefined(); }); it('should invoke method to track missing decimal information for ERC20 tokens only once', async () => { - (useGetTokenStandardAndDetails as jest.MockedFn).mockReturnValue({ + ( + useGetTokenStandardAndDetails as jest.MockedFn< + typeof useGetTokenStandardAndDetails + > + ).mockReturnValue({ symbol: 'TST', decimals: undefined, balance: undefined, @@ -98,15 +125,15 @@ describe('SimulationValueDisplay', () => { { state: mockInitialState }, ); - await act(async () => { - await Promise.resolve(); - }); - expect(mockTrackEvent).toHaveBeenCalledTimes(1); }); it('should not invoke method to track missing decimal information for ERC20 tokens', async () => { - (useGetTokenStandardAndDetails as jest.MockedFn).mockReturnValue({ + ( + useGetTokenStandardAndDetails as jest.MockedFn< + typeof useGetTokenStandardAndDetails + > + ).mockReturnValue({ symbol: 'TST', decimals: '4', balance: undefined, @@ -124,20 +151,16 @@ describe('SimulationValueDisplay', () => { { state: mockInitialState }, ); - await act(async () => { - await Promise.resolve(); - }); - expect(mockTrackEvent).not.toHaveBeenCalled(); }); describe('when token is an ERC721 token', () => { beforeEach(() => { jest.mocked(getTokenDetails).mockResolvedValue({ - name: 'TST', - symbol: 'TST', - standard: TokenStandard.ERC721, - }); + name: 'TST', + symbol: 'TST', + standard: TokenStandard.ERC721, + }); }); it('should not invoke method to track missing decimal information', async () => { diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/ValueDisplay.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/ValueDisplay.tsx index e59dcf7d43e..dce63ce66a5 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/ValueDisplay.tsx +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/components/ValueDisplay/ValueDisplay.tsx @@ -1,8 +1,9 @@ -import React, { useMemo, useState } from 'react'; +import React, { useState } from 'react'; import { TouchableOpacity, View } from 'react-native'; import { useSelector } from 'react-redux'; import { NetworkClientId } from '@metamask/network-controller'; import { Hex } from '@metamask/utils'; +import { BigNumber } from 'bignumber.js'; import ButtonPill from '../../../../../../../../../../component-library/components-temp/Buttons/ButtonPill/ButtonPill'; import { ButtonIconSizes } from '../../../../../../../../../../component-library/components/Buttons/ButtonIcon/ButtonIcon.types'; @@ -16,22 +17,24 @@ import { formatAmountMaxPrecision, } from '../../../../../../../../../UI/SimulationDetails/formatAmount'; -import Box from '../../../../../../../../../UI/Ramp/components/Box'; import Address from '../../../../../../UI/InfoRow/InfoValue/Address/Address'; import { selectContractExchangeRates } from '../../../../../../../../../../selectors/tokenRatesController'; import Logger from '../../../../../../../../../../util/Logger'; import { shortenString } from '../../../../../../../../../../util/notifications/methods/common'; +import { isNumberValue } from '../../../../../../../../../../util/number'; import { useTheme } from '../../../../../../../../../../util/theme'; import { calcTokenAmount } from '../../../../../../../../../../util/transactions'; import useGetTokenStandardAndDetails from '../../../../../../../hooks/useGetTokenStandardAndDetails'; import useTrackERC20WithoutDecimalInformation from '../../../../../../../hooks/useTrackERC20WithoutDecimalInformation'; +import { TOKEN_VALUE_UNLIMITED_THRESHOLD } from '../../../../../../../utils/confirm'; import { TokenDetailsERC20 } from '../../../../../../../utils/token'; import BottomModal from '../../../../../../UI/BottomModal'; import styleSheet from './ValueDisplay.styles'; +import { strings } from '../../../../../../../../../../../locales/i18n'; interface SimulationValueDisplayParams { /** ID of the associated chain. */ @@ -52,6 +55,9 @@ interface SimulationValueDisplayParams { // Optional + /** Whether a large amount can be substituted by "Unlimited" */ + canDisplayValueAsUnlimited?: boolean; + /** True if value is being credited to wallet */ credit?: boolean; @@ -80,6 +86,7 @@ const SimulationValueDisplay: React.FC< value, credit, debit, + canDisplayValueAsUnlimited = false, }) => { const [hasValueModalOpen, setHasValueModalOpen] = useState(false); @@ -101,26 +108,18 @@ const SimulationValueDisplay: React.FC< tokenDetails as TokenDetailsERC20, ); - const fiatValue = useMemo(() => { - if (exchangeRate && value && !tokenId) { - const tokenAmount = calcTokenAmount(value, tokenDecimals); - return tokenAmount.multipliedBy(exchangeRate).toNumber(); - } - return undefined; - }, [exchangeRate, tokenDecimals, tokenId, value]); + const tokenAmount = isNumberValue(value) && !tokenId ? calcTokenAmount(value as number | string, tokenDecimals) : null; + const isValidTokenAmount = tokenAmount !== null && tokenAmount !== undefined && tokenAmount instanceof BigNumber; - const { tokenValue, tokenValueMaxPrecision } = useMemo(() => { - if (!value || tokenId) { - return { tokenValue: null, tokenValueMaxPrecision: null }; - } + const fiatValue = isValidTokenAmount && exchangeRate && !tokenId + ? tokenAmount.multipliedBy(exchangeRate).toNumber() + : undefined; - const tokenAmount = calcTokenAmount(value, tokenDecimals); + const tokenValue = isValidTokenAmount ? formatAmount('en-US', tokenAmount) : null; + const tokenValueMaxPrecision = isValidTokenAmount ? formatAmountMaxPrecision('en-US', tokenAmount) : null; - return { - tokenValue: formatAmount('en-US', tokenAmount), - tokenValueMaxPrecision: formatAmountMaxPrecision('en-US', tokenAmount), - }; - }, [tokenDecimals, tokenId, value]); + const shouldShowUnlimitedValue = canDisplayValueAsUnlimited && + Number(value) > TOKEN_VALUE_UNLIMITED_THRESHOLD; /** Temporary error capturing as we are building out Permit Simulations */ if (!tokenContract) { @@ -137,10 +136,11 @@ const SimulationValueDisplay: React.FC< } return ( - - + + {credit && '+ '} {debit && '- '} - {tokenValue !== null && - shortenString(tokenValue || '', { + {shouldShowUnlimitedValue + ? strings('confirm.unlimited') + : tokenValue !== null && + shortenString(tokenValue || '', { truncatedCharLimit: 15, truncatedStartChars: 15, truncatedEndChars: 0, @@ -159,18 +161,18 @@ const SimulationValueDisplay: React.FC< {tokenId && `#${tokenId}`} - +
- + - - - {/* + + + {/** TODO - add fiat shorten prop after tooltip logic has been updated {@see {@link https://github.com/MetaMask/metamask-mobile/issues/12656} */} {fiatValue && } - + {hasValueModalOpen && ( /** * TODO replace BottomModal instances with BottomSheet @@ -201,7 +203,7 @@ const SimulationValueDisplay: React.FC< )} - + ); }; diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/index.ts b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/index.ts new file mode 100644 index 00000000000..50cee91255f --- /dev/null +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/Simulation/index.ts @@ -0,0 +1 @@ +export { default } from './Simulation'; diff --git a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.tsx b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.tsx index 57ba7552428..2caee661061 100644 --- a/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.tsx +++ b/app/components/Views/confirmations/components/Confirm/Info/TypedSignV3V4/TypedSignV3V4.tsx @@ -1,31 +1,14 @@ import React from 'react'; -import { useSelector } from 'react-redux'; -import { selectUseTransactionSimulations } from '../../../../../../../selectors/preferencesController'; -import useApprovalRequest from '../../../../hooks/useApprovalRequest'; -import { isRecognizedPermit } from '../../../../utils/signature'; import InfoRowOrigin from '../Shared/InfoRowOrigin'; -import PermitSimulation from './Simulation/TypedSignPermit'; import Message from './Message'; +import TypedSignV3V4Simulation from './Simulation'; -const TypedSignV3V4 = () => { - const { approvalRequest } = useApprovalRequest(); - const useSimulation = useSelector( - selectUseTransactionSimulations, - ); - - if (!approvalRequest) { - return null; - } - - const isPermit = isRecognizedPermit(approvalRequest); - - return ( +const TypedSignV3V4 = () => ( <> - {isPermit && useSimulation && } + ); -}; export default TypedSignV3V4; diff --git a/app/components/Views/confirmations/components/Confirm/NoChangeSimulation/NoChangeSimulation.test.tsx b/app/components/Views/confirmations/components/Confirm/NoChangeSimulation/NoChangeSimulation.test.tsx index 0f11ada192e..838361aee88 100644 --- a/app/components/Views/confirmations/components/Confirm/NoChangeSimulation/NoChangeSimulation.test.tsx +++ b/app/components/Views/confirmations/components/Confirm/NoChangeSimulation/NoChangeSimulation.test.tsx @@ -12,7 +12,7 @@ describe('NoChangeSimulation', () => { expect(getByText('Estimated changes')).toBeDefined(); expect( getByText( - 'You’re signing into a site and there are no predicted changes to your account.', + "You're signing into a site and there are no predicted changes to your account." ), ).toBeDefined(); }); diff --git a/app/components/Views/confirmations/components/SignatureRequest/Root/Root.tsx b/app/components/Views/confirmations/components/SignatureRequest/Root/Root.tsx index f610b170a57..9e8bb043755 100644 --- a/app/components/Views/confirmations/components/SignatureRequest/Root/Root.tsx +++ b/app/components/Views/confirmations/components/SignatureRequest/Root/Root.tsx @@ -6,7 +6,7 @@ import { useSelector } from 'react-redux'; import setSignatureRequestSecurityAlertResponse from '../../../../../../actions/signatureRequest'; import { store } from '../../../../../../store'; import { useTheme } from '../../../../../../util/theme'; -import useConfirmationRedesignEnabled from '../../../hooks/useConfirmationRedesignEnabled'; +import { useConfirmationRedesignEnabled } from '../../../hooks/useConfirmationRedesignEnabled'; import PersonalSign from '../../PersonalSign'; import TypedSign from '../../TypedSign'; import { MessageParams } from '../types'; diff --git a/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.tsx b/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.tsx index 0417a1c9ce2..4c738c73648 100644 --- a/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.tsx +++ b/app/components/Views/confirmations/components/UI/ExpandableSection/ExpandableSection.tsx @@ -61,7 +61,7 @@ const ExpandableSection = ({ {expanded && ( - + setExpanded(false)} canCloseOnBackdropClick> ({ getTotalFiatAccountBalance: () => ({ tokenFiat: 10 }), diff --git a/app/components/Views/confirmations/hooks/useConfirmationRedesignEnabled.ts b/app/components/Views/confirmations/hooks/useConfirmationRedesignEnabled.ts index 43ec8489268..9ca0e9919cc 100644 --- a/app/components/Views/confirmations/hooks/useConfirmationRedesignEnabled.ts +++ b/app/components/Views/confirmations/hooks/useConfirmationRedesignEnabled.ts @@ -7,7 +7,7 @@ import { selectRemoteFeatureFlags } from '../../../../selectors/featureFlagContr import useApprovalRequest from './useApprovalRequest'; import useQRHardwareAwareness from './useQRHardwareAwareness'; -const useConfirmationRedesignEnabled = () => { +export const useConfirmationRedesignEnabled = () => { const { approvalRequest } = useApprovalRequest(); const { isSigningQRObject, isSyncingQRHardware } = useQRHardwareAwareness(); const { confirmation_redesign } = useSelector(selectRemoteFeatureFlags); @@ -42,5 +42,3 @@ const useConfirmationRedesignEnabled = () => { return { isRedesignedEnabled }; }; - -export default useConfirmationRedesignEnabled; diff --git a/app/components/Views/confirmations/hooks/useTypedSignSimulationEnabled.ts b/app/components/Views/confirmations/hooks/useTypedSignSimulationEnabled.ts new file mode 100644 index 00000000000..0553f8fcae2 --- /dev/null +++ b/app/components/Views/confirmations/hooks/useTypedSignSimulationEnabled.ts @@ -0,0 +1,66 @@ +import { useSelector } from 'react-redux'; +import { SignTypedDataVersion } from '@metamask/eth-sig-util'; +import { MessageParamsTyped, SignatureRequest, SignatureRequestType } from '@metamask/signature-controller'; +import { selectUseTransactionSimulations } from '../../../../selectors/preferencesController'; +import { isRecognizedPermit, parseTypedDataMessage } from '../utils/signature'; +import { useSignatureRequest } from './useSignatureRequest'; + +const NON_PERMIT_SUPPORTED_TYPES_SIGNS = [ + { + domainName: 'Seaport', + primaryTypeList: ['BulkOrder'], + versionList: ['1.4', '1.5', '1.6'], + }, + { + domainName: 'Seaport', + primaryTypeList: ['OrderComponents'], + }, +]; + +const isNonPermitSupportedByDecodingAPI = ( + signatureRequest: SignatureRequest, +) => { + const data = signatureRequest.messageParams?.data as string; + if (!data) { return false; } + + const { + domain: { name, version }, + primaryType, + } = parseTypedDataMessage(data); + + return NON_PERMIT_SUPPORTED_TYPES_SIGNS.some( + ({ domainName, primaryTypeList, versionList }) => + name === domainName && + primaryTypeList.includes(primaryType) && + (!versionList || versionList.includes(version)), + ); +}; + +export function useTypedSignSimulationEnabled() { + const signatureRequest = useSignatureRequest(); + const useTransactionSimulations = useSelector( + selectUseTransactionSimulations, + ); + + if (!signatureRequest) { + return undefined; + } + + const requestType = signatureRequest.type; + const signatureMethod = (signatureRequest.messageParams as MessageParamsTyped)?.version; + + const isTypedSignV3V4 = requestType === SignatureRequestType.TypedSign && ( + signatureMethod === SignTypedDataVersion.V3 || + signatureMethod === SignTypedDataVersion.V4 + ); + const isPermit = isRecognizedPermit(signatureRequest); + + const nonPermitSupportedByDecodingAPI: boolean = + isTypedSignV3V4 && isNonPermitSupportedByDecodingAPI(signatureRequest); + + return ( + useTransactionSimulations && + isTypedSignV3V4 && + (isPermit || nonPermitSupportedByDecodingAPI) + ); +} diff --git a/app/components/Views/confirmations/utils/confirm.ts b/app/components/Views/confirmations/utils/confirm.ts index 6f9b61bdf5c..f4e414f1583 100644 --- a/app/components/Views/confirmations/utils/confirm.ts +++ b/app/components/Views/confirmations/utils/confirm.ts @@ -1,8 +1,11 @@ import { ApprovalTypes } from '../../../../core/RPCMethods/RPCMethodMiddleware'; +export const TOKEN_VALUE_UNLIMITED_THRESHOLD = 10 ** 15; + export function isSignatureRequest(requestType: string) { return [ ApprovalTypes.PERSONAL_SIGN, ApprovalTypes.ETH_SIGN_TYPED_DATA, ].includes(requestType as ApprovalTypes); } + diff --git a/app/components/Views/confirmations/utils/signature.test.ts b/app/components/Views/confirmations/utils/signature.test.ts index 13be1389591..08472ef3f27 100644 --- a/app/components/Views/confirmations/utils/signature.test.ts +++ b/app/components/Views/confirmations/utils/signature.test.ts @@ -1,6 +1,6 @@ -import { ApprovalRequest } from '@metamask/approval-controller'; import { parseTypedDataMessage, isRecognizedPermit } from './signature'; import { PRIMARY_TYPES_PERMIT } from '../constants/signatures'; +import { SignatureRequest } from '@metamask/signature-controller'; describe('Signature Utils', () => { describe('parseTypedDataMessage', () => { @@ -46,25 +46,25 @@ describe('Signature Utils', () => { describe('isRecognizedPermit', () => { it('should return true for recognized permit types', () => { - const mockRequest: ApprovalRequest<{ data: string }> = { - requestData: { + const mockRequest: SignatureRequest = { + messageParams: { data: JSON.stringify({ primaryType: PRIMARY_TYPES_PERMIT[0] }) } - } as ApprovalRequest<{ data: string }>; + } as SignatureRequest; expect(isRecognizedPermit(mockRequest)).toBe(true); }); it('should return false for unrecognized permit types', () => { - const mockRequest: ApprovalRequest<{ data: string }> = { - requestData: { + const mockRequest: SignatureRequest = { + messageParams: { data: JSON.stringify({ primaryType: 'UnrecognizedType' }) } - } as ApprovalRequest<{ data: string }>; + } as SignatureRequest; expect(isRecognizedPermit(mockRequest)).toBe(false); }); diff --git a/app/components/Views/confirmations/utils/signature.ts b/app/components/Views/confirmations/utils/signature.ts index 73c19e2b9b7..9035a2b3087 100644 --- a/app/components/Views/confirmations/utils/signature.ts +++ b/app/components/Views/confirmations/utils/signature.ts @@ -1,4 +1,4 @@ -import { ApprovalRequest } from '@metamask/approval-controller'; +import { SignatureRequest } from '@metamask/signature-controller'; import { PRIMARY_TYPES_PERMIT } from '../constants/signatures'; /** @@ -47,9 +47,15 @@ export const parseTypedDataMessage = (dataToParse: string) => { /** * Returns true if the request is a recognized Permit Typed Sign signature request * - * @param request - The confirmation request to check + * @param request - The signature request to check */ -export const isRecognizedPermit = (approvalRequest: ApprovalRequest<{ data: string }>) => { - const { primaryType } = parseTypedDataMessage(approvalRequest.requestData.data); +export const isRecognizedPermit = (request: SignatureRequest) => { + if (!request) { + return false; + } + + const data = (request as SignatureRequest).messageParams?.data as string; + + const { primaryType } = parseTypedDataMessage(data); return PRIMARY_TYPES_PERMIT.includes(primaryType); }; diff --git a/app/components/hooks/useAccounts/useAccounts.ts b/app/components/hooks/useAccounts/useAccounts.ts index 3f52f3ab50a..4c354d5f612 100644 --- a/app/components/hooks/useAccounts/useAccounts.ts +++ b/app/components/hooks/useAccounts/useAccounts.ts @@ -31,7 +31,7 @@ import { UseAccounts, UseAccountsParams, } from './useAccounts.types'; -import { InternalAccount } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import { getChainIdsToPoll } from '../../../selectors/tokensController'; import { useGetFormattedTokensPerChain } from '../useGetFormattedTokensPerChain'; import { useGetTotalFiatBalanceCrossChains } from '../useGetTotalFiatBalanceCrossChains'; diff --git a/app/components/hooks/useAccounts/utils.ts b/app/components/hooks/useAccounts/utils.ts index 248f9d8d704..438c4166dc7 100644 --- a/app/components/hooks/useAccounts/utils.ts +++ b/app/components/hooks/useAccounts/utils.ts @@ -1,4 +1,4 @@ -import { InternalAccount } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import { getFormattedAddressFromInternalAccount } from '../../../core/Multichain/utils'; import { BigNumber } from 'ethers'; import { diff --git a/app/components/hooks/useGetFormattedTokensPerChain.test.ts b/app/components/hooks/useGetFormattedTokensPerChain.test.ts index cac00b8f731..6192d713c59 100644 --- a/app/components/hooks/useGetFormattedTokensPerChain.test.ts +++ b/app/components/hooks/useGetFormattedTokensPerChain.test.ts @@ -5,7 +5,7 @@ import { import { backgroundState } from '../../util/test/initial-root-state'; import { RootState } from '../../reducers'; import { useGetFormattedTokensPerChain } from './useGetFormattedTokensPerChain'; -import { InternalAccount } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; const mockInitialState: DeepPartial = { settings: {}, diff --git a/app/components/hooks/useGetFormattedTokensPerChain.tsx b/app/components/hooks/useGetFormattedTokensPerChain.tsx index 526fd6874fe..c7c94472c72 100644 --- a/app/components/hooks/useGetFormattedTokensPerChain.tsx +++ b/app/components/hooks/useGetFormattedTokensPerChain.tsx @@ -16,7 +16,7 @@ import { selectCurrentCurrency, } from '../../selectors/currencyRateController'; import { MarketDataDetails, Token } from '@metamask/assets-controllers'; -import { InternalAccount } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import { isTestNet } from '../../util/networks'; import { selectShowFiatInTestnets } from '../../selectors/settings'; diff --git a/app/components/hooks/useGetTotalFiatBalanceCrossChains.test.ts b/app/components/hooks/useGetTotalFiatBalanceCrossChains.test.ts index a0a8383ca4a..8f014eaf5ac 100644 --- a/app/components/hooks/useGetTotalFiatBalanceCrossChains.test.ts +++ b/app/components/hooks/useGetTotalFiatBalanceCrossChains.test.ts @@ -5,7 +5,7 @@ import { import { backgroundState } from '../../util/test/initial-root-state'; import { RootState } from '../../reducers'; import { useGetTotalFiatBalanceCrossChains } from './useGetTotalFiatBalanceCrossChains'; -import { InternalAccount } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; const mockInitialState: DeepPartial = { settings: {}, diff --git a/app/components/hooks/useGetTotalFiatBalanceCrossChains.tsx b/app/components/hooks/useGetTotalFiatBalanceCrossChains.tsx index 624b6571164..8275aacab63 100644 --- a/app/components/hooks/useGetTotalFiatBalanceCrossChains.tsx +++ b/app/components/hooks/useGetTotalFiatBalanceCrossChains.tsx @@ -11,7 +11,7 @@ import { selectCurrencyRates, selectCurrentCurrency, } from '../../selectors/currencyRateController'; -import { InternalAccount } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import { selectShowFiatInTestnets } from '../../selectors/settings'; import { isTestNet } from '../../util/networks'; diff --git a/app/core/AppConstants.ts b/app/core/AppConstants.ts index 7d132663576..7f48671fa74 100644 --- a/app/core/AppConstants.ts +++ b/app/core/AppConstants.ts @@ -137,6 +137,7 @@ export default { 'https://support.metamask.io/transactions-and-gas/transactions/smart-transactions/', STAKING_RISK_DISCLOSURE: 'https://consensys.io/staking-risk-disclosures', }, + DECODING_API_URL: process.env.DECODING_API_URL || 'https://signature-insights.api.cx.metamask.io/v1', ERRORS: { INFURA_BLOCKED_MESSAGE: 'EthQuery - RPC Error - This service is not available in your country', diff --git a/app/core/Engine/Engine.ts b/app/core/Engine/Engine.ts index a0fe49693c3..740c50e7b45 100644 --- a/app/core/Engine/Engine.ts +++ b/app/core/Engine/Engine.ts @@ -1441,6 +1441,10 @@ export class Engine { }), // This casting expected due to mismatch of browser and react-native version of Sentry traceContext trace: trace as unknown as SignatureControllerOptions['trace'], + decodingApiUrl: AppConstants.DECODING_API_URL, + // TODO: check preferences useExternalServices + isDecodeSignatureRequestEnabled: () => + preferencesController.state.useTransactionSimulations, }), LoggingController: loggingController, ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) diff --git a/app/core/Multichain/test/utils.test.ts b/app/core/Multichain/test/utils.test.ts index d4cb0d7ce49..0553a6ae20d 100644 --- a/app/core/Multichain/test/utils.test.ts +++ b/app/core/Multichain/test/utils.test.ts @@ -1,10 +1,12 @@ import { - InternalAccount, EthAccountType, BtcAccountType, EthMethod, BtcMethod, + EthScopes, + BtcScopes, } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import { isEthAccount, isBtcAccount, @@ -28,6 +30,7 @@ const SOL_ADDRESSES = '7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV'; const mockEthEOAAccount: InternalAccount = { address: MOCK_ETH_ADDRESS, id: '1', + scopes: [EthScopes.Namespace], metadata: { name: 'Eth Account 1', importTime: 1684232000456, @@ -49,6 +52,7 @@ const mockEthEOAAccount: InternalAccount = { const mockEthERC4337Account: InternalAccount = { address: '0xC4966c0D659D99699BFD7EB54D8fafEE40e4a756', id: '1', + scopes: [EthScopes.Namespace], metadata: { name: 'Eth Account ERC4337 1', importTime: 1684232000456, @@ -70,6 +74,7 @@ const mockEthERC4337Account: InternalAccount = { const mockBTCAccount: InternalAccount = { address: MOCK_BTC_MAINNET_ADDRESS, id: '1', + scopes: [BtcScopes.Namespace], metadata: { name: 'Bitcoin Account', importTime: 1684232000456, diff --git a/app/core/Multichain/utils.ts b/app/core/Multichain/utils.ts index e963633786a..b853c207f97 100644 --- a/app/core/Multichain/utils.ts +++ b/app/core/Multichain/utils.ts @@ -1,6 +1,6 @@ import { toChecksumHexAddress } from '@metamask/controller-utils'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import { - InternalAccount, EthAccountType, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) BtcAccountType, diff --git a/app/core/SnapKeyring/BitcoinWalletSnap.ts b/app/core/SnapKeyring/BitcoinWalletSnap.ts index e49890aacf7..b3da42ea99f 100644 --- a/app/core/SnapKeyring/BitcoinWalletSnap.ts +++ b/app/core/SnapKeyring/BitcoinWalletSnap.ts @@ -1,5 +1,5 @@ import { SnapId } from '@metamask/snaps-sdk'; -import { Sender } from '@metamask/keyring-api'; +import { Sender } from '@metamask/keyring-snap-client'; import { HandlerType } from '@metamask/snaps-utils'; import { Json, JsonRpcRequest } from '@metamask/utils'; // This dependency is still installed as part of the `package.json`, however diff --git a/app/core/SnapKeyring/SnapKeyring.test.ts b/app/core/SnapKeyring/SnapKeyring.test.ts index 0e8a181830b..594e7efa483 100644 --- a/app/core/SnapKeyring/SnapKeyring.test.ts +++ b/app/core/SnapKeyring/SnapKeyring.test.ts @@ -1,9 +1,6 @@ import { ControllerMessenger } from '@metamask/base-controller'; -import { - EthAccountType, - InternalAccount, - KeyringEvent, -} from '@metamask/keyring-api'; +import { EthAccountType, EthScopes, KeyringEvent } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import { snapKeyringBuilder } from './SnapKeyring'; import { SnapKeyringBuilderAllowActions, @@ -28,6 +25,7 @@ const mockSetAccountName = jest.fn(); const mockFlowId = '123'; const address = '0x2a4d4b667D5f12C3F9Bf8F14a7B9f8D8d9b8c8fA'; const accountNameSuggestion = 'Suggested Account Name'; + const mockAccount = { type: EthAccountType.Eoa, id: '3afa663e-0600-4d93-868a-61c2e553013b', @@ -35,8 +33,9 @@ const mockAccount = { methods: [], options: {}, }; -const mockInternalAccount = { +const mockInternalAccount: InternalAccount = { ...mockAccount, + scopes: [EthScopes.Namespace], metadata: { snap: { enabled: true, diff --git a/app/reducers/transaction/index.js b/app/reducers/transaction/index.js index 5d1ab2efb94..3b37c620150 100644 --- a/app/reducers/transaction/index.js +++ b/app/reducers/transaction/index.js @@ -110,6 +110,8 @@ const transactionReducer = (state = initialState, action) => { ...getTxData(action.transaction), }, ...txMeta, + // Retain the securityAlertResponses from the old state + securityAlertResponses: state.securityAlertResponses, }; } case 'SET_TOKENS_TRANSACTION': { diff --git a/app/selectors/accountsController.test.ts b/app/selectors/accountsController.test.ts index 5b6e9dd0231..92598446eef 100644 --- a/app/selectors/accountsController.test.ts +++ b/app/selectors/accountsController.test.ts @@ -1,7 +1,8 @@ import { AccountsControllerState } from '@metamask/accounts-controller'; import { captureException } from '@sentry/react-native'; import { Hex, isValidChecksumAddress } from '@metamask/utils'; -import { BtcAccountType, InternalAccount } from '@metamask/keyring-api'; +import { BtcAccountType } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import StorageWrapper from '../store/storage-wrapper'; import { selectSelectedInternalAccount, @@ -84,6 +85,7 @@ describe('Accounts Controller Selectors', () => { address: '0xc4966c0d659d99699bfd7eb54d8fafee40e4a756', id: expectedUuid2, options: {}, + scopes: ['eip155'], metadata: { name: 'Account 2', importTime: 1684232000456, diff --git a/app/selectors/accountsController.ts b/app/selectors/accountsController.ts index 3dbf2580f7b..f6811b6b4d0 100644 --- a/app/selectors/accountsController.ts +++ b/app/selectors/accountsController.ts @@ -4,7 +4,8 @@ import { createSelector } from 'reselect'; import { RootState } from '../reducers'; import { createDeepEqualSelector } from './util'; import { selectFlattenedKeyringAccounts } from './keyringController'; -import { EthMethod, InternalAccount } from '@metamask/keyring-api'; +import { EthMethod } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import { getFormattedAddressFromInternalAccount, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) diff --git a/app/selectors/currencyRateController.test.ts b/app/selectors/currencyRateController.test.ts index e48f6b957a4..61a29d38137 100644 --- a/app/selectors/currencyRateController.test.ts +++ b/app/selectors/currencyRateController.test.ts @@ -2,6 +2,7 @@ import { selectConversionRate, selectCurrentCurrency, selectCurrencyRates, + selectConversionRateByChainId, } from './currencyRateController'; import { isTestNet } from '../../app/util/networks'; import { CurrencyRateState } from '@metamask/assets-controllers'; @@ -64,6 +65,41 @@ describe('CurrencyRateController Selectors', () => { }); }); + describe('selectConversionRateByChainId', () => { + const mockChainId = '1'; + const mockNativeCurrency = 'ETH'; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns undefined if on a testnet and fiat is disabled', () => { + (isTestNet as jest.Mock).mockReturnValue(true); + + const result = selectConversionRateByChainId.resultFunc( + mockCurrencyRateState.currencyRates as unknown as CurrencyRateState['currencyRates'], + mockChainId as `0x${string}`, + false, + mockNativeCurrency, + ); + + expect(result).toBeUndefined(); + }); + + it('returns the conversion rate for the native currency of the chain id', () => { + (isTestNet as jest.Mock).mockReturnValue(false); + + const result = selectConversionRateByChainId.resultFunc( + mockCurrencyRateState.currencyRates as unknown as CurrencyRateState['currencyRates'], + mockChainId as `0x${string}`, + true, + mockNativeCurrency, + ); + + expect(result).toBe(3000); + }); + }); + describe('selectCurrentCurrency', () => { it('returns the current currency from the state', () => { const result = selectCurrentCurrency.resultFunc( diff --git a/app/selectors/currencyRateController.ts b/app/selectors/currencyRateController.ts index 715ebeb4e8b..296cf3eedd5 100644 --- a/app/selectors/currencyRateController.ts +++ b/app/selectors/currencyRateController.ts @@ -1,7 +1,11 @@ import { createSelector } from 'reselect'; import { CurrencyRateState } from '@metamask/assets-controllers'; import { RootState } from '../reducers'; -import { selectChainId, selectTicker } from './networkController'; +import { + selectChainId, + selectNativeCurrencyByChainId, + selectTicker, +} from './networkController'; import { isTestNet } from '../../app/util/networks'; const selectCurrencyRateControllerState = (state: RootState) => @@ -56,3 +60,22 @@ export const selectConversionRateFoAllChains = createSelector( (currencyRateControllerState: CurrencyRateState) => currencyRateControllerState?.currencyRates, ); + +export const selectConversionRateByChainId = createSelector( + selectConversionRateFoAllChains, + (_state: RootState, chainId: string) => chainId, + (state: RootState) => state.settings.showFiatOnTestnets, + selectNativeCurrencyByChainId, + ( + currencyRates: CurrencyRateState['currencyRates'], + chainId, + showFiatOnTestnets, + nativeCurrency, + ) => { + if (isTestNet(chainId) && !showFiatOnTestnets) { + return undefined; + } + + return currencyRates?.[nativeCurrency]?.conversionRate; + }, +); diff --git a/app/store/migrations/036.test.ts b/app/store/migrations/036.test.ts index 95f6a354b94..ebf5f797375 100644 --- a/app/store/migrations/036.test.ts +++ b/app/store/migrations/036.test.ts @@ -1,7 +1,9 @@ -import { EthMethod, InternalAccount } from '@metamask/keyring-api'; +import { EthAccountType, EthMethod, EthScopes } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import migrate, { Identity } from './036'; import { captureException } from '@sentry/react-native'; import { getUUIDFromAddressOfNormalAccount } from '@metamask/accounts-controller'; +import { KeyringTypes } from '@metamask/keyring-controller'; jest.mock('@sentry/react-native', () => ({ captureException: jest.fn(), @@ -48,12 +50,13 @@ function expectedInternalAccount( ): InternalAccount { return { address, + scopes: [EthScopes.Namespace], id: getUUIDFromAddressOfNormalAccount(address), metadata: { name: nickname, importTime: Date.now(), keyring: { - type: 'HD Key Tree', + type: KeyringTypes.hd, }, lastSelected: lastSelected ? expect.any(Number) : undefined, }, @@ -65,7 +68,7 @@ function expectedInternalAccount( EthMethod.SignTypedDataV3, EthMethod.SignTypedDataV4, ], - type: 'eip155:eoa', + type: EthAccountType.Eoa, }; } diff --git a/app/store/migrations/036.ts b/app/store/migrations/036.ts index 84d5b6a521b..b73aeae3171 100644 --- a/app/store/migrations/036.ts +++ b/app/store/migrations/036.ts @@ -1,4 +1,5 @@ -import { EthAccountType, InternalAccount } from '@metamask/keyring-api'; +import { EthAccountType, EthScopes } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import { isObject, hasProperty } from '@metamask/utils'; import { captureException } from '@sentry/react-native'; import { getUUIDFromAddressOfNormalAccount } from '@metamask/accounts-controller'; @@ -112,6 +113,7 @@ function createInternalAccountsForAccountsController( accounts[expectedId] = { address: identity.address, + scopes: [EthScopes.Namespace], id: expectedId, options: {}, metadata: { diff --git a/app/store/migrations/042.ts b/app/store/migrations/042.ts index 86491ef8050..62ab512881e 100644 --- a/app/store/migrations/042.ts +++ b/app/store/migrations/042.ts @@ -5,7 +5,7 @@ import { AccountsControllerState, getUUIDFromAddressOfNormalAccount, } from '@metamask/accounts-controller'; -import { InternalAccount } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import { isDefaultAccountName } from '../../util/ENSUtils'; import { ETH_EOA_METHODS } from '../../constants/eth-methods'; diff --git a/app/store/migrations/066.test.ts b/app/store/migrations/066.test.ts new file mode 100644 index 00000000000..970593be3ea --- /dev/null +++ b/app/store/migrations/066.test.ts @@ -0,0 +1,403 @@ +import { + BtcScopes, + EthScopes, + SolScopes, + EthMethod, +} from '@metamask/keyring-api'; +import { AccountsControllerState } from '@metamask/accounts-controller'; +import { captureException } from '@sentry/react-native'; +import migration from './066'; + +jest.mock('../../util/Logger'); +jest.mock('@sentry/react-native', () => ({ + captureException: jest.fn(), +})); + +const mockedCaptureException = jest.mocked(captureException); + +interface StateType { + engine: { + backgroundState: { + AccountsController: AccountsControllerState; + }; + }; +} + +describe('migration #66', () => { + const MOCK_INVALID_STATE = { + someKey: 'someValue', + }; + + const MOCK_EMPTY_STATE: StateType = { + engine: { + backgroundState: { + AccountsController: { + internalAccounts: { + accounts: {}, + selectedAccount: '', + }, + }, + }, + }, + }; + + const MOCK_STATE_WITH_ACCOUNTS: StateType = { + engine: { + backgroundState: { + AccountsController: { + internalAccounts: { + selectedAccount: 'evm-1', + accounts: { + 'evm-1': { + id: 'evm-1', + type: 'eip155:eoa', + address: '0x123', + options: {}, + metadata: { + name: 'Account 1', + keyring: { type: 'HD Key Tree' }, + importTime: Date.now(), + }, + methods: [ + EthMethod.PersonalSign, + EthMethod.SignTransaction, + EthMethod.SignTypedDataV4, + ], + scopes: [], + }, + 'evm-2': { + id: 'evm-2', + type: 'eip155:erc4337', + address: '0x456', + options: {}, + metadata: { + name: 'Account 2', + keyring: { type: 'HD Key Tree' }, + importTime: Date.now(), + }, + methods: [ + EthMethod.PersonalSign, + EthMethod.SignTransaction, + EthMethod.SignTypedDataV4, + ], + scopes: [], + }, + 'btc-1': { + id: 'btc-1', + type: 'bip122:p2wpkh', + address: 'bc1abc', + options: {}, + metadata: { + name: 'BTC Account', + keyring: { type: 'HD Key Tree' }, + importTime: Date.now(), + }, + methods: [], + scopes: [], + }, + 'sol-1': { + id: 'sol-1', + type: 'solana:data-account', + address: 'solana123', + options: {}, + metadata: { + name: 'Solana Account', + keyring: { type: 'HD Key Tree' }, + importTime: Date.now(), + }, + methods: [], + scopes: [], + }, + }, + }, + }, + }, + }, + }; + + const MOCK_STATE_WITH_EXISTING_SCOPES: StateType = { + engine: { + backgroundState: { + AccountsController: { + internalAccounts: { + selectedAccount: 'evm-1', + accounts: { + 'evm-1': { + id: 'evm-1', + type: 'eip155:eoa', + address: '0x123', + options: {}, + metadata: { + name: 'Account 1', + keyring: { type: 'HD Key Tree' }, + importTime: Date.now(), + }, + methods: [ + EthMethod.PersonalSign, + EthMethod.SignTransaction, + EthMethod.SignTypedDataV4, + ], + scopes: [EthScopes.Namespace], + }, + }, + }, + }, + }, + }, + }; + + it('captures exception for invalid state structure', () => { + const invalidState = { + engine: { + backgroundState: { + AccountsController: 'not an object', // Invalid type + }, + }, + }; + + const result = migration(invalidState); + + expect(captureException).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringContaining( + 'Invalid state structure for AccountsController', + ), + }), + ); + expect(result).toBe(invalidState); + }); + + it('handles completely missing AccountsController', () => { + const stateWithoutAccounts = { + engine: { + backgroundState: {}, + }, + }; + + const result = migration(stateWithoutAccounts); + + expect(captureException).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringContaining( + 'Invalid state structure for AccountsController', + ), + }), + ); + expect(result).toBe(stateWithoutAccounts); + }); + + it('handles unexpected errors', () => { + const malformedState = null; + + const result = migration(malformedState); + + expect(captureException).toHaveBeenCalled(); + expect(result).toBe(malformedState); + }); + it('returns state if not valid', () => { + const result = migration(MOCK_INVALID_STATE); + expect(result).toEqual(MOCK_INVALID_STATE); + }); + + it('returns state if empty accounts', () => { + const result = migration(MOCK_EMPTY_STATE); + expect(result).toEqual(MOCK_EMPTY_STATE); + }); + + it('preserves accounts that have valid scopes', () => { + const stateCopy = JSON.parse( + JSON.stringify(MOCK_STATE_WITH_EXISTING_SCOPES), + ); + const result = migration(stateCopy) as StateType; + expect(result).toEqual(MOCK_STATE_WITH_EXISTING_SCOPES); + }); + + it('adds correct scopes for all account types', () => { + const stateCopy = JSON.parse(JSON.stringify(MOCK_STATE_WITH_ACCOUNTS)); + const result = migration(stateCopy) as StateType; + const accounts = + result.engine.backgroundState.AccountsController.internalAccounts + .accounts; + + // Check EVM EOA account + expect(accounts['evm-1']?.scopes).toEqual([EthScopes.Namespace]); + + // Check EVM ERC4337 account + expect(accounts['evm-2']?.scopes).toEqual([EthScopes.Namespace]); + + // Check BTC account + expect(accounts['btc-1']?.scopes).toEqual([BtcScopes.Mainnet]); + + // Check Solana account + expect(accounts['sol-1']?.scopes).toEqual([ + SolScopes.Mainnet, + SolScopes.Testnet, + SolScopes.Devnet, + ]); + }); + + it('handles malformed account objects gracefully', () => { + const malformedState: StateType = { + engine: { + backgroundState: { + AccountsController: { + internalAccounts: { + selectedAccount: 'valid-1', + accounts: { + 'valid-1': { + id: 'valid-1', + type: 'eip155:eoa', + address: '0x123', + options: {}, + metadata: { + name: 'Account 1', + keyring: { type: 'HD Key Tree' }, + importTime: Date.now(), + }, + methods: [ + EthMethod.PersonalSign, + EthMethod.SignTransaction, + EthMethod.SignTypedDataV4, + ], + scopes: [], + }, + }, + }, + }, + }, + }, + }; + + const result = migration(malformedState) as StateType; + const accounts = + result.engine.backgroundState.AccountsController.internalAccounts + .accounts; + + // Should still process valid accounts + expect(accounts['valid-1']?.scopes).toEqual([EthScopes.Namespace]); + }); + + it('handles invalid scopes property gracefully', () => { + const stateWithInvalidScopes: StateType = { + engine: { + backgroundState: { + AccountsController: { + internalAccounts: { + selectedAccount: 'invalid-1', + accounts: { + 'invalid-1': { + id: 'invalid-1', + type: 'eip155:eoa', + address: '0x123', + options: {}, + metadata: { + name: 'Account 1', + keyring: { type: 'HD Key Tree' }, + importTime: Date.now(), + }, + methods: [ + EthMethod.PersonalSign, + EthMethod.SignTransaction, + EthMethod.SignTypedDataV4, + ], + // @ts-expect-error Testing invalid scope type + scopes: null, + }, + 'invalid-2': { + id: 'invalid-2', + type: 'eip155:eoa', + address: '0x456', + options: {}, + metadata: { + name: 'Account 2', + keyring: { type: 'HD Key Tree' }, + importTime: Date.now(), + }, + methods: [ + EthMethod.PersonalSign, + EthMethod.SignTransaction, + EthMethod.SignTypedDataV4, + ], + scopes: [], + }, + 'invalid-3': { + id: 'invalid-3', + type: 'eip155:eoa', + address: '0x789', + options: {}, + metadata: { + name: 'Account 3', + keyring: { type: 'HD Key Tree' }, + importTime: Date.now(), + }, + methods: [ + EthMethod.PersonalSign, + EthMethod.SignTransaction, + EthMethod.SignTypedDataV4, + ], + // @ts-expect-error Testing invalid scope type + scopes: undefined, + }, + }, + }, + }, + }, + }, + }; + + const result = migration(stateWithInvalidScopes) as StateType; + const accounts = + result.engine.backgroundState.AccountsController.internalAccounts + .accounts; + + // Should fix accounts with invalid scopes + expect(accounts['invalid-1']?.scopes).toEqual([EthScopes.Namespace]); + expect(accounts['invalid-2']?.scopes).toEqual([EthScopes.Namespace]); + expect(accounts['invalid-3']?.scopes).toEqual([EthScopes.Namespace]); + }); + + it('logs unknown account types to Sentry', () => { + const stateWithUnknownType: StateType = { + engine: { + backgroundState: { + AccountsController: { + internalAccounts: { + selectedAccount: 'unknown-1', + accounts: { + 'unknown-1': { + id: 'unknown-1', + // @ts-expect-error Testing unknown account type + type: 'unknown-type', + address: '0x123', + options: {}, + metadata: { + name: 'Unknown Account', + keyring: { type: 'HD Key Tree' }, + importTime: Date.now(), + }, + methods: [], + scopes: [], + }, + }, + }, + }, + }, + }, + }; + + const result = migration(stateWithUnknownType) as StateType; + const accounts = + result.engine.backgroundState.AccountsController.internalAccounts + .accounts; + + // Verify scopes are set to default EVM namespace + expect(accounts['unknown-1']?.scopes).toEqual([EthScopes.Namespace]); + + // Verify Sentry exception was captured + expect(mockedCaptureException).toHaveBeenCalledWith( + new Error( + 'Migration 66: Unknown account type unknown-type, defaulting to EVM namespace', + ), + ); + }); +}); diff --git a/app/store/migrations/066.ts b/app/store/migrations/066.ts new file mode 100644 index 00000000000..210a0db07f4 --- /dev/null +++ b/app/store/migrations/066.ts @@ -0,0 +1,105 @@ +import { hasProperty, isObject } from '@metamask/utils'; +import { ensureValidState } from './util'; +import Logger from '../../util/Logger'; +import { + BtcAccountType, + BtcScopes, + EthAccountType, + EthScopes, + SolAccountType, + SolScopes, +} from '@metamask/keyring-api'; +import { captureException } from '@sentry/react-native'; + +const migrationVersion = 66; + +function getScopesForAccountType(accountType: string): string[] { + switch (accountType) { + case EthAccountType.Eoa: + case EthAccountType.Erc4337: + return [EthScopes.Namespace]; + case BtcAccountType.P2wpkh: + // Default to mainnet scope if address is missing or invalid + return [BtcScopes.Mainnet]; + case SolAccountType.DataAccount: + return [SolScopes.Mainnet, SolScopes.Testnet, SolScopes.Devnet]; + default: + // Default to EVM namespace for unknown account types + captureException( + new Error( + `Migration ${migrationVersion}: Unknown account type ${accountType}, defaulting to EVM namespace`, + ), + ); + return [EthScopes.Namespace]; + } +} + +/** + * Migration for adding scopes to accounts in the AccountsController. + * Each account type gets its appropriate scopes: + * - EVM EOA: [EthScopes.Namespace] + * - EVM ERC4337: [EthScopes.Namespace] + * - BTC P2WPKH: [BtcScopes.Mainnet] or [BtcScopes.Testnet] based on address + * - Solana: [SolScopes.Mainnet, SolScopes.Testnet, SolScopes.Devnet] + * + * @param state - The state to migrate + * @returns The migrated state + */ +export default function migrate(state: unknown) { + if (!ensureValidState(state, migrationVersion)) { + return state; + } + + if ( + !hasProperty(state.engine.backgroundState, 'AccountsController') || + !isObject(state.engine.backgroundState.AccountsController) || + !hasProperty( + state.engine.backgroundState.AccountsController, + 'internalAccounts', + ) || + !isObject( + state.engine.backgroundState.AccountsController.internalAccounts, + ) || + !hasProperty( + state.engine.backgroundState.AccountsController.internalAccounts, + 'accounts', + ) || + !isObject( + state.engine.backgroundState.AccountsController.internalAccounts.accounts, + ) + ) { + captureException( + new Error( + `Migration ${migrationVersion}: Invalid state structure for AccountsController`, + ), + ); + return state; + } + + const accounts = + state.engine.backgroundState.AccountsController.internalAccounts.accounts; + + for (const account of Object.values(accounts)) { + if (!isObject(account) || !hasProperty(account, 'type')) { + continue; + } + + // Skip if account already has valid scopes + if ( + hasProperty(account, 'scopes') && + Array.isArray(account.scopes) && + account.scopes.length > 0 && + account.scopes.every((scope) => typeof scope === 'string') + ) { + continue; + } + + Logger.log( + `Migration ${migrationVersion}: Adding scopes for account type ${account.type}`, + ); + + account.scopes = getScopesForAccountType(account.type as string); + } + + return state; +} diff --git a/app/store/validateMigration/accountsController.test.ts b/app/store/validateMigration/accountsController.test.ts index 63a636f8243..1ea53201d9a 100644 --- a/app/store/validateMigration/accountsController.test.ts +++ b/app/store/validateMigration/accountsController.test.ts @@ -4,6 +4,7 @@ import { RootState } from '../../reducers'; import { AccountsControllerState } from '@metamask/accounts-controller'; import { EngineState } from '../../core/Engine/types'; import { Json } from '@metamask/utils'; +import { EthScopes } from '@metamask/keyring-api'; describe('validateAccountsController', () => { const createMockState = ( @@ -25,6 +26,7 @@ describe('validateAccountsController', () => { id: 'account-1', address: '0x123', type: 'eip155:eoa', + scopes: [EthScopes.Namespace], options: {} as Record, methods: [], metadata: { @@ -87,6 +89,7 @@ describe('validateAccountsController', () => { id: 'account-1', address: '0x123', type: 'eip155:eoa', + scopes: [EthScopes.Namespace], options: {} as Record, methods: [], metadata: { @@ -117,6 +120,7 @@ describe('validateAccountsController', () => { id: 'account-1', address: '0x123', type: 'eip155:eoa', + scopes: [EthScopes.Namespace], options: {} as Record, methods: [], metadata: { diff --git a/app/util/address/index.ts b/app/util/address/index.ts index a961ed6cda5..d4019e5f459 100644 --- a/app/util/address/index.ts +++ b/app/util/address/index.ts @@ -33,7 +33,7 @@ import { selectChainId } from '../../selectors/networkController'; import { store } from '../../store'; import { regex } from '../../../app/util/regex'; import Logger from '../../../app/util/Logger'; -import { InternalAccount } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import { AddressBookControllerState } from '@metamask/address-book-controller'; import { NetworkType, toChecksumHexAddress } from '@metamask/controller-utils'; import { NetworkClientId, NetworkState } from '@metamask/network-controller'; diff --git a/app/util/networks/index.js b/app/util/networks/index.js index 461f8ea6b87..51cfed908a0 100644 --- a/app/util/networks/index.js +++ b/app/util/networks/index.js @@ -14,6 +14,7 @@ import { ChainId, NetworkType, toHex } from '@metamask/controller-utils'; import { toLowerCaseEquals } from '../general'; import { fastSplit } from '../number'; import { regex } from '../../../app/util/regex'; +import Device from '../../util/device'; /* eslint-disable */ const ethLogo = require('../../images/eth-logo-new.png'); @@ -498,4 +499,4 @@ export const isPermissionsSettingsV1Enabled = process.env.MM_PERMISSIONS_SETTINGS_V1_ENABLED === 'true'; export const isPortfolioViewEnabled = () => - process.env.PORTFOLIO_VIEW === 'true'; + Device.isIos() && process.env.PORTFOLIO_VIEW === 'true'; diff --git a/app/util/notifications/hooks/types.ts b/app/util/notifications/hooks/types.ts index 2e0886d5af1..1e5d193c9e9 100644 --- a/app/util/notifications/hooks/types.ts +++ b/app/util/notifications/hooks/types.ts @@ -1,4 +1,4 @@ -import type { InternalAccount } from '@metamask/keyring-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import { KeyringTypes } from '@metamask/keyring-controller'; import type { Notification } from '../../../util/notifications/types/notification'; diff --git a/app/util/number/index.js b/app/util/number/index.js index 894716562aa..f84cc95c189 100644 --- a/app/util/number/index.js +++ b/app/util/number/index.js @@ -16,6 +16,9 @@ import { isZero } from '../lodash'; import { regex } from '../regex'; export { BNToHex }; +const MAX_DECIMALS_FOR_TOKENS = 36; +BigNumber.config({ DECIMAL_PLACES: MAX_DECIMALS_FOR_TOKENS }); + // Big Number Constants const BIG_NUMBER_WEI_MULTIPLIER = new BigNumber('1000000000000000000'); const BIG_NUMBER_GWEI_MULTIPLIER = new BigNumber('1000000000'); @@ -349,7 +352,7 @@ export function isBN(value) { /** * Determines if a string is a valid decimal * - * @param {string} value - String to check + * @param {number | string} value - String to check * @returns {boolean} - True if the string is a valid decimal */ export function isDecimal(value) { @@ -380,6 +383,22 @@ export function isNumber(str) { return regex.number.test(str); } +/** + * Determines if a value is a number + * + * @param {number | string | null | undefined} value - Value to check + * @returns {boolean} - True if the value is a valid number + */ +export function isNumberValue(value) { + if (value === null || value === undefined) { return false; } + + if (typeof value === 'number') { + return !Number.isNaN(value) && Number.isFinite(value); + } + + return isDecimal(value); +} + export const dotAndCommaDecimalFormatter = (value) => { const valueStr = String(value); diff --git a/app/util/number/index.test.ts b/app/util/number/index.test.ts index 436f18c0c29..5849713f223 100644 --- a/app/util/number/index.test.ts +++ b/app/util/number/index.test.ts @@ -20,6 +20,7 @@ import { isBN, isDecimal, isNumber, + isNumberValue, isNumberScientificNotationWhenString, isZeroValue, limitToMaximumDecimalPlaces, @@ -925,6 +926,42 @@ describe('Number utils :: isNumber', () => { }); }); +describe('Number utils :: isNumberValue', () => { + it('should return true for valid number types', () => { + expect(isNumberValue(1650.7)).toBe(true); + expect(isNumberValue(1000)).toBe(true); + expect(isNumberValue(0.0001)).toBe(true); + expect(isNumberValue(-0.0001)).toBe(true); + expect(isNumberValue(1)).toBe(true); + expect(isNumberValue(1e-10)).toBe(true); + }); + + it('should be a valid number string types', () => { + expect(isNumberValue('1650.7')).toBe(true); + expect(isNumberValue('1000')).toBe(true); + expect(isNumberValue('.01')).toBe(true); + expect(isNumberValue('0.0001')).toBe(true); + expect(isNumberValue('0001')).toBe(true); + expect(isNumberValue('-0.0001')).toBe(true); + expect(isNumberValue('1')).toBe(true); + expect(isNumberValue('1e-10')).toBe(true); + }); + + it('should not be a valid number ', () => { + expect(isNumberValue('..7')).toBe(false); + expect(isNumberValue('1..1')).toBe(false); + expect(isNumberValue('0..')).toBe(false); + expect(isNumberValue('a.0001')).toBe(false); + expect(isNumberValue('00a01')).toBe(false); + expect(isNumberValue('1,.')).toBe(false); + expect(isNumberValue('1,')).toBe(false); + expect(isNumberValue('.')).toBe(false); + expect(isNumberValue('a¡1')).toBe(false); + expect(isNumberValue(undefined)).toBe(false); + expect(isNumberValue(null)).toBe(false); + }); +}); + describe('Number utils :: dotAndCommaDecimalFormatter', () => { it('should return the number if it does not contain a dot or comma', () => { expect(dotAndCommaDecimalFormatter('1650')).toBe('1650'); diff --git a/app/util/test/accountsControllerTestUtils.ts b/app/util/test/accountsControllerTestUtils.ts index 3aba07841fb..d4cfe766244 100644 --- a/app/util/test/accountsControllerTestUtils.ts +++ b/app/util/test/accountsControllerTestUtils.ts @@ -1,10 +1,15 @@ import { v4 as uuidV4 } from 'uuid'; import { EthAccountType, + BtcAccountType, + SolAccountType, EthMethod, - InternalAccount, + EthScopes, + BtcScopes, + SolScopes, KeyringAccountType, } from '@metamask/keyring-api'; +import { InternalAccount } from '@metamask/keyring-internal-api'; import { AccountsControllerState } from '@metamask/accounts-controller'; import { KeyringTypes } from '@metamask/keyring-controller'; import { @@ -24,6 +29,33 @@ export function createMockUuidFromAddress(address: string): string { }); } +/** + * Maps account types to their corresponding scopes + * @param accountType - The type of account (ETH, BTC, or Solana) + * @returns Array of scopes corresponding to the account type + */ +function getAccountTypeScopes(accountType: KeyringAccountType): string[] { + // Define scope mappings + const scopeMappings = { + // Ethereum account types + [EthAccountType.Eoa]: [EthScopes.Namespace], + [EthAccountType.Erc4337]: [EthScopes.Namespace], + + // Bitcoin account types + [BtcAccountType.P2wpkh]: [BtcScopes.Namespace], + + // Solana account types + [SolAccountType.DataAccount]: [SolScopes.Namespace], + }; + + const scopes = scopeMappings[accountType]; + if (!scopes) { + throw new Error(`Unsupported account type: ${accountType}`); + } + + return scopes; +} + export function createMockInternalAccount( address: string, nickname: string, @@ -62,6 +94,7 @@ export function createMockInternalAccount( EthMethod.SignTypedDataV4, ], type: accountType, + scopes: getAccountTypeScopes(accountType), }; } @@ -92,7 +125,8 @@ export function createMockSnapInternalAccount( EthMethod.SignTypedDataV3, EthMethod.SignTypedDataV4, ], - type: 'eip155:eoa', + type: EthAccountType.Eoa, + scopes: [EthScopes.Namespace], }; } diff --git a/app/util/test/confirm-data-helpers.ts b/app/util/test/confirm-data-helpers.ts index 535b3cf4d4a..485228a065a 100644 --- a/app/util/test/confirm-data-helpers.ts +++ b/app/util/test/confirm-data-helpers.ts @@ -255,8 +255,17 @@ export const typedSignV4ConfirmationState = { messageParams: { data: '{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Permit":[{"name":"owner","type":"address"},{"name":"spender","type":"address"},{"name":"value","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"deadline","type":"uint256"}]},"primaryType":"Permit","domain":{"name":"MyToken","version":"1","verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC","chainId":1},"message":{"owner":"0x935e73edb9ff52e23bac7f7e043a1ecd06d05477","spender":"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","value":3000,"nonce":0,"deadline":50000000000}}', from: '0x935e73edb9ff52e23bac7f7e043a1ecd06d05477', + version: 'V4', + requestId: 14, + signatureMethod: 'eth_signTypedData_v4', + origin: 'https://metamask.github.io', metamaskId: 'fb2029e0-b0ab-11ef-9227-05a11087c334', - origin: 'https://metamask.github.io' + meta: { + url: 'https://metamask.github.io/test-dapp/', + title: 'E2E Test Dapp', + icon: { uri: 'https://metamask.github.io/metamask-fox.svg' }, + analytics: { request_source: 'In-App-Browser' }, + }, }, networkClientId: '1', status: SignatureRequestStatus.Unapproved, diff --git a/app/util/transactions/index.js b/app/util/transactions/index.js index 8315154bf9a..eadb29d6e21 100644 --- a/app/util/transactions/index.js +++ b/app/util/transactions/index.js @@ -1428,9 +1428,14 @@ export function validateTransactionActionBalance(transaction, rate, accounts) { } } +/** + * @param {number|string|BigNumber} value + * @param {number=} decimals + * @returns {BigNumber} + */ export function calcTokenAmount(value, decimals) { - const multiplier = Math.pow(10, Number(decimals || 0)); - return new BigNumber(String(value)).div(multiplier); + const divisor = new BigNumber(10).pow(decimals ?? 0); + return new BigNumber(String(value)).div(divisor); } export function calcTokenValue(value, decimals) { diff --git a/app/util/transactions/index.test.ts b/app/util/transactions/index.test.ts index f2fcc535372..aa3d10495e0 100644 --- a/app/util/transactions/index.test.ts +++ b/app/util/transactions/index.test.ts @@ -11,6 +11,7 @@ import { UINT256_BN_MAX_VALUE } from '../../constants/transaction'; import { NEGATIVE_TOKEN_DECIMALS } from '../../constants/error'; import { generateTransferData, + calcTokenAmount, decodeApproveData, decodeTransferData, getMethodData, @@ -42,6 +43,7 @@ import Engine from '../../core/Engine'; import { strings } from '../../../locales/i18n'; import { TransactionType } from '@metamask/transaction-controller'; import { Provider } from '@metamask/network-controller'; +import BigNumber from 'bignumber.js'; jest.mock('@metamask/controller-utils', () => ({ ...jest.requireActual('@metamask/controller-utils'), @@ -106,6 +108,52 @@ describe('Transactions utils :: generateTransferData', () => { }); }); +describe('Transactions utils :: calcTokenAmount', () => { + it.each([ + // number values + [0, 5, '0'], + [123456, undefined, '123456'], + [123456, 5, '1.23456'], + [123456, 6, '0.123456'], + // Do not delete the following test. Testing decimal = 36 is important because it has broken + // BigNumber#div in the past when the value that was passed into it was not a BigNumber. + [123456, 36, '1.23456e-31'], + [3000123456789678, 6, '3000123456.789678'], + // eslint-disable-next-line @typescript-eslint/no-loss-of-precision + [3000123456789123456789123456789, 3, '3.0001234567891233e+27'], // expected precision lost + // eslint-disable-next-line @typescript-eslint/no-loss-of-precision + [3000123456789123456789123456789, 6, '3.0001234567891233e+24'], // expected precision lost + // string values + ['0', 5, '0'], + ['123456', undefined, '123456'], + ['123456', 5, '1.23456'], + ['123456', 6, '0.123456'], + ['3000123456789678', 6, '3000123456.789678'], + [ + '3000123456789123456789123456789', + 3, + '3.000123456789123456789123456789e+27', + ], + [ + '3000123456789123456789123456789', + 6, + '3.000123456789123456789123456789e+24', + ], + // BigNumber values + [new BigNumber('3000123456789678'), 6, '3000123456.789678'], + [ + new BigNumber('3000123456789123456789123456789'), + 6, + '3.000123456789123456789123456789e+24', + ], + ])( + 'returns the value %s divided by 10^%s = %s', + (value, decimals, expected) => { + expect(calcTokenAmount(value, decimals).toString()).toBe(expected); + }, + ); +}); + describe('Transactions utils :: decodeTransferData', () => { it('decodeTransferData transfer', () => { const [address, amount] = decodeTransferData( diff --git a/locales/languages/en.json b/locales/languages/en.json index 861594b3a49..b993d227b59 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -3607,13 +3607,23 @@ "balance": "Balance", "network": "Network", "simulation": { - "info_permit": "You’re giving the spender permission to spend this many tokens from your account.", + "decoded_tooltip_bid_nft": "The NFT will be reflected in your wallet, when the bid is accepted.", + "decoded_tooltip_list_nft": "Expect changes only if someone buys your NFTs.", + "info_permit": "You're giving the spender permission to spend this many tokens from your account.", + "label_change_type_bidding": "You bid", + "label_change_type_listing": "You list", + "label_change_type_nft_listing": "Listing price", "label_change_type_permit": "Spending cap", "label_change_type_permit_nft": "Withdraw", - "personal_sign_info": "You’re signing into a site and there are no predicted changes to your account.", + "label_change_type_receive": "You receive", + "label_change_type_revoke": "Revoke", + "label_change_type_transfer": "You send", + "personal_sign_info": "You're signing into a site and there are no predicted changes to your account.", "title": "Estimated changes", - "tooltip": "Estimated changes are what might happen if you go through with this transaction. This is just a prediction, not a guarantee." - } + "tooltip": "Estimated changes are what might happen if you go through with this transaction. This is just a prediction, not a guarantee.", + "unavailable": "Unavailable" + }, + "unlimited": "Unlimited" }, "change_in_simulation_modal": { "title": "Results have changed", diff --git a/package.json b/package.json index fb288afa5cb..5230866d3d8 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,6 @@ "prestorybook": "rnstl", "deduplicate": "yarn yarn-deduplicate && yarn install", "patch:tx": "./scripts/patch-transaction-controller.sh", - "patch:assets": "./scripts/patch-assets-controllers.sh", "patch:approval": "./scripts/patch-approval-controller.sh", "storybook-generate": "sb-rn-get-stories", "storybook-watch": "sb-rn-watcher", @@ -112,7 +111,7 @@ "react-native-level-fs/**/bl": "^1.2.3", "react-native-level-fs/levelup/semver": "^5.7.2", "@metamask/react-native-payments/validator": "^13.7.0", - "**/@metamask/utils": "^10.0.0", + "**/@metamask/utils": "^11.0.1", "**/minimist": "1.2.6", "d3-color": "3.1.0", "tough-cookie": "4.1.3", @@ -135,7 +134,7 @@ "send": "0.19.0", "ethereumjs-util/**/secp256k1": "3.8.1", "**/secp256k1": "4.0.4", - "**/@metamask/rpc-errors": "7.0.1", + "**/@metamask/rpc-errors": "7.0.2", "**/@expo/image-utils/semver": "7.5.2", "base58-js": "1.0.0", "bech32": "2.0.0", @@ -150,11 +149,11 @@ "@keystonehq/metamask-airgapped-keyring": "^0.13.1", "@keystonehq/ur-decoder": "^0.12.2", "@ledgerhq/react-native-hw-transport-ble": "^6.33.2", - "@metamask/accounts-controller": "^20.0.1", + "@metamask/accounts-controller": "^21.0.0", "@metamask/address-book-controller": "^6.0.1", "@metamask/approval-controller": "^7.1.0", - "@metamask/assets-controllers": "^45.1.1", - "@metamask/base-controller": "^7.0.1", + "@metamask/assets-controllers": "^46.0.0", + "@metamask/base-controller": "^7.1.1", "@metamask/bitcoin-wallet-snap": "^0.8.2", "@metamask/composable-controller": "^10.0.0", "@metamask/controller-utils": "^11.3.0", @@ -165,17 +164,19 @@ "@metamask/eth-ledger-bridge-keyring": "^8.0.0", "@metamask/eth-query": "^4.0.0", "@metamask/eth-sig-util": "^8.0.0", - "@metamask/eth-snap-keyring": "^5.0.1", + "@metamask/eth-snap-keyring": "^7.0.0", "@metamask/etherscan-link": "^2.0.0", "@metamask/ethjs-contract": "^0.4.1", "@metamask/ethjs-query": "^0.7.1", "@metamask/ethjs-unit": "^0.3.0", "@metamask/gas-fee-controller": "^22.0.2", - "@metamask/json-rpc-engine": "^10.0.0", - "@metamask/json-rpc-middleware-stream": "^8.0.2", + "@metamask/json-rpc-engine": "^10.0.2", + "@metamask/json-rpc-middleware-stream": "^8.0.6", "@metamask/key-tree": "^9.0.0", - "@metamask/keyring-api": "^10.1.0", + "@metamask/keyring-api": "^13.0.0", "@metamask/keyring-controller": "^19.0.1", + "@metamask/keyring-internal-api": "^2.0.0", + "@metamask/keyring-snap-client": "^2.0.0", "@metamask/logging-controller": "^6.0.1", "@metamask/message-signing-snap": "^0.3.3", "@metamask/network-controller": "^22.1.0", @@ -192,7 +193,7 @@ "@metamask/react-native-search-api": "1.0.1", "@metamask/react-native-webview": "^14.0.4", "@metamask/remote-feature-flag-controller": "^1.0.0", - "@metamask/rpc-errors": "^7.0.1", + "@metamask/rpc-errors": "^7.0.2", "@metamask/scure-bip39": "^2.1.0", "@metamask/sdk-communication-layer": "0.29.0-wallet", "@metamask/selected-network-controller": "^19.0.0", @@ -208,7 +209,7 @@ "@metamask/swappable-obj-proxy": "^2.1.0", "@metamask/swaps-controller": "^12.0.0", "@metamask/transaction-controller": "^42.0.0", - "@metamask/utils": "^10.0.1", + "@metamask/utils": "^11.0.1", "@ngraveio/bc-ur": "^1.1.6", "@notifee/react-native": "^9.0.0", "@react-native-async-storage/async-storage": "^1.23.1", @@ -390,7 +391,7 @@ "@metamask/eslint-plugin-design-tokens": "^1.0.0", "@metamask/mobile-provider": "^3.0.0", "@metamask/object-multiplex": "^1.1.0", - "@metamask/providers": "^18.1.0", + "@metamask/providers": "^18.3.1", "@metamask/test-dapp": "^8.9.0", "@octokit/rest": "^21.0.0", "@open-rpc/mock-server": "^1.7.5", diff --git a/patches/@metamask+assets-controllers+45.1.1.patch b/patches/@metamask+assets-controllers+46.0.0.patch similarity index 60% rename from patches/@metamask+assets-controllers+45.1.1.patch rename to patches/@metamask+assets-controllers+46.0.0.patch index 106e984e11c..7bff7d0e905 100644 --- a/patches/@metamask+assets-controllers+45.1.1.patch +++ b/patches/@metamask+assets-controllers+46.0.0.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@metamask/assets-controllers/dist/NftController.cjs b/node_modules/@metamask/assets-controllers/dist/NftController.cjs -index 6ccbe9c..49270d6 100644 +index 6ccbe9c..417a55e 100644 --- a/node_modules/@metamask/assets-controllers/dist/NftController.cjs +++ b/node_modules/@metamask/assets-controllers/dist/NftController.cjs @@ -13,7 +13,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function ( @@ -129,13 +129,13 @@ index 6ccbe9c..49270d6 100644 ]); + if (blockchainMetadata?.error && nftApiMetadata?.error) { + return { -+ image: null, -+ name: null, -+ description: null, -+ standard: blockchainMetadata.standard ?? null, -+ favorite: false, -+ tokenURI: blockchainMetadata.tokenURI ?? null, -+ error: 'Both import failed', ++ image: null, ++ name: null, ++ description: null, ++ standard: blockchainMetadata.standard ?? null, ++ favorite: false, ++ tokenURI: blockchainMetadata.tokenURI ?? null, ++ error: 'Both import failed', + }; + } return { @@ -145,7 +145,7 @@ index 6ccbe9c..49270d6 100644 tokenId: tokenId.toString(), standard: nftMetadata.standard, source, -+ tokenURI: nftMetadata.tokenURI, ++ tokenURI: nftMetadata.tokenURI }); } } @@ -161,109 +161,3 @@ index a34725f..21e9d20 100644 collection?: Collection; address?: string; attributes?: Attributes[]; -diff --git a/node_modules/@metamask/assets-controllers/dist/TokenDetectionController.cjs b/node_modules/@metamask/assets-controllers/dist/TokenDetectionController.cjs -index c5aa814..83c0664 100644 ---- a/node_modules/@metamask/assets-controllers/dist/TokenDetectionController.cjs -+++ b/node_modules/@metamask/assets-controllers/dist/TokenDetectionController.cjs -@@ -220,50 +220,57 @@ _TokenDetectionController_intervalId = new WeakMap(), _TokenDetectionController_ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/no-misused-promises - this.messagingSystem.subscribe('KeyringController:unlock', async () => { -- __classPrivateFieldSet(this, _TokenDetectionController_isUnlocked, true, "f"); -- await __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_restartTokenDetection).call(this); -- }); -- this.messagingSystem.subscribe('KeyringController:lock', () => { -- __classPrivateFieldSet(this, _TokenDetectionController_isUnlocked, false, "f"); -- __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_stopPolling).call(this); -- }); -- this.messagingSystem.subscribe('TokenListController:stateChange', -- // TODO: Either fix this lint violation or explain why it's necessary to ignore. -- // eslint-disable-next-line @typescript-eslint/no-misused-promises -- async ({ tokensChainsCache }) => { -- const isEqualValues = __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_compareTokensChainsCache).call(this, tokensChainsCache, __classPrivateFieldGet(this, _TokenDetectionController_tokensChainsCache, "f")); -- if (!isEqualValues) { -- await __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_restartTokenDetection).call(this); -- } -- }); -- this.messagingSystem.subscribe('PreferencesController:stateChange', -- // TODO: Either fix this lint violation or explain why it's necessary to ignore. -- // eslint-disable-next-line @typescript-eslint/no-misused-promises -- async ({ useTokenDetection }) => { -- const selectedAccount = __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_getSelectedAccount).call(this); -- const isDetectionChangedFromPreferences = __classPrivateFieldGet(this, _TokenDetectionController_isDetectionEnabledFromPreferences, "f") !== useTokenDetection; -- __classPrivateFieldSet(this, _TokenDetectionController_isDetectionEnabledFromPreferences, useTokenDetection, "f"); -- if (isDetectionChangedFromPreferences) { -- await __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_restartTokenDetection).call(this, { -- selectedAddress: selectedAccount.address, -- }); -- } -- }); -- this.messagingSystem.subscribe('AccountsController:selectedEvmAccountChange', -- // TODO: Either fix this lint violation or explain why it's necessary to ignore. -- // eslint-disable-next-line @typescript-eslint/no-misused-promises -- async (selectedAccount) => { -- const { networkConfigurationsByChainId } = this.messagingSystem.call('NetworkController:getState'); -- const chainIds = Object.keys(networkConfigurationsByChainId); -- const isSelectedAccountIdChanged = __classPrivateFieldGet(this, _TokenDetectionController_selectedAccountId, "f") !== selectedAccount.id; -- if (isSelectedAccountIdChanged) { -- __classPrivateFieldSet(this, _TokenDetectionController_selectedAccountId, selectedAccount.id, "f"); -- await __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_restartTokenDetection).call(this, { -- selectedAddress: selectedAccount.address, -- chainIds, -- }); -- } -- }); -+ const { networkConfigurationsByChainId } = this.messagingSystem.call('NetworkController:getState'); -+ const chainIds = Object.keys(networkConfigurationsByChainId); -+ __classPrivateFieldSet(this, _TokenDetectionController_isUnlocked, true, "f"); -+ await __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_restartTokenDetection).call(this, { chainIds }); -+ }); -+ this.messagingSystem.subscribe('KeyringController:lock', () => { -+ __classPrivateFieldSet(this, _TokenDetectionController_isUnlocked, false, "f"); -+ __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_stopPolling).call(this); -+ }); -+ this.messagingSystem.subscribe('TokenListController:stateChange', -+ // TODO: Either fix this lint violation or explain why it's necessary to ignore. -+ // eslint-disable-next-line @typescript-eslint/no-misused-promises -+ async ({ tokensChainsCache }) => { -+ const isEqualValues = __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_compareTokensChainsCache).call(this, tokensChainsCache, __classPrivateFieldGet(this, _TokenDetectionController_tokensChainsCache, "f")); -+ if (!isEqualValues) { -+ const { networkConfigurationsByChainId } = this.messagingSystem.call('NetworkController:getState'); -+ const chainIds = Object.keys(networkConfigurationsByChainId); -+ await __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_restartTokenDetection).call(this, { chainIds }); -+ } -+ }); -+ this.messagingSystem.subscribe('PreferencesController:stateChange', -+ // TODO: Either fix this lint violation or explain why it's necessary to ignore. -+ // eslint-disable-next-line @typescript-eslint/no-misused-promises -+ async ({ useTokenDetection }) => { -+ const { networkConfigurationsByChainId } = this.messagingSystem.call('NetworkController:getState'); -+ const selectedAccount = __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_getSelectedAccount).call(this); -+ const isDetectionChangedFromPreferences = __classPrivateFieldGet(this, _TokenDetectionController_isDetectionEnabledFromPreferences, "f") !== useTokenDetection; -+ __classPrivateFieldSet(this, _TokenDetectionController_isDetectionEnabledFromPreferences, useTokenDetection, "f"); -+ const chainIds = Object.keys(networkConfigurationsByChainId); -+ if (isDetectionChangedFromPreferences) { -+ await __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_restartTokenDetection).call(this, { -+ selectedAddress: selectedAccount.address, -+ chainIds, -+ }); -+ } -+ }); -+ this.messagingSystem.subscribe('AccountsController:selectedEvmAccountChange', -+ // TODO: Either fix this lint violation or explain why it's necessary to ignore. -+ // eslint-disable-next-line @typescript-eslint/no-misused-promises -+ async (selectedAccount) => { -+ const { networkConfigurationsByChainId } = this.messagingSystem.call('NetworkController:getState'); -+ const chainIds = Object.keys(networkConfigurationsByChainId); -+ const isSelectedAccountIdChanged = __classPrivateFieldGet(this, _TokenDetectionController_selectedAccountId, "f") !== selectedAccount.id; -+ if (isSelectedAccountIdChanged) { -+ __classPrivateFieldSet(this, _TokenDetectionController_selectedAccountId, selectedAccount.id, "f"); -+ await __classPrivateFieldGet(this, _TokenDetectionController_instances, "m", _TokenDetectionController_restartTokenDetection).call(this, { -+ selectedAddress: selectedAccount.address, -+ chainIds, -+ }); -+ } -+ }); - }, _TokenDetectionController_stopPolling = function _TokenDetectionController_stopPolling() { - if (__classPrivateFieldGet(this, _TokenDetectionController_intervalId, "f")) { - clearInterval(__classPrivateFieldGet(this, _TokenDetectionController_intervalId, "f")); diff --git a/yarn.lock b/yarn.lock index 3825d237ad7..61868ccf2e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4377,18 +4377,27 @@ "@metamask/superstruct" "^3.1.0" "@metamask/utils" "^9.0.0" -"@metamask/accounts-controller@^20.0.1": - version "20.0.1" - resolved "https://registry.yarnpkg.com/@metamask/accounts-controller/-/accounts-controller-20.0.1.tgz#6f3dc905418ad75e44fce830b0363a585cce2ea8" - integrity sha512-zjAzkA3Okxy9aptBAHtFKCaTq5VNE8EHV48ETzxl8Jhdl8wpaTGWcacbqdkmOooC1lPLe2oXGglHSdBP73JDMQ== +"@metamask/abi-utils@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@metamask/abi-utils/-/abi-utils-3.0.0.tgz#2eab9cb895922b94305364d9111b6dde724f6f9b" + integrity sha512-a/l0DiSIr7+CBYVpHygUa3ztSlYLFCQMsklLna+t6qmNY9+eIO5TedNxhyIyvaJ+4cN7TLy0NQFbp9FV3X2ktg== + dependencies: + "@metamask/superstruct" "^3.1.0" + "@metamask/utils" "^11.0.1" + +"@metamask/accounts-controller@^21.0.0": + version "21.0.0" + resolved "https://registry.yarnpkg.com/@metamask/accounts-controller/-/accounts-controller-21.0.0.tgz#d4ef858cd9ec126423fe4a287edada1b9aa9d45a" + integrity sha512-Jt5knLn6n9DQ3IUsfjmtx6NjOTSZrUxHWdvU+SHtQxqkrtNlldqv1C+hQv4DxCTmk6MfSttuEdWObCaxpB2sMA== dependencies: "@ethereumjs/util" "^8.1.0" - "@metamask/base-controller" "^7.0.2" - "@metamask/eth-snap-keyring" "^5.0.1" - "@metamask/keyring-api" "^10.1.0" + "@metamask/base-controller" "^7.1.1" + "@metamask/eth-snap-keyring" "^8.0.0" + "@metamask/keyring-api" "^13.0.0" + "@metamask/keyring-internal-api" "^2.0.0" "@metamask/snaps-sdk" "^6.7.0" "@metamask/snaps-utils" "^8.3.0" - "@metamask/utils" "^10.0.0" + "@metamask/utils" "^11.0.1" deepmerge "^4.2.2" ethereum-cryptography "^2.1.2" immer "^9.0.6" @@ -4413,10 +4422,10 @@ "@metamask/utils" "^10.0.0" nanoid "^3.1.31" -"@metamask/assets-controllers@^45.1.1": - version "45.1.1" - resolved "https://registry.yarnpkg.com/@metamask/assets-controllers/-/assets-controllers-45.1.1.tgz#365be66cbd14a0fb5be57b3ec30d54f3314d70ec" - integrity sha512-zKMIKv9w4sZu0S/flbgYLAy3wgoL11GuL5RW8AF6n2/jKlMxEA0ImIK0EZGhmkJAB78ZrcVIdoLeEEvSsGuO/w== +"@metamask/assets-controllers@^46.0.0": + version "46.0.0" + resolved "https://registry.yarnpkg.com/@metamask/assets-controllers/-/assets-controllers-46.0.0.tgz#b3bb495eae490b24ea451d2e1b1a7fba7de9580e" + integrity sha512-iHaS+74ROQBynyLKUuzub5+kYVHuE29L8YJExpq4UJf4vnO7L5FTYEcNCplxBrCzS8qWinZ2RwS5OoUfvXYAKg== dependencies: "@ethereumjs/util" "^8.1.0" "@ethersproject/abi" "^5.7.0" @@ -4425,17 +4434,19 @@ "@ethersproject/contracts" "^5.7.0" "@ethersproject/providers" "^5.7.0" "@metamask/abi-utils" "^2.0.3" - "@metamask/base-controller" "^7.0.2" + "@metamask/base-controller" "^7.1.1" "@metamask/contract-metadata" "^2.4.0" - "@metamask/controller-utils" "^11.4.4" + "@metamask/controller-utils" "^11.4.5" "@metamask/eth-query" "^4.0.0" "@metamask/metamask-eth-abis" "^3.1.1" "@metamask/polling-controller" "^12.0.2" - "@metamask/rpc-errors" "^7.0.1" - "@metamask/utils" "^10.0.0" + "@metamask/rpc-errors" "^7.0.2" + "@metamask/snaps-utils" "^8.3.0" + "@metamask/utils" "^11.0.1" "@types/bn.js" "^5.1.5" "@types/uuid" "^8.3.0" async-mutex "^0.5.0" + bitcoin-address-validation "^2.2.3" bn.js "^5.2.1" cockatiel "^3.1.2" immer "^9.0.6" @@ -4444,12 +4455,12 @@ single-call-balance-checker-abi "^1.0.0" uuid "^8.3.2" -"@metamask/base-controller@^7.0.1", "@metamask/base-controller@^7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@metamask/base-controller/-/base-controller-7.0.2.tgz#bf908858215cd4f7d072b3b0f7f0946cf886ee49" - integrity sha512-zeZ5QPKedGT/r2M1NsT4lE7z4u9ciSNcOXG2vUdmfA+QT9YLwIm5+t56UGku3ZTjKGxDn9Ukca3BEkRc57Gt0A== +"@metamask/base-controller@^7.0.1", "@metamask/base-controller@^7.0.2", "@metamask/base-controller@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@metamask/base-controller/-/base-controller-7.1.1.tgz#837216ee099563b2106202fa0ed376dc909dfbb9" + integrity sha512-4nbA6RL9y0SdHdn4MmMTREX6ISJL7OGHn0GXXszv0tp1fdjsn+SBs28uu1a9ceg1J7R/lO6JH7jAAz8zRtt8Nw== dependencies: - "@metamask/utils" "^10.0.0" + "@metamask/utils" "^11.0.1" immer "^9.0.6" "@metamask/bitcoin-wallet-snap@^0.8.2": @@ -4491,15 +4502,15 @@ resolved "https://registry.yarnpkg.com/@metamask/contract-metadata/-/contract-metadata-2.5.0.tgz#33921fa9c15eb1863f55dcd5f75467ae15614ebb" integrity sha512-+j7jEcp0P1OUMEpa/OIwfJs/ahBC/akwgWxaRTSWX2SWABvlUKBVRMtslfL94Qj2wN2xw8xjaUy5nSHqrznqDA== -"@metamask/controller-utils@^11.0.0", "@metamask/controller-utils@^11.3.0", "@metamask/controller-utils@^11.4.1", "@metamask/controller-utils@^11.4.4": - version "11.4.4" - resolved "https://registry.yarnpkg.com/@metamask/controller-utils/-/controller-utils-11.4.4.tgz#6e43e4cf53d34dad225bab8aaf4e7efcb1fe7623" - integrity sha512-0/gKC6jxlj8KRzi0RjGDQnml6l4b46Da/AIqnGJMOC59zl4qD5UN1GM+mq7L5duw/m8sSHa7VbL1hL0l7Cw1pg== +"@metamask/controller-utils@^11.0.0", "@metamask/controller-utils@^11.3.0", "@metamask/controller-utils@^11.4.1", "@metamask/controller-utils@^11.4.4", "@metamask/controller-utils@^11.4.5": + version "11.4.5" + resolved "https://registry.yarnpkg.com/@metamask/controller-utils/-/controller-utils-11.4.5.tgz#eb70dd403bc34e584a5b520956ef3bd71e7701e7" + integrity sha512-nSiZU0Yos6+cOtoBOzxMQKtdh3eRhUQJHVlH66fiyS/Eh3LARVMIcQ/BFvi3tcez35W21CnRzRbh2eBH9jeKoQ== dependencies: "@ethereumjs/util" "^8.1.0" "@metamask/eth-query" "^4.0.0" "@metamask/ethjs-unit" "^0.3.0" - "@metamask/utils" "^10.0.0" + "@metamask/utils" "^11.0.1" "@spruceid/siwe-parser" "2.1.0" "@types/bn.js" "^5.1.5" bignumber.js "^9.1.2" @@ -4637,14 +4648,14 @@ ethereum-cryptography "^2.1.2" tweetnacl "^1.0.3" -"@metamask/eth-sig-util@^8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-8.0.0.tgz#6310d93cd1101cab3cc6bc2a1ff526290ed2695b" - integrity sha512-IwE6aoxUL39IhmsAgE4nk+OZbNo+ThFZRNsUjE1pjdEa4MFpWzm1Rue4zJ5DMy1oUyZBi/aiCLMhdMnjl2bh2Q== +"@metamask/eth-sig-util@^8.0.0", "@metamask/eth-sig-util@^8.1.2": + version "8.1.2" + resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-8.1.2.tgz#8869bd9cdc989af7402812d5fa4d9a0f6cc30b98" + integrity sha512-+M7TKF8+RwqmfmDCfhgn7jDLtWfbpPCuBfkYPBpk9ptuqINu7+QzthNlU0Rn7jiJ//buyg2pModXVtpRBmgAeA== dependencies: "@ethereumjs/util" "^8.1.0" - "@metamask/abi-utils" "^2.0.4" - "@metamask/utils" "^9.0.0" + "@metamask/abi-utils" "^3.0.0" + "@metamask/utils" "^11.0.1" "@scure/base" "~1.1.3" ethereum-cryptography "^2.1.2" tweetnacl "^1.0.3" @@ -4660,20 +4671,45 @@ ethereum-cryptography "^2.1.2" randombytes "^2.1.0" -"@metamask/eth-snap-keyring@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@metamask/eth-snap-keyring/-/eth-snap-keyring-5.0.1.tgz#aa16a5d623a4c68b44afaa9cd47f45a409f922d1" - integrity sha512-XP/gmuLyI8jYItpFiqQUGB5/d+Qft6dCqs9vCcbMzTEvyatJtP0bkk2ZWmpfnrVGQIlepY/nhOD25NDE7mgwEg== +"@metamask/eth-snap-keyring@^7.0.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@metamask/eth-snap-keyring/-/eth-snap-keyring-7.1.0.tgz#d472ff8c9abee1f438398d749408cd12ee44ada7" + integrity sha512-aOP8WkapqFmne7xt7Xo39YPxA3fvwSzKEO+Eo+o76r4rBAutH6QLNO9gmy6e4wm2TG9hHzsQjceZmLns75suvg== dependencies: "@ethereumjs/tx" "^4.2.0" - "@metamask/eth-sig-util" "^8.0.0" + "@metamask/eth-sig-util" "^8.1.2" + "@metamask/keyring-api" "^13.0.0" + "@metamask/keyring-internal-api" "^1.1.0" + "@metamask/keyring-internal-snap-client" "^1.1.0" + "@metamask/keyring-utils" "^1.0.0" "@metamask/snaps-controllers" "^9.10.0" "@metamask/snaps-sdk" "^6.7.0" "@metamask/snaps-utils" "^8.3.0" "@metamask/superstruct" "^3.1.0" - "@metamask/utils" "^9.2.1" + "@metamask/utils" "^11.0.1" "@types/uuid" "^9.0.8" uuid "^9.0.1" + webextension-polyfill "^0.12.0" + +"@metamask/eth-snap-keyring@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@metamask/eth-snap-keyring/-/eth-snap-keyring-8.0.0.tgz#60f6bdf3ed80b096172ebd983773fdc095a94c28" + integrity sha512-NLJmEcJYA+EAnX40N18aVlUZkXARHLDsJT7YoAtVBppRXZRNl9o5FGMe7xh5NrMdcy/Yss1TbQOnqyD0Ox2boA== + dependencies: + "@ethereumjs/tx" "^4.2.0" + "@metamask/eth-sig-util" "^8.1.2" + "@metamask/keyring-api" "^13.0.0" + "@metamask/keyring-internal-api" "^2.0.0" + "@metamask/keyring-internal-snap-client" "^2.0.0" + "@metamask/keyring-utils" "^1.0.0" + "@metamask/snaps-controllers" "^9.10.0" + "@metamask/snaps-sdk" "^6.7.0" + "@metamask/snaps-utils" "^8.3.0" + "@metamask/superstruct" "^3.1.0" + "@metamask/utils" "^11.0.1" + "@types/uuid" "^9.0.8" + uuid "^9.0.1" + webextension-polyfill "^0.12.0" "@metamask/etherscan-link@^2.0.0": version "2.1.0" @@ -4756,14 +4792,14 @@ bn.js "^5.2.1" uuid "^8.3.2" -"@metamask/json-rpc-engine@^10.0.0", "@metamask/json-rpc-engine@^10.0.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@metamask/json-rpc-engine/-/json-rpc-engine-10.0.1.tgz#432e4b42770ecd4da8a89f94b52cdeac982bdca3" - integrity sha512-RmoKubUhK7BtZrllJjqMiSkW0p5QIKiO8ohJoa7/pewJIgPWzPFid/5EahQ4f/sPtTH9O9ypDQF9r7DFCPFSVQ== +"@metamask/json-rpc-engine@^10.0.0", "@metamask/json-rpc-engine@^10.0.1", "@metamask/json-rpc-engine@^10.0.2": + version "10.0.2" + resolved "https://registry.yarnpkg.com/@metamask/json-rpc-engine/-/json-rpc-engine-10.0.2.tgz#9173f90ebb16054fe20d5d73a910729a014750ce" + integrity sha512-UZKKvgEGVZyBOTKe0NrERv6J4QtR1X4a3Ppa10FZ2tY+nNvwQg3gFpWPRsYNQdPDFxtIsUdrMrqKvbkYSuHZkw== dependencies: - "@metamask/rpc-errors" "^7.0.1" + "@metamask/rpc-errors" "^7.0.2" "@metamask/safe-event-emitter" "^3.0.0" - "@metamask/utils" "^10.0.0" + "@metamask/utils" "^11.0.1" "@metamask/json-rpc-engine@^8.0.1": version "8.0.2" @@ -4784,14 +4820,14 @@ "@metamask/utils" "^8.3.0" readable-stream "^3.6.2" -"@metamask/json-rpc-middleware-stream@^8.0.2", "@metamask/json-rpc-middleware-stream@^8.0.5": - version "8.0.5" - resolved "https://registry.yarnpkg.com/@metamask/json-rpc-middleware-stream/-/json-rpc-middleware-stream-8.0.5.tgz#f91ba2ebc0285b2104f36e72359cc8ba33d08d75" - integrity sha512-g/1McYbBODSceBLA/rlSqzLyHcBCOsXok776Dh4PuCo5VjdLR11I24xPwR9VIdFVsrDd+MLH1q3xpS4loydLaw== +"@metamask/json-rpc-middleware-stream@^8.0.5", "@metamask/json-rpc-middleware-stream@^8.0.6": + version "8.0.6" + resolved "https://registry.yarnpkg.com/@metamask/json-rpc-middleware-stream/-/json-rpc-middleware-stream-8.0.6.tgz#3b8a6ccbdee2176285b05a3fe644d5f9a602669a" + integrity sha512-wE2CfuNZHnWbSjLEPCCb4MSyWgbQBUI5cslGZb+uRdXNzYOM/RDfq8FAdl6HhjmldHKdBFCW0L3kDr8frgahqA== dependencies: - "@metamask/json-rpc-engine" "^10.0.1" + "@metamask/json-rpc-engine" "^10.0.2" "@metamask/safe-event-emitter" "^3.0.0" - "@metamask/utils" "^10.0.0" + "@metamask/utils" "^11.0.1" readable-stream "^3.6.2" "@metamask/key-tree@^10.0.0", "@metamask/key-tree@^10.0.1": @@ -4829,6 +4865,16 @@ uuid "^9.0.1" webextension-polyfill "^0.12.0" +"@metamask/keyring-api@^13.0.0": + version "13.0.0" + resolved "https://registry.yarnpkg.com/@metamask/keyring-api/-/keyring-api-13.0.0.tgz#593607911ffbab9df699bd3335603262f0ba12e5" + integrity sha512-8eemwtSzG3c4Q+zcWPKxRKBMuiApfvND7j0l4xt561wkksueaU2uF/SHUJ3MuPYtKh3Mg1gCcnj9sZ3rh1yRgA== + dependencies: + "@metamask/keyring-utils" "^1.0.0" + "@metamask/superstruct" "^3.1.0" + "@metamask/utils" "^11.0.1" + bech32 "^2.0.0" + "@metamask/keyring-controller@^19.0.1": version "19.0.1" resolved "https://registry.yarnpkg.com/@metamask/keyring-controller/-/keyring-controller-19.0.1.tgz#6fee40a46a780a720f4c864ea779673569be06a7" @@ -4848,6 +4894,84 @@ ethereumjs-wallet "^1.0.1" immer "^9.0.6" +"@metamask/keyring-internal-api@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@metamask/keyring-internal-api/-/keyring-internal-api-1.1.0.tgz#3614c1a9d6f88e40421c2232789529cb395a2157" + integrity sha512-bKY7Iy0JfWyHK+E3HKrGgQrJM6TY2FjrBTaBiyc4Jrl1aOh55BIW57WygSkMvHT3rsBI/Vg3GWnq1io+7PG+Zw== + dependencies: + "@metamask/keyring-api" "^13.0.0" + "@metamask/keyring-utils" "^1.0.0" + "@metamask/superstruct" "^3.1.0" + "@metamask/utils" "^11.0.1" + +"@metamask/keyring-internal-api@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@metamask/keyring-internal-api/-/keyring-internal-api-2.0.0.tgz#809b1acea178384bb704e8025d8557a30072f6f1" + integrity sha512-CG9MSt3CdcnIQpvgJ4StQqUkdVfv3YX0dXQuZG6czKtW+TNV/43xbgoaQuAk+XsisqfY5zMCmr+XTL3Wvwfc7Q== + dependencies: + "@metamask/keyring-api" "^13.0.0" + "@metamask/keyring-utils" "^1.0.0" + "@metamask/superstruct" "^3.1.0" + "@metamask/utils" "^11.0.1" + +"@metamask/keyring-internal-snap-client@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@metamask/keyring-internal-snap-client/-/keyring-internal-snap-client-1.1.0.tgz#8e6bf842502f314fecb777c31a389f779c22bb62" + integrity sha512-5sl5c9QEZ7tCWLZgBXeDc0h/QquxYmnz5jetW5LEle1wa6WaUC/qryyt4FWe/Qy8mcMO05EIOMBDMKIQfea6ww== + dependencies: + "@metamask/keyring-api" "^13.0.0" + "@metamask/keyring-snap-client" "^1.1.0" + "@metamask/keyring-utils" "^1.0.0" + "@metamask/snaps-controllers" "^9.10.0" + "@metamask/snaps-sdk" "^6.7.0" + "@metamask/snaps-utils" "^8.3.0" + webextension-polyfill "^0.12.0" + +"@metamask/keyring-internal-snap-client@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@metamask/keyring-internal-snap-client/-/keyring-internal-snap-client-2.0.0.tgz#c44194af6d880c7b39fe583f4500e740a157fd8b" + integrity sha512-jfJkpsEgaUfbvT6gvqinZB72EnqZF1PkVByalAe2M9RGIvDkSkg6VNilgkEWXtzhz0xttaYD1wET6zJdmdaNFg== + dependencies: + "@metamask/keyring-api" "^13.0.0" + "@metamask/keyring-snap-client" "^2.0.0" + "@metamask/keyring-utils" "^1.0.0" + "@metamask/snaps-controllers" "^9.10.0" + "@metamask/snaps-sdk" "^6.7.0" + "@metamask/snaps-utils" "^8.3.0" + webextension-polyfill "^0.12.0" + +"@metamask/keyring-snap-client@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@metamask/keyring-snap-client/-/keyring-snap-client-1.1.0.tgz#0d215ed923d24bebb11721ffe093ea362176adce" + integrity sha512-Iv59YZlx/P67Jz9aq5XBE3AqS2TBXVcsGppw4busdhjgUG+vC9LXf7HeXwQmhnNh8IX8YAL03dX3cATg//d0KA== + dependencies: + "@metamask/keyring-api" "^13.0.0" + "@metamask/keyring-utils" "^1.0.0" + "@metamask/superstruct" "^3.1.0" + "@types/uuid" "^9.0.8" + uuid "^9.0.1" + webextension-polyfill "^0.12.0" + +"@metamask/keyring-snap-client@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@metamask/keyring-snap-client/-/keyring-snap-client-2.0.0.tgz#395af45471ba8bf79e4778d5afd6dd56327b9e97" + integrity sha512-P6xR4sbYEp9vhg5yxTcPLDW1fFve1FHgYT72HS10KXZQvKlGgoOwZe8kcNpQGarqa/Cr4IwzpULJP8Xm/sAF+w== + dependencies: + "@metamask/keyring-api" "^13.0.0" + "@metamask/keyring-utils" "^1.0.0" + "@metamask/superstruct" "^3.1.0" + "@types/uuid" "^9.0.8" + uuid "^9.0.1" + webextension-polyfill "^0.12.0" + +"@metamask/keyring-utils@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@metamask/keyring-utils/-/keyring-utils-1.0.0.tgz#06a5df63c57304811ec56ac6e250c4628da435b8" + integrity sha512-adxVCKPHnai4w1+ZUNwL0T2DfxMpjcQucMKfa74oQuxoqjbTBDKeW6FzJwRzFspYEuRMhOLFOMUuZQQMgyF1OQ== + dependencies: + "@metamask/superstruct" "^3.1.0" + "@metamask/utils" "^9.3.0" + "@metamask/logging-controller@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@metamask/logging-controller/-/logging-controller-6.0.1.tgz#cfe858e91ba6fa490ebcf4e50bebd5f6dee0417e" @@ -5086,17 +5210,17 @@ readable-stream "^3.6.2" webextension-polyfill "^0.10.0" -"@metamask/providers@^18.1.0", "@metamask/providers@^18.1.1": - version "18.2.0" - resolved "https://registry.yarnpkg.com/@metamask/providers/-/providers-18.2.0.tgz#557ca488a58966e288e50d629f104b061f8a76d2" - integrity sha512-GYgweLy6N1/aNNAg4CLhtfu9XwNNG+MZNlPCAl6Ai92+GKCUFRp8RTTBN81EzTLJvJu7NKIrw99AApJACJ8KRA== +"@metamask/providers@^18.1.1", "@metamask/providers@^18.3.1": + version "18.3.1" + resolved "https://registry.yarnpkg.com/@metamask/providers/-/providers-18.3.1.tgz#b49351134cbe77034db67774cc4ff473e259158c" + integrity sha512-4wHCA24KDwq/eVnAu+/+N7BEuMNN63kdN295u8Wkdc76puyig3lJdcGGne+TEjiILG34twr9rjZPOWTCwOUcDg== dependencies: - "@metamask/json-rpc-engine" "^10.0.1" - "@metamask/json-rpc-middleware-stream" "^8.0.5" + "@metamask/json-rpc-engine" "^10.0.2" + "@metamask/json-rpc-middleware-stream" "^8.0.6" "@metamask/object-multiplex" "^2.0.0" - "@metamask/rpc-errors" "^7.0.1" + "@metamask/rpc-errors" "^7.0.2" "@metamask/safe-event-emitter" "^3.1.1" - "@metamask/utils" "^10.0.0" + "@metamask/utils" "^11.0.1" detect-browser "^5.2.0" extension-port-stream "^4.1.0" fast-deep-equal "^3.1.3" @@ -5146,12 +5270,12 @@ "@metamask/utils" "^10.0.0" cockatiel "^3.1.2" -"@metamask/rpc-errors@7.0.1", "@metamask/rpc-errors@^6.2.1", "@metamask/rpc-errors@^7.0.0", "@metamask/rpc-errors@^7.0.1": - version "7.0.1" - resolved "https://registry.yarnpkg.com/@metamask/rpc-errors/-/rpc-errors-7.0.1.tgz#0eb2231a1d5e6bb102df5ac07f365c695bf70055" - integrity sha512-EeQGYioq845w2iBmiR9LHYqHhYIaeDTmxprHpPE3BTlkLB74P0xLv/TivOn4snNLowiC5ekOXfcUzCQszTDmSg== +"@metamask/rpc-errors@7.0.2", "@metamask/rpc-errors@^6.2.1", "@metamask/rpc-errors@^7.0.0", "@metamask/rpc-errors@^7.0.1", "@metamask/rpc-errors@^7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@metamask/rpc-errors/-/rpc-errors-7.0.2.tgz#d07b2ebfcf111556dfe93dc78699742ebe755359" + integrity sha512-YYYHsVYd46XwY2QZzpGeU4PSdRhHdxnzkB8piWGvJW2xbikZ3R+epAYEL4q/K8bh9JPTucsUdwRFnACor1aOYw== dependencies: - "@metamask/utils" "^10.0.0" + "@metamask/utils" "^11.0.1" fast-safe-stringify "^2.0.6" "@metamask/safe-event-emitter@^2.0.0": @@ -5415,10 +5539,10 @@ lodash "^4.17.21" uuid "^8.3.2" -"@metamask/utils@^10.0.0", "@metamask/utils@^10.0.1", "@metamask/utils@^8.2.0", "@metamask/utils@^8.3.0", "@metamask/utils@^9.0.0", "@metamask/utils@^9.1.0", "@metamask/utils@^9.2.1": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-10.0.1.tgz#a765f96c20e35fc51c068fb9f88a3332b40b215e" - integrity sha512-zHgAitJtRwviVVFnRUA2PLRMaAwatr3jiHgiH7mPicJaeSK4ma01aGR4fHy0iy5tlVo1ZiioTmJ1Hbp8FZ6pSg== +"@metamask/utils@^10.0.0", "@metamask/utils@^10.0.1", "@metamask/utils@^11.0.1", "@metamask/utils@^8.2.0", "@metamask/utils@^8.3.0", "@metamask/utils@^9.0.0", "@metamask/utils@^9.1.0", "@metamask/utils@^9.2.1", "@metamask/utils@^9.3.0": + version "11.0.1" + resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-11.0.1.tgz#16c4135489204fefe128b5e6c2b92c014453e1d5" + integrity sha512-tZlBvEJ6VhhfEiMV+Ad8rWRMjHKpbMogG01YU22JlsIeJptgIdZX1G8jJzhZH0Gxrixa2BeARh7m9lZWQo6rMg== dependencies: "@ethereumjs/tx" "^4.2.0" "@metamask/superstruct" "^3.1.0" @@ -12640,7 +12764,7 @@ bip66@^1.1.5: dependencies: safe-buffer "^5.0.1" -bitcoin-address-validation@2.2.3: +bitcoin-address-validation@2.2.3, bitcoin-address-validation@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/bitcoin-address-validation/-/bitcoin-address-validation-2.2.3.tgz#ffae6d48facd5ce7ef60574891aab979d21f9828" integrity sha512-1uGCGl26Ye8JG5qcExtFLQfuib6qEZWNDo1ZlLlwp/z7ygUFby3IxolgEfgMGaC+LG9csbVASLcH8fRLv7DIOg==