From 0f6a897836ca58d61246d41f4ac741757fe26c50 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Fri, 21 Jun 2024 23:21:07 +0800 Subject: [PATCH 01/22] fix: fix addresses in qr --- .../address-copy-button.js | 5 +- .../ui/qr-code-view/qr-code-view.test.tsx | 99 +++++++++++++++++++ .../{qr-code-view.js => qr-code-view.tsx} | 16 ++- ui/store/store.ts | 10 ++ 4 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 ui/components/ui/qr-code-view/qr-code-view.test.tsx rename ui/components/ui/qr-code-view/{qr-code-view.js => qr-code-view.tsx} (84%) diff --git a/ui/components/multichain/address-copy-button/address-copy-button.js b/ui/components/multichain/address-copy-button/address-copy-button.js index 1e632d9206d7..b14cf017790b 100644 --- a/ui/components/multichain/address-copy-button/address-copy-button.js +++ b/ui/components/multichain/address-copy-button/address-copy-button.js @@ -25,7 +25,7 @@ import { shortenAddress } from '../../../helpers/utils/util'; import Tooltip from '../../ui/tooltip/tooltip'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { MINUTE } from '../../../../shared/constants/time'; -import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; +import { normalizeSafeAddress } from '../../../../app/scripts/lib/multichain/address'; export const AddressCopyButton = ({ address, @@ -33,7 +33,7 @@ export const AddressCopyButton = ({ wrap = false, onClick, }) => { - const checksummedAddress = toChecksumHexAddress(address); + const checksummedAddress = normalizeSafeAddress(address); const displayAddress = shorten ? shortenAddress(checksummedAddress) : checksummedAddress; @@ -66,6 +66,7 @@ export const AddressCopyButton = ({ ///: END:ONLY_INCLUDE_IF backgroundColor={BackgroundColor.primaryMuted} onClick={() => { + console.log('address', checksummedAddress); handleCopy(checksummedAddress); onClick?.(); }} diff --git a/ui/components/ui/qr-code-view/qr-code-view.test.tsx b/ui/components/ui/qr-code-view/qr-code-view.test.tsx new file mode 100644 index 000000000000..c0b2da48ceca --- /dev/null +++ b/ui/components/ui/qr-code-view/qr-code-view.test.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { renderWithProvider } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import mockState from '../../../../test/data/mock-state.json'; +import QRCodeView from './qr-code-view'; +import userEvent from '@testing-library/user-event'; + +const mockCopy = jest.fn(); +jest.mock('copy-to-clipboard', () => ({ + default: () => mockCopy, +})); + +const mockEthAddress = '0x467060a50cb7bbd2209017323b794130184195a0'; +const mockBtcAddress = 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq'; + +const render = ( + { + Qr, + warning, + }: { Qr: { message: string; data: string }; warning: null | string } = { + Qr: { data: mockEthAddress, message: '' }, + warning: '', + }, +) => { + const store = configureStore({ + metamask: { + ...mockState.metamask, + }, + }); + return renderWithProvider(, store); +}; + +describe('QRCodeView', () => { + afterEach(() => jest.clearAllMocks()); + + it('renders QR code image', () => { + const { container } = render(); + const qrCodeImage = container.querySelector( + '[data-testid="qr-code-image"]', + ); + expect(qrCodeImage).toBeInTheDocument(); + }); + + // @ts-expect-error This is missing from the Mocha type definitions + it.each([ + { + test: 'lowercased ETH address to checksummed', + data: mockEthAddress.toLowerCase(), + expected: mockEthAddress, + message: '', + }, + // { + // test: 'checksummed ETH address', + // data: mockEthAddress, + // expected: mockEthAddress, + // message: '', + // }, + // { + // test: 'BTC address', + // data: mockBtcAddress, + // expected: mockBtcAddress, + // message: '', + // }, + ])( + 'it renders the $test', + async ({ + data, + message, + expected, + }: { + data: string; + message: string; + expected: string; + }) => { + const user = userEvent.setup(); + const { container, getByText } = render({ + Qr: { data, message }, + warning: '', + }); + const qrCodeImage = container.querySelector( + '[data-testid="qr-code-image"]', + ); + expect(qrCodeImage).toBeInTheDocument(); + + const copyButton = container.querySelector( + '[data-testid="address-copy-button-text"]', + ); + + expect(copyButton).toBeInTheDocument(); + await user.click(copyButton as HTMLElement); + + await waitFor(() => { + expect(getByText('Copied.')).toBeInTheDocument(); + expect(mockCopy).toHaveBeenCalledWith(expected); + }); + }, + ); +}); diff --git a/ui/components/ui/qr-code-view/qr-code-view.js b/ui/components/ui/qr-code-view/qr-code-view.tsx similarity index 84% rename from ui/components/ui/qr-code-view/qr-code-view.js rename to ui/components/ui/qr-code-view/qr-code-view.tsx index c10458e33bc9..f8a8cb4e1304 100644 --- a/ui/components/ui/qr-code-view/qr-code-view.js +++ b/ui/components/ui/qr-code-view/qr-code-view.tsx @@ -3,7 +3,7 @@ import React, { useContext } from 'react'; import qrCode from 'qrcode-generator'; import { connect } from 'react-redux'; import { isHexPrefixed } from 'ethereumjs-util'; -import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; +import { normalizeSafeAddress } from '../../../../app/scripts/lib/multichain/address'; import { AddressCopyButton } from '../../multichain'; import Box from '../box/box'; import { MetaMetricsContext } from '../../../contexts/metametrics'; @@ -11,10 +11,11 @@ import { MetaMetricsEventCategory, MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; +import type { CombinedBackgroundAndReduxState } from '../../../store/store'; export default connect(mapStateToProps)(QrCodeView); -function mapStateToProps(state) { +function mapStateToProps(state: CombinedBackgroundAndReduxState) { const { buyView, warning } = state.appState; return { // Qr code is not fetched from state. 'message' and 'data' props are passed instead. @@ -23,12 +24,18 @@ function mapStateToProps(state) { }; } -function QrCodeView({ Qr, warning }) { +function QrCodeView({ + Qr, + warning, +}: { + Qr: { message: string; data: string }; + warning: null | string; +}) { const trackEvent = useContext(MetaMetricsContext); const { message, data } = Qr; const address = `${ isHexPrefixed(data) ? 'ethereum:' : '' - }${toChecksumHexAddress(data)}`; + }${normalizeSafeAddress(data)}`; const qrImage = qrCode(4, 'M'); qrImage.addData(address); qrImage.make(); @@ -52,6 +59,7 @@ function QrCodeView({ Qr, warning }) { )} {warning ? {warning} : null}
Date: Wed, 26 Jun 2024 12:49:31 +0800 Subject: [PATCH 02/22] feat: support multichain blockexplorer --- shared/constants/multichain/networks.ts | 6 ++ .../view-explorer-menu-item.test.js | 30 -------- .../view-explorer-menu-item.test.tsx | 77 +++++++++++++++++++ ...nu-item.js => view-explorer-menu-item.tsx} | 49 +++++++----- ui/helpers/utils/multichain/blockExplorer.ts | 37 +++++++++ ui/selectors/multichain.ts | 31 +++++++- 6 files changed, 180 insertions(+), 50 deletions(-) delete mode 100644 ui/components/multichain/menu-items/view-explorer-menu-item.test.js create mode 100644 ui/components/multichain/menu-items/view-explorer-menu-item.test.tsx rename ui/components/multichain/menu-items/{view-explorer-menu-item.js => view-explorer-menu-item.tsx} (71%) create mode 100644 ui/helpers/utils/multichain/blockExplorer.ts diff --git a/shared/constants/multichain/networks.ts b/shared/constants/multichain/networks.ts index de5e50639374..6418051cca55 100644 --- a/shared/constants/multichain/networks.ts +++ b/shared/constants/multichain/networks.ts @@ -16,6 +16,10 @@ export enum MultichainNetworks { export const BITCOIN_TOKEN_IMAGE_URL = './images/bitcoin-logo.svg'; +export const MULTICHAIN_NETWORK_TO_EXPLORER_URL = { + [MultichainNetworks.BITCOIN]: 'https://blockstream.info/address', +} as const; + export const MULTICHAIN_TOKEN_IMAGE_MAP = { [MultichainNetworks.BITCOIN]: BITCOIN_TOKEN_IMAGE_URL, } as const; @@ -33,6 +37,8 @@ export const MULTICHAIN_PROVIDER_CONFIGS: Record< type: 'rpc', rpcPrefs: { imageUrl: MULTICHAIN_TOKEN_IMAGE_MAP[MultichainNetworks.BITCOIN], + blockExplorerUrl: + MULTICHAIN_NETWORK_TO_EXPLORER_URL[MultichainNetworks.BITCOIN], }, }, }; diff --git a/ui/components/multichain/menu-items/view-explorer-menu-item.test.js b/ui/components/multichain/menu-items/view-explorer-menu-item.test.js deleted file mode 100644 index f6b1efb671eb..000000000000 --- a/ui/components/multichain/menu-items/view-explorer-menu-item.test.js +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import { fireEvent, renderWithProvider } from '../../../../test/jest'; -import configureStore from '../../../store/store'; -import mockState from '../../../../test/data/mock-state.json'; -import { ViewExplorerMenuItem } from '.'; - -const render = () => { - const store = configureStore(mockState); - return renderWithProvider( - , - store, - ); -}; - -describe('ViewExplorerMenuItem', () => { - it('renders "View on explorer"', () => { - global.platform = { openTab: jest.fn() }; - - const { getByText, getByTestId } = render(); - expect(getByText('View on explorer')).toBeInTheDocument(); - - const openExplorerTabSpy = jest.spyOn(global.platform, 'openTab'); - fireEvent.click(getByTestId('account-list-menu-open-explorer')); - expect(openExplorerTabSpy).toHaveBeenCalled(); - }); -}); diff --git a/ui/components/multichain/menu-items/view-explorer-menu-item.test.tsx b/ui/components/multichain/menu-items/view-explorer-menu-item.test.tsx new file mode 100644 index 000000000000..5cabdfed4f6e --- /dev/null +++ b/ui/components/multichain/menu-items/view-explorer-menu-item.test.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { BtcAccountType } from '@metamask/keyring-api'; +import { fireEvent, renderWithProvider } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import mockState from '../../../../test/data/mock-state.json'; +import { createMockInternalAccount } from '../../../../test/jest/mocks'; +import { ViewExplorerMenuItem } from '.'; +import { + MULTICHAIN_NETWORK_TO_EXPLORER_URL, + MultichainNetworks, +} from '../../../../shared/constants/multichain/networks'; + +const mockAccount = createMockInternalAccount({ + name: 'Account 1', + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', + snapOptions: null, +}); + +const mockNonEvmAccount = createMockInternalAccount({ + address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', + // @ts-expect-error no type in createMockInternalAccount + type: BtcAccountType.P2wpkh, +}); + +const render = (account = mockAccount) => { + const defaultState = { + ...mockState, + metamask: { + ...mockState.metamask, + completedOnboarding: true, + }, + }; + const store = configureStore(defaultState); + return renderWithProvider( + , + store, + ); +}; + +describe('ViewExplorerMenuItem', () => { + it('renders "View on explorer"', () => { + global.platform = { openTab: jest.fn(), closeCurrentWindow: jest.fn() }; + + const { getByText, getByTestId } = render(); + expect(getByText('View on explorer')).toBeInTheDocument(); + + const openExplorerTabSpy = jest.spyOn(global.platform, 'openTab'); + fireEvent.click(getByTestId('account-list-menu-open-explorer')); + expect(openExplorerTabSpy).toHaveBeenCalled(); + }); + + it('renders "View on explorer" for non-EVM account', () => { + const expectedExplorerUrl = `${ + MULTICHAIN_NETWORK_TO_EXPLORER_URL[MultichainNetworks.BITCOIN] + }/${mockNonEvmAccount.address}`; + const expectedExplorerUrlOriginWithoutProtocol = new URL( + expectedExplorerUrl, + ).origin.replace(/^https?:\/\//u, ''); + global.platform = { openTab: jest.fn(), closeCurrentWindow: jest.fn() }; + + const { getByText, getByTestId } = render(mockNonEvmAccount); + expect(getByText('View on explorer')).toBeInTheDocument(); + expect( + getByText(expectedExplorerUrlOriginWithoutProtocol), + ).toBeInTheDocument(); + + const openExplorerTabSpy = jest.spyOn(global.platform, 'openTab'); + fireEvent.click(getByTestId('account-list-menu-open-explorer')); + expect(openExplorerTabSpy).toHaveBeenCalledWith({ + url: expectedExplorerUrl, + }); + }); +}); diff --git a/ui/components/multichain/menu-items/view-explorer-menu-item.js b/ui/components/multichain/menu-items/view-explorer-menu-item.tsx similarity index 71% rename from ui/components/multichain/menu-items/view-explorer-menu-item.js rename to ui/components/multichain/menu-items/view-explorer-menu-item.tsx index 302ccb5e372a..0180d89772f1 100644 --- a/ui/components/multichain/menu-items/view-explorer-menu-item.js +++ b/ui/components/multichain/menu-items/view-explorer-menu-item.tsx @@ -3,7 +3,11 @@ import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { getAccountLink } from '@metamask/etherscan-link'; +import { parseCaipChainId } from '@metamask/utils'; +import { + getMultichainAccountLink, + getMultichainBlockexplorerUrl, +} from '../../../helpers/utils/multichain/blockExplorer'; import { MenuItem } from '../../ui/menu'; import { useI18nContext } from '../../../hooks/useI18nContext'; @@ -14,34 +18,40 @@ import { MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; import { IconName, Text } from '../../component-library'; -import { - getBlockExplorerLinkText, - getCurrentChainId, - getRpcPrefsForCurrentProvider, -} from '../../../selectors'; +import { getBlockExplorerLinkText } from '../../../selectors'; import { getURLHostName } from '../../../helpers/utils/util'; import { NETWORKS_ROUTE } from '../../../helpers/constants/routes'; -import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; +import { getMultichainNetwork } from '../../../selectors/multichain'; +import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; + +export type ViewExplorerMenuItemProps = { + metricsLocation: string; + closeMenu?: () => void; + textProps?: object; + account: object; +}; export const ViewExplorerMenuItem = ({ metricsLocation, closeMenu, textProps, - address, -}) => { + account, +}: ViewExplorerMenuItemProps) => { const t = useI18nContext(); const trackEvent = useContext(MetaMetricsContext); const history = useHistory(); - const chainId = useSelector(getCurrentChainId); - const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); - const addressLink = getAccountLink( - toChecksumHexAddress(address), - chainId, - rpcPrefs, + const multichainNetwork = useMultichainSelector( + getMultichainNetwork, + account, ); + const addressLink = getMultichainAccountLink(account, multichainNetwork); - const { blockExplorerUrl } = rpcPrefs; + const chainId = parseCaipChainId(multichainNetwork.chainId).reference; + const blockExplorerUrl = getMultichainBlockexplorerUrl( + account, + multichainNetwork, + ); const blockExplorerUrlSubTitle = getURLHostName(blockExplorerUrl); const blockExplorerLinkText = useSelector(getBlockExplorerLinkText); const openBlockExplorer = () => { @@ -58,7 +68,7 @@ export const ViewExplorerMenuItem = ({ global.platform.openTab({ url: addressLink, }); - closeMenu(); + closeMenu?.(); }; const routeToAddBlockExplorerUrl = () => { @@ -68,6 +78,7 @@ export const ViewExplorerMenuItem = ({ const LABEL = t('viewOnExplorer'); return ( + // @ts-expect-error - TODO: Fix MenuItem props types { blockExplorerLinkText.firstPart === 'addBlockExplorer' @@ -104,9 +115,9 @@ ViewExplorerMenuItem.propTypes = { */ closeMenu: PropTypes.func, /** - * Address to show account details for + * Account to show account details for */ - address: PropTypes.string.isRequired, + account: PropTypes.object.isRequired, /** * Custom properties for the menu item text */ diff --git a/ui/helpers/utils/multichain/blockExplorer.ts b/ui/helpers/utils/multichain/blockExplorer.ts new file mode 100644 index 000000000000..ae0556d6b712 --- /dev/null +++ b/ui/helpers/utils/multichain/blockExplorer.ts @@ -0,0 +1,37 @@ +import { InternalAccount, isEvmAccountType } from '@metamask/keyring-api'; +import { getAccountLink } from '@metamask/etherscan-link'; +import { isCaipChainId, parseCaipChainId } from '@metamask/utils'; +import { MultichainNetwork } from '../../../selectors/multichain'; +import { MULTICHAIN_NETWORK_TO_EXPLORER_URL } from '../../../../shared/constants/multichain/networks'; + +export const getMultichainBlockexplorerUrl = ( + account: InternalAccount, + network: MultichainNetwork, +): string => { + if (isEvmAccountType(account.type)) { + return network.network?.rpcPrefs?.blockExplorerUrl ?? ''; + } + + const multichainExplorerUrl = + MULTICHAIN_NETWORK_TO_EXPLORER_URL[ + network.chainId as keyof typeof MULTICHAIN_NETWORK_TO_EXPLORER_URL + ] ?? ''; + + return multichainExplorerUrl; +}; + +export const getMultichainAccountLink = ( + account: InternalAccount, + network: MultichainNetwork, +): string => { + if (isEvmAccountType(account.type)) { + const chainId = parseCaipChainId(network.chainId).reference; + return getAccountLink(account.address, chainId, network.network?.rpcPrefs); + } + + const multichainExplorerUrl = getMultichainBlockexplorerUrl(account, network); + + return multichainExplorerUrl + ? `${multichainExplorerUrl}/${account.address}` + : ''; +}; diff --git a/ui/selectors/multichain.ts b/ui/selectors/multichain.ts index 1fd7c00b9f34..7a5bed5e5ff8 100644 --- a/ui/selectors/multichain.ts +++ b/ui/selectors/multichain.ts @@ -1,3 +1,4 @@ +import PropTypes from 'prop-types'; import { InternalAccount, isEvmAccountType } from '@metamask/keyring-api'; import { ProviderConfig } from '@metamask/network-controller'; import type { RatesControllerState } from '@metamask/assets-controllers'; @@ -44,10 +45,38 @@ export type MultichainState = AccountsState & RatesState & BalancesState; export type MultichainNetwork = { nickname: string; isEvmNetwork: boolean; - chainId?: CaipChainId; + chainId: CaipChainId; network: ProviderConfig | MultichainProviderConfig; }; +export const MultichainNetworkProptype = PropTypes.shape({ + nickname: PropTypes.string.isRequired, + isEvmNetwork: PropTypes.bool.isRequired, + chainId: PropTypes.string, + network: PropTypes.oneOfType([ + PropTypes.shape({ + rpcUrl: PropTypes.string, + type: PropTypes.string.isRequired, + chainId: PropTypes.string.isRequired, + ticker: PropTypes.string.isRequired, + rpcPrefs: PropTypes.shape({ + blockExplorerUrl: PropTypes.string, + imageUrl: PropTypes.string, + }), + nickname: PropTypes.string, + id: PropTypes.string, + }), + PropTypes.shape({ + chainId: PropTypes.string.isRequired, + ticker: PropTypes.string.isRequired, + rpcPrefs: PropTypes.shape({ + blockExplorerUrl: PropTypes.string, + imageUrl: PropTypes.string, + }), + }), + ]).isRequired, +}); + export function getMultichainNetworkProviders( _state: MultichainState, ): MultichainProviderConfig[] { From 4d109eeab2209ced64bd1022e1596379e5f98d49 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Wed, 26 Jun 2024 16:55:54 +0800 Subject: [PATCH 03/22] fix: update confirm remove account to support non evm accounts --- .../confirm-remove-account.component.js | 24 ++-- .../confirm-remove-account.container.js | 10 +- .../confirm-remove-account.test.js | 119 +++++++++++++----- 3 files changed, 104 insertions(+), 49 deletions(-) diff --git a/ui/components/app/modals/confirm-remove-account/confirm-remove-account.component.js b/ui/components/app/modals/confirm-remove-account/confirm-remove-account.component.js index 30766907854f..aee2da8564fa 100644 --- a/ui/components/app/modals/confirm-remove-account/confirm-remove-account.component.js +++ b/ui/components/app/modals/confirm-remove-account/confirm-remove-account.component.js @@ -1,11 +1,13 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { getAccountLink } from '@metamask/etherscan-link'; +import { isEvmAccountType } from '@metamask/keyring-api'; +import { getMultichainAccountLink } from '../../../../helpers/utils/multichain/blockExplorer'; import Modal from '../../modal'; import { addressSummary, getURLHostName } from '../../../../helpers/utils/util'; import Identicon from '../../../ui/identicon'; import { MetaMetricsEventCategory } from '../../../../../shared/constants/metametrics'; import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url'; +import { MultichainNetworkProptype } from '../../../../selectors/multichain'; export default class ConfirmRemoveAccount extends Component { static propTypes = { @@ -26,8 +28,7 @@ export default class ConfirmRemoveAccount extends Component { }).isRequired, }).isRequired, }).isRequired, - chainId: PropTypes.string.isRequired, - rpcPrefs: PropTypes.object.isRequired, + network: MultichainNetworkProptype.isRequired, }; static contextTypes = { @@ -47,7 +48,8 @@ export default class ConfirmRemoveAccount extends Component { renderSelectedAccount() { const { t } = this.context; - const { account, rpcPrefs, chainId } = this.props; + const { account, network } = this.props; + return (
@@ -64,17 +66,18 @@ export default class ConfirmRemoveAccount extends Component { {t('publicAddress')} - {addressSummary(account.address, 4, 4)} + {addressSummary( + account.address, + 4, + 4, + isEvmAccountType(account.type), + )}
{ - const accountLink = getAccountLink( - account.address, - chainId, - rpcPrefs, - ); + const accountLink = getMultichainAccountLink(account, network); this.context.trackEvent({ category: MetaMetricsEventCategory.Accounts, event: 'Clicked Block Explorer Link', @@ -91,6 +94,7 @@ export default class ConfirmRemoveAccount extends Component { target="_blank" rel="noopener noreferrer" title={t('etherscanView')} + data-testid="explorer-link" > { +const mapStateToProps = (state, ownProps) => { return { - chainId: getCurrentChainId(state), - rpcPrefs: getRpcPrefsForCurrentProvider(state), + network: getMultichainNetwork(state, ownProps.account), }; }; diff --git a/ui/components/app/modals/confirm-remove-account/confirm-remove-account.test.js b/ui/components/app/modals/confirm-remove-account/confirm-remove-account.test.js index 51810a2554e9..d307ea0d8caf 100644 --- a/ui/components/app/modals/confirm-remove-account/confirm-remove-account.test.js +++ b/ui/components/app/modals/confirm-remove-account/confirm-remove-account.test.js @@ -1,34 +1,67 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; -import { fireEvent } from '@testing-library/react'; -import { EthAccountType } from '@metamask/keyring-api'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { BtcAccountType } from '@metamask/keyring-api'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; -import { ETH_EOA_METHODS } from '../../../../../shared/constants/eth-methods'; +import { createMockInternalAccount } from '../../../../../test/jest/mocks'; +import { addressSummary } from '../../../../helpers/utils/util'; +import { getMultichainAccountLink } from '../../../../helpers/utils/multichain/blockExplorer'; import ConfirmRemoveAccount from '.'; +global.platform = { openTab: jest.fn(), closeCurrentWindow: jest.fn() }; + +const mockAccount = createMockInternalAccount({ + id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + name: 'Account 1', + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', +}); + +const mockNonEvmAccount = createMockInternalAccount({ + id: 'e3a1c914-0bf3-41b3-b569-7c00185ad982', + name: 'Account 1', + address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', + type: BtcAccountType.P2wpkh, +}); + +const mockEvmNetwork = { + nickname: 'network', + isEvmNetwork: true, + chainId: 'eip155:99', + network: { + type: 'rpc', + chainId: '0x99', + rpcUrl: 'https://rpc.network', + rpcPrefs: { + blockExplorerUrl: 'https://explorer.network', + }, + }, +}; + +const mockNonEvmNetwork = { + nickname: 'network', + isEvmNetwork: true, + chainId: 'bip122:000000000019d6689c085ae165831e93', + network: { + chainId: 'bip122:000000000019d6689c085ae165831e93', + rpcPrefs: { + blockExplorerUrl: 'https://blockstream.info', + }, + }, +}; + describe('Confirm Remove Account', () => { const state = { metamask: { + completedOnboarding: true, providerConfig: { chainId: '0x99', }, internalAccounts: { accounts: { - 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3': { - address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - metadata: { - name: 'Account 1', - keyring: { - type: 'HD Key Tree', - }, - }, - options: {}, - methods: ETH_EOA_METHODS, - type: EthAccountType.Eoa, - }, + [mockAccount.id]: mockAccount, + [mockNonEvmAccount.id]: mockNonEvmAccount, }, - selectedAccount: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', + selectedAccount: mockNonEvmAccount, }, }, }; @@ -36,21 +69,8 @@ describe('Confirm Remove Account', () => { const props = { hideModal: jest.fn(), removeAccount: jest.fn().mockResolvedValue(), - account: { - address: '0x0', - id: 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3', - metadata: { - name: 'Account 1', - keyring: { - type: 'HD Key Tree', - }, - }, - options: {}, - mmethods: ETH_EOA_METHODS, - type: EthAccountType.Eoa, - }, - chainId: '0x99', - rpcPrefs: {}, + account: mockAccount, + network: mockEvmNetwork, }; const mockStore = configureMockStore()(state); @@ -98,4 +118,39 @@ describe('Confirm Remove Account', () => { expect(props.hideModal).toHaveBeenCalled(); }); + + it('should display non-EVM accounts and explorer link leads to non-EVM explorer', async () => { + const updatedProps = { + ...props, + account: mockNonEvmAccount, + network: mockNonEvmNetwork, + }; + const expectedAddressSummary = addressSummary( + mockNonEvmAccount.address, + 4, + 4, + false, + ); + + const expectedAccountLink = getMultichainAccountLink( + mockNonEvmAccount, + mockNonEvmNetwork, + ); + + const { getByText, getByTestId } = renderWithProvider( + , + mockStore, + ); + + expect(getByText(expectedAddressSummary)).toBeInTheDocument(); + + const explorerLink = getByTestId('explorer-link'); + expect(explorerLink).toBeInTheDocument(); + + fireEvent.click(explorerLink); + + expect(global.platform.openTab).toHaveBeenCalledWith({ + url: expectedAccountLink, + }); + }); }); From 6bdfe8175764445a9637648c7fdc499b2db36cf2 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Wed, 26 Jun 2024 16:57:34 +0800 Subject: [PATCH 04/22] fix: update util --- ui/helpers/utils/multichain/blockExplorer.ts | 2 +- ui/helpers/utils/util.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/helpers/utils/multichain/blockExplorer.ts b/ui/helpers/utils/multichain/blockExplorer.ts index ae0556d6b712..0baf24aa5216 100644 --- a/ui/helpers/utils/multichain/blockExplorer.ts +++ b/ui/helpers/utils/multichain/blockExplorer.ts @@ -1,6 +1,6 @@ import { InternalAccount, isEvmAccountType } from '@metamask/keyring-api'; import { getAccountLink } from '@metamask/etherscan-link'; -import { isCaipChainId, parseCaipChainId } from '@metamask/utils'; +import { parseCaipChainId } from '@metamask/utils'; import { MultichainNetwork } from '../../../selectors/multichain'; import { MULTICHAIN_NETWORK_TO_EXPLORER_URL } from '../../../../shared/constants/multichain/networks'; diff --git a/ui/helpers/utils/util.js b/ui/helpers/utils/util.js index cae0ed769267..cc5e6e182403 100644 --- a/ui/helpers/utils/util.js +++ b/ui/helpers/utils/util.js @@ -30,6 +30,7 @@ import { OUTDATED_BROWSER_VERSIONS } from '../constants/common'; import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; import { hexToDecimal } from '../../../shared/modules/conversion.utils'; import { SNAPS_VIEW_ROUTE } from '../constants/routes'; +import { normalizeSafeAddress } from '../../../app/scripts/lib/multichain/address'; export function formatDate(date, format = "M/d/y 'at' T") { if (!date) { @@ -102,7 +103,7 @@ export function addressSummary( if (!address) { return ''; } - let checked = toChecksumHexAddress(address); + let checked = normalizeSafeAddress(address); if (!includeHex) { checked = stripHexPrefix(checked); } From 27afed22891b4409e3154668dcaa684db68e5505 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Wed, 26 Jun 2024 22:33:44 +0800 Subject: [PATCH 05/22] fix: nickname popover test --- test/jest/mocks.js | 2 +- .../nickname-popovers.component.js | 37 ++++--- .../nickname-popovers.component.test.tsx | 101 ++++++++++++++++++ ui/selectors/multichain.ts | 17 +++ 4 files changed, 137 insertions(+), 20 deletions(-) create mode 100644 ui/components/app/modals/nickname-popovers/nickname-popovers.component.test.tsx diff --git a/test/jest/mocks.js b/test/jest/mocks.js index b52a0d984df3..49796dd30965 100644 --- a/test/jest/mocks.js +++ b/test/jest/mocks.js @@ -175,7 +175,7 @@ export function createMockInternalAccount({ name, type = EthAccountType.Eoa, keyringType = KeyringTypes.hd, - snapOptions, + snapOptions = undefined, } = {}) { let methods; diff --git a/ui/components/app/modals/nickname-popovers/nickname-popovers.component.js b/ui/components/app/modals/nickname-popovers/nickname-popovers.component.js index adc2ae25489c..257a4b628eb3 100644 --- a/ui/components/app/modals/nickname-popovers/nickname-popovers.component.js +++ b/ui/components/app/modals/nickname-popovers/nickname-popovers.component.js @@ -1,20 +1,22 @@ import React, { useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; -import { getAccountLink } from '@metamask/etherscan-link'; + +import { getMultichainAccountLink } from '../../../../helpers/utils/multichain/blockExplorer'; import { addToAddressBook } from '../../../../store/actions'; -import { - getRpcPrefsForCurrentProvider, - getCurrentChainId, - getAddressBook, -} from '../../../../selectors'; +import { getAddressBook } from '../../../../selectors'; import NicknamePopover from '../../../ui/nickname-popover'; import UpdateNicknamePopover from '../../../ui/update-nickname-popover/update-nickname-popover'; +import { useMultichainSelector } from '../../../../hooks/useMultichainSelector'; +import { + InternalAccountPropType, + getMultichainNetwork, +} from '../../../../selectors/multichain'; const SHOW_NICKNAME_POPOVER = 'SHOW_NICKNAME_POPOVER'; const ADD_NICKNAME_POPOVER = 'ADD_NICKNAME_POPOVER'; -const NicknamePopovers = ({ address, onClose }) => { +const NicknamePopovers = ({ account, onClose }) => { const dispatch = useDispatch(); const [popoverToDisplay, setPopoverToDisplay] = useState( @@ -22,26 +24,23 @@ const NicknamePopovers = ({ address, onClose }) => { ); const addressBook = useSelector(getAddressBook); - const chainId = useSelector(getCurrentChainId); const addressBookEntryObject = addressBook.find( - (entry) => entry.address === address, + (entry) => entry.address === account.address, ); const recipientNickname = addressBookEntryObject?.name; - const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); - - const explorerLink = getAccountLink( - address, - chainId, - { blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null }, - null, + const multichainNetwork = useMultichainSelector( + getMultichainNetwork, + account, ); + const explorerLink = getMultichainAccountLink(account, multichainNetwork); + if (popoverToDisplay === ADD_NICKNAME_POPOVER) { return ( setPopoverToDisplay(SHOW_NICKNAME_POPOVER)} @@ -55,7 +54,7 @@ const NicknamePopovers = ({ address, onClose }) => { // SHOW_NICKNAME_POPOVER case return ( setPopoverToDisplay(ADD_NICKNAME_POPOVER)} @@ -65,7 +64,7 @@ const NicknamePopovers = ({ address, onClose }) => { }; NicknamePopovers.propTypes = { - address: PropTypes.string, + account: InternalAccountPropType, onClose: PropTypes.func, }; diff --git a/ui/components/app/modals/nickname-popovers/nickname-popovers.component.test.tsx b/ui/components/app/modals/nickname-popovers/nickname-popovers.component.test.tsx new file mode 100644 index 000000000000..81c5743acf92 --- /dev/null +++ b/ui/components/app/modals/nickname-popovers/nickname-popovers.component.test.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { fireEvent } from '@testing-library/react'; +import { BtcAccountType, InternalAccount } from '@metamask/keyring-api'; +import { renderWithProvider } from '../../../../../test/jest'; +import configureStore from '../../../../store/store'; +import mockState from '../../../../../test/data/mock-state.json'; +import { + MULTICHAIN_NETWORK_TO_EXPLORER_URL, + MultichainNetworks, +} from '../../../../../shared/constants/multichain/networks'; +import { createMockInternalAccount } from '../../../../../test/jest/mocks'; +import NicknamePopover from './nickname-popovers.component'; + +const mockAccount = createMockInternalAccount({ + name: 'Account 1', + address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', +}); + +const mockNonEvmAccount = createMockInternalAccount({ + name: 'Account 1', + address: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', + // @ts-expect-error type not defined in js file + type: BtcAccountType.P2wpkh, +}); + +const mockEvmExplorer = 'http://mock-explorer.com'; + +const render = ( + { + props, + }: { + props: { + account: InternalAccount; + onClose?: () => void; + }; + } = { + props: { + account: mockAccount, + onClose: jest.fn(), + }, + }, +) => { + const store = configureStore({ + metamask: { + ...mockState.metamask, + networkConfigurations: { + chain5: { + type: 'rpc', + chainId: '0x5', + ticker: 'ETH', + nickname: 'Chain 5', + id: 'chain5', + rpcPrefs: { + blockExplorerUrl: mockEvmExplorer, + }, + }, + }, + completedOnboarding: true, + }, + }); + + return renderWithProvider(, store); +}; + +describe('NicknamePopover', () => { + it('matches snapshot', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('opens EVM block explorer', () => { + global.platform = { openTab: jest.fn(), closeCurrentWindow: jest.fn() }; + + const expectedExplorerUrl = `${mockEvmExplorer}/address/${mockAccount.address}`; + const { getByText } = render({ props: { account: mockAccount } }); + + const viewExplorerButton = getByText('View on block explorer'); + fireEvent.click(viewExplorerButton); + expect(global.platform.openTab).toHaveBeenCalledWith({ + url: expectedExplorerUrl, + }); + }); + + it('opens non-EVM block explorer', () => { + global.platform = { openTab: jest.fn(), closeCurrentWindow: jest.fn() }; + const expectedExplorerUrl = `${ + MULTICHAIN_NETWORK_TO_EXPLORER_URL[MultichainNetworks.BITCOIN] + }/${mockNonEvmAccount.address}`; + + const { getByText } = render({ + props: { account: mockNonEvmAccount }, + }); + + const viewExplorerButton = getByText('View on block explorer'); + + fireEvent.click(viewExplorerButton); + expect(global.platform.openTab).toHaveBeenCalledWith({ + url: expectedExplorerUrl, + }); + }); +}); diff --git a/ui/selectors/multichain.ts b/ui/selectors/multichain.ts index 7a5bed5e5ff8..4d20137ddde3 100644 --- a/ui/selectors/multichain.ts +++ b/ui/selectors/multichain.ts @@ -77,6 +77,23 @@ export const MultichainNetworkProptype = PropTypes.shape({ ]).isRequired, }); +export const InternalAccountPropType = PropTypes.shape({ + id: PropTypes.string.isRequired, + address: PropTypes.string.isRequired, + metadata: PropTypes.shape({ + name: PropTypes.string.isRequired, + snap: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string, + enabled: PropTypes.bool, + }), + keyring: PropTypes.shape({ + type: PropTypes.string.isRequired, + }).isRequired, + }).isRequired, + type: PropTypes.string.isRequired, +}).isRequired; + export function getMultichainNetworkProviders( _state: MultichainState, ): MultichainProviderConfig[] { From 2b73a65bb41b64a781fd2bd0645c983c70dcac33 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Wed, 26 Jun 2024 22:34:15 +0800 Subject: [PATCH 06/22] refactor: use InternalAccountPropType --- .../confirm-remove-account.component.js | 21 +++++-------------- .../confirm-remove-account.test.js | 2 +- .../view-explorer-menu-item.test.tsx | 3 +-- .../menu-items/view-explorer-menu-item.tsx | 7 +++++-- 4 files changed, 12 insertions(+), 21 deletions(-) diff --git a/ui/components/app/modals/confirm-remove-account/confirm-remove-account.component.js b/ui/components/app/modals/confirm-remove-account/confirm-remove-account.component.js index aee2da8564fa..2eaedc4f114d 100644 --- a/ui/components/app/modals/confirm-remove-account/confirm-remove-account.component.js +++ b/ui/components/app/modals/confirm-remove-account/confirm-remove-account.component.js @@ -7,27 +7,16 @@ import { addressSummary, getURLHostName } from '../../../../helpers/utils/util'; import Identicon from '../../../ui/identicon'; import { MetaMetricsEventCategory } from '../../../../../shared/constants/metametrics'; import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url'; -import { MultichainNetworkProptype } from '../../../../selectors/multichain'; +import { + InternalAccountPropType, + MultichainNetworkProptype, +} from '../../../../selectors/multichain'; export default class ConfirmRemoveAccount extends Component { static propTypes = { hideModal: PropTypes.func.isRequired, removeAccount: PropTypes.func.isRequired, - account: PropTypes.shape({ - id: PropTypes.string.isRequired, - address: PropTypes.string.isRequired, - metadata: PropTypes.shape({ - name: PropTypes.string.isRequired, - snap: PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string, - enabled: PropTypes.bool, - }), - keyring: PropTypes.shape({ - type: PropTypes.string.isRequired, - }).isRequired, - }).isRequired, - }).isRequired, + account: InternalAccountPropType.isRequired, network: MultichainNetworkProptype.isRequired, }; diff --git a/ui/components/app/modals/confirm-remove-account/confirm-remove-account.test.js b/ui/components/app/modals/confirm-remove-account/confirm-remove-account.test.js index d307ea0d8caf..454c967b8b63 100644 --- a/ui/components/app/modals/confirm-remove-account/confirm-remove-account.test.js +++ b/ui/components/app/modals/confirm-remove-account/confirm-remove-account.test.js @@ -1,6 +1,6 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; -import { fireEvent, waitFor } from '@testing-library/react'; +import { fireEvent } from '@testing-library/react'; import { BtcAccountType } from '@metamask/keyring-api'; import { renderWithProvider } from '../../../../../test/lib/render-helpers'; import { createMockInternalAccount } from '../../../../../test/jest/mocks'; diff --git a/ui/components/multichain/menu-items/view-explorer-menu-item.test.tsx b/ui/components/multichain/menu-items/view-explorer-menu-item.test.tsx index 5cabdfed4f6e..519f971e5206 100644 --- a/ui/components/multichain/menu-items/view-explorer-menu-item.test.tsx +++ b/ui/components/multichain/menu-items/view-explorer-menu-item.test.tsx @@ -4,16 +4,15 @@ import { fireEvent, renderWithProvider } from '../../../../test/jest'; import configureStore from '../../../store/store'; import mockState from '../../../../test/data/mock-state.json'; import { createMockInternalAccount } from '../../../../test/jest/mocks'; -import { ViewExplorerMenuItem } from '.'; import { MULTICHAIN_NETWORK_TO_EXPLORER_URL, MultichainNetworks, } from '../../../../shared/constants/multichain/networks'; +import { ViewExplorerMenuItem } from '.'; const mockAccount = createMockInternalAccount({ name: 'Account 1', address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', - snapOptions: null, }); const mockNonEvmAccount = createMockInternalAccount({ diff --git a/ui/components/multichain/menu-items/view-explorer-menu-item.tsx b/ui/components/multichain/menu-items/view-explorer-menu-item.tsx index 0180d89772f1..04907c221853 100644 --- a/ui/components/multichain/menu-items/view-explorer-menu-item.tsx +++ b/ui/components/multichain/menu-items/view-explorer-menu-item.tsx @@ -21,7 +21,10 @@ import { IconName, Text } from '../../component-library'; import { getBlockExplorerLinkText } from '../../../selectors'; import { getURLHostName } from '../../../helpers/utils/util'; import { NETWORKS_ROUTE } from '../../../helpers/constants/routes'; -import { getMultichainNetwork } from '../../../selectors/multichain'; +import { + InternalAccountPropType, + getMultichainNetwork, +} from '../../../selectors/multichain'; import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; export type ViewExplorerMenuItemProps = { @@ -117,7 +120,7 @@ ViewExplorerMenuItem.propTypes = { /** * Account to show account details for */ - account: PropTypes.object.isRequired, + account: InternalAccountPropType.isRequired, /** * Custom properties for the menu item text */ From 0fc049a168b94db23f981760a1b25fa9ed7bab80 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Wed, 26 Jun 2024 22:41:13 +0800 Subject: [PATCH 07/22] fix: test --- .../address-copy-button.js | 1 - .../ui/qr-code-view/qr-code-view.test.tsx | 37 +++++++++---------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/ui/components/multichain/address-copy-button/address-copy-button.js b/ui/components/multichain/address-copy-button/address-copy-button.js index b14cf017790b..460b0b48bd5d 100644 --- a/ui/components/multichain/address-copy-button/address-copy-button.js +++ b/ui/components/multichain/address-copy-button/address-copy-button.js @@ -66,7 +66,6 @@ export const AddressCopyButton = ({ ///: END:ONLY_INCLUDE_IF backgroundColor={BackgroundColor.primaryMuted} onClick={() => { - console.log('address', checksummedAddress); handleCopy(checksummedAddress); onClick?.(); }} diff --git a/ui/components/ui/qr-code-view/qr-code-view.test.tsx b/ui/components/ui/qr-code-view/qr-code-view.test.tsx index c0b2da48ceca..90d0903f8b4a 100644 --- a/ui/components/ui/qr-code-view/qr-code-view.test.tsx +++ b/ui/components/ui/qr-code-view/qr-code-view.test.tsx @@ -1,17 +1,17 @@ import React from 'react'; -import { fireEvent, waitFor } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { renderWithProvider } from '../../../../test/jest'; import configureStore from '../../../store/store'; import mockState from '../../../../test/data/mock-state.json'; import QRCodeView from './qr-code-view'; -import userEvent from '@testing-library/user-event'; const mockCopy = jest.fn(); -jest.mock('copy-to-clipboard', () => ({ - default: () => mockCopy, +jest.mock('../../../hooks/useCopyToClipboard', () => ({ + useCopyToClipboard: () => [null, mockCopy], })); -const mockEthAddress = '0x467060a50cb7bbd2209017323b794130184195a0'; +const mockEthAddress = '0x467060a50CB7bBd2209017323b794130184195a0'; const mockBtcAddress = 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq'; const render = ( @@ -50,18 +50,18 @@ describe('QRCodeView', () => { expected: mockEthAddress, message: '', }, - // { - // test: 'checksummed ETH address', - // data: mockEthAddress, - // expected: mockEthAddress, - // message: '', - // }, - // { - // test: 'BTC address', - // data: mockBtcAddress, - // expected: mockBtcAddress, - // message: '', - // }, + { + test: 'checksummed ETH address', + data: mockEthAddress, + expected: mockEthAddress, + message: '', + }, + { + test: 'BTC address', + data: mockBtcAddress, + expected: mockBtcAddress, + message: '', + }, ])( 'it renders the $test', async ({ @@ -74,7 +74,7 @@ describe('QRCodeView', () => { expected: string; }) => { const user = userEvent.setup(); - const { container, getByText } = render({ + const { container } = render({ Qr: { data, message }, warning: '', }); @@ -91,7 +91,6 @@ describe('QRCodeView', () => { await user.click(copyButton as HTMLElement); await waitFor(() => { - expect(getByText('Copied.')).toBeInTheDocument(); expect(mockCopy).toHaveBeenCalledWith(expected); }); }, From 87a440d604d1b4f0c3478355222f314db7d40eaa Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Wed, 26 Jun 2024 23:21:47 +0800 Subject: [PATCH 08/22] fix: account link --- .../account-list-item-menu/account-list-item-menu.js | 2 +- ui/components/multichain/global-menu/global-menu.js | 2 +- ui/helpers/utils/multichain/blockExplorer.ts | 9 ++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ui/components/multichain/account-list-item-menu/account-list-item-menu.js b/ui/components/multichain/account-list-item-menu/account-list-item-menu.js index 3644ba6ecb67..3bb1295eabce 100644 --- a/ui/components/multichain/account-list-item-menu/account-list-item-menu.js +++ b/ui/components/multichain/account-list-item-menu/account-list-item-menu.js @@ -181,7 +181,7 @@ export const AccountListItemMenu = ({ metricsLocation={METRICS_LOCATION} closeMenu={closeMenu} textProps={{ variant: TextVariant.bodySm }} - address={account.address} + account={account} /> {isHidden ? null : ( { )} diff --git a/ui/helpers/utils/multichain/blockExplorer.ts b/ui/helpers/utils/multichain/blockExplorer.ts index 0baf24aa5216..3fa44976ff40 100644 --- a/ui/helpers/utils/multichain/blockExplorer.ts +++ b/ui/helpers/utils/multichain/blockExplorer.ts @@ -1,8 +1,8 @@ import { InternalAccount, isEvmAccountType } from '@metamask/keyring-api'; import { getAccountLink } from '@metamask/etherscan-link'; -import { parseCaipChainId } from '@metamask/utils'; import { MultichainNetwork } from '../../../selectors/multichain'; import { MULTICHAIN_NETWORK_TO_EXPLORER_URL } from '../../../../shared/constants/multichain/networks'; +import { normalizeSafeAddress } from '../../../../app/scripts/lib/multichain/address'; export const getMultichainBlockexplorerUrl = ( account: InternalAccount, @@ -25,8 +25,11 @@ export const getMultichainAccountLink = ( network: MultichainNetwork, ): string => { if (isEvmAccountType(account.type)) { - const chainId = parseCaipChainId(network.chainId).reference; - return getAccountLink(account.address, chainId, network.network?.rpcPrefs); + return getAccountLink( + normalizeSafeAddress(account.address), + network.network.chainId, + network.network?.rpcPrefs, + ); } const multichainExplorerUrl = getMultichainBlockexplorerUrl(account, network); From 191a3626c5806ed09461ac24b357c8c808deffe7 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Wed, 26 Jun 2024 23:30:06 +0800 Subject: [PATCH 09/22] fix: update snapshot --- .../nickname-popovers.component.test.tsx.snap | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 ui/components/app/modals/nickname-popovers/__snapshots__/nickname-popovers.component.test.tsx.snap diff --git a/ui/components/app/modals/nickname-popovers/__snapshots__/nickname-popovers.component.test.tsx.snap b/ui/components/app/modals/nickname-popovers/__snapshots__/nickname-popovers.component.test.tsx.snap new file mode 100644 index 000000000000..b9fb82f037bf --- /dev/null +++ b/ui/components/app/modals/nickname-popovers/__snapshots__/nickname-popovers.component.test.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NicknamePopover matches snapshot 1`] = ` +
+
+
+`; From 5b09f95018b25b7fa6ceac38b4469bb3764ad231 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Wed, 26 Jun 2024 23:42:37 +0800 Subject: [PATCH 10/22] fix: test --- .../nickname-popovers.component.test.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ui/components/app/modals/nickname-popovers/nickname-popovers.component.test.tsx b/ui/components/app/modals/nickname-popovers/nickname-popovers.component.test.tsx index 81c5743acf92..7a427029d662 100644 --- a/ui/components/app/modals/nickname-popovers/nickname-popovers.component.test.tsx +++ b/ui/components/app/modals/nickname-popovers/nickname-popovers.component.test.tsx @@ -10,6 +10,7 @@ import { } from '../../../../../shared/constants/multichain/networks'; import { createMockInternalAccount } from '../../../../../test/jest/mocks'; import NicknamePopover from './nickname-popovers.component'; +import { normalizeSafeAddress } from '../../../../../app/scripts/lib/multichain/address'; const mockAccount = createMockInternalAccount({ name: 'Account 1', @@ -71,7 +72,10 @@ describe('NicknamePopover', () => { it('opens EVM block explorer', () => { global.platform = { openTab: jest.fn(), closeCurrentWindow: jest.fn() }; - const expectedExplorerUrl = `${mockEvmExplorer}/address/${mockAccount.address}`; + // Accounts controlelr addresses are lower cased but it gets converted to checksummed in this util + const expectedExplorerUrl = `${mockEvmExplorer}/address/${normalizeSafeAddress( + mockAccount.address, + )}`; const { getByText } = render({ props: { account: mockAccount } }); const viewExplorerButton = getByText('View on block explorer'); @@ -85,7 +89,7 @@ describe('NicknamePopover', () => { global.platform = { openTab: jest.fn(), closeCurrentWindow: jest.fn() }; const expectedExplorerUrl = `${ MULTICHAIN_NETWORK_TO_EXPLORER_URL[MultichainNetworks.BITCOIN] - }/${mockNonEvmAccount.address}`; + }/${normalizeSafeAddress(mockNonEvmAccount.address)}`; const { getByText } = render({ props: { account: mockNonEvmAccount }, From 2165ef03b2f3032281cbd57ac7bd8e4e220c8919 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Thu, 27 Jun 2024 00:32:28 +0800 Subject: [PATCH 11/22] fix: checksum --- .../lib/accounts/BalancesController.test.ts | 1 + ui/components/app/confirm/info/row/address.tsx | 10 ++++++++-- .../confirm-remove-account.test.js.snap | 17 +++++++++-------- .../confirm-remove-account.component.js | 4 +++- .../nickname-popovers.component.js | 6 ++++-- .../nickname-popovers.component.test.tsx | 2 +- .../menu-items/view-explorer-menu-item.tsx | 2 +- ...seMultichainAccountTotalFiatBalance.test.tsx | 1 - 8 files changed, 27 insertions(+), 16 deletions(-) diff --git a/app/scripts/lib/accounts/BalancesController.test.ts b/app/scripts/lib/accounts/BalancesController.test.ts index 01ce1f88c608..034d82e21a01 100644 --- a/app/scripts/lib/accounts/BalancesController.test.ts +++ b/app/scripts/lib/accounts/BalancesController.test.ts @@ -20,6 +20,7 @@ const mockBtcAccount = createMockInternalAccount({ name: 'Btc Account', // @ts-expect-error - account type may be btc or eth, mock file is not typed type: BtcAccountType.P2wpkh, + // @ts-expect-error - snap options is not typed and defaults to undefined snapOptions: { id: 'mock-btc-snap', name: 'mock-btc-snap', diff --git a/ui/components/app/confirm/info/row/address.tsx b/ui/components/app/confirm/info/row/address.tsx index 693dc41ea899..84246fc9726a 100644 --- a/ui/components/app/confirm/info/row/address.tsx +++ b/ui/components/app/confirm/info/row/address.tsx @@ -8,7 +8,10 @@ import { FlexDirection, TextColor, } from '../../../../../helpers/constants/design-system'; -import { getPetnamesEnabled } from '../../../../../selectors'; +import { + getInternalAccountByAddress, + getPetnamesEnabled, +} from '../../../../../selectors'; import { AvatarAccount, AvatarAccountSize, @@ -33,6 +36,9 @@ export const ConfirmInfoRowAddress = ({ const [isNicknamePopoverShown, setIsNicknamePopoverShown] = useState(false); const handleDisplayNameClick = () => setIsNicknamePopoverShown(true); const onCloseHandler = () => setIsNicknamePopoverShown(false); + const account = useSelector((state) => + getInternalAccountByAddress(state, address), + ); return ( {isNicknamePopoverShown ? ( - + ) : null} ) diff --git a/ui/components/app/modals/confirm-remove-account/__snapshots__/confirm-remove-account.test.js.snap b/ui/components/app/modals/confirm-remove-account/__snapshots__/confirm-remove-account.test.js.snap index e62c58582592..08f3bd4364cd 100644 --- a/ui/components/app/modals/confirm-remove-account/__snapshots__/confirm-remove-account.test.js.snap +++ b/ui/components/app/modals/confirm-remove-account/__snapshots__/confirm-remove-account.test.js.snap @@ -36,7 +36,7 @@ exports[`Confirm Remove Account should match snapshot 1`] = ` style="height: 32px; width: 32px; border-radius: 16px;" >