1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

use etherscan-link customBlockExplorer methods with customNetwork usage tracking (#11017)

* use etherscan-link customBlockExplorer methods with customNetwork usage tracking

* consolidate blockexplorer events, add domain to metametrics event

* lint fix
This commit is contained in:
Alex Donesky 2021-05-19 09:51:47 -05:00 committed by GitHub
parent b7a1c8c302
commit f19207ca87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 247 additions and 296 deletions

View File

@ -1,8 +1,8 @@
import extension from 'extensionizer'; import extension from 'extensionizer';
import { getBlockExplorerLink } from '@metamask/etherscan-link';
import { getEnvironmentType, checkForError } from '../lib/util'; import { getEnvironmentType, checkForError } from '../lib/util';
import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app'; import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app';
import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction'; import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction';
import { getBlockExplorerUrlForTx } from '../../../shared/modules/transaction.utils';
export default class ExtensionPlatform { export default class ExtensionPlatform {
// //
@ -192,7 +192,7 @@ export default class ExtensionPlatform {
_showConfirmedTransaction(txMeta, rpcPrefs) { _showConfirmedTransaction(txMeta, rpcPrefs) {
this._subscribeToNotificationClicked(); this._subscribeToNotificationClicked();
const url = getBlockExplorerUrlForTx(txMeta, rpcPrefs); const url = getBlockExplorerLink(txMeta, rpcPrefs);
const nonce = parseInt(txMeta.txParams.nonce, 16); const nonce = parseInt(txMeta.txParams.nonce, 16);
const title = 'Confirmed transaction'; const title = 'Confirmed transaction';

View File

@ -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,
);
});
});
});

View File

@ -1,37 +1,6 @@
import {
createExplorerLink,
createExplorerLinkForChain,
} from '@metamask/etherscan-link';
export function transactionMatchesNetwork(transaction, chainId, networkId) { export function transactionMatchesNetwork(transaction, chainId, networkId) {
if (typeof transaction.chainId !== 'undefined') { if (typeof transaction.chainId !== 'undefined') {
return transaction.chainId === chainId; return transaction.chainId === chainId;
} }
return transaction.metamaskNetworkId === networkId; 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);
}

View File

@ -2,11 +2,11 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { getAccountLink } from '@metamask/etherscan-link';
import { showModal } from '../../../store/actions'; import { showModal } from '../../../store/actions';
import { CONNECTED_ROUTE } from '../../../helpers/constants/routes'; import { CONNECTED_ROUTE } from '../../../helpers/constants/routes';
import { Menu, MenuItem } from '../../ui/menu'; import { Menu, MenuItem } from '../../ui/menu';
import getAccountLink from '../../../helpers/utils/account-link';
import { import {
getCurrentChainId, getCurrentChainId,
getCurrentKeyring, getCurrentKeyring,
@ -14,7 +14,10 @@ import {
getSelectedIdentity, getSelectedIdentity,
} from '../../../selectors'; } from '../../../selectors';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import { useMetricEvent } from '../../../hooks/useMetricEvent'; import {
useMetricEvent,
useNewMetricEvent,
} from '../../../hooks/useMetricEvent';
import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app'; import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app';
@ -22,6 +25,14 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) {
const t = useI18nContext(); const t = useI18nContext();
const dispatch = useDispatch(); const dispatch = useDispatch();
const history = useHistory(); 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({ const openFullscreenEvent = useMetricEvent({
eventOpts: { eventOpts: {
category: 'Navigation', category: 'Navigation',
@ -36,13 +47,7 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) {
name: 'Viewed Account Details', name: 'Viewed Account Details',
}, },
}); });
const viewOnEtherscanEvent = useMetricEvent({
eventOpts: {
category: 'Navigation',
action: 'Account Options',
name: 'Clicked View on Etherscan',
},
});
const openConnectedSitesEvent = useMetricEvent({ const openConnectedSitesEvent = useMetricEvent({
eventOpts: { eventOpts: {
category: 'Navigation', category: 'Navigation',
@ -51,12 +56,16 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) {
}, },
}); });
const keyring = useSelector(getCurrentKeyring); const blockExplorerLinkClickedEvent = useNewMetricEvent({
const chainId = useSelector(getCurrentChainId); category: 'Navigation',
const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); event: 'Clicked Block Explorer Link',
const selectedIdentity = useSelector(getSelectedIdentity); 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'; const isRemovable = keyring.type !== 'HD Key Tree';
return ( return (
@ -90,9 +99,9 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) {
</MenuItem> </MenuItem>
<MenuItem <MenuItem
onClick={() => { onClick={() => {
viewOnEtherscanEvent(); blockExplorerLinkClickedEvent();
global.platform.openTab({ global.platform.openTab({
url: getAccountLink(address, chainId, rpcPrefs), url: addressLink,
}); });
onClose(); onClose();
}} }}

View File

@ -1,7 +1,8 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { getAccountLink } from '@metamask/etherscan-link';
import AccountModalContainer from '../account-modal-container'; import AccountModalContainer from '../account-modal-container';
import getAccountLink from '../../../../helpers/utils/account-link';
import QrView from '../../../ui/qr-code'; import QrView from '../../../ui/qr-code';
import EditableLabel from '../../../ui/editable-label'; import EditableLabel from '../../../ui/editable-label';
import Button from '../../../ui/button'; import Button from '../../../ui/button';
@ -18,6 +19,7 @@ export default class AccountDetailsModal extends Component {
static contextTypes = { static contextTypes = {
t: PropTypes.func, t: PropTypes.func,
trackEvent: PropTypes.func,
}; };
render() { render() {
@ -61,8 +63,20 @@ export default class AccountDetailsModal extends Component {
type="secondary" type="secondary"
className="account-details-modal__button" className="account-details-modal__button"
onClick={() => { 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({ global.platform.openTab({
url: getAccountLink(address, chainId, rpcPrefs), url: accountLink,
}); });
}} }}
> >

View File

@ -36,6 +36,7 @@ describe('Account Details Modal', () => {
wrapper = shallow(<AccountDetailsModal.WrappedComponent {...props} />, { wrapper = shallow(<AccountDetailsModal.WrappedComponent {...props} />, {
context: { context: {
t: (str) => str, t: (str) => str,
trackEvent: (e) => e,
}, },
}); });
}); });

View File

@ -1,9 +1,9 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { getAccountLink } from '@metamask/etherscan-link';
import Modal from '../../modal'; import Modal from '../../modal';
import { addressSummary } from '../../../../helpers/utils/util'; import { addressSummary } from '../../../../helpers/utils/util';
import Identicon from '../../../ui/identicon'; import Identicon from '../../../ui/identicon';
import getAccountLink from '../../../../helpers/utils/account-link';
export default class ConfirmRemoveAccount extends Component { export default class ConfirmRemoveAccount extends Component {
static propTypes = { static propTypes = {
@ -16,6 +16,7 @@ export default class ConfirmRemoveAccount extends Component {
static contextTypes = { static contextTypes = {
t: PropTypes.func, t: PropTypes.func,
trackEvent: PropTypes.func,
}; };
handleRemove = () => { handleRemove = () => {
@ -30,7 +31,7 @@ export default class ConfirmRemoveAccount extends Component {
renderSelectedAccount() { renderSelectedAccount() {
const { t } = this.context; const { t } = this.context;
const { identity } = this.props; const { identity, rpcPrefs, chainId } = this.props;
return ( return (
<div className="confirm-remove-account__account"> <div className="confirm-remove-account__account">
<div className="confirm-remove-account__account__identicon"> <div className="confirm-remove-account__account__identicon">
@ -53,11 +54,27 @@ export default class ConfirmRemoveAccount extends Component {
<div className="confirm-remove-account__account__link"> <div className="confirm-remove-account__account__link">
<a <a
className="" className=""
href={getAccountLink( onClick={() => {
identity.address, const accountLink = getAccountLink(
this.props.chainId, identity.address,
this.props.rpcPrefs, 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" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
title={t('etherscanView')} title={t('etherscanView')}

View File

@ -2,18 +2,19 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import { getBlockExplorerLink } from '@metamask/etherscan-link';
import { import {
getEthConversionFromWeiHex, getEthConversionFromWeiHex,
getValueFromWeiHex, getValueFromWeiHex,
} from '../../../helpers/utils/conversions.util'; } from '../../../helpers/utils/conversions.util';
import { formatDate } from '../../../helpers/utils/util'; import { formatDate } from '../../../helpers/utils/util';
import { getBlockExplorerUrlForTx } from '../../../../shared/modules/transaction.utils';
import TransactionActivityLogIcon from './transaction-activity-log-icon'; import TransactionActivityLogIcon from './transaction-activity-log-icon';
import { CONFIRMED_STATUS } from './transaction-activity-log.constants'; import { CONFIRMED_STATUS } from './transaction-activity-log.constants';
export default class TransactionActivityLog extends PureComponent { export default class TransactionActivityLog extends PureComponent {
static contextTypes = { static contextTypes = {
t: PropTypes.func, t: PropTypes.func,
trackEvent: PropTypes.func,
}; };
static propTypes = { static propTypes = {
@ -31,10 +32,21 @@ export default class TransactionActivityLog extends PureComponent {
}; };
handleActivityClick = (activity) => { handleActivityClick = (activity) => {
const etherscanUrl = getBlockExplorerUrlForTx( const { rpcPrefs } = this.props;
activity, const etherscanUrl = getBlockExplorerLink(activity, rpcPrefs);
this.props.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 }); global.platform.openTab({ url: etherscanUrl });
}; };

View File

@ -1,6 +1,7 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import copyToClipboard from 'copy-to-clipboard'; import copyToClipboard from 'copy-to-clipboard';
import { getBlockExplorerLink } from '@metamask/etherscan-link';
import SenderToRecipient from '../../ui/sender-to-recipient'; import SenderToRecipient from '../../ui/sender-to-recipient';
import { FLAT_VARIANT } from '../../ui/sender-to-recipient/sender-to-recipient.constants'; import { FLAT_VARIANT } from '../../ui/sender-to-recipient/sender-to-recipient.constants';
import TransactionActivityLog from '../transaction-activity-log'; import TransactionActivityLog from '../transaction-activity-log';
@ -9,13 +10,13 @@ import Button from '../../ui/button';
import Tooltip from '../../ui/tooltip'; import Tooltip from '../../ui/tooltip';
import Copy from '../../ui/icon/copy-icon.component'; import Copy from '../../ui/icon/copy-icon.component';
import Popover from '../../ui/popover'; import Popover from '../../ui/popover';
import { getBlockExplorerUrlForTx } from '../../../../shared/modules/transaction.utils';
import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction'; import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction';
export default class TransactionListItemDetails extends PureComponent { export default class TransactionListItemDetails extends PureComponent {
static contextTypes = { static contextTypes = {
t: PropTypes.func, t: PropTypes.func,
metricsEvent: PropTypes.func, metricsEvent: PropTypes.func,
trackEvent: PropTypes.func,
}; };
static defaultProps = { static defaultProps = {
@ -47,22 +48,30 @@ export default class TransactionListItemDetails extends PureComponent {
justCopied: false, justCopied: false,
}; };
handleEtherscanClick = () => { handleBlockExplorerClick = () => {
const { const {
transactionGroup: { primaryTransaction }, transactionGroup: { primaryTransaction },
rpcPrefs, rpcPrefs,
} = this.props; } = this.props;
const blockExplorerLink = getBlockExplorerLink(
primaryTransaction,
rpcPrefs,
);
this.context.metricsEvent({ this.context.trackEvent({
eventOpts: { category: 'Transactions',
category: 'Navigation', event: 'Clicked Block Explorer Link',
action: 'Activity Log', properties: {
name: 'Clicked "View on Etherscan"', link_type: 'Transaction Block Explorer',
action: 'Transaction Details',
block_explorer_domain: blockExplorerLink
? new URL(blockExplorerLink)?.hostname
: '',
}, },
}); });
global.platform.openTab({ global.platform.openTab({
url: getBlockExplorerUrlForTx(primaryTransaction, rpcPrefs), url: blockExplorerLink,
}); });
}; };
@ -203,7 +212,7 @@ export default class TransactionListItemDetails extends PureComponent {
> >
<Button <Button
type="raised" type="raised"
onClick={this.handleEtherscanClick} onClick={this.handleBlockExplorerClick}
disabled={!hash} disabled={!hash}
> >
<img src="/images/arrow-popout.svg" alt="" /> <img src="/images/arrow-popout.svg" alt="" />

View File

@ -1,12 +0,0 @@
import { createAccountLinkForChain } from '@metamask/etherscan-link';
export default function getAccountLink(address, chainId, rpcPrefs) {
if (rpcPrefs && rpcPrefs.blockExplorerUrl) {
return `${rpcPrefs.blockExplorerUrl.replace(
/\/+$/u,
'',
)}/address/${address}`;
}
return createAccountLinkForChain(address, chainId);
}

View File

@ -1,49 +0,0 @@
import {
MAINNET_CHAIN_ID,
ROPSTEN_CHAIN_ID,
} from '../../../shared/constants/network';
import getAccountLink from './account-link';
describe('Account link', () => {
describe('getAccountLink', () => {
it('should return the correct block explorer url for an account', () => {
const tests = [
{
expected: 'https://etherscan.io/address/0xabcd',
chainId: MAINNET_CHAIN_ID,
address: '0xabcd',
},
{
expected: 'https://ropsten.etherscan.io/address/0xdef0',
chainId: ROPSTEN_CHAIN_ID,
address: '0xdef0',
rpcPrefs: {},
},
{
// test handling of `blockExplorerUrl` for a custom RPC
expected: 'https://block.explorer/address/0xabcd',
chainId: '0x21',
address: '0xabcd',
rpcPrefs: {
blockExplorerUrl: 'https://block.explorer',
},
},
{
// test handling of trailing `/` in `blockExplorerUrl` for a custom RPC
expected: 'https://another.block.explorer/address/0xdef0',
chainId: '0x1f',
address: '0xdef0',
rpcPrefs: {
blockExplorerUrl: 'https://another.block.explorer/',
},
},
];
tests.forEach(({ expected, address, chainId, rpcPrefs }) => {
expect(getAccountLink(address, chainId, rpcPrefs)).toStrictEqual(
expected,
);
});
});
});
});

View File

@ -6,10 +6,11 @@ import { Menu, MenuItem } from '../../../components/ui/menu';
const AssetOptions = ({ const AssetOptions = ({
onRemove, onRemove,
onViewEtherscan, onClickBlockExplorer,
onViewAccountDetails, onViewAccountDetails,
tokenSymbol, tokenSymbol,
isNativeAsset, isNativeAsset,
isEthNetwork,
}) => { }) => {
const t = useContext(I18nContext); const t = useContext(I18nContext);
const [assetOptionsButtonElement, setAssetOptionsButtonElement] = useState( const [assetOptionsButtonElement, setAssetOptionsButtonElement] = useState(
@ -46,10 +47,10 @@ const AssetOptions = ({
data-testid="asset-options__etherscan" data-testid="asset-options__etherscan"
onClick={() => { onClick={() => {
setAssetOptionsOpen(false); setAssetOptionsOpen(false);
onViewEtherscan(); onClickBlockExplorer();
}} }}
> >
{t('viewOnEtherscan')} {isEthNetwork ? t('viewOnEtherscan') : t('viewinExplorer')}
</MenuItem> </MenuItem>
{isNativeAsset ? null : ( {isNativeAsset ? null : (
<MenuItem <MenuItem
@ -70,9 +71,10 @@ const AssetOptions = ({
}; };
AssetOptions.propTypes = { AssetOptions.propTypes = {
isEthNetwork: PropTypes.bool,
isNativeAsset: PropTypes.bool, isNativeAsset: PropTypes.bool,
onRemove: PropTypes.func.isRequired, onRemove: PropTypes.func.isRequired,
onViewEtherscan: PropTypes.func.isRequired, onClickBlockExplorer: PropTypes.func.isRequired,
onViewAccountDetails: PropTypes.func.isRequired, onViewAccountDetails: PropTypes.func.isRequired,
tokenSymbol: PropTypes.string, tokenSymbol: PropTypes.string,
}; };

View File

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { getAccountLink } from '@metamask/etherscan-link';
import TransactionList from '../../../components/app/transaction-list'; import TransactionList from '../../../components/app/transaction-list';
import { EthOverview } from '../../../components/app/wallet-overview'; import { EthOverview } from '../../../components/app/wallet-overview';
import { import {
@ -11,8 +12,8 @@ import {
getSelectedAddress, getSelectedAddress,
} from '../../../selectors/selectors'; } from '../../../selectors/selectors';
import { showModal } from '../../../store/actions'; import { showModal } from '../../../store/actions';
import getAccountLink from '../../../helpers/utils/account-link';
import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'; import { DEFAULT_ROUTE } from '../../../helpers/constants/routes';
import { useNewMetricEvent } from '../../../hooks/useMetricEvent';
import AssetNavigation from './asset-navigation'; import AssetNavigation from './asset-navigation';
import AssetOptions from './asset-options'; import AssetOptions from './asset-options';
@ -26,6 +27,17 @@ export default function NativeAsset({ nativeCurrency }) {
const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider);
const address = useSelector(getSelectedAddress); const address = useSelector(getSelectedAddress);
const history = useHistory(); const history = useHistory();
const accountLink = getAccountLink(address, chainId, rpcPrefs);
const blockExplorerLinkClickedEvent = useNewMetricEvent({
category: 'Navigation',
event: 'Clicked Block Explorer Link',
properties: {
link_type: 'Account Tracker',
action: 'Asset Options',
block_explorer_domain: accountLink ? new URL(accountLink)?.hostname : '',
},
});
return ( return (
<> <>
@ -33,12 +45,14 @@ export default function NativeAsset({ nativeCurrency }) {
accountName={selectedAccountName} accountName={selectedAccountName}
assetName={nativeCurrency} assetName={nativeCurrency}
onBack={() => history.push(DEFAULT_ROUTE)} onBack={() => history.push(DEFAULT_ROUTE)}
isEthNetwork={!rpcPrefs.blockExplorerUrl}
optionsButton={ optionsButton={
<AssetOptions <AssetOptions
isNativeAsset isNativeAsset
onViewEtherscan={() => { onClickBlockExplorer={() => {
blockExplorerLinkClickedEvent();
global.platform.openTab({ global.platform.openTab({
url: getAccountLink(address, chainId, rpcPrefs), url: accountLink,
}); });
}} }}
onViewAccountDetails={() => { onViewAccountDetails={() => {

View File

@ -2,16 +2,17 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { createTokenTrackerLinkForChain } from '@metamask/etherscan-link'; import { getTokenTrackerLink } from '@metamask/etherscan-link';
import TransactionList from '../../../components/app/transaction-list'; import TransactionList from '../../../components/app/transaction-list';
import { TokenOverview } from '../../../components/app/wallet-overview'; import { TokenOverview } from '../../../components/app/wallet-overview';
import { import {
getCurrentChainId, getCurrentChainId,
getSelectedIdentity, getSelectedIdentity,
getRpcPrefsForCurrentProvider,
} from '../../../selectors/selectors'; } from '../../../selectors/selectors';
import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'; import { DEFAULT_ROUTE } from '../../../helpers/constants/routes';
import { showModal } from '../../../store/actions'; import { showModal } from '../../../store/actions';
import { useNewMetricEvent } from '../../../hooks/useMetricEvent';
import AssetNavigation from './asset-navigation'; import AssetNavigation from './asset-navigation';
import AssetOptions from './asset-options'; import AssetOptions from './asset-options';
@ -19,10 +20,30 @@ import AssetOptions from './asset-options';
export default function TokenAsset({ token }) { export default function TokenAsset({ token }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const chainId = useSelector(getCurrentChainId); const chainId = useSelector(getCurrentChainId);
const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider);
const selectedIdentity = useSelector(getSelectedIdentity); const selectedIdentity = useSelector(getSelectedIdentity);
const selectedAccountName = selectedIdentity.name; const selectedAccountName = selectedIdentity.name;
const selectedAddress = selectedIdentity.address; const selectedAddress = selectedIdentity.address;
const history = useHistory(); const history = useHistory();
const tokenTrackerLink = getTokenTrackerLink(
token.address,
chainId,
null,
selectedAddress,
rpcPrefs,
);
const blockExplorerLinkClickedEvent = useNewMetricEvent({
category: 'Navigation',
event: 'Clicked Block Explorer Link',
properties: {
link_type: 'Token Tracker',
action: 'Token Options',
block_explorer_domain: tokenTrackerLink
? new URL(tokenTrackerLink)?.hostname
: '',
},
});
return ( return (
<> <>
@ -35,13 +56,10 @@ export default function TokenAsset({ token }) {
onRemove={() => onRemove={() =>
dispatch(showModal({ name: 'HIDE_TOKEN_CONFIRMATION', token })) dispatch(showModal({ name: 'HIDE_TOKEN_CONFIRMATION', token }))
} }
onViewEtherscan={() => { isEthNetwork={!rpcPrefs.blockExplorerUrl}
const url = createTokenTrackerLinkForChain( onClickBlockExplorer={() => {
token.address, blockExplorerLinkClickedEvent();
chainId, global.platform.openTab({ url: tokenTrackerLink });
selectedAddress,
);
global.platform.openTab({ url });
}} }}
onViewAccountDetails={() => { onViewAccountDetails={() => {
dispatch(showModal({ name: 'ACCOUNT_DETAILS' })); dispatch(showModal({ name: 'ACCOUNT_DETAILS' }));

View File

@ -1,6 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import getAccountLink from '../../../helpers/utils/account-link'; import { getAccountLink } from '@metamask/etherscan-link';
import Button from '../../../components/ui/button'; import Button from '../../../components/ui/button';
import Checkbox from '../../../components/ui/check-box'; import Checkbox from '../../../components/ui/check-box';
import Dropdown from '../../../components/ui/dropdown'; import Dropdown from '../../../components/ui/dropdown';
@ -83,7 +84,7 @@ class AccountList extends Component {
} }
renderAccounts() { renderAccounts() {
const { accounts, connectedAccounts } = this.props; const { accounts, connectedAccounts, rpcPrefs, chainId } = this.props;
return ( return (
<div className="hw-account-list"> <div className="hw-account-list">
@ -130,11 +131,27 @@ class AccountList extends Component {
</div> </div>
<a <a
className="hw-account-list__item__link" className="hw-account-list__item__link"
href={getAccountLink( onClick={() => {
account.address, const accountLink = getAccountLink(
this.props.chainId, account.address,
this.props.rpcPrefs, chainId,
)} rpcPrefs,
);
this.context.trackEvent({
category: 'Account',
event: 'Clicked Block Explorer Link',
properties: {
actions: 'Hardware Connect',
link_type: 'Account Tracker',
block_explorer_domain: accountLink
? new URL(accountLink)?.hostname
: '',
},
});
global.platform.openTab({
url: accountLink,
});
}}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
title={this.context.t('etherscanView')} title={this.context.t('etherscanView')}
@ -282,6 +299,7 @@ AccountList.propTypes = {
AccountList.contextTypes = { AccountList.contextTypes = {
t: PropTypes.func, t: PropTypes.func,
trackEvent: PropTypes.func,
}; };
export default AccountList; export default AccountList;

View File

@ -3,7 +3,7 @@ import React, { useContext, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { createCustomExplorerLink } from '@metamask/etherscan-link'; import { getBlockExplorerLink } from '@metamask/etherscan-link';
import { I18nContext } from '../../../contexts/i18n'; import { I18nContext } from '../../../contexts/i18n';
import { useNewMetricEvent } from '../../../hooks/useMetricEvent'; import { useNewMetricEvent } from '../../../hooks/useMetricEvent';
import { MetaMetricsContext } from '../../../contexts/metametrics.new'; import { MetaMetricsContext } from '../../../contexts/metametrics.new';
@ -37,7 +37,6 @@ import {
OFFLINE_FOR_MAINTENANCE, OFFLINE_FOR_MAINTENANCE,
SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP, SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP,
} from '../../../../shared/constants/swaps'; } from '../../../../shared/constants/swaps';
import { CHAIN_ID_TO_TYPE_MAP as VALID_INFURA_CHAIN_IDS } from '../../../../shared/constants/network';
import { isSwapsDefaultTokenSymbol } from '../../../../shared/modules/swaps.utils'; import { isSwapsDefaultTokenSymbol } from '../../../../shared/modules/swaps.utils';
import PulseLoader from '../../../components/ui/pulse-loader'; import PulseLoader from '../../../components/ui/pulse-loader';
@ -45,7 +44,6 @@ import { ASSET_ROUTE, DEFAULT_ROUTE } from '../../../helpers/constants/routes';
import { getRenderableNetworkFeesForQuote } from '../swaps.util'; import { getRenderableNetworkFeesForQuote } from '../swaps.util';
import SwapsFooter from '../swaps-footer'; import SwapsFooter from '../swaps-footer';
import { getBlockExplorerUrlForTx } from '../../../../shared/modules/transaction.utils';
import SwapFailureIcon from './swap-failure-icon'; import SwapFailureIcon from './swap-failure-icon';
import SwapSuccessIcon from './swap-success-icon'; import SwapSuccessIcon from './swap-success-icon';
@ -116,17 +114,14 @@ export default function AwaitingSwap({
category: 'swaps', category: 'swaps',
}); });
let blockExplorerUrl; const baseNetworkUrl =
if (txHash && rpcPrefs.blockExplorerUrl) { rpcPrefs.blockExplorerUrl ??
blockExplorerUrl = getBlockExplorerUrlForTx({ hash: txHash }, rpcPrefs); SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ??
} else if (txHash && SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId]) { null;
blockExplorerUrl = createCustomExplorerLink( const blockExplorerUrl = getBlockExplorerLink(
txHash, { hash: txHash, chainId },
SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId], { blockExplorerUrl: baseNetworkUrl },
); );
} else if (txHash && VALID_INFURA_CHAIN_IDS[chainId]) {
blockExplorerUrl = getBlockExplorerUrlForTx({ chainId, hash: txHash });
}
const isCustomBlockExplorerUrl = const isCustomBlockExplorerUrl =
SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] || SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ||

View File

@ -2,6 +2,7 @@ import React, { useContext } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import { I18nContext } from '../../../../contexts/i18n'; import { I18nContext } from '../../../../contexts/i18n';
import { useNewMetricEvent } from '../../../../hooks/useMetricEvent';
export default function ViewOnEtherScanLink({ export default function ViewOnEtherScanLink({
txHash, txHash,
@ -9,13 +10,29 @@ export default function ViewOnEtherScanLink({
isCustomBlockExplorerUrl, isCustomBlockExplorerUrl,
}) { }) {
const t = useContext(I18nContext); const t = useContext(I18nContext);
const blockExplorerLinkClickedEvent = useNewMetricEvent({
category: 'Swaps',
event: 'Clicked Block Explorer Link',
properties: {
link_type: 'Transaction Block Explorer',
action: 'Swap Transaction',
block_explorer_domain: blockExplorerUrl
? new URL(blockExplorerUrl)?.hostname
: '',
},
});
return ( return (
<div <div
className={classnames('awaiting-swap__view-on-etherscan', { className={classnames('awaiting-swap__view-on-etherscan', {
'awaiting-swap__view-on-etherscan--visible': txHash, 'awaiting-swap__view-on-etherscan--visible': txHash,
'awaiting-swap__view-on-etherscan--invisible': !txHash, 'awaiting-swap__view-on-etherscan--invisible': !txHash,
})} })}
onClick={() => global.platform.openTab({ url: blockExplorerUrl })} onClick={() => {
blockExplorerLinkClickedEvent();
global.platform.openTab({ url: blockExplorerUrl });
}}
> >
{isCustomBlockExplorerUrl {isCustomBlockExplorerUrl
? t('viewOnCustomBlockExplorer', [new URL(blockExplorerUrl).hostname]) ? t('viewOnCustomBlockExplorer', [new URL(blockExplorerUrl).hostname])

View File

@ -4,11 +4,9 @@ import { useDispatch, useSelector } from 'react-redux';
import classnames from 'classnames'; import classnames from 'classnames';
import { uniqBy, isEqual } from 'lodash'; import { uniqBy, isEqual } from 'lodash';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { import { getTokenTrackerLink } from '@metamask/etherscan-link';
createCustomTokenTrackerLink,
createTokenTrackerLinkForChain,
} from '@metamask/etherscan-link';
import { MetaMetricsContext } from '../../../contexts/metametrics.new'; import { MetaMetricsContext } from '../../../contexts/metametrics.new';
import { useNewMetricEvent } from '../../../hooks/useMetricEvent';
import { import {
useTokensToSearch, useTokensToSearch,
getRenderableTokenData, getRenderableTokenData,
@ -223,29 +221,34 @@ export default function BuildQuote({
); );
}; };
let blockExplorerTokenLink; const blockExplorerTokenLink = getTokenTrackerLink(
let blockExplorerLabel; selectedToToken.address,
if (rpcPrefs.blockExplorerUrl) { chainId,
blockExplorerTokenLink = createCustomTokenTrackerLink( null, // no networkId
selectedToToken.address, null, // no holderAddress
rpcPrefs.blockExplorerUrl, {
); blockExplorerUrl:
blockExplorerLabel = new URL(rpcPrefs.blockExplorerUrl).hostname; rpcPrefs.blockExplorerUrl ??
} else if (SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId]) { SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId] ??
blockExplorerTokenLink = createCustomTokenTrackerLink( null,
selectedToToken.address, },
SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId], );
);
blockExplorerLabel = new URL( const blockExplorerLabel = rpcPrefs.blockExplorerUrl
SWAPS_CHAINID_DEFAULT_BLOCK_EXPLORER_URL_MAP[chainId], ? new URL(blockExplorerTokenLink).hostname
).hostname; : t('etherscan');
} else {
blockExplorerTokenLink = createTokenTrackerLinkForChain( const blockExplorerLinkClickedEvent = useNewMetricEvent({
selectedToToken.address, category: 'Swaps',
chainId, event: 'Clicked Block Explorer Link',
); properties: {
blockExplorerLabel = t('etherscan'); link_type: 'Token Tracker',
} action: 'Swaps Confirmation',
block_explorer_domain: blockExplorerTokenLink
? new URL(blockExplorerTokenLink)?.hostname
: '',
},
});
const { destinationTokenAddedForSwap } = fetchParams || {}; const { destinationTokenAddedForSwap } = fetchParams || {};
const { address: toAddress } = toToken || {}; const { address: toAddress } = toToken || {};
@ -449,7 +452,12 @@ export default function BuildQuote({
<a <a
className="build-quote__token-etherscan-link build-quote__underline" className="build-quote__token-etherscan-link build-quote__underline"
key="build-quote-etherscan-link" key="build-quote-etherscan-link"
href={blockExplorerTokenLink} onClick={() => {
blockExplorerLinkClickedEvent();
global.platform.openTab({
url: blockExplorerTokenLink,
});
}}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
@ -488,7 +496,12 @@ export default function BuildQuote({
<a <a
className="build-quote__token-etherscan-link" className="build-quote__token-etherscan-link"
key="build-quote-etherscan-link" key="build-quote-etherscan-link"
href={blockExplorerTokenLink} onClick={() => {
blockExplorerLinkClickedEvent();
global.platform.openTab({
url: blockExplorerTokenLink,
});
}}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >