diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 8d0d8af55..d0cf9c1a0 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -319,7 +319,15 @@ export default class MetamaskController extends EventEmitter { status === TRANSACTION_STATUSES.FAILED ) { const txMeta = this.txController.txStateManager.getTx(txId); - this.platform.showTransactionNotification(txMeta); + const frequentRpcListDetail = this.preferencesController.getFrequentRpcListDetail(); + let rpcPrefs = {}; + if (txMeta.chainId) { + const rpcSettings = frequentRpcListDetail.find( + (rpc) => txMeta.chainId === rpc.chainId, + ); + rpcPrefs = rpcSettings?.rpcPrefs ?? {}; + } + this.platform.showTransactionNotification(txMeta, rpcPrefs); const { txReceipt } = txMeta; if (txReceipt && txReceipt.status === '0x0') { diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index 98196c946..0eaeefaa8 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -1,8 +1,8 @@ import extension from 'extensionizer'; -import { createExplorerLink as explorerLink } 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 { // @@ -110,7 +110,7 @@ export default class ExtensionPlatform { } } - showTransactionNotification(txMeta) { + showTransactionNotification(txMeta, rpcPrefs) { const { status, txReceipt: { status: receiptStatus } = {} } = txMeta; if (status === TRANSACTION_STATUSES.CONFIRMED) { @@ -120,7 +120,7 @@ export default class ExtensionPlatform { txMeta, 'Transaction encountered an error.', ) - : this._showConfirmedTransaction(txMeta); + : this._showConfirmedTransaction(txMeta, rpcPrefs); } else if (status === TRANSACTION_STATUSES.FAILED) { this._showFailedTransaction(txMeta); } @@ -189,10 +189,10 @@ export default class ExtensionPlatform { }); } - _showConfirmedTransaction(txMeta) { + _showConfirmedTransaction(txMeta, rpcPrefs) { this._subscribeToNotificationClicked(); - const url = explorerLink(txMeta.hash, txMeta.metamaskNetworkId); + const url = getBlockExplorerUrlForTx(txMeta, rpcPrefs); const nonce = parseInt(txMeta.txParams.nonce, 16); const title = 'Confirmed transaction'; diff --git a/package.json b/package.json index 3d26e47d8..690fd6096 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,9 @@ "forwarder": "node ./development/static-server.js ./node_modules/@metamask/forwarder/dist/ --port 9010", "dapp-forwarder": "concurrently -k -n forwarder,dapp -p '[{time}][{name}]' 'yarn forwarder' 'yarn dapp'", "sendwithprivatedapp": "node development/static-server.js test/e2e/send-eth-with-private-key-test --port 8080", - "test:unit": "mocha --exit --require test/env.js --require test/setup.js --recursive \"test/unit/**/*.test.js\" \"ui/app/**/*.test.js\"", + "test:unit": "mocha --exit --require test/env.js --require test/setup.js --recursive \"test/unit/**/*.test.js\" \"ui/app/**/*.test.js\" \"shared/**/*.test.js\"", "test:unit:global": "mocha --exit --require test/env.js --require test/setup.js --recursive test/unit-global/*.test.js", - "test:unit:lax": "mocha --exit --require test/env.js --require test/setup.js --recursive \"test/unit/{,**/!(permissions)}/*.test.js\" \"ui/app/**/*.test.js\"", + "test:unit:lax": "mocha --exit --require test/env.js --require test/setup.js --recursive \"test/unit/{,**/!(permissions)}/*.test.js\" \"ui/app/**/*.test.js\" \"shared/**/*.test.js\"", "test:unit:strict": "mocha --exit --require test/env.js --require test/setup.js --recursive \"test/unit/**/permissions/*.test.js\"", "test:unit:path": "mocha --exit --require test/env.js --require test/setup.js --recursive", "test:e2e:chrome": "SELENIUM_BROWSER=chrome test/e2e/run-all.sh", @@ -45,7 +45,7 @@ "verify-locales": "node ./development/verify-locale-strings.js", "verify-locales:fix": "node ./development/verify-locale-strings.js --fix", "mozilla-lint": "addons-linter dist/firefox", - "watch": "mocha --watch --require test/env.js --require test/setup.js --reporter min --recursive \"test/unit/**/*.js\" \"ui/app/**/*.test.js\"", + "watch": "mocha --watch --require test/env.js --require test/setup.js --reporter min --recursive \"test/unit/**/*.js\" \"ui/app/**/*.test.js\" \"shared/**/*.test.js\"", "devtools:react": "react-devtools", "devtools:redux": "remotedev --hostname=localhost --port=8000", "start:dev": "concurrently -k -n build,react,redux yarn:start yarn:devtools:react yarn:devtools:redux", diff --git a/shared/modules/tests/transaction.utils.test.js b/shared/modules/tests/transaction.utils.test.js new file mode 100644 index 000000000..2aac76828 --- /dev/null +++ b/shared/modules/tests/transaction.utils.test.js @@ -0,0 +1,96 @@ +import 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 9e89679f8..47a0a4334 100644 --- a/shared/modules/transaction.utils.js +++ b/shared/modules/transaction.utils.js @@ -1,6 +1,37 @@ +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/app/components/app/transaction-activity-log/tests/transaction-activity-log.container.test.js b/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.container.test.js index b1608684c..612a0f046 100644 --- a/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.container.test.js +++ b/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.container.test.js @@ -19,12 +19,42 @@ describe('TransactionActivityLog container', function () { metamask: { conversionRate: 280.45, nativeCurrency: 'ETH', + frequentRpcListDetail: [], }, }; assert.deepStrictEqual(mapStateToProps(mockState), { conversionRate: 280.45, nativeCurrency: 'ETH', + rpcPrefs: {}, + }); + }); + + it('should return the correct props when on a custom network', function () { + const mockState = { + metamask: { + conversionRate: 280.45, + nativeCurrency: 'ETH', + frequentRpcListDetail: [ + { + rpcUrl: 'https://customnetwork.com/', + rpcPrefs: { + blockExplorerUrl: 'https://customblockexplorer.com/', + }, + }, + ], + provider: { + rpcUrl: 'https://customnetwork.com/', + }, + }, + }; + + assert.deepStrictEqual(mapStateToProps(mockState), { + conversionRate: 280.45, + nativeCurrency: 'ETH', + rpcPrefs: { + blockExplorerUrl: 'https://customblockexplorer.com/', + }, }); }); }); diff --git a/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.util.test.js b/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.util.test.js index e01a5a9de..8421dca96 100644 --- a/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.util.test.js +++ b/ui/app/components/app/transaction-activity-log/tests/transaction-activity-log.util.test.js @@ -1,4 +1,8 @@ import assert from 'assert'; +import { + ROPSTEN_CHAIN_ID, + ROPSTEN_NETWORK_ID, +} from '../../../../../../shared/constants/network'; import { TRANSACTION_STATUSES, TRANSACTION_TYPES, @@ -24,7 +28,8 @@ describe('TransactionActivityLog utils', function () { id: 6400627574331058, time: 1543958845581, status: TRANSACTION_STATUSES.UNAPPROVED, - metamaskNetworkId: '3', + metamaskNetworkId: ROPSTEN_NETWORK_ID, + chainId: ROPSTEN_CHAIN_ID, loadingDefaults: true, txParams: { from: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706', @@ -71,7 +76,8 @@ describe('TransactionActivityLog utils', function () { ], id: 6400627574331058, loadingDefaults: false, - metamaskNetworkId: '3', + metamaskNetworkId: ROPSTEN_NETWORK_ID, + chainId: ROPSTEN_CHAIN_ID, status: TRANSACTION_STATUSES.DROPPED, submittedTime: 1543958848135, time: 1543958845581, @@ -93,7 +99,8 @@ describe('TransactionActivityLog utils', function () { id: 6400627574331060, time: 1543958857697, status: TRANSACTION_STATUSES.UNAPPROVED, - metamaskNetworkId: '3', + metamaskNetworkId: ROPSTEN_NETWORK_ID, + chainId: ROPSTEN_CHAIN_ID, loadingDefaults: false, txParams: { from: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706', @@ -163,7 +170,8 @@ describe('TransactionActivityLog utils', function () { id: 6400627574331060, lastGasPrice: '0x4190ab00', loadingDefaults: false, - metamaskNetworkId: '3', + metamaskNetworkId: ROPSTEN_NETWORK_ID, + chainId: ROPSTEN_CHAIN_ID, status: TRANSACTION_STATUSES.CONFIRMED, submittedTime: 1543958860054, time: 1543958857697, @@ -185,6 +193,8 @@ describe('TransactionActivityLog utils', function () { const expected = [ { id: 6400627574331058, + metamaskNetworkId: ROPSTEN_NETWORK_ID, + chainId: ROPSTEN_CHAIN_ID, hash: '0xa14f13d36b3901e352ce3a7acb9b47b001e5a3370f06232a0953c6fc6fad91b3', eventKey: 'transactionCreated', @@ -193,6 +203,8 @@ describe('TransactionActivityLog utils', function () { }, { id: 6400627574331058, + metamaskNetworkId: ROPSTEN_NETWORK_ID, + chainId: ROPSTEN_CHAIN_ID, hash: '0xa14f13d36b3901e352ce3a7acb9b47b001e5a3370f06232a0953c6fc6fad91b3', eventKey: 'transactionSubmitted', @@ -201,6 +213,8 @@ describe('TransactionActivityLog utils', function () { }, { id: 6400627574331060, + metamaskNetworkId: ROPSTEN_NETWORK_ID, + chainId: ROPSTEN_CHAIN_ID, hash: '0xecbe181ee67c4291d04a7cb9ffbf1d5d831e4fbaa89994fd06bab5dd4cc79b33', eventKey: 'transactionResubmitted', @@ -209,6 +223,8 @@ describe('TransactionActivityLog utils', function () { }, { id: 6400627574331060, + metamaskNetworkId: ROPSTEN_NETWORK_ID, + chainId: ROPSTEN_CHAIN_ID, hash: '0xecbe181ee67c4291d04a7cb9ffbf1d5d831e4fbaa89994fd06bab5dd4cc79b33', eventKey: 'transactionConfirmed', @@ -249,7 +265,8 @@ describe('TransactionActivityLog utils', function () { { id: 5559712943815343, loadingDefaults: true, - metamaskNetworkId: '3', + metamaskNetworkId: ROPSTEN_NETWORK_ID, + chainId: ROPSTEN_CHAIN_ID, status: TRANSACTION_STATUSES.UNAPPROVED, time: 1535507561452, txParams: { @@ -389,6 +406,8 @@ describe('TransactionActivityLog utils', function () { value: '0x2386f26fc10000', }, hash: '0xabc', + chainId: ROPSTEN_CHAIN_ID, + metamaskNetworkId: ROPSTEN_NETWORK_ID, }; const expectedResult = [ @@ -398,6 +417,8 @@ describe('TransactionActivityLog utils', function () { value: '0x2386f26fc10000', id: 1, hash: '0xabc', + chainId: ROPSTEN_CHAIN_ID, + metamaskNetworkId: ROPSTEN_NETWORK_ID, }, { eventKey: 'transactionSubmitted', @@ -405,6 +426,8 @@ describe('TransactionActivityLog utils', function () { value: '0x2632e314a000', id: 1, hash: '0xabc', + chainId: ROPSTEN_CHAIN_ID, + metamaskNetworkId: ROPSTEN_NETWORK_ID, }, { eventKey: 'transactionConfirmed', @@ -412,6 +435,8 @@ describe('TransactionActivityLog utils', function () { value: '0x2632e314a000', id: 1, hash: '0xabc', + chainId: ROPSTEN_CHAIN_ID, + metamaskNetworkId: ROPSTEN_NETWORK_ID, }, ]; diff --git a/ui/app/components/app/transaction-activity-log/transaction-activity-log.component.js b/ui/app/components/app/transaction-activity-log/transaction-activity-log.component.js index 64fa9dc3e..215932e9f 100644 --- a/ui/app/components/app/transaction-activity-log/transaction-activity-log.component.js +++ b/ui/app/components/app/transaction-activity-log/transaction-activity-log.component.js @@ -1,13 +1,13 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { createExplorerLink } 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'; @@ -28,14 +28,14 @@ export default class TransactionActivityLog extends PureComponent { onRetry: PropTypes.func, primaryTransaction: PropTypes.object, isEarliestNonce: PropTypes.bool, + rpcPrefs: PropTypes.object, }; - handleActivityClick = (hash) => { - const { primaryTransaction } = this.props; - const { metamaskNetworkId } = primaryTransaction; - - const etherscanUrl = createExplorerLink(hash, metamaskNetworkId); - + handleActivityClick = (activity) => { + const etherscanUrl = getBlockExplorerUrlForTx( + activity, + this.props.rpcPrefs, + ); global.platform.openTab({ url: etherscanUrl }); }; @@ -79,7 +79,7 @@ export default class TransactionActivityLog extends PureComponent { renderActivity(activity, index) { const { conversionRate, nativeCurrency } = this.props; - const { eventKey, value, timestamp, hash } = activity; + const { eventKey, value, timestamp } = activity; const ethValue = index === 0 ? `${getValueFromWeiHex({ @@ -111,7 +111,7 @@ export default class TransactionActivityLog extends PureComponent {