diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index 0eaeefaa8..e8820d602 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -1,8 +1,8 @@ import extension from 'extensionizer'; +import { getBlockExplorerLink } from '@metamask/etherscan-link'; import { getEnvironmentType, checkForError } from '../lib/util'; import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app'; import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction'; -import { getBlockExplorerUrlForTx } from '../../../shared/modules/transaction.utils'; export default class ExtensionPlatform { // @@ -192,7 +192,7 @@ export default class ExtensionPlatform { _showConfirmedTransaction(txMeta, rpcPrefs) { this._subscribeToNotificationClicked(); - const url = getBlockExplorerUrlForTx(txMeta, rpcPrefs); + const url = getBlockExplorerLink(txMeta, rpcPrefs); const nonce = parseInt(txMeta.txParams.nonce, 16); const title = 'Confirmed transaction'; diff --git a/shared/modules/tests/transaction.utils.test.js b/shared/modules/tests/transaction.utils.test.js deleted file mode 100644 index 0da399f34..000000000 --- a/shared/modules/tests/transaction.utils.test.js +++ /dev/null @@ -1,96 +0,0 @@ -import { strict as assert } from 'assert'; -import { - MAINNET_CHAIN_ID, - MAINNET_NETWORK_ID, - ROPSTEN_CHAIN_ID, - ROPSTEN_NETWORK_ID, -} from '../../constants/network'; -import { getBlockExplorerUrlForTx } from '../transaction.utils'; - -const tests = [ - { - expected: 'https://etherscan.io/tx/0xabcd', - transaction: { - metamaskNetworkId: MAINNET_NETWORK_ID, - hash: '0xabcd', - }, - }, - { - expected: 'https://ropsten.etherscan.io/tx/0xdef0', - transaction: { - metamaskNetworkId: ROPSTEN_NETWORK_ID, - hash: '0xdef0', - }, - rpcPrefs: {}, - }, - { - // test handling of `blockExplorerUrl` for a custom RPC - expected: 'https://block.explorer/tx/0xabcd', - transaction: { - metamaskNetworkId: '31', - hash: '0xabcd', - }, - rpcPrefs: { - blockExplorerUrl: 'https://block.explorer', - }, - }, - { - // test handling of trailing `/` in `blockExplorerUrl` for a custom RPC - expected: 'https://another.block.explorer/tx/0xdef0', - transaction: { - networkId: '33', - hash: '0xdef0', - }, - rpcPrefs: { - blockExplorerUrl: 'https://another.block.explorer/', - }, - }, - { - expected: 'https://etherscan.io/tx/0xabcd', - transaction: { - chainId: MAINNET_CHAIN_ID, - hash: '0xabcd', - }, - }, - { - expected: 'https://ropsten.etherscan.io/tx/0xdef0', - transaction: { - chainId: ROPSTEN_CHAIN_ID, - hash: '0xdef0', - }, - rpcPrefs: {}, - }, - { - // test handling of `blockExplorerUrl` for a custom RPC - expected: 'https://block.explorer/tx/0xabcd', - transaction: { - chainId: '0x1f', - hash: '0xabcd', - }, - rpcPrefs: { - blockExplorerUrl: 'https://block.explorer', - }, - }, - { - // test handling of trailing `/` in `blockExplorerUrl` for a custom RPC - expected: 'https://another.block.explorer/tx/0xdef0', - transaction: { - chainId: '0x21', - hash: '0xdef0', - }, - rpcPrefs: { - blockExplorerUrl: 'https://another.block.explorer/', - }, - }, -]; - -describe('getBlockExplorerUrlForTx', function () { - tests.forEach((test) => { - it(`should return '${test.expected}' for transaction with hash: '${test.transaction.hash}'`, function () { - assert.strictEqual( - getBlockExplorerUrlForTx(test.transaction, test.rpcPrefs), - test.expected, - ); - }); - }); -}); diff --git a/shared/modules/transaction.utils.js b/shared/modules/transaction.utils.js index 47a0a4334..9e89679f8 100644 --- a/shared/modules/transaction.utils.js +++ b/shared/modules/transaction.utils.js @@ -1,37 +1,6 @@ -import { - createExplorerLink, - createExplorerLinkForChain, -} from '@metamask/etherscan-link'; - export function transactionMatchesNetwork(transaction, chainId, networkId) { if (typeof transaction.chainId !== 'undefined') { return transaction.chainId === chainId; } return transaction.metamaskNetworkId === networkId; } - -/** - * build the etherscan link for a transaction by either chainId, if available - * or metamaskNetworkId as a fallback. If rpcPrefs is provided will build the - * url for the provided blockExplorerUrl. - * - * @param {Object} transaction - a transaction object from state - * @param {string} [transaction.metamaskNetworkId] - network id tx occurred on - * @param {string} [transaction.chainId] - chain id tx occurred on - * @param {string} [transaction.hash] - hash of the transaction - * @param {Object} [rpcPrefs] - the rpc preferences for the current RPC network - * @param {string} [rpcPrefs.blockExplorerUrl] - the block explorer url for RPC - * networks - * @returns {string} - */ -export function getBlockExplorerUrlForTx(transaction, rpcPrefs = {}) { - if (rpcPrefs.blockExplorerUrl) { - return `${rpcPrefs.blockExplorerUrl.replace(/\/+$/u, '')}/tx/${ - transaction.hash - }`; - } - if (transaction.chainId) { - return createExplorerLinkForChain(transaction.hash, transaction.chainId); - } - return createExplorerLink(transaction.hash, transaction.metamaskNetworkId); -} diff --git a/ui/components/app/menu-bar/account-options-menu.js b/ui/components/app/menu-bar/account-options-menu.js index f71908aae..3ae13c385 100644 --- a/ui/components/app/menu-bar/account-options-menu.js +++ b/ui/components/app/menu-bar/account-options-menu.js @@ -2,11 +2,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useHistory } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; +import { getAccountLink } from '@metamask/etherscan-link'; import { showModal } from '../../../store/actions'; import { CONNECTED_ROUTE } from '../../../helpers/constants/routes'; import { Menu, MenuItem } from '../../ui/menu'; -import getAccountLink from '../../../helpers/utils/account-link'; import { getCurrentChainId, getCurrentKeyring, @@ -14,7 +14,10 @@ import { getSelectedIdentity, } from '../../../selectors'; import { useI18nContext } from '../../../hooks/useI18nContext'; -import { useMetricEvent } from '../../../hooks/useMetricEvent'; +import { + useMetricEvent, + useNewMetricEvent, +} from '../../../hooks/useMetricEvent'; import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app'; @@ -22,6 +25,14 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) { const t = useI18nContext(); const dispatch = useDispatch(); const history = useHistory(); + + const keyring = useSelector(getCurrentKeyring); + const chainId = useSelector(getCurrentChainId); + const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); + const selectedIdentity = useSelector(getSelectedIdentity); + const { address } = selectedIdentity; + const addressLink = getAccountLink(address, chainId, rpcPrefs); + const openFullscreenEvent = useMetricEvent({ eventOpts: { category: 'Navigation', @@ -36,13 +47,7 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) { name: 'Viewed Account Details', }, }); - const viewOnEtherscanEvent = useMetricEvent({ - eventOpts: { - category: 'Navigation', - action: 'Account Options', - name: 'Clicked View on Etherscan', - }, - }); + const openConnectedSitesEvent = useMetricEvent({ eventOpts: { category: 'Navigation', @@ -51,12 +56,16 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) { }, }); - const keyring = useSelector(getCurrentKeyring); - const chainId = useSelector(getCurrentChainId); - const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); - const selectedIdentity = useSelector(getSelectedIdentity); + const blockExplorerLinkClickedEvent = useNewMetricEvent({ + category: 'Navigation', + event: 'Clicked Block Explorer Link', + properties: { + link_type: 'Account Tracker', + action: 'Account Options', + block_explorer_domain: addressLink ? new URL(addressLink)?.hostname : '', + }, + }); - const { address } = selectedIdentity; const isRemovable = keyring.type !== 'HD Key Tree'; return ( @@ -90,9 +99,9 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) { { - viewOnEtherscanEvent(); + blockExplorerLinkClickedEvent(); global.platform.openTab({ - url: getAccountLink(address, chainId, rpcPrefs), + url: addressLink, }); onClose(); }} diff --git a/ui/components/app/modals/account-details-modal/account-details-modal.component.js b/ui/components/app/modals/account-details-modal/account-details-modal.component.js index c314d6979..6dc47f3d2 100644 --- a/ui/components/app/modals/account-details-modal/account-details-modal.component.js +++ b/ui/components/app/modals/account-details-modal/account-details-modal.component.js @@ -1,7 +1,8 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import { getAccountLink } from '@metamask/etherscan-link'; + import AccountModalContainer from '../account-modal-container'; -import getAccountLink from '../../../../helpers/utils/account-link'; import QrView from '../../../ui/qr-code'; import EditableLabel from '../../../ui/editable-label'; import Button from '../../../ui/button'; @@ -18,6 +19,7 @@ export default class AccountDetailsModal extends Component { static contextTypes = { t: PropTypes.func, + trackEvent: PropTypes.func, }; render() { @@ -61,8 +63,20 @@ export default class AccountDetailsModal extends Component { type="secondary" className="account-details-modal__button" onClick={() => { + const accountLink = getAccountLink(address, chainId, rpcPrefs); + this.context.trackEvent({ + category: 'Navigation', + event: 'Clicked Block Explorer Link', + properties: { + link_type: 'Account Tracker', + action: 'Account Details Modal', + block_explorer_domain: accountLink + ? new URL(accountLink)?.hostname + : '', + }, + }); global.platform.openTab({ - url: getAccountLink(address, chainId, rpcPrefs), + url: accountLink, }); }} > diff --git a/ui/components/app/modals/account-details-modal/account-details-modal.test.js b/ui/components/app/modals/account-details-modal/account-details-modal.test.js index a0e7a93ed..2f0fdb788 100644 --- a/ui/components/app/modals/account-details-modal/account-details-modal.test.js +++ b/ui/components/app/modals/account-details-modal/account-details-modal.test.js @@ -36,6 +36,7 @@ describe('Account Details Modal', () => { wrapper = shallow(, { context: { t: (str) => str, + trackEvent: (e) => e, }, }); }); 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 4a7785d4d..5b90f0359 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,9 +1,9 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import { getAccountLink } from '@metamask/etherscan-link'; import Modal from '../../modal'; import { addressSummary } from '../../../../helpers/utils/util'; import Identicon from '../../../ui/identicon'; -import getAccountLink from '../../../../helpers/utils/account-link'; export default class ConfirmRemoveAccount extends Component { static propTypes = { @@ -16,6 +16,7 @@ export default class ConfirmRemoveAccount extends Component { static contextTypes = { t: PropTypes.func, + trackEvent: PropTypes.func, }; handleRemove = () => { @@ -30,7 +31,7 @@ export default class ConfirmRemoveAccount extends Component { renderSelectedAccount() { const { t } = this.context; - const { identity } = this.props; + const { identity, rpcPrefs, chainId } = this.props; return (
@@ -53,11 +54,27 @@ export default class ConfirmRemoveAccount extends Component {
{ + const accountLink = getAccountLink( + identity.address, + chainId, + rpcPrefs, + ); + this.context.trackEvent({ + category: 'Accounts', + event: 'Clicked Block Explorer Link', + properties: { + link_type: 'Account Tracker', + action: 'Remove Account', + block_explorer_domain: accountLink + ? new URL(accountLink)?.hostname + : '', + }, + }); + global.platform.openTab({ + url: accountLink, + }); + }} target="_blank" rel="noopener noreferrer" title={t('etherscanView')} diff --git a/ui/components/app/transaction-activity-log/transaction-activity-log.component.js b/ui/components/app/transaction-activity-log/transaction-activity-log.component.js index 641976046..2e7cb77d3 100644 --- a/ui/components/app/transaction-activity-log/transaction-activity-log.component.js +++ b/ui/components/app/transaction-activity-log/transaction-activity-log.component.js @@ -2,18 +2,19 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; +import { getBlockExplorerLink } from '@metamask/etherscan-link'; import { getEthConversionFromWeiHex, getValueFromWeiHex, } from '../../../helpers/utils/conversions.util'; import { formatDate } from '../../../helpers/utils/util'; -import { getBlockExplorerUrlForTx } from '../../../../shared/modules/transaction.utils'; import TransactionActivityLogIcon from './transaction-activity-log-icon'; import { CONFIRMED_STATUS } from './transaction-activity-log.constants'; export default class TransactionActivityLog extends PureComponent { static contextTypes = { t: PropTypes.func, + trackEvent: PropTypes.func, }; static propTypes = { @@ -31,10 +32,21 @@ export default class TransactionActivityLog extends PureComponent { }; handleActivityClick = (activity) => { - const etherscanUrl = getBlockExplorerUrlForTx( - activity, - this.props.rpcPrefs, - ); + const { rpcPrefs } = this.props; + const etherscanUrl = getBlockExplorerLink(activity, rpcPrefs); + + this.context.trackEvent({ + category: 'Transactions', + event: 'Clicked Block Explorer Link', + properties: { + link_type: 'Transaction Block Explorer', + action: 'Activity Details', + block_explorer_domain: etherscanUrl + ? new URL(etherscanUrl)?.hostname + : '', + }, + }); + global.platform.openTab({ url: etherscanUrl }); }; diff --git a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js index c5f03eeb4..f90bf6a9a 100644 --- a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js +++ b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js @@ -1,6 +1,7 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import copyToClipboard from 'copy-to-clipboard'; +import { getBlockExplorerLink } from '@metamask/etherscan-link'; import SenderToRecipient from '../../ui/sender-to-recipient'; import { FLAT_VARIANT } from '../../ui/sender-to-recipient/sender-to-recipient.constants'; import TransactionActivityLog from '../transaction-activity-log'; @@ -9,13 +10,13 @@ import Button from '../../ui/button'; import Tooltip from '../../ui/tooltip'; import Copy from '../../ui/icon/copy-icon.component'; import Popover from '../../ui/popover'; -import { getBlockExplorerUrlForTx } from '../../../../shared/modules/transaction.utils'; import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction'; export default class TransactionListItemDetails extends PureComponent { static contextTypes = { t: PropTypes.func, metricsEvent: PropTypes.func, + trackEvent: PropTypes.func, }; static defaultProps = { @@ -47,22 +48,30 @@ export default class TransactionListItemDetails extends PureComponent { justCopied: false, }; - handleEtherscanClick = () => { + handleBlockExplorerClick = () => { const { transactionGroup: { primaryTransaction }, rpcPrefs, } = this.props; + const blockExplorerLink = getBlockExplorerLink( + primaryTransaction, + rpcPrefs, + ); - this.context.metricsEvent({ - eventOpts: { - category: 'Navigation', - action: 'Activity Log', - name: 'Clicked "View on Etherscan"', + this.context.trackEvent({ + category: 'Transactions', + event: 'Clicked Block Explorer Link', + properties: { + link_type: 'Transaction Block Explorer', + action: 'Transaction Details', + block_explorer_domain: blockExplorerLink + ? new URL(blockExplorerLink)?.hostname + : '', }, }); global.platform.openTab({ - url: getBlockExplorerUrlForTx(primaryTransaction, rpcPrefs), + url: blockExplorerLink, }); }; @@ -203,7 +212,7 @@ export default class TransactionListItemDetails extends PureComponent { >