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

Merge pull request #7323 from MetaMask/Version-v7.4.0

Version v7.4.0 RC
This commit is contained in:
Thomas Huang 2019-11-04 15:39:57 -08:00 committed by GitHub
commit 41e834f88c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
109 changed files with 1821 additions and 4046 deletions

View File

@ -35,6 +35,9 @@ workflows:
- test-unit:
requires:
- prep-deps
- test-unit-global:
requires:
- prep-deps
- test-mozilla-lint:
requires:
- prep-deps
@ -51,6 +54,7 @@ workflows:
requires:
- test-lint
- test-unit
- test-unit-global
- test-mozilla-lint
- test-e2e-chrome
- test-e2e-firefox
@ -310,6 +314,16 @@ jobs:
paths:
- .nyc_output
- coverage
test-unit-global:
docker:
- image: circleci/node:10.16-browsers
steps:
- checkout
- attach_workspace:
at: .
- run:
name: test:unit:global
command: yarn test:unit:global
test-mozilla-lint:
docker:
- image: circleci/node:10.16-browsers

View File

@ -2,8 +2,21 @@
## Current Develop Branch
## 7.4.0 Tue Oct 29 2019
- [#7186](https://github.com/MetaMask/metamask-extension/pull/7186): Use `AdvancedGasInputs` in `AdvancedTabContent`
- [#7304](https://github.com/MetaMask/metamask-extension/pull/7304): Move signTypedData signing out to keyrings
- [#7306](https://github.com/MetaMask/metamask-extension/pull/7306): correct the zh-TW translation
- [#7309](https://github.com/MetaMask/metamask-extension/pull/7309): Freeze Promise global on boot
- [#7296](https://github.com/MetaMask/metamask-extension/pull/7296): Add "Retry" option for failed transactions
- [#7319](https://github.com/MetaMask/metamask-extension/pull/7319): Fix transaction list item status spacing issue
- [#7218](https://github.com/MetaMask/metamask-extension/pull/7218): Add hostname and extensionId to site metadata
- [#7324](https://github.com/MetaMask/metamask-extension/pull/7324): Fix contact deletion
- [#7326](https://github.com/MetaMask/metamask-extension/pull/7326): Fix edit contact details
- [#7325](https://github.com/MetaMask/metamask-extension/pull/7325): Update eth-json-rpc-filters to fix memory leak
- [#7334](https://github.com/MetaMask/metamask-extension/pull/7334): Add web3 deprecation warning
## 7.3.1 Mon Oct 21 2019
- [#7298](https://github.com/MetaMask/metamask-extension/pull/7298): Turn off full screen vs popup a/b test
- [#7298](https://github.com/MetaMask/metamask-extension/pull/7298): Turn off full screen vs popup a/b test
## 7.3.0 Fri Sep 27 2019
- [#6972](https://github.com/MetaMask/metamask-extension/pull/6972): 3box integration

View File

@ -1442,6 +1442,9 @@
"viewOnEtherscan": {
"message": "View on Etherscan"
},
"retryTransaction": {
"message": "Retry Transaction"
},
"visitWebSite": {
"message": "Visit our web site"
},

View File

@ -162,7 +162,7 @@
"description": "helper for inputting hex as decimal input"
},
"blockExplorerUrl": {
"message": "封鎖 Explorer"
"message": "區塊鏈瀏覽器"
},
"blockExplorerView": {
"message": "在 $1 觀看帳號 ",
@ -199,7 +199,7 @@
"message": "開啟"
},
"optionalBlockExplorerUrl": {
"message": "封鎖 Explorer URL非必要"
"message": "區塊鏈瀏覽器 URL非必要"
},
"cancel": {
"message": "取消"
@ -641,7 +641,7 @@
"message": "無效的 RPC URI"
},
"invalidBlockExplorerURL": {
"message": "無效的 Block Explorer URI"
"message": "無效的區塊鏈瀏覽器 URL"
},
"invalidSeedPhrase": {
"message": "無效的助憶詞"

View File

@ -1,7 +1,7 @@
{
"name": "__MSG_appName__",
"short_name": "__MSG_appName__",
"version": "7.3.1",
"version": "7.4.0",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "__MSG_appDescription__",

View File

@ -2,13 +2,14 @@
* @file The entry point for the web extension singleton process.
*/
// this needs to run before anything else
// these need to run before anything else
require('./lib/freezeGlobals')
require('./lib/setupFetchDebugging')()
// polyfills
import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'
const urlUtil = require('url')
const endOfStream = require('end-of-stream')
const pump = require('pump')
const debounce = require('debounce-stream')
@ -350,7 +351,10 @@ function setupController (initState, initLangCode) {
const portStream = new PortStream(remotePort)
// communication with popup
controller.isClientOpen = true
controller.setupTrustedCommunication(portStream, 'MetaMask')
// construct fake URL for identifying internal messages
const metamaskUrl = new URL(window.location)
metamaskUrl.hostname = 'metamask'
controller.setupTrustedCommunication(portStream, metamaskUrl)
if (processName === ENVIRONMENT_TYPE_POPUP) {
popupIsOpen = true
@ -386,9 +390,13 @@ function setupController (initState, initLangCode) {
// communication with page or other extension
function connectExternal (remotePort) {
const originDomain = urlUtil.parse(remotePort.sender.url).hostname
const senderUrl = new URL(remotePort.sender.url)
let extensionId
if (remotePort.sender.id !== extension.runtime.id) {
extensionId = remotePort.sender.id
}
const portStream = new PortStream(remotePort)
controller.setupUntrustedCommunication(portStream, originDomain)
controller.setupUntrustedCommunication(portStream, senderUrl, extensionId)
}
//

View File

@ -31,19 +31,26 @@ class ProviderApprovalController extends SafeEventEmitter {
*
* @param {object} opts - opts for the middleware contains the origin for the middleware
*/
createMiddleware ({ origin, getSiteMetadata }) {
createMiddleware ({ senderUrl, extensionId, getSiteMetadata }) {
return createAsyncMiddleware(async (req, res, next) => {
// only handle requestAccounts
if (req.method !== 'eth_requestAccounts') return next()
// if already approved or privacy mode disabled, return early
const isUnlocked = this.keyringController.memStore.getState().isUnlocked
const origin = senderUrl.hostname
if (this.shouldExposeAccounts(origin) && isUnlocked) {
res.result = [this.preferencesController.getSelectedAddress()]
return
}
// register the provider request
const metadata = await getSiteMetadata(origin)
this._handleProviderRequest(origin, metadata.name, metadata.icon)
const metadata = { hostname: senderUrl.hostname, origin }
if (extensionId) {
metadata.extensionId = extensionId
} else {
const siteMetadata = await getSiteMetadata(origin)
Object.assign(metadata, { siteTitle: siteMetadata.name, siteImage: siteMetadata.icon})
}
this._handleProviderRequest(metadata)
// wait for resolution of request
const approved = await new Promise(resolve => this.once(`resolvedRequest:${origin}`, ({ approved }) => resolve(approved)))
if (approved) {
@ -54,19 +61,26 @@ class ProviderApprovalController extends SafeEventEmitter {
})
}
/**
* @typedef {Object} SiteMetadata
* @param {string} hostname - The hostname of the site
* @param {string} origin - The origin of the site
* @param {string} [siteTitle] - The title of the site
* @param {string} [siteImage] - The icon for the site
* @param {string} [extensionId] - The extension ID of the extension
*/
/**
* Called when a tab requests access to a full Ethereum provider API
*
* @param {string} origin - Origin of the window requesting full provider access
* @param {string} siteTitle - The title of the document requesting full provider access
* @param {string} siteImage - The icon of the window requesting full provider access
* @param {SiteMetadata} siteMetadata - The metadata for the site requesting full provider access
*/
_handleProviderRequest (origin, siteTitle, siteImage) {
_handleProviderRequest (siteMetadata) {
const { providerRequests } = this.memStore.getState()
const origin = siteMetadata.origin
this.memStore.updateState({
providerRequests: [
...providerRequests,
{ origin, siteTitle, siteImage },
siteMetadata,
],
})
const isUnlocked = this.keyringController.memStore.getState().isUnlocked
@ -98,6 +112,7 @@ class ProviderApprovalController extends SafeEventEmitter {
[origin]: {
siteTitle: providerRequest ? providerRequest.siteTitle : null,
siteImage: providerRequest ? providerRequest.siteImage : null,
hostname: providerRequest ? providerRequest.hostname : null,
},
},
})

View File

@ -174,27 +174,6 @@ log.debug('MetaMask - injected web3')
setupDappAutoReload(web3, inpageProvider.publicConfigStore)
// export global web3, with usage-detection and deprecation warning
/* TODO: Uncomment this area once auto-reload.js has been deprecated:
let hasBeenWarned = false
global.web3 = new Proxy(web3, {
get: (_web3, key) => {
// show warning once on web3 access
if (!hasBeenWarned && key !== 'currentProvider') {
console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider \nhttps://github.com/MetaMask/faq/blob/master/detecting_metamask.md#web3-deprecation')
hasBeenWarned = true
}
// return value normally
return _web3[key]
},
set: (_web3, key, value) => {
// set value normally
_web3[key] = value
},
})
*/
// set web3 defaultAccount
inpageProvider.publicConfigStore.subscribe(function (state) {
web3.eth.defaultAccount = state.selectedAddress

View File

@ -5,11 +5,17 @@ function setupDappAutoReload (web3, observable) {
let reloadInProgress = false
let lastTimeUsed
let lastSeenNetwork
let hasBeenWarned = false
global.web3 = new Proxy(web3, {
get: (_web3, key) => {
// get the time of use
lastTimeUsed = Date.now()
// show warning once on web3 access
if (!hasBeenWarned && key !== 'currentProvider') {
console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider\nhttps://medium.com/metamask/4a899ad6e59e')
hasBeenWarned = true
}
// return value normally
return _web3[key]
},

View File

@ -0,0 +1,41 @@
/**
* Freezes the Promise global and prevents its reassignment.
*/
const deepFreeze = require('deep-freeze-strict')
if (
process.env.IN_TEST !== 'true' &&
process.env.METAMASK_ENV !== 'test'
) {
freeze(global, 'Promise')
}
/**
* Makes a key:value pair on a target object immutable, with limitations.
* The key cannot be reassigned or deleted, and the value is recursively frozen
* using Object.freeze.
*
* Because of JavaScript language limitations, this is does not mean that the
* value is completely immutable. It is, however, better than nothing.
*
* @param {Object} target - The target object to freeze a property on.
* @param {String} key - The key to freeze.
* @param {any} [value] - The value to freeze, if different from the existing value on the target.
* @param {boolean} [enumerable=true] - If given a value, whether the property is enumerable.
*/
function freeze (target, key, value, enumerable = true) {
const opts = {
configurable: false, writable: false,
}
if (value !== undefined) {
opts.value = deepFreeze(value)
opts.enumerable = enumerable
} else {
target[key] = deepFreeze(target[key])
}
Object.defineProperty(target, key, opts)
}

View File

@ -53,10 +53,8 @@ const seedPhraseVerifier = require('./lib/seed-phrase-verifier')
const log = require('loglevel')
const TrezorKeyring = require('eth-trezor-keyring')
const LedgerBridgeKeyring = require('eth-ledger-bridge-keyring')
const HW_WALLETS_KEYRINGS = [TrezorKeyring.type, LedgerBridgeKeyring.type]
const EthQuery = require('eth-query')
const ethUtil = require('ethereumjs-util')
const sigUtil = require('eth-sig-util')
const contractMap = require('eth-contract-metadata')
const {
AddressBookController,
@ -336,7 +334,7 @@ module.exports = class MetamaskController extends EventEmitter {
// Expose no accounts if this origin has not been approved, preventing
// account-requring RPC methods from completing successfully
const exposeAccounts = this.providerApprovalController.shouldExposeAccounts(origin)
if (origin !== 'MetaMask' && !exposeAccounts) { return [] }
if (origin !== 'metamask' && !exposeAccounts) { return [] }
const isUnlocked = this.keyringController.memStore.getState().isUnlocked
const selectedAddress = this.preferencesController.getSelectedAddress()
// only show address if account is unlocked
@ -1168,28 +1166,16 @@ module.exports = class MetamaskController extends EventEmitter {
const version = msgParams.version
try {
const cleanMsgParams = await this.typedMessageManager.approveMessage(msgParams)
const address = sigUtil.normalize(cleanMsgParams.from)
const keyring = await this.keyringController.getKeyringForAccount(address)
let signature
// HW Wallet keyrings don't expose private keys
// so we need to handle it separately
if (!HW_WALLETS_KEYRINGS.includes(keyring.type)) {
const wallet = keyring._getWalletForAccount(address)
const privKey = ethUtil.toBuffer(wallet.getPrivateKey())
switch (version) {
case 'V1':
signature = sigUtil.signTypedDataLegacy(privKey, { data: cleanMsgParams.data })
break
case 'V3':
signature = sigUtil.signTypedData(privKey, { data: JSON.parse(cleanMsgParams.data) })
break
case 'V4':
signature = sigUtil.signTypedData_v4(privKey, { data: JSON.parse(cleanMsgParams.data) })
break
// For some reason every version after V1 used stringified params.
if (version !== 'V1') {
// But we don't have to require that. We can stop suggesting it now:
if (typeof cleanMsgParams.data === 'string') {
cleanMsgParams.data = JSON.parse(cleanMsgParams.data)
}
} else {
signature = await keyring.signTypedData(address, cleanMsgParams.data)
}
const signature = await this.keyringController.signTypedMessage(cleanMsgParams, { version })
this.typedMessageManager.setMsgStatusSigned(msgId, signature)
return this.getState()
} catch (error) {
@ -1332,23 +1318,25 @@ module.exports = class MetamaskController extends EventEmitter {
* Used to create a multiplexed stream for connecting to an untrusted context
* like a Dapp or other extension.
* @param {*} connectionStream - The Duplex stream to connect to.
* @param {string} originDomain - The domain requesting the stream, which
* may trigger a blacklist reload.
* @param {URL} senderUrl - The URL of the resource requesting the stream,
* which may trigger a blacklist reload.
* @param {string} extensionId - The extension id of the sender, if the sender
* is an extension
*/
setupUntrustedCommunication (connectionStream, originDomain) {
setupUntrustedCommunication (connectionStream, senderUrl, extensionId) {
// Check if new connection is blacklisted
if (this.phishingController.test(originDomain)) {
log.debug('MetaMask - sending phishing warning for', originDomain)
this.sendPhishingWarning(connectionStream, originDomain)
if (this.phishingController.test(senderUrl.hostname)) {
log.debug('MetaMask - sending phishing warning for', senderUrl.hostname)
this.sendPhishingWarning(connectionStream, senderUrl.hostname)
return
}
// setup multiplexing
const mux = setupMultiplex(connectionStream)
// connect features
const publicApi = this.setupPublicApi(mux.createStream('publicApi'), originDomain)
this.setupProviderConnection(mux.createStream('provider'), originDomain, publicApi)
this.setupPublicConfig(mux.createStream('publicConfig'), originDomain)
const publicApi = this.setupPublicApi(mux.createStream('publicApi'))
this.setupProviderConnection(mux.createStream('provider'), senderUrl, extensionId, publicApi)
this.setupPublicConfig(mux.createStream('publicConfig'), senderUrl)
}
/**
@ -1358,15 +1346,15 @@ module.exports = class MetamaskController extends EventEmitter {
* functions, like the ability to approve transactions or sign messages.
*
* @param {*} connectionStream - The duplex stream to connect to.
* @param {string} originDomain - The domain requesting the connection,
* @param {URL} senderUrl - The URL requesting the connection,
* used in logging and error reporting.
*/
setupTrustedCommunication (connectionStream, originDomain) {
setupTrustedCommunication (connectionStream, senderUrl) {
// setup multiplexing
const mux = setupMultiplex(connectionStream)
// connect features
this.setupControllerConnection(mux.createStream('controller'))
this.setupProviderConnection(mux.createStream('provider'), originDomain)
this.setupProviderConnection(mux.createStream('provider'), senderUrl)
}
/**
@ -1419,11 +1407,14 @@ module.exports = class MetamaskController extends EventEmitter {
/**
* A method for serving our ethereum provider over a given stream.
* @param {*} outStream - The stream to provide over.
* @param {string} origin - The URI of the requesting resource.
* @param {URL} senderUrl - The URI of the requesting resource.
* @param {string} extensionId - The id of the extension, if the requesting
* resource is an extension.
* @param {object} publicApi - The public API
*/
setupProviderConnection (outStream, origin, publicApi) {
setupProviderConnection (outStream, senderUrl, extensionId, publicApi) {
const getSiteMetadata = publicApi && publicApi.getSiteMetadata
const engine = this.setupProviderEngine(origin, getSiteMetadata)
const engine = this.setupProviderEngine(senderUrl, extensionId, getSiteMetadata)
// setup connection
const providerStream = createEngineStream({ engine })
@ -1433,7 +1424,7 @@ module.exports = class MetamaskController extends EventEmitter {
providerStream,
outStream,
(err) => {
// cleanup filter polyfill middleware
// handle any middleware cleanup
engine._middleware.forEach((mid) => {
if (mid.destroy && typeof mid.destroy === 'function') {
mid.destroy()
@ -1447,7 +1438,8 @@ module.exports = class MetamaskController extends EventEmitter {
/**
* A method for creating a provider that is safely restricted for the requesting domain.
**/
setupProviderEngine (origin, getSiteMetadata) {
setupProviderEngine (senderUrl, extensionId, getSiteMetadata) {
const origin = senderUrl.hostname
// setup json rpc engine stack
const engine = new RpcEngine()
const provider = this.provider
@ -1470,7 +1462,8 @@ module.exports = class MetamaskController extends EventEmitter {
engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController))
// requestAccounts
engine.push(this.providerApprovalController.createMiddleware({
origin,
senderUrl,
extensionId,
getSiteMetadata,
}))
// forward to metamask primary provider
@ -1487,11 +1480,12 @@ module.exports = class MetamaskController extends EventEmitter {
* this is a good candidate for deprecation.
*
* @param {*} outStream - The stream to provide public config over.
* @param {URL} senderUrl - The URL of requesting resource
*/
setupPublicConfig (outStream, originDomain) {
setupPublicConfig (outStream, senderUrl) {
const configStore = this.createPublicConfigStore({
// check the providerApprovalController's approvedOrigins
checkIsEnabled: () => this.providerApprovalController.shouldExposeAccounts(originDomain),
checkIsEnabled: () => this.providerApprovalController.shouldExposeAccounts(senderUrl.hostname),
})
const configStream = asStream(configStore)

View File

@ -1,3 +1,7 @@
// this must run before anything else
require('./lib/freezeGlobals')
// polyfills
import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'

View File

@ -15,6 +15,7 @@
"watch:test:unit": "nodemon --exec \"yarn test:unit\" ./test ./app ./ui",
"sendwithprivatedapp": "static-server test/e2e/send-eth-with-private-key-test --port 8080",
"test:unit": "cross-env METAMASK_ENV=test mocha --exit --require test/setup.js --recursive \"test/unit/**/*.js\" \"ui/app/**/*.test.js\"",
"test:unit:global": "mocha test/unit-global/*",
"test:single": "cross-env METAMASK_ENV=test mocha --require test/helper.js",
"test:integration": "yarn test:integration:build && yarn test:flat",
"test:integration:build": "gulp build:scss",
@ -50,7 +51,7 @@
},
"resolutions": {
"3box/ipfs/ipld-zcash/zcash-bitcore-lib/lodash": "^4.17.12",
"pubnub/superagent-proxy/proxy-agent/pac-proxy-agent/https-proxy-agent": "^3.0.0"
"pubnub/superagent-proxy": "^2.0.0"
},
"dependencies": {
"3box": "^1.10.2",
@ -80,6 +81,7 @@
"debounce": "1.1.0",
"debounce-stream": "^2.0.0",
"deep-extend": "^0.5.1",
"deep-freeze-strict": "1.1.1",
"detect-node": "^2.0.3",
"detectrtc": "^1.3.6",
"dnode": "^1.2.2",
@ -88,10 +90,10 @@
"eth-contract-metadata": "^1.9.2",
"eth-ens-namehash": "^2.0.8",
"eth-json-rpc-errors": "^1.1.0",
"eth-json-rpc-filters": "^4.1.0",
"eth-json-rpc-filters": "^4.1.1",
"eth-json-rpc-infura": "^4.0.1",
"eth-json-rpc-middleware": "^4.2.0",
"eth-keyring-controller": "^5.1.0",
"eth-keyring-controller": "^5.3.0",
"eth-ledger-bridge-keyring": "^0.2.0",
"eth-method-registry": "^1.2.0",
"eth-phishing-detect": "^1.1.4",
@ -112,7 +114,7 @@
"extensionizer": "^1.0.1",
"fast-json-patch": "^2.0.4",
"fuse.js": "^3.2.0",
"gaba": "^1.7.0",
"gaba": "^1.7.5",
"human-standard-token-abi": "^2.0.0",
"jazzicon": "^1.2.0",
"json-rpc-engine": "^5.1.4",
@ -201,9 +203,7 @@
"coveralls": "^3.0.0",
"cross-env": "^5.1.4",
"css-loader": "^2.1.1",
"deep-freeze-strict": "^1.1.1",
"del": "^3.0.0",
"read-installed": "^4.0.3",
"deps-dump": "^1.1.0",
"envify": "^4.0.0",
"enzyme": "^3.4.4",
@ -266,6 +266,7 @@
"radgrad-jsdoc-template": "^1.1.3",
"react-devtools": "^3.6.1",
"react-test-renderer": "^15.6.2",
"read-installed": "^4.0.3",
"redux-mock-store": "^1.5.3",
"redux-test-utils": "^0.2.2",
"remote-redux-devtools": "^0.5.16",
@ -283,7 +284,6 @@
"style-loader": "^0.21.0",
"stylelint": "^9.10.1",
"stylelint-config-standard": "^18.2.0",
"tape": "^4.5.1",
"testem": "^2.16.0",
"through2": "^2.0.3",
"vinyl-buffer": "^1.0.1",

View File

@ -8,14 +8,13 @@ const {
checkBrowserForConsoleErrors,
findElement,
findElements,
loadExtension,
verboseReportOnFailure,
setupFetchMocking,
prepareExtensionForTesting,
} = require('./helpers')
const enLocaleMessages = require('../../app/_locales/en/messages.json')
describe('MetaMask', function () {
let extensionId
let driver
const testSeedPhrase = 'forum vessel pink push lonely enact gentle tail admit parrot grunt dress'
@ -29,7 +28,6 @@ describe('MetaMask', function () {
before(async function () {
const result = await prepareExtensionForTesting()
driver = result.driver
extensionId = result.extensionId
await setupFetchMocking(driver)
})
@ -54,7 +52,7 @@ describe('MetaMask', function () {
describe('Going through the first time flow', () => {
it('clicks the continue button on the welcome screen', async () => {
await findElement(driver, By.css('.welcome-page__header'))
const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button'))
const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`))
welcomeScreenBtn.click()
await delay(largeDelayMs)
})
@ -99,7 +97,7 @@ describe('MetaMask', function () {
assert.equal(seedPhrase.split(' ').length, 12)
await delay(regularDelayMs)
const nextScreen = (await findElements(driver, By.css('button.first-time-flow__button')))[1]
const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.next.message}')]`))
await nextScreen.click()
await delay(regularDelayMs)
})
@ -112,37 +110,12 @@ describe('MetaMask', function () {
await delay(tinyDelayMs)
}
async function retypeSeedPhrase (words, wasReloaded, count = 0) {
try {
if (wasReloaded) {
const byRevealButton = By.css('.reveal-seed-phrase__secret-blocker .reveal-seed-phrase__reveal-button')
await driver.wait(until.elementLocated(byRevealButton, 10000))
const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000)
await revealSeedPhraseButton.click()
await delay(regularDelayMs)
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
await nextScreen.click()
await delay(regularDelayMs)
}
for (let i = 0; i < 12; i++) {
await clickWordAndWait(words[i])
}
} catch (e) {
if (count > 2) {
throw e
} else {
await loadExtension(driver, extensionId)
await retypeSeedPhrase(words, true, count + 1)
}
}
}
it('can retype the seed phrase', async () => {
const words = seedPhrase.split(' ')
await retypeSeedPhrase(words)
for (const word of words) {
await clickWordAndWait(word)
}
const confirm = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirm.click()
@ -151,7 +124,7 @@ describe('MetaMask', function () {
it('clicks through the success screen', async () => {
await findElement(driver, By.xpath(`//div[contains(text(), 'Congratulations')]`))
const doneButton = await findElement(driver, By.css('button.first-time-flow__button'))
const doneButton = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`))
await doneButton.click()
await delay(regularDelayMs)
})
@ -183,7 +156,7 @@ describe('MetaMask', function () {
await passwordInputs[0].sendKeys('correct horse battery staple')
await passwordInputs[1].sendKeys('correct horse battery staple')
await driver.findElement(By.css('.first-time-flow__button')).click()
await driver.findElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.restore.message}')]`)).click()
await delay(regularDelayMs)
})

View File

@ -7,7 +7,6 @@ const {
const {
checkBrowserForConsoleErrors,
findElement,
findElements,
openNewPage,
verboseReportOnFailure,
waitUntilXWindowHandles,
@ -15,6 +14,7 @@ const {
setupFetchMocking,
prepareExtensionForTesting,
} = require('./helpers')
const enLocaleMessages = require('../../app/_locales/en/messages.json')
describe('MetaMask', function () {
let driver
@ -54,7 +54,7 @@ describe('MetaMask', function () {
describe('Going through the first time flow, but skipping the seed phrase challenge', () => {
it('clicks the continue button on the welcome screen', async () => {
await findElement(driver, By.css('.welcome-page__header'))
const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button'))
const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`))
welcomeScreenBtn.click()
await delay(largeDelayMs)
})
@ -87,8 +87,8 @@ describe('MetaMask', function () {
})
it('skips the seed phrase challenge', async () => {
const buttons = await findElements(driver, By.css('.first-time-flow__button'))
await buttons[0].click()
const button = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.remindMeLater.message}')]`))
await button.click()
await delay(regularDelayMs)
const detailsButton = await findElement(driver, By.css('.account-details__details-button'))

View File

@ -12,6 +12,7 @@ const {
setupFetchMocking,
prepareExtensionForTesting,
} = require('./helpers')
const enLocaleMessages = require('../../app/_locales/en/messages.json')
describe('Using MetaMask with an existing account', function () {
let driver
@ -54,7 +55,7 @@ describe('Using MetaMask with an existing account', function () {
describe('First time flow starting from an existing seed phrase', () => {
it('clicks the continue button on the welcome screen', async () => {
await findElement(driver, By.css('.welcome-page__header'))
const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button'))
const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`))
welcomeScreenBtn.click()
await delay(largeDelayMs)
})
@ -91,7 +92,7 @@ describe('Using MetaMask with an existing account', function () {
it('clicks through the success screen', async () => {
await findElement(driver, By.xpath(`//div[contains(text(), 'Congratulations')]`))
const doneButton = await findElement(driver, By.css('button.first-time-flow__button'))
const doneButton = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`))
await doneButton.click()
await delay(regularDelayMs)
})

View File

@ -9,15 +9,14 @@ const {
checkBrowserForConsoleErrors,
findElement,
findElements,
loadExtension,
openNewPage,
verboseReportOnFailure,
setupFetchMocking,
prepareExtensionForTesting,
} = require('./helpers')
const enLocaleMessages = require('../../app/_locales/en/messages.json')
describe('MetaMask', function () {
let extensionId
let driver
let publicAddress
@ -31,7 +30,6 @@ describe('MetaMask', function () {
before(async function () {
const result = await prepareExtensionForTesting()
driver = result.driver
extensionId = result.extensionId
await setupFetchMocking(driver)
})
@ -56,7 +54,7 @@ describe('MetaMask', function () {
describe('Going through the first time flow, but skipping the seed phrase challenge', () => {
it('clicks the continue button on the welcome screen', async () => {
await findElement(driver, By.css('.welcome-page__header'))
const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button'))
const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`))
welcomeScreenBtn.click()
await delay(largeDelayMs)
})
@ -89,8 +87,8 @@ describe('MetaMask', function () {
})
it('skips the seed phrase challenge', async () => {
const buttons = await findElements(driver, By.css('.first-time-flow__button'))
await buttons[0].click()
const button = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.remindMeLater.message}')]`))
await button.click()
await delay(regularDelayMs)
const detailsButton = await findElement(driver, By.css('.account-details__details-button'))
@ -173,7 +171,7 @@ describe('MetaMask', function () {
assert.equal(seedPhrase.split(' ').length, 12)
await delay(regularDelayMs)
const nextScreen = (await findElements(driver, By.css('button.first-time-flow__button')))[1]
const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.next.message}')]`))
await nextScreen.click()
await delay(regularDelayMs)
})
@ -186,37 +184,12 @@ describe('MetaMask', function () {
await delay(tinyDelayMs)
}
async function retypeSeedPhrase (words, wasReloaded, count = 0) {
try {
if (wasReloaded) {
const byRevealButton = By.css('.reveal-seed-phrase__secret-blocker .reveal-seed-phrase__reveal-button')
await driver.wait(until.elementLocated(byRevealButton, 10000))
const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000)
await revealSeedPhraseButton.click()
await delay(regularDelayMs)
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
await nextScreen.click()
await delay(regularDelayMs)
}
for (let i = 0; i < 12; i++) {
await clickWordAndWait(words[i])
}
} catch (e) {
if (count > 2) {
throw e
} else {
await loadExtension(driver, extensionId)
await retypeSeedPhrase(words, true, count + 1)
}
}
}
it('can retype the seed phrase', async () => {
const words = seedPhrase.split(' ')
await retypeSeedPhrase(words)
for (const word of words) {
await clickWordAndWait(word)
}
const confirm = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirm.click()

View File

@ -8,14 +8,13 @@ const {
checkBrowserForConsoleErrors,
findElement,
findElements,
loadExtension,
verboseReportOnFailure,
setupFetchMocking,
prepareExtensionForTesting,
} = require('./helpers')
const enLocaleMessages = require('../../app/_locales/en/messages.json')
describe('MetaMask', function () {
let extensionId
let driver
const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'
@ -29,7 +28,6 @@ describe('MetaMask', function () {
before(async function () {
const result = await prepareExtensionForTesting({ responsive: true })
driver = result.driver
extensionId = result.extensionId
await setupFetchMocking(driver)
})
@ -54,7 +52,7 @@ describe('MetaMask', function () {
describe('Going through the first time flow', () => {
it('clicks the continue button on the welcome screen', async () => {
await findElement(driver, By.css('.welcome-page__header'))
const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button'))
const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`))
welcomeScreenBtn.click()
await delay(largeDelayMs)
})
@ -99,7 +97,7 @@ describe('MetaMask', function () {
assert.equal(seedPhrase.split(' ').length, 12)
await delay(regularDelayMs)
const nextScreen = (await findElements(driver, By.css('button.first-time-flow__button')))[1]
const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.next.message}')]`))
await nextScreen.click()
await delay(regularDelayMs)
})
@ -112,37 +110,12 @@ describe('MetaMask', function () {
await delay(tinyDelayMs)
}
async function retypeSeedPhrase (words, wasReloaded, count = 0) {
try {
if (wasReloaded) {
const byRevealButton = By.css('.reveal-seed-phrase__secret-blocker .reveal-seed-phrase__reveal-button')
await driver.wait(until.elementLocated(byRevealButton, 10000))
const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000)
await revealSeedPhraseButton.click()
await delay(regularDelayMs)
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
await nextScreen.click()
await delay(regularDelayMs)
}
for (let i = 0; i < 12; i++) {
await clickWordAndWait(words[i])
}
} catch (e) {
if (count > 2) {
throw e
} else {
await loadExtension(driver, extensionId)
await retypeSeedPhrase(words, true, count + 1)
}
}
}
it('can retype the seed phrase', async () => {
const words = seedPhrase.split(' ')
await retypeSeedPhrase(words)
for (const word of words) {
await clickWordAndWait(word)
}
const confirm = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirm.click()
@ -151,7 +124,7 @@ describe('MetaMask', function () {
it('clicks through the success screen', async () => {
await findElement(driver, By.xpath(`//div[contains(text(), 'Congratulations')]`))
const doneButton = await findElement(driver, By.css('button.first-time-flow__button'))
const doneButton = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`))
await doneButton.click()
await delay(regularDelayMs)
})
@ -192,7 +165,7 @@ describe('MetaMask', function () {
await passwordInputs[0].sendKeys('correct horse battery staple')
await passwordInputs[1].sendKeys('correct horse battery staple')
await driver.findElement(By.css('.first-time-flow__button')).click()
await driver.findElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.restore.message}')]`)).click()
await delay(regularDelayMs)
})

View File

@ -10,7 +10,6 @@ const {
closeAllWindowHandlesExcept,
findElement,
findElements,
loadExtension,
openNewPage,
switchToWindowWithTitle,
verboseReportOnFailure,
@ -18,9 +17,9 @@ const {
setupFetchMocking,
prepareExtensionForTesting,
} = require('./helpers')
const enLocaleMessages = require('../../app/_locales/en/messages.json')
describe('MetaMask', function () {
let extensionId
let driver
let tokenAddress
@ -35,7 +34,6 @@ describe('MetaMask', function () {
before(async function () {
const result = await prepareExtensionForTesting()
driver = result.driver
extensionId = result.extensionId
await setupFetchMocking(driver)
})
@ -60,7 +58,7 @@ describe('MetaMask', function () {
describe('Going through the first time flow', () => {
it('clicks the continue button on the welcome screen', async () => {
await findElement(driver, By.css('.welcome-page__header'))
const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button'))
const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`))
welcomeScreenBtn.click()
await delay(largeDelayMs)
})
@ -105,7 +103,7 @@ describe('MetaMask', function () {
assert.equal(seedPhrase.split(' ').length, 12)
await delay(regularDelayMs)
const nextScreen = (await findElements(driver, By.css('button.first-time-flow__button')))[1]
const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.next.message}')]`))
await nextScreen.click()
await delay(regularDelayMs)
})
@ -118,37 +116,12 @@ describe('MetaMask', function () {
await delay(tinyDelayMs)
}
async function retypeSeedPhrase (words, wasReloaded, count = 0) {
try {
if (wasReloaded) {
const byRevealButton = By.css('.reveal-seed-phrase__secret-blocker .reveal-seed-phrase__reveal-button')
await driver.wait(until.elementLocated(byRevealButton, 10000))
const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000)
await revealSeedPhraseButton.click()
await delay(regularDelayMs)
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
await nextScreen.click()
await delay(regularDelayMs)
}
for (let i = 0; i < 12; i++) {
await clickWordAndWait(words[i])
}
} catch (e) {
if (count > 2) {
throw e
} else {
await loadExtension(driver, extensionId)
await retypeSeedPhrase(words, true, count + 1)
}
}
}
it('can retype the seed phrase', async () => {
const words = seedPhrase.split(' ')
await retypeSeedPhrase(words)
for (const word of words) {
await clickWordAndWait(word)
}
const confirm = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirm.click()
@ -157,7 +130,7 @@ describe('MetaMask', function () {
it('clicks through the success screen', async () => {
await findElement(driver, By.xpath(`//div[contains(text(), 'Congratulations')]`))
const doneButton = await findElement(driver, By.css('button.first-time-flow__button'))
const doneButton = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`))
await doneButton.click()
await delay(regularDelayMs)
})
@ -249,7 +222,7 @@ describe('MetaMask', function () {
await passwordInputs[0].sendKeys('correct horse battery staple')
await passwordInputs[1].sendKeys('correct horse battery staple')
await driver.findElement(By.css('.first-time-flow__button')).click()
await driver.findElement(By.xpath(`//button[contains(text(), '${enLocaleMessages.restore.message}')]`)).click()
await delay(regularDelayMs)
})
@ -475,13 +448,13 @@ describe('MetaMask', function () {
await approveButton.click()
await driver.switchTo().window(dapp)
await delay(regularDelayMs)
await delay(2000)
})
it('initiates a send from the dapp', async () => {
const send3eth = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`), 10000)
await send3eth.click()
await delay(5000)
await delay(2000)
windowHandles = await driver.getAllWindowHandles()
await switchToWindowWithTitle(driver, 'MetaMask Notification', windowHandles)
@ -494,8 +467,6 @@ describe('MetaMask', function () {
await delay(50)
await gasPriceInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasPriceInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasPriceInput.sendKeys('10')
@ -504,9 +475,9 @@ describe('MetaMask', function () {
await delay(50)
await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'a'))
await delay(50)
await gasLimitInput.sendKeys('25000')
await delay(largeDelayMs * 2)
await delay(1000)
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`), 10000)
await confirmButton.click()
@ -792,14 +763,12 @@ describe('MetaMask', function () {
await modalTabs[1].click()
await delay(regularDelayMs)
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-tab__gas-edit-row__input'))
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-gas-inputs__gas-edit-row__input'))
const gasLimitValue = await gasLimitInput.getAttribute('value')
assert(Number(gasLimitValue) < 100000, 'Gas Limit too high')
await gasPriceInput.sendKeys(Key.chord(Key.CONTROL, 'a'))
await delay(50)
await gasPriceInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasPriceInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasPriceInput.sendKeys('10')
@ -808,18 +777,9 @@ describe('MetaMask', function () {
await delay(50)
await gasLimitInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasLimitInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasLimitInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasLimitInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasLimitInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasLimitInput.sendKeys('60001')
await delay(50)
await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'e'))
await delay(50)
await delay(1000)
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
await save.click()
@ -914,7 +874,7 @@ describe('MetaMask', function () {
await advancedTabButton.click()
await delay(tinyDelayMs)
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-tab__gas-edit-row__input'))
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-gas-inputs__gas-edit-row__input'))
assert(gasPriceInput.getAttribute('value'), 20)
assert(gasLimitInput.getAttribute('value'), 4700000)
@ -1103,12 +1063,10 @@ describe('MetaMask', function () {
await modalTabs[1].click()
await delay(regularDelayMs)
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-tab__gas-edit-row__input'))
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-gas-inputs__gas-edit-row__input'))
await gasPriceInput.sendKeys(Key.chord(Key.CONTROL, 'a'))
await delay(50)
await gasPriceInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasPriceInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasPriceInput.sendKeys('10')
@ -1117,18 +1075,9 @@ describe('MetaMask', function () {
await delay(50)
await gasLimitInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasLimitInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasLimitInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasLimitInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasLimitInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasLimitInput.sendKeys('60000')
await delay(50)
await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'e'))
await delay(50)
await delay(1000)
const save = await findElement(driver, By.css('.page-container__footer-button'))
await save.click()
@ -1246,12 +1195,10 @@ describe('MetaMask', function () {
await modalTabs[1].click()
await delay(regularDelayMs)
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-tab__gas-edit-row__input'))
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-gas-inputs__gas-edit-row__input'))
await gasPriceInput.sendKeys(Key.chord(Key.CONTROL, 'a'))
await delay(50)
await gasPriceInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasPriceInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasPriceInput.sendKeys('10')
@ -1260,18 +1207,9 @@ describe('MetaMask', function () {
await delay(50)
await gasLimitInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasLimitInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasLimitInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasLimitInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasLimitInput.sendKeys(Key.BACK_SPACE)
await delay(50)
await gasLimitInput.sendKeys('60001')
await delay(50)
await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'e'))
await delay(50)
await delay(1000)
const save = await findElement(driver, By.css('.page-container__footer-button'))
await save.click()

View File

@ -12,6 +12,7 @@ const {
setupFetchMocking,
prepareExtensionForTesting,
} = require('./helpers')
const enLocaleMessages = require('../../app/_locales/en/messages.json')
describe('Using MetaMask with an existing account', function () {
let driver
@ -51,7 +52,7 @@ describe('Using MetaMask with an existing account', function () {
describe('First time flow starting from an existing seed phrase', () => {
it('clicks the continue button on the welcome screen', async () => {
await findElement(driver, By.css('.welcome-page__header'))
const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button'))
const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`))
welcomeScreenBtn.click()
await delay(largeDelayMs)
})
@ -88,7 +89,7 @@ describe('Using MetaMask with an existing account', function () {
it('clicks through the success screen', async () => {
await findElement(driver, By.xpath(`//div[contains(text(), 'Congratulations')]`))
const doneButton = await findElement(driver, By.css('button.first-time-flow__button'))
const doneButton = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`))
await doneButton.click()
await delay(regularDelayMs)
})
@ -113,7 +114,7 @@ describe('Using MetaMask with an existing account', function () {
const gasModal = await driver.findElement(By.css('span .modal'))
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-tab__gas-edit-row__input'))
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-gas-inputs__gas-edit-row__input'))
await gasPriceInput.sendKeys(Key.chord(Key.CONTROL, 'a'))
await delay(50)
@ -131,6 +132,8 @@ describe('Using MetaMask with an existing account', function () {
await gasLimitInput.sendKeys('25000')
await delay(1000)
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
await save.click()
await driver.wait(until.stalenessOf(gasModal))
@ -170,7 +173,7 @@ describe('Using MetaMask with an existing account', function () {
const gasModal = await driver.findElement(By.css('span .modal'))
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-tab__gas-edit-row__input'))
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.advanced-gas-inputs__gas-edit-row__input'))
await gasPriceInput.sendKeys(Key.chord(Key.CONTROL, 'a'))
await delay(50)
@ -187,6 +190,8 @@ describe('Using MetaMask with an existing account', function () {
await gasLimitInput.sendKeys('100000')
await delay(1000)
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
await save.click()
await driver.wait(until.stalenessOf(gasModal))

View File

@ -12,6 +12,7 @@ const {
setupFetchMocking,
prepareExtensionForTesting,
} = require('./helpers')
const enLocaleMessages = require('../../app/_locales/en/messages.json')
describe('MetaMask', function () {
let driver
@ -53,7 +54,7 @@ describe('MetaMask', function () {
describe('First time flow starting from an existing seed phrase', () => {
it('clicks the continue button on the welcome screen', async () => {
await findElement(driver, By.css('.welcome-page__header'))
const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button'))
const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`))
welcomeScreenBtn.click()
await delay(largeDelayMs)
})
@ -90,7 +91,7 @@ describe('MetaMask', function () {
it('clicks through the success screen', async () => {
await findElement(driver, By.xpath(`//div[contains(text(), 'Congratulations')]`))
const doneButton = await findElement(driver, By.css('button.first-time-flow__button'))
const doneButton = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`))
await doneButton.click()
await delay(regularDelayMs)
})
@ -176,7 +177,7 @@ describe('MetaMask', function () {
describe('First time flow starting from an existing seed phrase', () => {
it('clicks the continue button on the welcome screen', async () => {
await findElement(driver2, By.css('.welcome-page__header'))
const welcomeScreenBtn = await findElement(driver2, By.css('.first-time-flow__button'))
const welcomeScreenBtn = await findElement(driver2, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`))
welcomeScreenBtn.click()
await delay(largeDelayMs)
})
@ -213,7 +214,7 @@ describe('MetaMask', function () {
it('clicks through the success screen', async () => {
await findElement(driver2, By.xpath(`//div[contains(text(), 'Congratulations')]`))
const doneButton = await findElement(driver2, By.css('button.first-time-flow__button'))
const doneButton = await findElement(driver2, By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`))
await doneButton.click()
await delay(regularDelayMs)
})

View File

@ -15,6 +15,7 @@ const {
setupFetchMocking,
prepareExtensionForTesting,
} = require('./helpers')
const enLocaleMessages = require('../../app/_locales/en/messages.json')
describe('Using MetaMask with an existing account', function () {
let driver
@ -65,7 +66,7 @@ describe('Using MetaMask with an existing account', function () {
describe('First time flow starting from an existing seed phrase', () => {
it('clicks the continue button on the welcome screen', async () => {
await findElement(driver, By.css('.welcome-page__header'))
const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button'))
const welcomeScreenBtn = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.getStarted.message}')]`))
welcomeScreenBtn.click()
await delay(largeDelayMs)
})
@ -102,7 +103,7 @@ describe('Using MetaMask with an existing account', function () {
it('clicks through the success screen', async () => {
await findElement(driver, By.xpath(`//div[contains(text(), 'Congratulations')]`))
const doneButton = await findElement(driver, By.css('button.first-time-flow__button'))
const doneButton = await findElement(driver, By.xpath(`//button[contains(text(), '${enLocaleMessages.endOfFlowMessage10.message}')]`))
await doneButton.click()
await delay(regularDelayMs)
})

View File

@ -0,0 +1,55 @@
/* eslint-disable no-native-reassign */
// this is what we're testing
require('../../app/scripts/lib/freezeGlobals')
const assert = require('assert')
describe('Promise global is immutable', () => {
it('throws when reassinging promise (syntax 1)', () => {
try {
Promise = {}
assert.fail('did not throw error')
} catch (err) {
assert.ok(err, 'did throw error')
}
})
it('throws when reassinging promise (syntax 2)', () => {
try {
global.Promise = {}
assert.fail('did not throw error')
} catch (err) {
assert.ok(err, 'did throw error')
}
})
it('throws when mutating existing Promise property', () => {
try {
Promise.all = () => {}
assert.fail('did not throw error')
} catch (err) {
assert.ok(err, 'did throw error')
}
})
it('throws when adding new Promise property', () => {
try {
Promise.foo = 'bar'
assert.fail('did not throw error')
} catch (err) {
assert.ok(err, 'did throw error')
}
})
it('throws when deleting Promise from global', () => {
try {
delete global.Promise
assert.fail('did not throw error')
} catch (err) {
assert.ok(err, 'did throw error')
}
})
})

View File

@ -0,0 +1,55 @@
const assert = require('assert')
const ObservableStore = require('obs-store')
const PollingBlockTracker = require('eth-block-tracker')
const BalanceController = require('../../../../app/scripts/controllers/balance')
const AccountTracker = require('../../../../app/scripts/lib/account-tracker')
const TransactionController = require('../../../../app/scripts/controllers/transactions')
const { createTestProviderTools } = require('../../../stub/provider')
const provider = createTestProviderTools({ scaffold: {}}).provider
const TEST_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'
const accounts = {
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': {
balance: '0x5e942b06dc24c4d50',
address: TEST_ADDRESS,
},
}
describe('Balance Controller', () => {
let balanceController
it('errors when address, accountTracker, txController, or blockTracker', function () {
try {
balanceController = new BalanceController()
} catch (error) {
assert.equal(error.message, 'Cannot construct a balance checker without address, accountTracker, txController, and blockTracker.')
}
})
beforeEach(() => {
balanceController = new BalanceController({
address: TEST_ADDRESS,
accountTracker: new AccountTracker({
provider,
blockTracker: new PollingBlockTracker({ provider }),
}),
txController: new TransactionController({
provider,
networkStore: new ObservableStore(),
blockTracker: new PollingBlockTracker({ provider }),
}),
blockTracker: new PollingBlockTracker({ provider }),
})
balanceController.accountTracker.store.updateState({ accounts })
})
it('updates balance controller ethBalance from account tracker', async function () {
await balanceController.updateBalance()
const balanceControllerState = balanceController.store.getState()
assert.equal(balanceControllerState.ethBalance, '0x5e942b06dc24c4d50')
})
})

View File

@ -2,6 +2,7 @@ const assert = require('assert')
const sinon = require('sinon')
const clone = require('clone')
const nock = require('nock')
const ethUtil = require('ethereumjs-util')
const createThoughStream = require('through2').obj
const blacklistJSON = require('eth-phishing-detect/src/config')
const firstTimeState = require('../../../unit/localhostState')
@ -103,6 +104,51 @@ describe('MetaMaskController', function () {
sandbox.restore()
})
describe('#getAccounts', function () {
beforeEach(async function () {
const password = 'a-fake-password'
await metamaskController.createNewVaultAndRestore(password, TEST_SEED)
})
it('returns first address when dapp calls web3.eth.getAccounts', function () {
metamaskController.networkController._baseProviderParams.getAccounts((err, res) => {
assert.ifError(err)
assert.equal(res.length, 1)
assert.equal(res[0], '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc')
})
})
})
describe('#importAccountWithStrategy', function () {
const importPrivkey = '4cfd3e90fc78b0f86bf7524722150bb8da9c60cd532564d7ff43f5716514f553'
beforeEach(async function () {
const password = 'a-fake-password'
await metamaskController.createNewVaultAndRestore(password, TEST_SEED)
await metamaskController.importAccountWithStrategy('Private Key', [ importPrivkey ])
})
it('adds private key to keyrings in KeyringController', async function () {
const simpleKeyrings = metamaskController.keyringController.getKeyringsByType('Simple Key Pair')
const privKeyBuffer = simpleKeyrings[0].wallets[0]._privKey
const pubKeyBuffer = simpleKeyrings[0].wallets[0]._pubKey
const addressBuffer = ethUtil.pubToAddress(pubKeyBuffer)
const privKey = ethUtil.bufferToHex(privKeyBuffer)
const pubKey = ethUtil.bufferToHex(addressBuffer)
assert.equal(privKey, ethUtil.addHexPrefix(importPrivkey))
assert.equal(pubKey, '0xe18035bf8712672935fdb4e5e431b1a0183d2dfc')
})
it('adds private key to keyrings in KeyringController', async function () {
const keyringAccounts = await metamaskController.keyringController.getAccounts()
assert.equal(keyringAccounts[keyringAccounts.length - 1], '0xe18035bf8712672935fdb4e5e431b1a0183d2dfc')
})
})
describe('submitPassword', function () {
const password = 'password'
@ -751,7 +797,7 @@ describe('MetaMaskController', function () {
describe('#setupUntrustedCommunication', function () {
let streamTest
const phishingUrl = 'myethereumwalletntw.com'
const phishingUrl = new URL('http://myethereumwalletntw.com')
afterEach(function () {
streamTest.end()
@ -764,7 +810,7 @@ describe('MetaMaskController', function () {
streamTest = createThoughStream((chunk, _, cb) => {
if (chunk.name !== 'phishing') return cb()
assert.equal(chunk.data.hostname, phishingUrl)
assert.equal(chunk.data.hostname, phishingUrl.hostname)
resolve()
cb()
})

View File

@ -25,14 +25,17 @@ describe('ProviderApprovalController', () => {
keyringController: mockUnlockedKeyringController,
})
controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg')
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
assert.deepEqual(controller._getMergedState(), {
approvedOrigins: {},
providerRequests: [{
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}],
providerRequests: [metadata],
})
})
@ -41,14 +44,16 @@ describe('ProviderApprovalController', () => {
keyringController: mockLockedKeyringController,
})
controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg')
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
assert.deepEqual(controller._getMergedState(), {
approvedOrigins: {},
providerRequests: [{
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}],
providerRequests: [metadata],
})
})
@ -57,19 +62,23 @@ describe('ProviderApprovalController', () => {
keyringController: mockUnlockedKeyringController,
})
controller._handleProviderRequest('example1.com', 'Example 1', 'https://example1.com/logo.svg')
controller._handleProviderRequest('example2.com', 'Example 2', 'https://example2.com/logo.svg')
const metadata = [{
hostname: 'https://example1.com',
origin: 'example1.com',
siteTitle: 'Example 1',
siteImage: 'https://example1.com/logo.svg',
}, {
hostname: 'https://example2.com',
origin: 'example2.com',
siteTitle: 'Example 2',
siteImage: 'https://example2.com/logo.svg',
}]
controller._handleProviderRequest(metadata[0])
controller._handleProviderRequest(metadata[1])
assert.deepEqual(controller._getMergedState(), {
approvedOrigins: {},
providerRequests: [{
origin: 'example1.com',
siteTitle: 'Example 1',
siteImage: 'https://example1.com/logo.svg',
}, {
origin: 'example2.com',
siteTitle: 'Example 2',
siteImage: 'https://example2.com/logo.svg',
}],
providerRequests: metadata,
})
})
@ -78,19 +87,23 @@ describe('ProviderApprovalController', () => {
keyringController: mockLockedKeyringController,
})
controller._handleProviderRequest('example1.com', 'Example 1', 'https://example1.com/logo.svg')
controller._handleProviderRequest('example2.com', 'Example 2', 'https://example2.com/logo.svg')
const metadata = [{
hostname: 'https://example1.com',
origin: 'example1.com',
siteTitle: 'Example 1',
siteImage: 'https://example1.com/logo.svg',
}, {
hostname: 'https://example2.com',
origin: 'example2.com',
siteTitle: 'Example 2',
siteImage: 'https://example2.com/logo.svg',
}]
controller._handleProviderRequest(metadata[0])
controller._handleProviderRequest(metadata[1])
assert.deepEqual(controller._getMergedState(), {
approvedOrigins: {},
providerRequests: [{
origin: 'example1.com',
siteTitle: 'Example 1',
siteImage: 'https://example1.com/logo.svg',
}, {
origin: 'example2.com',
siteTitle: 'Example 2',
siteImage: 'https://example2.com/logo.svg',
}],
providerRequests: metadata,
})
})
@ -101,7 +114,13 @@ describe('ProviderApprovalController', () => {
keyringController: mockUnlockedKeyringController,
})
controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg')
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
assert.ok(openPopup.calledOnce)
})
@ -112,7 +131,13 @@ describe('ProviderApprovalController', () => {
keyringController: mockLockedKeyringController,
})
controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg')
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
assert.ok(openPopup.calledOnce)
})
@ -131,7 +156,13 @@ describe('ProviderApprovalController', () => {
},
},
})
controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg')
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
assert.ok(openPopup.notCalled)
})
})
@ -142,12 +173,19 @@ describe('ProviderApprovalController', () => {
keyringController: mockUnlockedKeyringController,
})
controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg')
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
controller.approveProviderRequestByOrigin('example.com')
assert.deepEqual(controller._getMergedState(), {
providerRequests: [],
approvedOrigins: {
'example.com': {
hostname: 'https://example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
},
@ -160,13 +198,20 @@ describe('ProviderApprovalController', () => {
keyringController: mockUnlockedKeyringController,
})
controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg')
controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg')
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
controller._handleProviderRequest(metadata)
controller.approveProviderRequestByOrigin('example.com')
assert.deepEqual(controller._getMergedState(), {
providerRequests: [],
approvedOrigins: {
'example.com': {
hostname: 'https://example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
},
@ -184,6 +229,7 @@ describe('ProviderApprovalController', () => {
providerRequests: [],
approvedOrigins: {
'example.com': {
hostname: null,
siteTitle: null,
siteImage: null,
},
@ -198,7 +244,13 @@ describe('ProviderApprovalController', () => {
keyringController: mockUnlockedKeyringController,
})
controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg')
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
controller.approveProviderRequestByOrigin('example.com')
controller.rejectProviderRequestByOrigin('example.com')
assert.deepEqual(controller._getMergedState(), {
@ -226,7 +278,13 @@ describe('ProviderApprovalController', () => {
keyringController: mockUnlockedKeyringController,
})
controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg')
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
controller.approveProviderRequestByOrigin('example.com')
controller.clearApprovedOrigins()
assert.deepEqual(controller._getMergedState(), {
@ -242,7 +300,13 @@ describe('ProviderApprovalController', () => {
keyringController: mockUnlockedKeyringController,
})
controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg')
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
controller.approveProviderRequestByOrigin('example.com')
assert.ok(controller.shouldExposeAccounts('example.com'))
})
@ -252,7 +316,13 @@ describe('ProviderApprovalController', () => {
keyringController: mockUnlockedKeyringController,
})
controller._handleProviderRequest('example.com', 'Example', 'https://example.com/logo.svg')
const metadata = {
hostname: 'https://example.com',
origin: 'example.com',
siteTitle: 'Example',
siteImage: 'https://example.com/logo.svg',
}
controller._handleProviderRequest(metadata)
controller.approveProviderRequestByOrigin('example.com')
assert.ok(!controller.shouldExposeAccounts('bad.website'))
})

View File

@ -0,0 +1,116 @@
import assert from 'assert'
import sinon from 'sinon'
import NetworkController from '../../../app/scripts/controllers/network/index'
import TypedMessageManager from '../../../app/scripts/lib/typed-message-manager'
describe('Typed Message Manager', () => {
let typedMessageManager, msgParamsV1, msgParamsV3, typedMsgs, messages, msgId, numberMsgId
const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'
const networkController = new NetworkController()
sinon.stub(networkController, 'getNetworkState').returns('1')
beforeEach(() => {
typedMessageManager = new TypedMessageManager({
networkController,
})
msgParamsV1 = {
from: address,
data: [
{ type: 'string', name: 'unit test', value: 'hello there' },
{ type: 'uint32', name: 'A number, but not really a number', value: '$$$' },
],
}
msgParamsV3 = {
from: address,
data: JSON.stringify({
'types': {
'EIP712Domain': [
{'name': 'name', 'type': 'string' },
{'name': 'version', 'type': 'string' },
{'name': 'chainId', ' type': 'uint256' },
{'name': 'verifyingContract', ' type': 'address' },
],
'Person': [
{'name': 'name', 'type': 'string' },
{'name': 'wallet', ' type': 'address' },
],
'Mail': [
{'name': 'from', 'type': 'Person' },
{'name': 'to', 'type': 'Person' },
{'name': 'contents', 'type': 'string' },
],
},
'primaryType': 'Mail',
'domain': {
'name': 'Ether Mainl',
'version': '1',
'chainId': 1,
'verifyingContract': '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
},
'message': {
'from': {
'name': 'Cow',
'wallet': '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
},
'to': {
'name': 'Bob',
'wallet': '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
},
'contents': 'Hello, Bob!',
},
}),
}
typedMessageManager.addUnapprovedMessage(msgParamsV3, 'V3')
typedMsgs = typedMessageManager.getUnapprovedMsgs()
messages = typedMessageManager.messages
msgId = Object.keys(typedMsgs)[0]
messages[0].msgParams.metamaskId = parseInt(msgId)
numberMsgId = parseInt(msgId)
})
it('supports version 1 of signedTypedData', () => {
typedMessageManager.addUnapprovedMessage(msgParamsV1, 'V1')
assert.equal(messages[messages.length - 1].msgParams.data, msgParamsV1.data)
})
it('has params address', function () {
assert.equal(typedMsgs[msgId].msgParams.from, address)
})
it('adds to unapproved messages and sets status to unapproved', function () {
assert.equal(typedMsgs[msgId].status, 'unapproved')
})
it('validates params', function () {
assert.doesNotThrow(() => {
typedMessageManager.validateParams(messages[0])
}, 'Does not throw with valid parameters')
})
it('gets unapproved by id', function () {
const getMsg = typedMessageManager.getMsg(numberMsgId)
assert.equal(getMsg.id, numberMsgId)
})
it('approves messages', async function () {
const messageMetaMaskId = messages[0].msgParams
typedMessageManager.approveMessage(messageMetaMaskId)
assert.equal(messages[0].status, 'approved')
})
it('sets msg status to signed and adds a raw sig to message details', function () {
typedMessageManager.setMsgStatusSigned(numberMsgId, 'raw sig')
assert.equal(messages[0].status, 'signed')
assert.equal(messages[0].rawSig, 'raw sig')
})
it('rejects message', function () {
typedMessageManager.rejectMsg(numberMsgId)
assert.equal(messages[0].status, 'rejected')
})
})

View File

@ -1,3 +1,4 @@
/* eslint-disable */
// Used to inspect long objects
// util.inspect({JSON}, false, null))
// const util = require('util')
@ -29,6 +30,8 @@ describe('Actions', () => {
const noop = () => {}
const currentNetworkId = 42
let background, metamaskController
const TEST_SEED = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium'
@ -37,7 +40,6 @@ describe('Actions', () => {
beforeEach(async () => {
metamaskController = new MetaMaskController({
provider,
keyringController: new KeyringController({}),
@ -88,11 +90,9 @@ describe('Actions', () => {
submitPasswordSpy = sinon.spy(background, 'submitPassword')
verifySeedPhraseSpy = sinon.spy(background, 'verifySeedPhrase')
return store.dispatch(actions.tryUnlockMetamask())
.then(() => {
assert(submitPasswordSpy.calledOnce)
assert(verifySeedPhraseSpy.calledOnce)
})
await store.dispatch(actions.tryUnlockMetamask())
assert(submitPasswordSpy.calledOnce)
assert(verifySeedPhraseSpy.calledOnce)
})
it('errors on submitPassword will fail', async () => {
@ -193,15 +193,17 @@ describe('Actions', () => {
describe('#requestRevealSeedWords', () => {
let submitPasswordSpy
it('calls submitPassword in background', () => {
afterEach(() => {
submitPasswordSpy.restore()
})
it('calls submitPassword in background', async () => {
const store = mockStore()
submitPasswordSpy = sinon.spy(background, 'verifySeedPhrase')
return store.dispatch(actions.requestRevealSeedWords())
.then(() => {
assert(submitPasswordSpy.calledOnce)
})
await store.dispatch(actions.requestRevealSeedWords())
assert(submitPasswordSpy.calledOnce)
})
it('displays warning error message then callback in background errors', async () => {
@ -235,8 +237,9 @@ describe('Actions', () => {
removeAccountSpy.restore()
})
it('calls removeAccount in background and expect actions to show account', () => {
it('calls removeAccount in background and expect actions to show account', async () => {
const store = mockStore(devState)
const expectedActions = [
{ type: 'SHOW_LOADING_INDICATION', value: undefined },
{ type: 'HIDE_LOADING_INDICATION' },
@ -245,20 +248,20 @@ describe('Actions', () => {
removeAccountSpy = sinon.spy(background, 'removeAccount')
return store.dispatch(actions.removeAccount('0xe18035bf8712672935fdb4e5e431b1a0183d2dfc'))
.then(() => {
assert(removeAccountSpy.calledOnce)
assert.deepEqual(store.getActions(), expectedActions)
})
await store.dispatch(actions.removeAccount('0xe18035bf8712672935fdb4e5e431b1a0183d2dfc'))
assert(removeAccountSpy.calledOnce)
assert.deepEqual(store.getActions(), expectedActions)
})
it('displays warning error message when removeAccount callback errors', async () => {
const store = mockStore()
const expectedActions = [
{ type: 'SHOW_LOADING_INDICATION', value: undefined },
{ type: 'HIDE_LOADING_INDICATION' },
{ type: 'DISPLAY_WARNING', value: 'error' },
]
removeAccountSpy = sinon.stub(background, 'removeAccount')
removeAccountSpy.callsFake((_, callback) => {
callback(new Error('error'))
@ -331,11 +334,9 @@ describe('Actions', () => {
resetAccountSpy = sinon.spy(background, 'resetAccount')
return store.dispatch(actions.resetAccount())
.then(() => {
assert(resetAccountSpy.calledOnce)
assert.deepEqual(store.getActions(), expectedActions)
})
await store.dispatch(actions.resetAccount())
assert(resetAccountSpy.calledOnce)
assert.deepEqual(store.getActions(), expectedActions)
})
it('throws if resetAccount throws', async () => {
@ -376,10 +377,8 @@ describe('Actions', () => {
const importPrivkey = 'c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3'
return store.dispatch(actions.importNewAccount('Private Key', [ importPrivkey ]))
.then(() => {
assert(importAccountWithStrategySpy.calledOnce)
})
store.dispatch(actions.importNewAccount('Private Key', [ importPrivkey ]))
assert(importAccountWithStrategySpy.calledOnce)
})
it('displays warning error message when importAccount in background callback errors', async () => {
@ -407,21 +406,181 @@ describe('Actions', () => {
describe('#addNewAccount', () => {
let addNewAccountSpy
afterEach(() => {
addNewAccountSpy.restore()
})
it('Adds a new account', () => {
const store = mockStore({ metamask: devState })
addNewAccountSpy = sinon.spy(background, 'addNewAccount')
const addNewAccountSpy = sinon.spy(background, 'addNewAccount')
return store.dispatch(actions.addNewAccount())
.then(() => {
assert(addNewAccountSpy.calledOnce)
})
store.dispatch(actions.addNewAccount())
assert(addNewAccountSpy.calledOnce)
})
})
describe('#checkHardwareStatus', () => {
let checkHardwareStatusSpy
beforeEach(() => {
checkHardwareStatusSpy = sinon.stub(background, 'checkHardwareStatus')
})
afterEach(() => {
checkHardwareStatusSpy.restore()
})
it('calls checkHardwareStatus in background', async () => {
const store = mockStore()
store.dispatch(await actions.checkHardwareStatus('ledger', `m/44'/60'/0'/0`))
assert.equal(checkHardwareStatusSpy.calledOnce, true)
})
it('shows loading indicator and displays error', async () => {
const store = mockStore()
const expectedActions = [
{ type: 'SHOW_LOADING_INDICATION', value: undefined },
{ type: 'DISPLAY_WARNING', value: 'error' },
]
checkHardwareStatusSpy.callsFake((deviceName, hdPath, callback) => {
callback(new Error('error'))
})
try {
await store.dispatch(actions.checkHardwareStatus())
assert.fail('Should have thrown error')
} catch (_) {
assert.deepEqual(store.getActions(), expectedActions)
}
})
})
describe('#forgetDevice', () => {
let forgetDeviceSpy
beforeEach(() => {
forgetDeviceSpy = sinon.stub(background, 'forgetDevice')
})
afterEach(() => {
forgetDeviceSpy.restore()
})
it('calls forgetDevice in background', () => {
const store = mockStore()
store.dispatch(actions.forgetDevice('ledger'))
assert.equal(forgetDeviceSpy.calledOnce, true)
})
it('shows loading indicator and displays error', async () => {
const store = mockStore()
const expectedActions = [
{ type: 'SHOW_LOADING_INDICATION', value: undefined },
{ type: 'DISPLAY_WARNING', value: 'error' },
]
forgetDeviceSpy.callsFake((deviceName, callback) => {
callback(new Error('error'))
})
try {
await store.dispatch(actions.forgetDevice())
assert.fail('Should have thrown error')
} catch (_) {
assert.deepEqual(store.getActions(), expectedActions)
}
})
})
describe('#connectHardware', () => {
let connectHardwareSpy
beforeEach(() => {
connectHardwareSpy = sinon.stub(background, 'connectHardware')
})
afterEach(() => {
connectHardwareSpy.restore()
})
it('calls connectHardware in background', () => {
const store = mockStore()
store.dispatch(actions.connectHardware('ledger', 0, `m/44'/60'/0'/0`))
assert.equal(connectHardwareSpy.calledOnce, true)
})
it('shows loading indicator and displays error', async () => {
const store = mockStore()
const expectedActions = [
{ type: 'SHOW_LOADING_INDICATION', value: undefined },
{ type: 'DISPLAY_WARNING', value: 'error' },
]
connectHardwareSpy.callsFake((deviceName, page, hdPath, callback) => {
callback(new Error('error'))
})
try {
await store.dispatch(actions.connectHardware())
assert.fail('Should have thrown error')
} catch (_) {
assert.deepEqual(store.getActions(), expectedActions)
}
})
})
describe('#unlockHardwareWalletAccount', () => {
let unlockHardwareWalletAccountSpy
beforeEach(() => {
unlockHardwareWalletAccountSpy = sinon.stub(background, 'unlockHardwareWalletAccount')
})
afterEach(() => {
unlockHardwareWalletAccountSpy.restore()
})
it('calls unlockHardwareWalletAccount in background', () => {
const store = mockStore()
store.dispatch(actions.unlockHardwareWalletAccount('ledger', 0, `m/44'/60'/0'/0`))
assert.equal(unlockHardwareWalletAccountSpy.calledOnce, true)
})
it('shows loading indicator and displays error', async() => {
const store = mockStore()
const expectedActions = [
{ type: 'SHOW_LOADING_INDICATION', value: undefined },
{ type: 'DISPLAY_WARNING', value: 'error' },
]
unlockHardwareWalletAccountSpy.callsFake((deviceName, page, hdPath, callback) => {
callback(new Error('error'))
})
try {
await store.dispatch(actions.unlockHardwareWalletAccount())
assert.fail('Should have thrown error')
} catch (error) {
assert.deepEqual(store.getActions(), expectedActions)
}
})
})
@ -485,11 +644,8 @@ describe('Actions', () => {
const store = mockStore()
signMessageSpy = sinon.spy(background, 'signMessage')
return store.dispatch(actions.signMsg(msgParams))
.then(() => {
assert(signMessageSpy.calledOnce)
})
store.dispatch(actions.signMsg(msgParams))
assert(signMessageSpy.calledOnce)
})
@ -543,10 +699,8 @@ describe('Actions', () => {
signPersonalMessageSpy = sinon.spy(background, 'signPersonalMessage')
return store.dispatch(actions.signPersonalMsg(msgParams))
.then(() => {
assert(signPersonalMessageSpy.calledOnce)
})
store.dispatch(actions.signPersonalMsg(msgParams))
assert(signPersonalMessageSpy.calledOnce)
})
@ -574,12 +728,100 @@ describe('Actions', () => {
})
describe('#signTypedMsg', () => {
let signTypedMsgSpy, messages, typedMessages, msgId
const msgParamsV3 = {
from: '0x0DCD5D886577d5081B0c52e242Ef29E70Be3E7bc',
data: JSON.stringify({
'types': {
'EIP712Domain': [
{'name': 'name', 'type': 'string'},
{'name': 'version', 'type': 'string'},
{'name': 'chainId', 'type': 'uint256'},
{'name': 'verifyingContract', 'type': 'address'},
],
'Person': [
{'name': 'name', 'type': 'string'},
{'name': 'wallet', 'type': 'address'},
],
'Mail': [
{'name': 'from', 'type': 'Person'},
{'name': 'to', 'type': 'Person'},
{'name': 'contents', 'type': 'string'},
],
},
'primaryType': 'Mail',
'domain': {
'name': 'Ether Mainl',
'version': '1',
'chainId': 1,
'verifyingContract': '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
},
'message': {
'from': {
'name': 'Cow',
'wallet': '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
},
'to': {
'name': 'Bob',
'wallet': '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB',
},
'contents': 'Hello, Bob!',
},
}),
}
beforeEach(() => {
metamaskController.newUnsignedTypedMessage(msgParamsV3, 'V3')
messages = metamaskController.typedMessageManager.getUnapprovedMsgs()
typedMessages = metamaskController.typedMessageManager.messages
msgId = Object.keys(messages)[0]
typedMessages[0].msgParams.metamaskId = parseInt(msgId)
})
afterEach(() => {
signTypedMsgSpy.restore()
})
it('calls signTypedMsg in background with no error', () => {
const store = mockStore()
signTypedMsgSpy = sinon.stub(background, 'signTypedMessage')
store.dispatch(actions.signTypedMsg(msgParamsV3))
assert(signTypedMsgSpy.calledOnce)
})
it('returns expected actions with error', async () => {
const store = mockStore()
const expectedActions = [
{ type: 'SHOW_LOADING_INDICATION', value: undefined },
{ type: 'UPDATE_METAMASK_STATE', value: undefined },
{ type: 'HIDE_LOADING_INDICATION' },
{ type: 'DISPLAY_WARNING', value: 'error' },
]
signTypedMsgSpy = sinon.stub(background, 'signTypedMessage')
signTypedMsgSpy.callsFake((_, callback) => {
callback(new Error('error'))
})
try {
await store.dispatch(actions.signTypedMsg())
assert.fail('Should have thrown error')
} catch (_) {
assert.deepEqual(store.getActions(), expectedActions)
}
})
})
describe('#signTx', () => {
let sendTransactionSpy
beforeEach(() => {
global.ethQuery = new EthQuery(provider)
sendTransactionSpy = sinon.stub(global.ethQuery, 'sendTransaction')
})
@ -589,6 +831,7 @@ describe('Actions', () => {
it('calls sendTransaction in global ethQuery', () => {
const store = mockStore()
store.dispatch(actions.signTx())
assert(sendTransactionSpy.calledOnce)
})
@ -608,6 +851,71 @@ describe('Actions', () => {
})
})
describe('#updatedGasData', () => {
it('errors when get code does not return', async () => {
const store = mockStore()
const expectedActions = [
{ type: 'GAS_LOADING_STARTED' },
{ type: 'UPDATE_SEND_ERRORS', value: { gasLoadingError: 'gasLoadingError' } },
{ type: 'GAS_LOADING_FINISHED' },
]
const mockData = {
gasPrice: '0x3b9aca00', //
blockGasLimit: '0x6ad79a', // 7002010
selectedAddress: '0x0DCD5D886577d5081B0c52e242Ef29E70Be3E7bc',
to: '0xEC1Adf982415D2Ef5ec55899b9Bfb8BC0f29251B',
value: '0xde0b6b3a7640000', // 1000000000000000000
}
try {
await store.dispatch(actions.updateGasData(mockData))
assert.fail('Should have thrown error')
} catch (error) {
assert.deepEqual(store.getActions(), expectedActions)
}
})
})
describe('#updatedGasData', () => {
const stub = sinon.stub().returns('0x')
const mockData = {
gasPrice: '0x3b9aca00', //
blockGasLimit: '0x6ad79a', // 7002010
selectedAddress: '0x0DCD5D886577d5081B0c52e242Ef29E70Be3E7bc',
to: '0xEC1Adf982415D2Ef5ec55899b9Bfb8BC0f29251B',
value: '0xde0b6b3a7640000', // 1000000000000000000
}
beforeEach(() => {
global.eth = {
getCode: stub,
}
})
afterEach(() => {
stub.reset()
})
it('returns default gas limit for basic eth transaction', async () => {
const store = mockStore()
const expectedActions = [
{ type: 'GAS_LOADING_STARTED' },
{ type: 'UPDATE_GAS_LIMIT', value: '0x5208' },
{ type: 'metamask/gas/SET_CUSTOM_GAS_LIMIT', value: '0x5208' },
{ type: 'UPDATE_SEND_ERRORS', value: { gasLoadingError: null } },
{ type: 'GAS_LOADING_FINISHED' },
]
await store.dispatch(actions.updateGasData(mockData))
assert.deepEqual(store.getActions(), expectedActions)
})
})
describe('#signTokenTx', () => {
let tokenSpy
@ -628,6 +936,61 @@ describe('Actions', () => {
})
})
describe('#updateTransaction', () => {
let updateTransactionSpy, updateTransactionParamsSpy
const txParams = {
'from': '0x1',
'gas': '0x5208',
'gasPrice': '0x3b9aca00',
'to': '0x2',
'value': '0x0',
}
const txData = { id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: txParams }
beforeEach( async () => {
await metamaskController.txController.txStateManager.addTx(txData)
})
afterEach(() => {
updateTransactionSpy.restore()
updateTransactionParamsSpy.restore()
})
it('updates transaction', async () => {
const store = mockStore()
updateTransactionSpy = sinon.spy(background, 'updateTransaction')
updateTransactionParamsSpy = sinon.spy(actions, 'updateTransactionParams')
const result = [ txData.id, txParams ]
await store.dispatch(actions.updateTransaction(txData))
assert(updateTransactionSpy.calledOnce)
assert(updateTransactionParamsSpy.calledOnce)
assert.deepEqual(updateTransactionParamsSpy.args[0], result)
})
it('rejects with error message', async () => {
const store = mockStore()
updateTransactionSpy = sinon.stub(background, 'updateTransaction')
updateTransactionSpy.callsFake((res, callback) => {
callback(new Error('error'))
})
try {
await store.dispatch(actions.updateTransaction(txData))
assert.fail('Should have thrown error')
} catch (error) {
assert.equal(error.message, 'error')
}
})
})
describe('#lockMetamask', () => {
let backgroundSetLockedSpy
@ -635,18 +998,16 @@ describe('Actions', () => {
backgroundSetLockedSpy.restore()
})
it('calls setLocked', () => {
it('calls setLocked', async () => {
const store = mockStore()
backgroundSetLockedSpy = sinon.spy(background, 'setLocked')
return store.dispatch(actions.lockMetamask())
.then(() => {
assert(backgroundSetLockedSpy.calledOnce)
})
await store.dispatch(actions.lockMetamask())
assert(backgroundSetLockedSpy.calledOnce)
})
it('returns display warning error with value when setLocked in background callback errors', () => {
it('returns display warning error with value when setLocked in background callback errors', async () => {
const store = mockStore()
const expectedActions = [
@ -660,10 +1021,13 @@ describe('Actions', () => {
callback(new Error('error'))
})
return store.dispatch(actions.lockMetamask())
.then(() => {
assert.deepEqual(store.getActions(), expectedActions)
})
try {
await store.dispatch(actions.lockMetamask())
assert.fail('Should have thrown error')
} catch (error) {
assert.deepEqual(store.getActions(), expectedActions)
}
})
})
@ -748,13 +1112,11 @@ describe('Actions', () => {
addTokenSpy.restore()
})
it('calls addToken in background', () => {
it('calls addToken in background', async () => {
const store = mockStore()
store.dispatch(actions.addToken())
.then(() => {
assert(addTokenSpy.calledOnce)
})
assert(addTokenSpy.calledOnce)
})
it('errors when addToken in background throws', async () => {
@ -790,12 +1152,10 @@ describe('Actions', () => {
removeTokenSpy.restore()
})
it('calls removeToken in background', () => {
it('calls removeToken in background', async () => {
const store = mockStore()
store.dispatch(actions.removeToken())
.then(() => {
assert(removeTokenSpy.calledOnce)
})
store.dispatch(await actions.removeToken())
assert(removeTokenSpy.calledOnce)
})
it('errors when removeToken in background fails', async () => {
@ -910,7 +1270,7 @@ describe('Actions', () => {
exportAccountSpy.restore()
})
it('returns expected actions for successful action', () => {
it('returns expected actions for successful action', async () => {
const store = mockStore(devState)
const expectedActions = [
{ type: 'SHOW_LOADING_INDICATION', value: undefined },
@ -921,12 +1281,10 @@ describe('Actions', () => {
submitPasswordSpy = sinon.spy(background, 'submitPassword')
exportAccountSpy = sinon.spy(background, 'exportAccount')
return store.dispatch(actions.exportAccount(password, '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'))
.then(() => {
assert(submitPasswordSpy.calledOnce)
assert(exportAccountSpy.calledOnce)
assert.deepEqual(store.getActions(), expectedActions)
})
await store.dispatch(actions.exportAccount(password, '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'))
assert(submitPasswordSpy.calledOnce)
assert(exportAccountSpy.calledOnce)
assert.deepEqual(store.getActions(), expectedActions)
})
it('returns action errors when first func callback errors', async () => {
@ -1082,9 +1440,7 @@ describe('Actions', () => {
getTransactionCountSpy = sinon.spy(global.ethQuery, 'getTransactionCount')
store.dispatch(actions.updateNetworkNonce())
.then(() => {
assert(getTransactionCountSpy.calledOnce)
})
assert(getTransactionCountSpy.calledOnce)
})
it('errors when getTransactionCount throws', async () => {
@ -1155,7 +1511,7 @@ describe('Actions', () => {
fetchMock.restore()
})
it('calls expected actions', () => {
it('calls expected actions', async () => {
const store = mockStore()
setCurrentLocaleSpy = sinon.spy(background, 'setCurrentLocale')
@ -1165,14 +1521,12 @@ describe('Actions', () => {
{ type: 'HIDE_LOADING_INDICATION' },
]
return store.dispatch(actions.updateCurrentLocale('en'))
.then(() => {
assert(setCurrentLocaleSpy.calledOnce)
assert.deepEqual(store.getActions(), expectedActions)
})
await store.dispatch(actions.updateCurrentLocale('en'))
assert(setCurrentLocaleSpy.calledOnce)
assert.deepEqual(store.getActions(), expectedActions)
})
it('calls expected actions', () => {
it('errors when setCurrentLocale throws', async () => {
const store = mockStore()
const expectedActions = [
{ type: 'SHOW_LOADING_INDICATION', value: undefined },
@ -1184,48 +1538,54 @@ describe('Actions', () => {
callback(new Error('error'))
})
return store.dispatch(actions.updateCurrentLocale('en'))
.then(() => {
assert.deepEqual(store.getActions(), expectedActions)
})
try {
await store.dispatch(actions.updateCurrentLocale('en'))
assert.fail('Should have thrown error')
} catch (_) {
assert.deepEqual(store.getActions(), expectedActions)
}
})
})
describe('#markPasswordForgotten', () => {
let markPasswordForgottenSpy
let markPasswordForgottenSpy, forgotPasswordSpy
beforeEach(() => {
markPasswordForgottenSpy = sinon.stub(background, 'markPasswordForgotten')
markPasswordForgottenSpy = sinon.spy(background, 'markPasswordForgotten')
forgotPasswordSpy = sinon.spy(actions, 'forgotPassword')
})
afterEach(() => {
markPasswordForgottenSpy.restore()
forgotPasswordSpy.restore()
})
it('calls markPasswordForgotten', () => {
const store = mockStore()
store.dispatch(actions.markPasswordForgotten())
assert(forgotPasswordSpy.calledOnce)
assert(markPasswordForgottenSpy.calledOnce)
})
})
describe('#unMarkPasswordForgotten', () => {
let unMarkPasswordForgottenSpy
let unMarkPasswordForgottenSpy, forgotPasswordSpy
beforeEach(() => {
unMarkPasswordForgottenSpy = sinon.stub(background, 'unMarkPasswordForgotten')
unMarkPasswordForgottenSpy = sinon.stub(background, 'unMarkPasswordForgotten').returns(forgotPasswordSpy)
forgotPasswordSpy = sinon.spy(actions, 'forgotPassword')
})
afterEach(() => {
unMarkPasswordForgottenSpy.restore()
forgotPasswordSpy.restore()
})
it('calls unMarkPasswordForgotten', () => {
it('calls unMarkPasswordForgotten', async () => {
const store = mockStore()
store.dispatch(actions.unMarkPasswordForgotten())
store.dispatch(await actions.unMarkPasswordForgotten())
assert(unMarkPasswordForgottenSpy.calledOnce)
})
})
})

View File

@ -3,22 +3,11 @@ import PropTypes from 'prop-types'
import classnames from 'classnames'
import debounce from 'lodash.debounce'
export default class AdvancedTabContent extends Component {
export default class AdvancedGasInputs extends Component {
static contextTypes = {
t: PropTypes.func,
}
constructor (props) {
super(props)
this.state = {
gasPrice: this.props.customGasPrice,
gasLimit: this.props.customGasLimit,
}
this.changeGasPrice = debounce(this.changeGasPrice, 500)
this.changeGasLimit = debounce(this.changeGasLimit, 500)
}
static propTypes = {
updateCustomGasPrice: PropTypes.func,
updateCustomGasLimit: PropTypes.func,
@ -31,6 +20,16 @@ export default class AdvancedTabContent extends Component {
showGasLimitInfoModal: PropTypes.func,
}
constructor (props) {
super(props)
this.state = {
gasPrice: this.props.customGasPrice,
gasLimit: this.props.customGasLimit,
}
this.changeGasPrice = debounce(this.changeGasPrice, 500)
this.changeGasLimit = debounce(this.changeGasLimit, 500)
}
componentDidUpdate (prevProps) {
const { customGasPrice: prevCustomGasPrice, customGasLimit: prevCustomGasLimit } = prevProps
const { customGasPrice, customGasLimit } = this.props
@ -50,12 +49,7 @@ export default class AdvancedTabContent extends Component {
}
changeGasLimit = (e) => {
if (e.target.value < 21000) {
this.setState({ gasLimit: 21000 })
this.props.updateCustomGasLimit(21000)
} else {
this.props.updateCustomGasLimit(Number(e.target.value))
}
this.props.updateCustomGasLimit(Number(e.target.value))
}
onChangeGasPrice = (e) => {
@ -67,89 +61,83 @@ export default class AdvancedTabContent extends Component {
this.props.updateCustomGasPrice(Number(e.target.value))
}
gasInputError ({ labelKey, insufficientBalance, customPriceIsSafe, isSpeedUp, value }) {
gasPriceError ({ insufficientBalance, customPriceIsSafe, isSpeedUp, gasPrice }) {
const { t } = this.context
let errorText
let errorType
let isInError = true
if (insufficientBalance) {
errorText = t('insufficientBalance')
errorType = 'error'
} else if (labelKey === 'gasPrice' && isSpeedUp && value === 0) {
errorText = t('zeroGasPriceOnSpeedUpError')
errorType = 'error'
} else if (labelKey === 'gasPrice' && !customPriceIsSafe) {
errorText = t('gasPriceExtremelyLow')
errorType = 'warning'
} else {
isInError = false
return {
errorText: t('insufficientBalance'),
errorType: 'error',
}
} else if (isSpeedUp && gasPrice === 0) {
return {
errorText: t('zeroGasPriceOnSpeedUpError'),
errorType: 'error',
}
} else if (!customPriceIsSafe) {
return {
errorText: t('gasPriceExtremelyLow'),
errorType: 'warning',
}
}
return {
isInError,
errorText,
errorType,
return {}
}
gasLimitError ({ insufficientBalance, gasLimit }) {
const { t } = this.context
if (insufficientBalance) {
return {
errorText: t('insufficientBalance'),
errorType: 'error',
}
} else if (gasLimit < 21000) {
return {
errorText: t('gasLimitTooLow'),
errorType: 'error',
}
}
return {}
}
gasInput ({ labelKey, value, onChange, insufficientBalance, customPriceIsSafe, isSpeedUp }) {
const {
isInError,
errorText,
errorType,
} = this.gasInputError({ labelKey, insufficientBalance, customPriceIsSafe, isSpeedUp, value })
return (
<div className="advanced-gas-inputs__gas-edit-row__input-wrapper">
<input
className={classnames('advanced-gas-inputs__gas-edit-row__input', {
'advanced-gas-inputs__gas-edit-row__input--error': isInError && errorType === 'error',
'advanced-gas-inputs__gas-edit-row__input--warning': isInError && errorType === 'warning',
})}
type="number"
value={value}
onChange={onChange}
/>
<div className={classnames('advanced-gas-inputs__gas-edit-row__input-arrows', {
'advanced-gas-inputs__gas-edit-row__input--error': isInError && errorType === 'error',
'advanced-gas-inputs__gas-edit-row__input--warning': isInError && errorType === 'warning',
})}>
<div
className="advanced-gas-inputs__gas-edit-row__input-arrows__i-wrap"
onClick={() => onChange({ target: { value: value + 1 } })}
>
<i className="fa fa-sm fa-angle-up" />
</div>
<div
className="advanced-gas-inputs__gas-edit-row__input-arrows__i-wrap"
onClick={() => onChange({ target: { value: Math.max(value - 1, 0) } })}
>
<i className="fa fa-sm fa-angle-down" />
</div>
</div>
{ isInError
? <div className={`advanced-gas-inputs__gas-edit-row__${errorType}-text`}>
{ errorText }
</div>
: null }
</div>
)
}
infoButton (onClick) {
return <i className="fa fa-info-circle" onClick={onClick} />
}
renderGasEditRow (gasInputArgs) {
renderGasInput ({ value, onChange, errorComponent, errorType, infoOnClick, label }) {
return (
<div className="advanced-gas-inputs__gas-edit-row">
<div className="advanced-gas-inputs__gas-edit-row__label">
{ this.context.t(gasInputArgs.labelKey) }
{ this.infoButton(() => gasInputArgs.infoOnClick()) }
{ label }
<i className="fa fa-info-circle" onClick={infoOnClick} />
</div>
<div className="advanced-gas-inputs__gas-edit-row__input-wrapper">
<input
className={classnames('advanced-gas-inputs__gas-edit-row__input', {
'advanced-gas-inputs__gas-edit-row__input--error': errorType === 'error',
'advanced-gas-inputs__gas-edit-row__input--warning': errorType === 'warning',
})}
type="number"
value={value}
onChange={onChange}
/>
<div className={classnames('advanced-gas-inputs__gas-edit-row__input-arrows', {
'advanced-gas-inputs__gas-edit-row__input--error': errorType === 'error',
'advanced-gas-inputs__gas-edit-row__input--warning': errorType === 'warning',
})}>
<div
className="advanced-gas-inputs__gas-edit-row__input-arrows__i-wrap"
onClick={() => onChange({ target: { value: value + 1 } })}
>
<i className="fa fa-sm fa-angle-up" />
</div>
<div
className="advanced-gas-inputs__gas-edit-row__input-arrows__i-wrap"
onClick={() => onChange({ target: { value: Math.max(value - 1, 0) } })}
>
<i className="fa fa-sm fa-angle-down" />
</div>
</div>
{ errorComponent }
</div>
{ this.gasInput(gasInputArgs) }
</div>
)
}
@ -162,25 +150,47 @@ export default class AdvancedTabContent extends Component {
showGasPriceInfoModal,
showGasLimitInfoModal,
} = this.props
const {
gasPrice,
gasLimit,
} = this.state
const {
errorText: gasPriceErrorText,
errorType: gasPriceErrorType,
} = this.gasPriceError({ insufficientBalance, customPriceIsSafe, isSpeedUp, gasPrice })
const gasPriceErrorComponent = gasPriceErrorType ?
<div className={`advanced-gas-inputs__gas-edit-row__${gasPriceErrorType}-text`}>
{ gasPriceErrorText }
</div> :
null
const {
errorText: gasLimitErrorText,
errorType: gasLimitErrorType,
} = this.gasLimitError({ insufficientBalance, gasLimit })
const gasLimitErrorComponent = gasLimitErrorType ?
<div className={`advanced-gas-inputs__gas-edit-row__${gasLimitErrorType}-text`}>
{ gasLimitErrorText }
</div> :
null
return (
<div className="advanced-gas-inputs__gas-edit-rows">
{ this.renderGasEditRow({
labelKey: 'gasPrice',
{ this.renderGasInput({
label: this.context.t('gasPrice'),
value: this.state.gasPrice,
onChange: this.onChangeGasPrice,
insufficientBalance,
customPriceIsSafe,
showGWEI: true,
isSpeedUp,
errorComponent: gasPriceErrorComponent,
errorType: gasPriceErrorType,
infoOnClick: showGasPriceInfoModal,
}) }
{ this.renderGasEditRow({
labelKey: 'gasLimit',
{ this.renderGasInput({
label: this.context.t('gasLimit'),
value: this.state.gasLimit,
onChange: this.onChangeGasLimit,
insufficientBalance,
customPriceIsSafe,
errorComponent: gasLimitErrorComponent,
errorType: gasLimitErrorType,
infoOnClick: showGasLimitInfoModal,
}) }
</div>

View File

@ -25,9 +25,9 @@ const mapDispatchToProps = dispatch => {
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const {customGasPrice, customGasLimit, updateCustomGasPrice, updateCustomGasLimit} = ownProps
return {
...ownProps,
...stateProps,
...dispatchProps,
...ownProps,
customGasPrice: convertGasPriceForInputs(customGasPrice),
customGasLimit: convertGasLimitForInputs(customGasLimit),
updateCustomGasPrice: (price) => updateCustomGasPrice(decGWEIToHexWEI(price)),

View File

@ -1,9 +1,8 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import Loading from '../../../../ui/loading-screen'
import GasPriceChart from '../../gas-price-chart'
import debounce from 'lodash.debounce'
import AdvancedGasInputs from '../../advanced-gas-inputs'
export default class AdvancedTabContent extends Component {
static contextTypes = {
@ -13,8 +12,8 @@ export default class AdvancedTabContent extends Component {
static propTypes = {
updateCustomGasPrice: PropTypes.func,
updateCustomGasLimit: PropTypes.func,
customGasPrice: PropTypes.number,
customGasLimit: PropTypes.number,
customModalGasPriceInHex: PropTypes.string,
customModalGasLimitInHex: PropTypes.string,
gasEstimatesLoading: PropTypes.bool,
millisecondsRemaining: PropTypes.number,
transactionFee: PropTypes.string,
@ -26,95 +25,6 @@ export default class AdvancedTabContent extends Component {
isEthereumNetwork: PropTypes.bool,
}
constructor (props) {
super(props)
this.debouncedGasLimitReset = debounce((dVal) => {
if (dVal < 21000) {
props.updateCustomGasLimit(21000)
}
}, 1000, { trailing: true })
this.onChangeGasLimit = (val) => {
props.updateCustomGasLimit(val)
this.debouncedGasLimitReset(val)
}
}
gasInputError ({ labelKey, insufficientBalance, customPriceIsSafe, isSpeedUp, value }) {
const { t } = this.context
let errorText
let errorType
let isInError = true
if (insufficientBalance) {
errorText = t('insufficientBalance')
errorType = 'error'
} else if (labelKey === 'gasPrice' && isSpeedUp && value === 0) {
errorText = t('zeroGasPriceOnSpeedUpError')
errorType = 'error'
} else if (labelKey === 'gasPrice' && !customPriceIsSafe) {
errorText = t('gasPriceExtremelyLow')
errorType = 'warning'
} else {
isInError = false
}
return {
isInError,
errorText,
errorType,
}
}
gasInput ({ labelKey, value, onChange, insufficientBalance, customPriceIsSafe, isSpeedUp }) {
const {
isInError,
errorText,
errorType,
} = this.gasInputError({ labelKey, insufficientBalance, customPriceIsSafe, isSpeedUp, value })
return (
<div className="advanced-tab__gas-edit-row__input-wrapper">
<input
className={classnames('advanced-tab__gas-edit-row__input', {
'advanced-tab__gas-edit-row__input--error': isInError && errorType === 'error',
'advanced-tab__gas-edit-row__input--warning': isInError && errorType === 'warning',
})}
type="number"
value={value}
onChange={event => onChange(Number(event.target.value))}
/>
<div className={classnames('advanced-tab__gas-edit-row__input-arrows', {
'advanced-tab__gas-edit-row__input--error': isInError && errorType === 'error',
'advanced-tab__gas-edit-row__input--warning': isInError && errorType === 'warning',
})}>
<div
className="advanced-tab__gas-edit-row__input-arrows__i-wrap"
onClick={() => onChange(value + 1)}
>
<i className="fa fa-sm fa-angle-up" />
</div>
<div
className="advanced-tab__gas-edit-row__input-arrows__i-wrap"
onClick={() => onChange(Math.max(value - 1, 0))}
>
<i className="fa fa-sm fa-angle-down" />
</div>
</div>
{ isInError
? <div className={`advanced-tab__gas-edit-row__${errorType}-text`}>
{ errorText }
</div>
: null }
</div>
)
}
infoButton (onClick) {
return <i className="fa fa-info-circle" onClick={onClick} />
}
renderDataSummary (transactionFee, timeRemaining) {
return (
<div className="advanced-tab__transaction-data-summary">
@ -132,56 +42,14 @@ export default class AdvancedTabContent extends Component {
)
}
renderGasEditRow (gasInputArgs) {
return (
<div className="advanced-tab__gas-edit-row">
<div className="advanced-tab__gas-edit-row__label">
{ this.context.t(gasInputArgs.labelKey) }
{ this.infoButton(() => {}) }
</div>
{ this.gasInput(gasInputArgs) }
</div>
)
}
renderGasEditRows ({
customGasPrice,
updateCustomGasPrice,
customGasLimit,
insufficientBalance,
customPriceIsSafe,
isSpeedUp,
}) {
return (
<div className="advanced-tab__gas-edit-rows">
{ this.renderGasEditRow({
labelKey: 'gasPrice',
value: customGasPrice,
onChange: updateCustomGasPrice,
insufficientBalance,
customPriceIsSafe,
showGWEI: true,
isSpeedUp,
}) }
{ this.renderGasEditRow({
labelKey: 'gasLimit',
value: customGasLimit,
onChange: this.onChangeGasLimit,
insufficientBalance,
customPriceIsSafe,
}) }
</div>
)
}
render () {
const { t } = this.context
const {
updateCustomGasPrice,
updateCustomGasLimit,
timeRemaining,
customGasPrice,
customGasLimit,
customModalGasPriceInHex,
customModalGasLimitInHex,
insufficientBalance,
gasChartProps,
gasEstimatesLoading,
@ -195,15 +63,17 @@ export default class AdvancedTabContent extends Component {
<div className="advanced-tab">
{ this.renderDataSummary(transactionFee, timeRemaining) }
<div className="advanced-tab__fee-chart">
{ this.renderGasEditRows({
customGasPrice,
updateCustomGasPrice,
customGasLimit,
updateCustomGasLimit,
insufficientBalance,
customPriceIsSafe,
isSpeedUp,
}) }
<div className="advanced-tab__gas-inputs">
<AdvancedGasInputs
updateCustomGasPrice={updateCustomGasPrice}
updateCustomGasLimit={updateCustomGasLimit}
customGasPrice={customModalGasPriceInHex}
customGasLimit={customModalGasLimitInHex}
insufficientBalance={insufficientBalance}
customPriceIsSafe={customPriceIsSafe}
isSpeedUp={isSpeedUp}
/>
</div>
{ isEthereumNetwork
? <div>
<div className="advanced-tab__fee-chart__title">{ t('liveGasPricePredictions') }</div>

View File

@ -92,137 +92,13 @@
padding-right: 27px;
}
&__gas-edit-rows {
height: 73px;
&__gas-inputs {
display: flex;
flex-flow: row;
justify-content: space-between;
margin-left: 20px;
margin-right: 10px;
margin-top: 9px;
}
&__gas-edit-row {
display: flex;
flex-flow: column;
&__label {
color: #313B5E;
font-size: 14px;
display: flex;
justify-content: space-between;
align-items: center;
.fa-info-circle {
color: $silver;
margin-left: 10px;
cursor: pointer;
}
.fa-info-circle:hover {
color: $mid-gray;
}
}
&__error-text {
font-size: 12px;
color: red;
}
&__warning-text {
font-size: 12px;
color: orange;
}
&__input-wrapper {
position: relative;
}
&__input {
/*rtl:ignore*/
direction: ltr;
border: 1px solid $dusty-gray;
border-radius: 4px;
color: $mid-gray;
font-size: 16px;
height: 24px;
width: 155px;
padding-left: 8px;
padding-top: 2px;
margin-top: 7px;
}
&__input--error {
border: 1px solid $red;
}
&__input--warning {
border: 1px solid $orange;
}
&__input-arrows {
position: absolute;
top: 7px;
/*rtl:ignore*/
right: 0px;
width: 17px;
height: 24px;
border: 1px solid #dadada;
border-top-right-radius: 4px;
display: flex;
flex-direction: column;
color: #9b9b9b;
font-size: .8em;
border-bottom-right-radius: 4px;
cursor: pointer;
&__i-wrap {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
cursor: pointer;
}
&__i-wrap:hover {
background: #4EADE7;
color: $white;
}
i:hover {
background: #4EADE7;
}
i {
font-size: 10px;
}
}
&__input-arrows--error {
border: 1px solid $red;
}
&__input-arrows--warning {
border: 1px solid $orange;
}
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
-moz-appearance: none;
display: none;
}
input[type="number"]:hover::-webkit-inner-spin-button {
-webkit-appearance: none;
-moz-appearance: none;
display: none;
}
&__gwei-symbol {
position: absolute;
top: 8px;
right: 10px;
color: $dusty-gray;
}
margin-top: 10px;
margin-bottom: 20px
}
}

View File

@ -12,11 +12,7 @@ const propsMethodSpies = {
updateCustomGasLimit: sinon.spy(),
}
sinon.spy(AdvancedTabContent.prototype, 'renderGasEditRow')
sinon.spy(AdvancedTabContent.prototype, 'gasInput')
sinon.spy(AdvancedTabContent.prototype, 'renderGasEditRows')
sinon.spy(AdvancedTabContent.prototype, 'renderDataSummary')
sinon.spy(AdvancedTabContent.prototype, 'gasInputError')
describe('AdvancedTabContent Component', function () {
let wrapper
@ -25,23 +21,20 @@ describe('AdvancedTabContent Component', function () {
wrapper = shallow(<AdvancedTabContent
updateCustomGasPrice={propsMethodSpies.updateCustomGasPrice}
updateCustomGasLimit={propsMethodSpies.updateCustomGasLimit}
customGasPrice={11}
customGasLimit={23456}
timeRemaining={21500}
customModalGasPriceInHex={'11'}
customModalGasLimitInHex={'23456'}
timeRemaining={'21500'}
transactionFee={'$0.25'}
insufficientBalance={false}
customPriceIsSafe={true}
isSpeedUp={false}
isEthereumNetwork={true}
/>, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } })
/>)
})
afterEach(() => {
propsMethodSpies.updateCustomGasPrice.resetHistory()
propsMethodSpies.updateCustomGasLimit.resetHistory()
AdvancedTabContent.prototype.renderGasEditRow.resetHistory()
AdvancedTabContent.prototype.gasInput.resetHistory()
AdvancedTabContent.prototype.renderGasEditRows.resetHistory()
AdvancedTabContent.prototype.renderDataSummary.resetHistory()
})
@ -59,7 +52,6 @@ describe('AdvancedTabContent Component', function () {
const feeChartDiv = advancedTabChildren.at(1)
assert(feeChartDiv.childAt(0).hasClass('advanced-tab__gas-edit-rows'))
assert(feeChartDiv.childAt(1).childAt(0).hasClass('advanced-tab__fee-chart__title'))
assert(feeChartDiv.childAt(1).childAt(1).is(GasPriceChart))
assert(feeChartDiv.childAt(1).childAt(2).hasClass('advanced-tab__fee-chart__speed-buttons'))
@ -75,31 +67,15 @@ describe('AdvancedTabContent Component', function () {
const feeChartDiv = advancedTabChildren.at(1)
assert(feeChartDiv.childAt(0).hasClass('advanced-tab__gas-edit-rows'))
assert(feeChartDiv.childAt(1).childAt(0).hasClass('advanced-tab__fee-chart__title'))
assert(feeChartDiv.childAt(1).childAt(1).is(Loading))
assert(feeChartDiv.childAt(1).childAt(2).hasClass('advanced-tab__fee-chart__speed-buttons'))
})
it('should call renderDataSummary with the expected params', () => {
assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1)
const renderDataSummaryArgs = AdvancedTabContent.prototype.renderDataSummary.getCall(0).args
assert.deepEqual(renderDataSummaryArgs, ['$0.25', 21500])
})
it('should call renderGasEditRows with the expected params', () => {
assert.equal(AdvancedTabContent.prototype.renderGasEditRows.callCount, 1)
const renderGasEditRowArgs = AdvancedTabContent.prototype.renderGasEditRows.getCall(0).args
assert.deepEqual(renderGasEditRowArgs, [{
customGasPrice: 11,
customGasLimit: 23456,
insufficientBalance: false,
customPriceIsSafe: true,
updateCustomGasPrice: propsMethodSpies.updateCustomGasPrice,
updateCustomGasLimit: propsMethodSpies.updateCustomGasLimit,
isSpeedUp: false,
}])
})
})
describe('renderDataSummary()', () => {
@ -129,237 +105,4 @@ describe('AdvancedTabContent Component', function () {
})
})
describe('renderGasEditRow()', () => {
let gasEditRow
beforeEach(() => {
AdvancedTabContent.prototype.gasInput.resetHistory()
gasEditRow = shallow(wrapper.instance().renderGasEditRow({
labelKey: 'mockLabelKey',
someArg: 'argA',
}))
})
it('should render the gas-edit-row root node', () => {
assert(gasEditRow.hasClass('advanced-tab__gas-edit-row'))
})
it('should render a label and an input', () => {
const gasEditRowChildren = gasEditRow.children()
assert.equal(gasEditRowChildren.length, 2)
assert(gasEditRowChildren.at(0).hasClass('advanced-tab__gas-edit-row__label'))
assert(gasEditRowChildren.at(1).hasClass('advanced-tab__gas-edit-row__input-wrapper'))
})
it('should render the label key and info button', () => {
const gasRowLabelChildren = gasEditRow.children().at(0).children()
assert.equal(gasRowLabelChildren.length, 2)
assert(gasRowLabelChildren.at(0), 'mockLabelKey')
assert(gasRowLabelChildren.at(1).hasClass('fa-info-circle'))
})
it('should call this.gasInput with the correct args', () => {
const gasInputSpyArgs = AdvancedTabContent.prototype.gasInput.args
assert.deepEqual(gasInputSpyArgs[0], [ { labelKey: 'mockLabelKey', someArg: 'argA' } ])
})
})
describe('renderGasEditRows()', () => {
let gasEditRows
let tempOnChangeGasLimit
beforeEach(() => {
tempOnChangeGasLimit = wrapper.instance().onChangeGasLimit
wrapper.instance().onChangeGasLimit = () => 'mockOnChangeGasLimit'
AdvancedTabContent.prototype.renderGasEditRow.resetHistory()
gasEditRows = shallow(wrapper.instance().renderGasEditRows(
'mockGasPrice',
() => 'mockUpdateCustomGasPriceReturn',
'mockGasLimit',
() => 'mockUpdateCustomGasLimitReturn',
false
))
})
afterEach(() => {
wrapper.instance().onChangeGasLimit = tempOnChangeGasLimit
})
it('should render the gas-edit-rows root node', () => {
assert(gasEditRows.hasClass('advanced-tab__gas-edit-rows'))
})
it('should render two rows', () => {
const gasEditRowsChildren = gasEditRows.children()
assert.equal(gasEditRowsChildren.length, 2)
assert(gasEditRowsChildren.at(0).hasClass('advanced-tab__gas-edit-row'))
assert(gasEditRowsChildren.at(1).hasClass('advanced-tab__gas-edit-row'))
})
it('should call this.renderGasEditRow twice, with the expected args', () => {
const renderGasEditRowSpyArgs = AdvancedTabContent.prototype.renderGasEditRow.args
assert.equal(renderGasEditRowSpyArgs.length, 2)
assert.deepEqual(renderGasEditRowSpyArgs[0].map(String), [{
labelKey: 'gasPrice',
value: 'mockGasLimit',
onChange: () => 'mockOnChangeGasLimit',
insufficientBalance: false,
customPriceIsSafe: true,
showGWEI: true,
}].map(String))
assert.deepEqual(renderGasEditRowSpyArgs[1].map(String), [{
labelKey: 'gasPrice',
value: 'mockGasPrice',
onChange: () => 'mockUpdateCustomGasPriceReturn',
insufficientBalance: false,
customPriceIsSafe: true,
showGWEI: true,
}].map(String))
})
})
describe('infoButton()', () => {
let infoButton
beforeEach(() => {
AdvancedTabContent.prototype.renderGasEditRow.resetHistory()
infoButton = shallow(wrapper.instance().infoButton(() => 'mockOnClickReturn'))
})
it('should render the i element', () => {
assert(infoButton.hasClass('fa-info-circle'))
})
it('should pass the onClick argument to the i tag onClick prop', () => {
assert(infoButton.props().onClick(), 'mockOnClickReturn')
})
})
describe('gasInput()', () => {
let gasInput
beforeEach(() => {
AdvancedTabContent.prototype.renderGasEditRow.resetHistory()
AdvancedTabContent.prototype.gasInputError.resetHistory()
gasInput = shallow(wrapper.instance().gasInput({
labelKey: 'gasPrice',
value: 321,
onChange: value => value + 7,
insufficientBalance: false,
showGWEI: true,
customPriceIsSafe: true,
isSpeedUp: false,
}))
})
it('should render the input-wrapper root node', () => {
assert(gasInput.hasClass('advanced-tab__gas-edit-row__input-wrapper'))
})
it('should render two children, including an input', () => {
assert.equal(gasInput.children().length, 2)
assert(gasInput.children().at(0).hasClass('advanced-tab__gas-edit-row__input'))
})
it('should call the passed onChange method with the value of the input onChange event', () => {
const inputOnChange = gasInput.find('input').props().onChange
assert.equal(inputOnChange({ target: { value: 8} }), 15)
})
it('should have two input arrows', () => {
const upArrow = gasInput.find('.fa-angle-up')
assert.equal(upArrow.length, 1)
const downArrow = gasInput.find('.fa-angle-down')
assert.equal(downArrow.length, 1)
})
it('should call onChange with the value incremented decremented when its onchange method is called', () => {
const upArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(0)
assert.equal(upArrow.props().onClick(), 329)
const downArrow = gasInput.find('.advanced-tab__gas-edit-row__input-arrows__i-wrap').at(1)
assert.equal(downArrow.props().onClick(), 327)
})
it('should call gasInputError with the expected params', () => {
assert.equal(AdvancedTabContent.prototype.gasInputError.callCount, 1)
const gasInputErrorArgs = AdvancedTabContent.prototype.gasInputError.getCall(0).args
assert.deepEqual(gasInputErrorArgs, [{
labelKey: 'gasPrice',
insufficientBalance: false,
customPriceIsSafe: true,
value: 321,
isSpeedUp: false,
}])
})
})
describe('gasInputError()', () => {
let gasInputError
beforeEach(() => {
AdvancedTabContent.prototype.renderGasEditRow.resetHistory()
gasInputError = wrapper.instance().gasInputError({
labelKey: '',
insufficientBalance: false,
customPriceIsSafe: true,
isSpeedUp: false,
})
})
it('should return an insufficientBalance error', () => {
const gasInputError = wrapper.instance().gasInputError({
labelKey: 'gasPrice',
insufficientBalance: true,
customPriceIsSafe: true,
isSpeedUp: false,
value: 1,
})
assert.deepEqual(gasInputError, {
isInError: true,
errorText: 'insufficientBalance',
errorType: 'error',
})
})
it('should return a zero gas on retry error', () => {
const gasInputError = wrapper.instance().gasInputError({
labelKey: 'gasPrice',
insufficientBalance: false,
customPriceIsSafe: false,
isSpeedUp: true,
value: 0,
})
assert.deepEqual(gasInputError, {
isInError: true,
errorText: 'zeroGasPriceOnSpeedUpError',
errorType: 'error',
})
})
it('should return a low gas warning', () => {
const gasInputError = wrapper.instance().gasInputError({
labelKey: 'gasPrice',
insufficientBalance: false,
customPriceIsSafe: false,
isSpeedUp: false,
value: 1,
})
assert.deepEqual(gasInputError, {
isInError: true,
errorText: 'gasPriceExtremelyLow',
errorType: 'warning',
})
})
it('should return isInError false if there is no error', () => {
gasInputError = wrapper.instance().gasInputError({
labelKey: 'gasPrice',
insufficientBalance: false,
customPriceIsSafe: true,
value: 1,
})
assert.equal(gasInputError.isInError, false)
})
})
})

View File

@ -16,11 +16,15 @@ export default class GasModalPageContainer extends Component {
hideBasic: PropTypes.bool,
updateCustomGasPrice: PropTypes.func,
updateCustomGasLimit: PropTypes.func,
currentTimeEstimate: PropTypes.string,
customGasPrice: PropTypes.number,
customGasLimit: PropTypes.number,
insufficientBalance: PropTypes.bool,
fetchBasicGasAndTimeEstimates: PropTypes.func,
fetchGasEstimates: PropTypes.func,
gasPriceButtonGroupProps: PropTypes.object,
gasChartProps: PropTypes.object,
gasEstimatesLoading: PropTypes.bool,
infoRowProps: PropTypes.shape({
originalTotalFiat: PropTypes.string,
originalTotalEth: PropTypes.string,
@ -38,6 +42,7 @@ export default class GasModalPageContainer extends Component {
]),
customPriceIsSafe: PropTypes.bool,
isSpeedUp: PropTypes.bool,
isRetry: PropTypes.bool,
disableSave: PropTypes.bool,
isEthereumNetwork: PropTypes.bool,
}
@ -64,35 +69,39 @@ export default class GasModalPageContainer extends Component {
)
}
renderAdvancedTabContent ({
convertThenUpdateCustomGasPrice,
convertThenUpdateCustomGasLimit,
customGasPrice,
customGasLimit,
newTotalFiat,
gasChartProps,
currentTimeEstimate,
insufficientBalance,
gasEstimatesLoading,
customPriceIsSafe,
isSpeedUp,
transactionFee,
isEthereumNetwork,
}) {
renderAdvancedTabContent () {
const {
updateCustomGasPrice,
updateCustomGasLimit,
customModalGasPriceInHex,
customModalGasLimitInHex,
gasChartProps,
currentTimeEstimate,
insufficientBalance,
gasEstimatesLoading,
customPriceIsSafe,
isSpeedUp,
isRetry,
infoRowProps: {
transactionFee,
},
isEthereumNetwork,
} = this.props
return (
<AdvancedTabContent
updateCustomGasPrice={convertThenUpdateCustomGasPrice}
updateCustomGasLimit={convertThenUpdateCustomGasLimit}
customGasPrice={customGasPrice}
customGasLimit={customGasLimit}
updateCustomGasPrice={updateCustomGasPrice}
updateCustomGasLimit={updateCustomGasLimit}
customModalGasPriceInHex={customModalGasPriceInHex}
customModalGasLimitInHex={customModalGasLimitInHex}
timeRemaining={currentTimeEstimate}
transactionFee={transactionFee}
totalFee={newTotalFiat}
gasChartProps={gasChartProps}
insufficientBalance={insufficientBalance}
gasEstimatesLoading={gasEstimatesLoading}
customPriceIsSafe={customPriceIsSafe}
isSpeedUp={isSpeedUp}
isRetry={isRetry}
isEthereumNetwork={isEthereumNetwork}
/>
)
@ -122,20 +131,27 @@ export default class GasModalPageContainer extends Component {
)
}
renderTabs ({
newTotalFiat,
newTotalEth,
sendAmount,
transactionFee,
},
{
gasPriceButtonGroupProps,
hideBasic,
...advancedTabProps
}) {
renderTabs () {
const {
gasPriceButtonGroupProps,
hideBasic,
infoRowProps: {
newTotalFiat,
newTotalEth,
sendAmount,
transactionFee,
},
} = this.props
let tabsToRender = [
{ name: 'basic', content: this.renderBasicTabContent(gasPriceButtonGroupProps) },
{ name: 'advanced', content: this.renderAdvancedTabContent({ transactionFee, ...advancedTabProps }) },
{
name: this.context.t('basic'),
content: this.renderBasicTabContent(gasPriceButtonGroupProps),
},
{
name: this.context.t('advanced'),
content: this.renderAdvancedTabContent(),
},
]
if (hideBasic) {
@ -144,7 +160,7 @@ export default class GasModalPageContainer extends Component {
return (
<Tabs>
{tabsToRender.map(({ name, content }, i) => <Tab name={this.context.t(name)} key={`gas-modal-tab-${i}`}>
{tabsToRender.map(({ name, content }, i) => <Tab name={name} key={`gas-modal-tab-${i}`}>
<div className="gas-modal-content">
{ content }
{ this.renderInfoRows(newTotalFiat, newTotalEth, sendAmount, transactionFee) }
@ -158,13 +174,11 @@ export default class GasModalPageContainer extends Component {
render () {
const {
cancelAndClose,
infoRowProps,
onSubmit,
customModalGasPriceInHex,
customModalGasLimitInHex,
disableSave,
isSpeedUp,
...tabProps
} = this.props
return (
@ -172,7 +186,7 @@ export default class GasModalPageContainer extends Component {
<PageContainer
title={this.context.t('customGas')}
subtitle={this.context.t('customGasSubTitle')}
tabsComponent={this.renderTabs(infoRowProps, tabProps)}
tabsComponent={this.renderTabs()}
disabled={disableSave}
onCancel={() => cancelAndClose()}
onClose={() => cancelAndClose()}

View File

@ -6,6 +6,7 @@ import {
setGasLimit,
setGasPrice,
createSpeedUpTransaction,
createRetryTransaction,
hideSidebar,
updateSendAmount,
setGasTotal,
@ -56,7 +57,6 @@ import {
addHexWEIsToDec,
subtractHexWEIsToDec,
decEthToConvertedCurrency as ethTotalToConvertedCurrency,
decGWEIToHexWEI,
hexWEIToDecGWEI,
} from '../../../../helpers/utils/conversions.util'
import {
@ -154,6 +154,7 @@ const mapStateToProps = (state, ownProps) => {
},
transaction: txData || transaction,
isSpeedUp: transaction.status === 'submitted',
isRetry: transaction.status === 'failed',
txId: transaction.id,
insufficientBalance,
gasEstimatesLoading,
@ -175,8 +176,7 @@ const mapDispatchToProps = dispatch => {
},
hideModal: () => dispatch(hideModal()),
updateCustomGasPrice,
convertThenUpdateCustomGasPrice: newPrice => updateCustomGasPrice(decGWEIToHexWEI(newPrice)),
convertThenUpdateCustomGasLimit: newLimit => dispatch(setCustomGasLimit(addHexPrefix(newLimit.toString(16)))),
updateCustomGasLimit: newLimit => dispatch(setCustomGasLimit(addHexPrefix(newLimit))),
setGasData: (newLimit, newPrice) => {
dispatch(setGasLimit(newLimit))
dispatch(setGasPrice(newPrice))
@ -189,6 +189,9 @@ const mapDispatchToProps = dispatch => {
createSpeedUpTransaction: (txId, gasPrice) => {
return dispatch(createSpeedUpTransaction(txId, gasPrice))
},
createRetryTransaction: (txId, gasPrice) => {
return dispatch(createRetryTransaction(txId, gasPrice))
},
hideGasButtonGroup: () => dispatch(hideGasButtonGroup()),
setCustomTimeEstimate: (timeEstimateInSeconds) => dispatch(setCustomTimeEstimate(timeEstimateInSeconds)),
hideSidebar: () => dispatch(hideSidebar()),
@ -208,6 +211,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
isConfirm,
txId,
isSpeedUp,
isRetry,
insufficientBalance,
maxModeOn,
customGasPrice,
@ -219,11 +223,11 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
transaction,
} = stateProps
const {
updateCustomGasPrice: dispatchUpdateCustomGasPrice,
hideGasButtonGroup: dispatchHideGasButtonGroup,
setGasData: dispatchSetGasData,
updateConfirmTxGasAndCalculate: dispatchUpdateConfirmTxGasAndCalculate,
createSpeedUpTransaction: dispatchCreateSpeedUpTransaction,
createRetryTransaction: dispatchCreateRetryTransaction,
hideSidebar: dispatchHideSidebar,
cancelAndClose: dispatchCancelAndClose,
hideModal: dispatchHideModal,
@ -251,6 +255,10 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
dispatchCreateSpeedUpTransaction(txId, gasPrice)
dispatchHideSidebar()
dispatchCancelAndClose()
} else if (isRetry) {
dispatchCreateRetryTransaction(txId, gasPrice)
dispatchHideSidebar()
dispatchCancelAndClose()
} else {
dispatchSetGasData(gasLimit, gasPrice)
dispatchHideGasButtonGroup()
@ -267,11 +275,11 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
},
gasPriceButtonGroupProps: {
...gasPriceButtonGroupProps,
handleGasPriceSelection: dispatchUpdateCustomGasPrice,
handleGasPriceSelection: otherDispatchProps.updateCustomGasPrice,
},
cancelAndClose: () => {
dispatchCancelAndClose()
if (isSpeedUp) {
if (isSpeedUp || isRetry) {
dispatchHideSidebar()
}
},

View File

@ -79,7 +79,7 @@ describe('GasModalPageContainer Component', function () {
customGasLimitInHex={'mockCustomGasLimitInHex'}
insufficientBalance={false}
disableSave={false}
/>, { context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } })
/>)
})
afterEach(() => {
@ -158,10 +158,7 @@ describe('GasModalPageContainer Component', function () {
})
it('should render a Tabs component with "Basic" and "Advanced" tabs', () => {
const renderTabsResult = wrapper.instance().renderTabs(mockInfoRowProps, {
gasPriceButtonGroupProps: mockGasPriceButtonGroupProps,
otherProps: 'mockAdvancedTabProps',
})
const renderTabsResult = wrapper.instance().renderTabs()
const renderedTabs = shallow(renderTabsResult)
assert.equal(renderedTabs.props().className, 'tabs')
@ -175,23 +172,10 @@ describe('GasModalPageContainer Component', function () {
assert.equal(tabs.at(1).childAt(0).props().className, 'gas-modal-content')
})
it('should call renderBasicTabContent and renderAdvancedTabContent with the expected props', () => {
assert.equal(GP.renderBasicTabContent.callCount, 0)
assert.equal(GP.renderAdvancedTabContent.callCount, 0)
wrapper.instance().renderTabs(mockInfoRowProps, { gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, otherProps: 'mockAdvancedTabProps' })
assert.equal(GP.renderBasicTabContent.callCount, 1)
assert.equal(GP.renderAdvancedTabContent.callCount, 1)
assert.deepEqual(GP.renderBasicTabContent.getCall(0).args[0], mockGasPriceButtonGroupProps)
assert.deepEqual(GP.renderAdvancedTabContent.getCall(0).args[0], { transactionFee: 'mockTransactionFee', otherProps: 'mockAdvancedTabProps' })
})
it('should call renderInfoRows with the expected props', () => {
assert.equal(GP.renderInfoRows.callCount, 0)
wrapper.instance().renderTabs(mockInfoRowProps, { gasPriceButtonGroupProps: mockGasPriceButtonGroupProps, otherProps: 'mockAdvancedTabProps' })
wrapper.instance().renderTabs()
assert.equal(GP.renderInfoRows.callCount, 2)
@ -200,11 +184,25 @@ describe('GasModalPageContainer Component', function () {
})
it('should not render the basic tab if hideBasic is true', () => {
const renderTabsResult = wrapper.instance().renderTabs(mockInfoRowProps, {
gasPriceButtonGroupProps: mockGasPriceButtonGroupProps,
otherProps: 'mockAdvancedTabProps',
hideBasic: true,
})
wrapper = shallow(<GasModalPageContainer
cancelAndClose={propsMethodSpies.cancelAndClose}
onSubmit={propsMethodSpies.onSubmit}
fetchBasicGasAndTimeEstimates={propsMethodSpies.fetchBasicGasAndTimeEstimates}
fetchGasEstimates={propsMethodSpies.fetchGasEstimates}
updateCustomGasPrice={() => 'mockupdateCustomGasPrice'}
updateCustomGasLimit={() => 'mockupdateCustomGasLimit'}
customGasPrice={21}
customGasLimit={54321}
gasPriceButtonGroupProps={mockGasPriceButtonGroupProps}
infoRowProps={mockInfoRowProps}
currentTimeEstimate={'1 min 31 sec'}
customGasPriceInHex={'mockCustomGasPriceInHex'}
customGasLimitInHex={'mockCustomGasLimitInHex'}
insufficientBalance={false}
disableSave={false}
hideBasic={true}
/>)
const renderTabsResult = wrapper.instance().renderTabs()
const renderedTabs = shallow(renderTabsResult)
const tabs = renderedTabs.find(Tab)
@ -224,28 +222,6 @@ describe('GasModalPageContainer Component', function () {
})
})
describe('renderAdvancedTabContent', () => {
it('should render with the correct props', () => {
const renderAdvancedTabContentResult = wrapper.instance().renderAdvancedTabContent({
convertThenUpdateCustomGasPrice: () => 'mockConvertThenUpdateCustomGasPrice',
convertThenUpdateCustomGasLimit: () => 'mockConvertThenUpdateCustomGasLimit',
customGasPrice: 123,
customGasLimit: 456,
newTotalFiat: '$0.30',
currentTimeEstimate: '1 min 31 sec',
gasEstimatesLoading: 'mockGasEstimatesLoading',
})
const advancedTabContentProps = renderAdvancedTabContentResult.props
assert.equal(advancedTabContentProps.updateCustomGasPrice(), 'mockConvertThenUpdateCustomGasPrice')
assert.equal(advancedTabContentProps.updateCustomGasLimit(), 'mockConvertThenUpdateCustomGasLimit')
assert.equal(advancedTabContentProps.customGasPrice, 123)
assert.equal(advancedTabContentProps.customGasLimit, 456)
assert.equal(advancedTabContentProps.timeRemaining, '1 min 31 sec')
assert.equal(advancedTabContentProps.totalFee, '$0.30')
assert.equal(advancedTabContentProps.gasEstimatesLoading, 'mockGasEstimatesLoading')
})
})
describe('renderInfoRows', () => {
it('should render the info rows with the passed data', () => {
const baseClassName = 'gas-modal-content__info-row'

View File

@ -157,6 +157,7 @@ describe('gas-modal-page-container container', () => {
},
insufficientBalance: true,
isSpeedUp: false,
isRetry: false,
txId: 34,
isEthereumNetwork: true,
isMainnet: true,
@ -297,19 +298,19 @@ describe('gas-modal-page-container container', () => {
})
})
describe('convertThenUpdateCustomGasPrice()', () => {
it('should dispatch a setCustomGasPrice action with the arg passed to convertThenUpdateCustomGasPrice converted to WEI', () => {
mapDispatchToPropsObject.convertThenUpdateCustomGasPrice('0xffff')
describe('updateCustomGasPrice()', () => {
it('should dispatch a setCustomGasPrice action', () => {
mapDispatchToPropsObject.updateCustomGasPrice('0xffff')
assert(dispatchSpy.calledOnce)
assert(gasActionSpies.setCustomGasPrice.calledOnce)
assert.equal(gasActionSpies.setCustomGasPrice.getCall(0).args[0], '0x3b9a8e653600')
assert.equal(gasActionSpies.setCustomGasPrice.getCall(0).args[0], '0xffff')
})
})
describe('convertThenUpdateCustomGasLimit()', () => {
it('should dispatch a setCustomGasLimit action with the arg passed to convertThenUpdateCustomGasLimit converted to hex', () => {
mapDispatchToPropsObject.convertThenUpdateCustomGasLimit(16)
describe('updateCustomGasLimit()', () => {
it('should dispatch a setCustomGasLimit action', () => {
mapDispatchToPropsObject.updateCustomGasLimit('0x10')
assert(dispatchSpy.calledOnce)
assert(gasActionSpies.setCustomGasLimit.calledOnce)
assert.equal(gasActionSpies.setCustomGasLimit.getCall(0).args[0], '0x10')

View File

@ -2,9 +2,11 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ButtonGroup from '../../../ui/button-group'
import Button from '../../../ui/button'
import { GAS_ESTIMATE_TYPES } from '../../../../helpers/constants/common'
const GAS_OBJECT_PROPTYPES_SHAPE = {
label: PropTypes.string,
gasEstimateType: PropTypes.oneOf(Object.values(GAS_ESTIMATE_TYPES)).isRequired,
feeInPrimaryCurrency: PropTypes.string,
feeInSecondaryCurrency: PropTypes.string,
timeEstimate: PropTypes.string,
@ -27,8 +29,19 @@ export default class GasPriceButtonGroup extends Component {
showCheck: PropTypes.bool,
}
gasEstimateTypeLabel (gasEstimateType) {
if (gasEstimateType === GAS_ESTIMATE_TYPES.SLOW) {
return this.context.t('slow')
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.AVERAGE) {
return this.context.t('average')
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.FAST) {
return this.context.t('fast')
}
throw new Error(`Unrecognized gas estimate type: ${gasEstimateType}`)
}
renderButtonContent ({
labelKey,
gasEstimateType,
feeInPrimaryCurrency,
feeInSecondaryCurrency,
timeEstimate,
@ -37,7 +50,7 @@ export default class GasPriceButtonGroup extends Component {
showCheck,
}) {
return (<div>
{ labelKey && <div className={`${className}__label`}>{ this.context.t(labelKey) }</div> }
{ gasEstimateType && <div className={`${className}__label`}>{ this.gasEstimateTypeLabel(gasEstimateType) }</div> }
{ timeEstimate && <div className={`${className}__time-estimate`}>{ timeEstimate }</div> }
{ feeInPrimaryCurrency && <div className={`${className}__primary-currency`}>{ feeInPrimaryCurrency }</div> }
{ feeInSecondaryCurrency && <div className={`${className}__secondary-currency`}>{ feeInSecondaryCurrency }</div> }

View File

@ -156,15 +156,15 @@ describe('GasPriceButtonGroup Component', function () {
})
describe('renderButtonContent', () => {
it('should render a label if passed a labelKey', () => {
it('should render a label if passed a gasEstimateType', () => {
const renderButtonContentResult = wrapper.instance().renderButtonContent({
labelKey: 'mockLabelKey',
gasEstimateType: 'SLOW',
}, {
className: 'someClass',
})
const wrappedRenderButtonContentResult = shallow(renderButtonContentResult)
assert.equal(wrappedRenderButtonContentResult.childAt(0).children().length, 1)
assert.equal(wrappedRenderButtonContentResult.find('.someClass__label').text(), 'mockLabelKey')
assert.equal(wrappedRenderButtonContentResult.find('.someClass__label').text(), 'slow')
})
it('should render a feeInPrimaryCurrency if passed a feeInPrimaryCurrency', () => {
@ -211,7 +211,7 @@ describe('GasPriceButtonGroup Component', function () {
it('should render all elements if all args passed', () => {
const renderButtonContentResult = wrapper.instance().renderButtonContent({
labelKey: 'mockLabel',
gasEstimateType: 'SLOW',
feeInPrimaryCurrency: 'mockFeeInPrimaryCurrency',
feeInSecondaryCurrency: 'mockFeeInSecondaryCurrency',
timeEstimate: 'mockTimeEstimate',

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class AdvancedTabContent extends Component {
export default class GasSlider extends Component {
static propTypes = {
onChange: PropTypes.func,
lowLabel: PropTypes.string,

View File

@ -7,15 +7,17 @@ export default class ProviderPageContainerContent extends PureComponent {
origin: PropTypes.string.isRequired,
selectedIdentity: PropTypes.object.isRequired,
siteImage: PropTypes.string,
siteTitle: PropTypes.string.isRequired,
siteTitle: PropTypes.string,
hostname: PropTypes.string,
extensionId: PropTypes.string,
}
static contextTypes = {
t: PropTypes.func,
};
renderConnectVisual = () => {
const { origin, selectedIdentity, siteImage, siteTitle } = this.props
renderConnectVisual = (title, identifier) => {
const { selectedIdentity, siteImage } = this.props
return (
<div className="provider-approval-visual">
@ -27,11 +29,11 @@ export default class ProviderPageContainerContent extends PureComponent {
/>
) : (
<i className="provider-approval-visual__identicon--default">
{siteTitle.charAt(0).toUpperCase()}
{title.charAt(0).toUpperCase()}
</i>
)}
<h1>{siteTitle}</h1>
<h2>{origin}</h2>
<h1>{title}</h1>
<h2>{identifier}</h2>
</section>
<span className="provider-approval-visual__check" />
<section>
@ -47,15 +49,23 @@ export default class ProviderPageContainerContent extends PureComponent {
}
render () {
const { siteTitle } = this.props
const { siteTitle, hostname, extensionId } = this.props
const { t } = this.context
const title = extensionId ?
'External Extension' :
siteTitle || hostname
const identifier = extensionId ?
`Extension ID: '${extensionId}'` :
hostname
return (
<div className="provider-approval-container__content">
<section>
<h2>{t('connectRequest')}</h2>
{this.renderConnectVisual()}
<h1>{t('providerRequest', [siteTitle])}</h1>
{this.renderConnectVisual(title, identifier)}
<h1>{t('providerRequest', [title])}</h1>
<p>
{t('providerRequestInfo')}
<br/>

View File

@ -9,7 +9,9 @@ export default class ProviderPageContainer extends PureComponent {
rejectProviderRequestByOrigin: PropTypes.func.isRequired,
origin: PropTypes.string.isRequired,
siteImage: PropTypes.string,
siteTitle: PropTypes.string.isRequired,
siteTitle: PropTypes.string,
hostname: PropTypes.string,
extensionId: PropTypes.string,
};
static contextTypes = {
@ -52,7 +54,7 @@ export default class ProviderPageContainer extends PureComponent {
}
render () {
const {origin, siteImage, siteTitle} = this.props
const {origin, siteImage, siteTitle, hostname, extensionId} = this.props
return (
<div className="page-container provider-approval-container">
@ -61,6 +63,8 @@ export default class ProviderPageContainer extends PureComponent {
origin={origin}
siteImage={siteImage}
siteTitle={siteTitle}
hostname={hostname}
extensionId={extensionId}
/>
<PageContainerFooter
onCancel={() => this.onCancel()}

View File

@ -70,7 +70,7 @@ describe('TransactionListItemDetails Component', () => {
const wrapper = shallow(
<TransactionListItemDetails
transactionGroup={transactionGroup}
showRetry={true}
showSpeedUp={true}
/>,
{ context: { t: (str1, str2) => str2 ? str1 + str2 : str1 } }
)

View File

@ -21,6 +21,7 @@ export default class TransactionListItemDetails extends PureComponent {
onCancel: PropTypes.func,
onRetry: PropTypes.func,
showCancel: PropTypes.bool,
showSpeedUp: PropTypes.bool,
showRetry: PropTypes.bool,
isEarliestNonce: PropTypes.bool,
cancelDisabled: PropTypes.bool,
@ -123,6 +124,7 @@ export default class TransactionListItemDetails extends PureComponent {
const { justCopied } = this.state
const {
transactionGroup,
showSpeedUp,
showRetry,
onCancel,
onRetry,
@ -138,7 +140,7 @@ export default class TransactionListItemDetails extends PureComponent {
<div>{ t('details') }</div>
<div className="transaction-list-item-details__header-buttons">
{
showRetry && (
showSpeedUp && (
<Button
type="raised"
onClick={this.handleRetry}
@ -172,6 +174,17 @@ export default class TransactionListItemDetails extends PureComponent {
<img src="/images/arrow-popout.svg" />
</Button>
</Tooltip>
{
showRetry && <Tooltip title={blockExplorerUrl ? t('viewOnCustomBlockExplorer', [blockExplorerUrl]) : t('retryTransaction')}>
<Button
type="raised"
onClick={this.handleRetry}
className="transaction-list-item-details__header-button"
>
<i className="fa fa-refresh"></i>
</Button>
</Tooltip>
}
</div>
</div>
<div className="transaction-list-item-details__body">

View File

@ -17,6 +17,7 @@
grid-template-areas:
"identicon action status primary-amount"
"identicon nonce status secondary-amount";
grid-template-rows: 24px;
@media screen and (max-width: $break-small) {
padding: .5rem 1rem;
@ -25,6 +26,7 @@
"nonce nonce nonce"
"identicon action primary-amount"
"identicon status secondary-amount";
grid-template-rows: auto 24px;
}
&:hover {

View File

@ -24,7 +24,7 @@ export default class TransactionListItem extends PureComponent {
showCancelModal: PropTypes.func,
showCancel: PropTypes.bool,
hasEnoughCancelGas: PropTypes.bool,
showRetry: PropTypes.bool,
showSpeedUp: PropTypes.bool,
isEarliestNonce: PropTypes.bool,
showFiat: PropTypes.bool,
token: PropTypes.object,
@ -177,7 +177,7 @@ export default class TransactionListItem extends PureComponent {
primaryTransaction,
showCancel,
hasEnoughCancelGas,
showRetry,
showSpeedUp,
tokenData,
transactionGroup,
rpcPrefs,
@ -233,7 +233,8 @@ export default class TransactionListItem extends PureComponent {
<TransactionListItemDetails
transactionGroup={transactionGroup}
onRetry={this.handleRetry}
showRetry={showRetry}
showSpeedUp={showSpeedUp}
showRetry={getStatusKey(primaryTransaction) === 'failed'}
isEarliestNonce={isEarliestNonce}
onCancel={this.handleCancel}
showCancel={showCancel}

View File

@ -37,7 +37,7 @@ export default class TransactionList extends PureComponent {
}
}
shouldShowRetry = (transactionGroup, isEarliestNonce) => {
shouldShowSpeedUp = (transactionGroup, isEarliestNonce) => {
const { transactions = [], hasRetried } = transactionGroup
const [earliestTransaction = {}] = transactions
const { submittedTime } = earliestTransaction
@ -100,7 +100,7 @@ export default class TransactionList extends PureComponent {
<TransactionListItem
transactionGroup={transactionGroup}
key={`${transactionGroup.nonce}:${index}`}
showRetry={isPendingTx && this.shouldShowRetry(transactionGroup, index === 0)}
showSpeedUp={isPendingTx && this.shouldShowSpeedUp(transactionGroup, index === 0)}
showCancel={isPendingTx && this.shouldShowCancel(transactionGroup)}
isEarliestNonce={isPendingTx && index === 0}
token={selectedToken}

View File

@ -50,6 +50,7 @@
color: $curious-blue;
border-bottom: 3px solid $curious-blue;
cursor: initial;
pointer-events: none;
}
}
}

View File

@ -12,3 +12,9 @@ export const NETWORK_TYPES = {
ROPSTEN: 'ropsten',
GOERLI: 'goerli',
}
export const GAS_ESTIMATE_TYPES = {
SLOW: 'SLOW',
AVERAGE: 'AVERAGE',
FAST: 'FAST',
}

View File

@ -67,7 +67,7 @@ const customDimensionsNameIdMap = {
}
function composeUrlRefParamAddition (previousPath, confirmTransactionOrigin) {
const externalOrigin = confirmTransactionOrigin && confirmTransactionOrigin !== 'MetaMask'
const externalOrigin = confirmTransactionOrigin && confirmTransactionOrigin !== 'metamask'
return `&urlref=${externalOrigin ? 'EXTERNAL' : encodeURIComponent(previousPath.replace(/chrome-extension:\/\/\w+/, METAMETRICS_TRACKING_URL))}`
}

View File

@ -14,6 +14,7 @@ import {
import { CONFIRMED_STATUS, DROPPED_STATUS } from '../../helpers/constants/transactions'
import UserPreferencedCurrencyDisplay from '../../components/app/user-preferenced-currency-display'
import { PRIMARY, SECONDARY } from '../../helpers/constants/common'
import { hexToDecimal } from '../../helpers/utils/conversions.util'
import AdvancedGasInputs from '../../components/app/gas-customization/advanced-gas-inputs'
import TextField from '../../components/ui/text-field'
@ -171,7 +172,7 @@ export default class ConfirmTransactionBase extends Component {
}
}
if (customGas.gasLimit < 21000) {
if (hexToDecimal(customGas.gasLimit) < 21000) {
return {
valid: false,
errorKey: GAS_LIMIT_TOO_LOW_ERROR_KEY,

View File

@ -0,0 +1,79 @@
import React, { Component } from 'react'
import { Switch, Route, matchPath } from 'react-router-dom'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import NewAccountCreateForm from './new-account.container'
import NewAccountImportForm from './import-account'
import ConnectHardwareForm from './connect-hardware'
import {
NEW_ACCOUNT_ROUTE,
IMPORT_ACCOUNT_ROUTE,
CONNECT_HARDWARE_ROUTE,
} from '../../helpers/constants/routes'
export default class CreateAccountPage extends Component {
renderTabs () {
const { history, location: { pathname }} = this.props
const getClassNames = path => classnames('new-account__tabs__tab', {
'new-account__tabs__selected': matchPath(pathname, {
path,
exact: true,
}),
})
return (
<div className="new-account__tabs">
<div className={getClassNames(NEW_ACCOUNT_ROUTE)} onClick={() => history.push(NEW_ACCOUNT_ROUTE)}>{
this.context.t('create')
}</div>
<div className={getClassNames(IMPORT_ACCOUNT_ROUTE)} onClick={() => history.push(IMPORT_ACCOUNT_ROUTE)}>{
this.context.t('import')
}</div>
<div className={getClassNames(CONNECT_HARDWARE_ROUTE)} onClick={() => history.push(CONNECT_HARDWARE_ROUTE)}>{
this.context.t('connect')
}</div>
</div>
)
}
render () {
return (
<div className="new-account">
<div className="new-account__header">
<div className={`new-account__header ${this.context.t('newAccount')}`}>
{this.renderTabs()}
</div>
</div>
<div className="new-account__form">
<Switch>
<Route
exact={true}
path={NEW_ACCOUNT_ROUTE}
component={NewAccountCreateForm}
/>
<Route
exact={true}
path={IMPORT_ACCOUNT_ROUTE}
component={NewAccountImportForm}
/>
<Route
exact={true}
path={CONNECT_HARDWARE_ROUTE}
component={ConnectHardwareForm}
/>
</Switch>
</div>
</div>
)
}
}
CreateAccountPage.propTypes = {
location: PropTypes.object,
history: PropTypes.object,
t: PropTypes.func,
}
CreateAccountPage.contextTypes = {
t: PropTypes.func,
}

View File

@ -0,0 +1,20 @@
import { connect } from 'react-redux'
import actions from '../../store/actions'
import { getCurrentViewContext } from '../../selectors/selectors'
import CreateAccountPage from './create-account.component'
const mapStateToProps = state => ({
displayedForm: getCurrentViewContext(state),
})
const mapDispatchToProps = dispatch => ({
displayForm: form => dispatch(actions.setNewAccountForm(form)),
showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)),
showExportPrivateKeyModal: () => {
dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' }))
},
hideModal: () => dispatch(actions.hideModal()),
setAccountLabel: (address, label) => dispatch(actions.setAccountLabel(address, label)),
})
export default connect(mapStateToProps, mapDispatchToProps)(CreateAccountPage)

View File

@ -1,113 +1 @@
const Component = require('react').Component
const { Switch, Route, matchPath } = require('react-router-dom')
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('../../store/actions')
const { getCurrentViewContext } = require('../../selectors/selectors')
const classnames = require('classnames')
const NewAccountCreateForm = require('./new-account')
const NewAccountImportForm = require('./import-account')
const ConnectHardwareForm = require('./connect-hardware')
const {
NEW_ACCOUNT_ROUTE,
IMPORT_ACCOUNT_ROUTE,
CONNECT_HARDWARE_ROUTE,
} = require('../../helpers/constants/routes')
class CreateAccountPage extends Component {
renderTabs () {
const { history, location } = this.props
return h('div.new-account__tabs', [
h('div.new-account__tabs__tab', {
className: classnames('new-account__tabs__tab', {
'new-account__tabs__selected': matchPath(location.pathname, {
path: NEW_ACCOUNT_ROUTE, exact: true,
}),
}),
onClick: () => history.push(NEW_ACCOUNT_ROUTE),
}, [
this.context.t('create'),
]),
h('div.new-account__tabs__tab', {
className: classnames('new-account__tabs__tab', {
'new-account__tabs__selected': matchPath(location.pathname, {
path: IMPORT_ACCOUNT_ROUTE, exact: true,
}),
}),
onClick: () => history.push(IMPORT_ACCOUNT_ROUTE),
}, [
this.context.t('import'),
]),
h(
'div.new-account__tabs__tab',
{
className: classnames('new-account__tabs__tab', {
'new-account__tabs__selected': matchPath(location.pathname, {
path: CONNECT_HARDWARE_ROUTE,
exact: true,
}),
}),
onClick: () => history.push(CONNECT_HARDWARE_ROUTE),
},
this.context.t('connect')
),
])
}
render () {
return h('div.new-account', {}, [
h('div.new-account__header', [
h('div.new-account__title', this.context.t('newAccount')),
this.renderTabs(),
]),
h('div.new-account__form', [
h(Switch, [
h(Route, {
exact: true,
path: NEW_ACCOUNT_ROUTE,
component: NewAccountCreateForm,
}),
h(Route, {
exact: true,
path: IMPORT_ACCOUNT_ROUTE,
component: NewAccountImportForm,
}),
h(Route, {
exact: true,
path: CONNECT_HARDWARE_ROUTE,
component: ConnectHardwareForm,
}),
]),
]),
])
}
}
CreateAccountPage.propTypes = {
location: PropTypes.object,
history: PropTypes.object,
t: PropTypes.func,
}
CreateAccountPage.contextTypes = {
t: PropTypes.func,
}
const mapStateToProps = state => ({
displayedForm: getCurrentViewContext(state),
})
const mapDispatchToProps = dispatch => ({
displayForm: form => dispatch(actions.setNewAccountForm(form)),
showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)),
showExportPrivateKeyModal: () => {
dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' }))
},
hideModal: () => dispatch(actions.hideModal()),
setAccountLabel: (address, label) => dispatch(actions.setAccountLabel(address, label)),
})
module.exports = connect(mapStateToProps, mapDispatchToProps)(CreateAccountPage)
export { default } from './create-account.container'

View File

@ -0,0 +1,91 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { DEFAULT_ROUTE } from '../../helpers/constants/routes'
import Button from '../../components/ui/button'
export default class NewAccountCreateForm extends Component {
constructor (props, context) {
super(props)
const { newAccountNumber = 0 } = props
this.state = {
newAccountName: '',
defaultAccountName: context.t('newAccountNumberName', [newAccountNumber]),
}
}
render () {
const { newAccountName, defaultAccountName } = this.state
const { history, createAccount } = this.props
const createClick = _ => {
createAccount(newAccountName || defaultAccountName)
.then(() => {
this.context.metricsEvent({
eventOpts: {
category: 'Accounts',
action: 'Add New Account',
name: 'Added New Account',
},
})
history.push(DEFAULT_ROUTE)
})
.catch((e) => {
this.context.metricsEvent({
eventOpts: {
category: 'Accounts',
action: 'Add New Account',
name: 'Error',
},
customVariables: {
errorMessage: e.message,
},
})
})
}
return (
<div className="new-account-create-form">
<div className="new-account-create-form__input-label">
{this.context.t('accountName')}
</div>
<div className="new-account-create-form__input-wrapper">
<input className="new-account-create-form__input"
value={newAccountName}
placeholder={defaultAccountName}
onChange={event => this.setState({ newAccountName: event.target.value })}
/>
</div>
<div className="new-account-create-form__buttons">
<Button
type="default"
large={true}
className="new-account-create-form__button"
onClick={() => history.push(DEFAULT_ROUTE)}
>{this.context.t('cancel')}</Button>
<Button
type="secondary"
large={true}
className="new-account-create-form__button"
onClick={createClick}
>{this.context.t('create')}</Button>
</div>
</div>
)
}
}
NewAccountCreateForm.propTypes = {
hideModal: PropTypes.func,
showImportPage: PropTypes.func,
showConnectPage: PropTypes.func,
createAccount: PropTypes.func,
numberOfExistingAccounts: PropTypes.number,
newAccountNumber: PropTypes.number,
history: PropTypes.object,
t: PropTypes.func,
}
NewAccountCreateForm.contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}

View File

@ -0,0 +1,35 @@
import { connect } from 'react-redux'
import actions from '../../store/actions'
import NewAccountCreateForm from './new-account.component'
const mapStateToProps = state => {
const { metamask: { network, selectedAddress, identities = {} } } = state
const numberOfExistingAccounts = Object.keys(identities).length
const newAccountNumber = numberOfExistingAccounts + 1
return {
network,
address: selectedAddress,
numberOfExistingAccounts,
newAccountNumber,
}
}
const mapDispatchToProps = dispatch => {
return {
toCoinbase: address => dispatch(actions.buyEth({ network: '1', address, amount: 0 })),
hideModal: () => dispatch(actions.hideModal()),
createAccount: newAccountName => {
return dispatch(actions.addNewAccount())
.then(newAccountAddress => {
if (newAccountName) {
dispatch(actions.setAccountLabel(newAccountAddress, newAccountName))
}
})
},
showImportPage: () => dispatch(actions.showImportPage()),
showConnectPage: () => dispatch(actions.showConnectPage()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(NewAccountCreateForm)

View File

@ -1,130 +0,0 @@
const { Component } = require('react')
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('../../store/actions')
const { DEFAULT_ROUTE } = require('../../helpers/constants/routes')
import Button from '../../components/ui/button'
class NewAccountCreateForm extends Component {
constructor (props, context) {
super(props)
const { numberOfExistingAccounts = 0 } = props
const newAccountNumber = numberOfExistingAccounts + 1
this.state = {
newAccountName: '',
defaultAccountName: context.t('newAccountNumberName', [newAccountNumber]),
}
}
render () {
const { newAccountName, defaultAccountName } = this.state
const { history, createAccount } = this.props
return h('div.new-account-create-form', [
h('div.new-account-create-form__input-label', {}, [
this.context.t('accountName'),
]),
h('div.new-account-create-form__input-wrapper', {}, [
h('input.new-account-create-form__input', {
value: newAccountName,
placeholder: defaultAccountName,
onChange: event => this.setState({ newAccountName: event.target.value }),
}, []),
]),
h('div.new-account-create-form__buttons', {}, [
h(Button, {
type: 'default',
large: true,
className: 'new-account-create-form__button',
onClick: () => history.push(DEFAULT_ROUTE),
}, [this.context.t('cancel')]),
h(Button, {
type: 'secondary',
large: true,
className: 'new-account-create-form__button',
onClick: () => {
createAccount(newAccountName || defaultAccountName)
.then(() => {
this.context.metricsEvent({
eventOpts: {
category: 'Accounts',
action: 'Add New Account',
name: 'Added New Account',
},
})
history.push(DEFAULT_ROUTE)
})
.catch((e) => {
this.context.metricsEvent({
eventOpts: {
category: 'Accounts',
action: 'Add New Account',
name: 'Error',
},
customVariables: {
errorMessage: e.message,
},
})
})
},
}, [this.context.t('create')]),
]),
])
}
}
NewAccountCreateForm.propTypes = {
hideModal: PropTypes.func,
showImportPage: PropTypes.func,
showConnectPage: PropTypes.func,
createAccount: PropTypes.func,
numberOfExistingAccounts: PropTypes.number,
history: PropTypes.object,
t: PropTypes.func,
}
const mapStateToProps = state => {
const { metamask: { network, selectedAddress, identities = {} } } = state
const numberOfExistingAccounts = Object.keys(identities).length
return {
network,
address: selectedAddress,
numberOfExistingAccounts,
}
}
const mapDispatchToProps = dispatch => {
return {
toCoinbase: address => dispatch(actions.buyEth({ network: '1', address, amount: 0 })),
hideModal: () => dispatch(actions.hideModal()),
createAccount: newAccountName => {
return dispatch(actions.addNewAccount())
.then(newAccountAddress => {
if (newAccountName) {
dispatch(actions.setAccountLabel(newAccountAddress, newAccountName))
}
})
},
showImportPage: () => dispatch(actions.showImportPage()),
showConnectPage: () => dispatch(actions.showConnectPage()),
}
}
NewAccountCreateForm.contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(NewAccountCreateForm)

View File

@ -6,7 +6,13 @@ export default class ProviderApproval extends Component {
static propTypes = {
approveProviderRequestByOrigin: PropTypes.func.isRequired,
rejectProviderRequestByOrigin: PropTypes.func.isRequired,
providerRequest: PropTypes.object.isRequired,
providerRequest: PropTypes.exact({
hostname: PropTypes.string.isRequired,
siteImage: PropTypes.string,
siteTitle: PropTypes.string,
origin: PropTypes.string.isRequired,
extensionId: PropTypes.string,
}).isRequired,
};
static contextTypes = {
@ -20,9 +26,10 @@ export default class ProviderApproval extends Component {
approveProviderRequestByOrigin={approveProviderRequestByOrigin}
rejectProviderRequestByOrigin={rejectProviderRequestByOrigin}
origin={providerRequest.origin}
tabID={providerRequest.tabID}
siteImage={providerRequest.siteImage}
siteTitle={providerRequest.siteTitle}
hostname={providerRequest.hostname}
extensionId={providerRequest.extensionId}
/>
)
}

View File

@ -12,7 +12,7 @@ import classnames from 'classnames'
// init
import FirstTimeFlow from '../first-time-flow'
// accounts
const SendTransactionScreen = require('../send/send.container')
import SendTransactionScreen from '../send'
const ConfirmTransaction = require('../confirm-transaction')
// slideout menu
@ -31,7 +31,7 @@ const MobileSyncPage = require('../mobile-sync')
const AddTokenPage = require('../add-token')
const ConfirmAddTokenPage = require('../confirm-add-token')
const ConfirmAddSuggestedTokenPage = require('../confirm-add-suggested-token')
const CreateAccountPage = require('../create-account')
import CreateAccountPage from '../create-account'
const Loading = require('../../components/ui/loading-screen')
const LoadingNetwork = require('../../components/app/loading-network-screen').default
@ -206,6 +206,10 @@ class Routes extends Component {
}
: null
const sidebarShouldClose = sidebarTransaction &&
!sidebarTransaction.status === 'failed' &&
!submittedPendingTransactions.find(({ id }) => id === sidebarTransaction.id)
return (
<div
className={classnames('app', { 'mouse-user-styles': isMouseUser})}
@ -232,7 +236,7 @@ class Routes extends Component {
}
<Sidebar
sidebarOpen={sidebarIsOpen}
sidebarShouldClose={sidebarTransaction && !submittedPendingTransactions.find(({ id }) => id === sidebarTransaction.id)}
sidebarShouldClose={sidebarShouldClose}
hideSidebar={this.props.hideSidebar}
transitionName={sidebarTransitionName}
type={sidebarType}

View File

@ -27,7 +27,7 @@ export default class SendFooter extends Component {
unapprovedTxs: PropTypes.object,
update: PropTypes.func,
sendErrors: PropTypes.object,
gasChangedLabel: PropTypes.string,
gasEstimateType: PropTypes.string,
}
static contextTypes = {
@ -58,7 +58,7 @@ export default class SendFooter extends Component {
update,
toAccounts,
history,
gasChangedLabel,
gasEstimateType,
} = this.props
const { metricsEvent } = this.context
@ -94,7 +94,7 @@ export default class SendFooter extends Component {
name: 'Complete',
},
customVariables: {
gasChanged: gasChangedLabel,
gasChanged: gasEstimateType,
},
})
history.push(CONFIRM_TRANSACTION_ROUTE)
@ -102,9 +102,10 @@ export default class SendFooter extends Component {
}
formShouldBeDisabled () {
const { data, inError, selectedToken, tokenBalance, gasTotal, to } = this.props
const { data, inError, selectedToken, tokenBalance, gasTotal, to, gasLimit } = this.props
const missingTokenBalance = selectedToken && !tokenBalance
const shouldBeDisabled = inError || !gasTotal || missingTokenBalance || !(data || to)
const gasLimitTooLow = gasLimit < 5208 // 5208 is hex value of 21000, minimum gas limit
const shouldBeDisabled = inError || !gasTotal || missingTokenBalance || !(data || to) || gasLimitTooLow
return shouldBeDisabled
}

View File

@ -42,8 +42,8 @@ function mapStateToProps (state) {
const gasButtonInfo = getRenderableEstimateDataForSmallButtonsFromGWEI(state)
const gasPrice = getGasPrice(state)
const activeButtonIndex = getDefaultActiveButtonIndex(gasButtonInfo, gasPrice)
const gasChangedLabel = activeButtonIndex >= 0
? gasButtonInfo[activeButtonIndex].labelKey
const gasEstimateType = activeButtonIndex >= 0
? gasButtonInfo[activeButtonIndex].gasEstimateType
: 'custom'
return {
@ -61,7 +61,7 @@ function mapStateToProps (state) {
tokenBalance: getTokenBalance(state),
unapprovedTxs: getUnapprovedTxs(state),
sendErrors: getSendErrors(state),
gasChangedLabel,
gasEstimateType,
}
}

View File

@ -48,7 +48,7 @@ proxyquire('../send-footer.container.js', {
'./send-footer.selectors': { isSendFormInError: (s) => `mockInError:${s}` },
'./send-footer.utils': utilsStubs,
'../../../selectors/custom-gas': {
getRenderableEstimateDataForSmallButtonsFromGWEI: (s) => ([{ labelKey: `mockLabel:${s}` }]),
getRenderableEstimateDataForSmallButtonsFromGWEI: (s) => ([{ gasEstimateType: `mockGasEstimateType:${s}` }]),
getDefaultActiveButtonIndex: () => 0,
},
})
@ -73,7 +73,7 @@ describe('send-footer container', () => {
tokenBalance: 'mockTokenBalance:mockState',
unapprovedTxs: 'mockUnapprovedTxs:mockState',
sendErrors: 'mockSendErrors:mockState',
gasChangedLabel: 'mockLabel:mockState',
gasEstimateType: 'mockGasEstimateType:mockState',
})
})

View File

@ -62,11 +62,6 @@ import {
SEND_ROUTE,
} from '../../helpers/constants/routes'
module.exports = compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(SendEther)
function mapStateToProps (state) {
return {
amount: getSendAmount(state),
@ -140,3 +135,8 @@ function mapDispatchToProps (dispatch) {
},
}
}
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(SendEther)

View File

@ -18,22 +18,29 @@ export default class EditContact extends PureComponent {
history: PropTypes.object,
name: PropTypes.string,
address: PropTypes.string,
chainId: PropTypes.string,
memo: PropTypes.string,
viewRoute: PropTypes.string,
listRoute: PropTypes.string,
setAccountLabel: PropTypes.func,
}
static defaultProps = {
name: '',
address: '',
memo: '',
}
state = {
newName: '',
newAddress: '',
newMemo: '',
newName: this.props.name,
newAddress: this.props.address,
newMemo: this.props.memo,
error: '',
}
render () {
const { t } = this.context
const { history, name, addToAddressBook, removeFromAddressBook, address, memo, viewRoute, listRoute, setAccountLabel } = this.props
const { history, name, addToAddressBook, removeFromAddressBook, address, chainId, memo, viewRoute, listRoute, setAccountLabel } = this.props
return (
<div className="settings-page__content-row address-book__edit-contact">
@ -43,7 +50,7 @@ export default class EditContact extends PureComponent {
type="link"
className="settings-page__address-book-button"
onClick={() => {
removeFromAddressBook(address)
removeFromAddressBook(chainId, address)
history.push(listRoute)
}}
>
@ -59,7 +66,7 @@ export default class EditContact extends PureComponent {
type="text"
id="nickname"
placeholder={this.context.t('addAlias')}
value={this.state.newName || name}
value={this.state.newName}
onChange={e => this.setState({ newName: e.target.value })}
fullWidth
margin="dense"
@ -73,8 +80,7 @@ export default class EditContact extends PureComponent {
<TextField
type="text"
id="address"
placeholder={address}
value={this.state.newAddress || address}
value={this.state.newAddress}
error={this.state.error}
onChange={e => this.setState({ newAddress: e.target.value })}
fullWidth
@ -90,7 +96,7 @@ export default class EditContact extends PureComponent {
type="text"
id="memo"
placeholder={memo}
value={this.state.newMemo || memo}
value={this.state.newMemo}
onChange={e => this.setState({ newMemo: e.target.value })}
fullWidth
margin="dense"
@ -109,12 +115,12 @@ export default class EditContact extends PureComponent {
if (this.state.newAddress !== '' && this.state.newAddress !== address) {
// if the user makes a valid change to the address field, remove the original address
if (isValidAddress(this.state.newAddress)) {
removeFromAddressBook(address)
removeFromAddressBook(chainId, address)
addToAddressBook(this.state.newAddress, this.state.newName || name, this.state.newMemo || memo)
setAccountLabel(this.state.newAddress, this.state.newName || name)
history.push(listRoute)
} else {
this.setState({ error: 'invalid address' })
this.setState({ error: this.context.t('invalidAddress') })
}
} else {
// update name

View File

@ -21,10 +21,13 @@ const mapStateToProps = (state, ownProps) => {
const { memo, name } = getAddressBookEntry(state, address) || state.metamask.identities[address]
const chainId = state.metamask.network
const showingMyAccounts = Boolean(pathname.match(CONTACT_MY_ACCOUNTS_EDIT_ROUTE))
return {
address,
chainId,
name,
memo,
viewRoute: showingMyAccounts ? CONTACT_MY_ACCOUNTS_VIEW_ROUTE : CONTACT_VIEW_ROUTE,
@ -36,7 +39,7 @@ const mapStateToProps = (state, ownProps) => {
const mapDispatchToProps = dispatch => {
return {
addToAddressBook: (recipient, nickname, memo) => dispatch(addToAddressBook(recipient, nickname, memo)),
removeFromAddressBook: (addressToRemove) => dispatch(removeFromAddressBook(addressToRemove)),
removeFromAddressBook: (chainId, addressToRemove) => dispatch(removeFromAddressBook(chainId, addressToRemove)),
setAccountLabel: (address, label) => dispatch(setAccountLabel(address, label)),
}
}

View File

@ -98,6 +98,8 @@
&--copy-icon {
padding-left: 4px;
width: 30px;
height: 20px;
}
}

View File

@ -21,6 +21,8 @@ import {
} from '../pages/send/send.utils'
import { addHexPrefix } from 'ethereumjs-util'
import { GAS_ESTIMATE_TYPES } from '../helpers/constants/common'
const selectors = {
formatTimeEstimate,
getAveragePriceEstimateInHexWEI,
@ -250,7 +252,7 @@ function getRenderableBasicEstimateData (state, gasLimit) {
return [
{
labelKey: 'slow',
gasEstimateType: GAS_ESTIMATE_TYPES.SLOW,
feeInPrimaryCurrency: getRenderableEthFee(safeLow, gasLimit),
feeInSecondaryCurrency: showFiat
? getRenderableConvertedCurrencyFee(safeLow, gasLimit, currentCurrency, conversionRate)
@ -259,7 +261,7 @@ function getRenderableBasicEstimateData (state, gasLimit) {
priceInHexWei: getGasPriceInHexWei(safeLow),
},
{
labelKey: 'average',
gasEstimateType: GAS_ESTIMATE_TYPES.AVERAGE,
feeInPrimaryCurrency: getRenderableEthFee(average, gasLimit),
feeInSecondaryCurrency: showFiat
? getRenderableConvertedCurrencyFee(average, gasLimit, currentCurrency, conversionRate)
@ -268,7 +270,7 @@ function getRenderableBasicEstimateData (state, gasLimit) {
priceInHexWei: getGasPriceInHexWei(average),
},
{
labelKey: 'fast',
gasEstimateType: GAS_ESTIMATE_TYPES.FAST,
feeInPrimaryCurrency: getRenderableEthFee(fast, gasLimit),
feeInSecondaryCurrency: showFiat
? getRenderableConvertedCurrencyFee(fast, gasLimit, currentCurrency, conversionRate)
@ -302,7 +304,7 @@ function getRenderableEstimateDataForSmallButtonsFromGWEI (state) {
return [
{
labelKey: 'slow',
gasEstimateType: GAS_ESTIMATE_TYPES.SLOW,
feeInSecondaryCurrency: showFiat
? getRenderableConvertedCurrencyFee(safeLow, gasLimit, currentCurrency, conversionRate)
: '',
@ -310,7 +312,7 @@ function getRenderableEstimateDataForSmallButtonsFromGWEI (state) {
priceInHexWei: getGasPriceInHexWei(safeLow, true),
},
{
labelKey: 'average',
gasEstimateType: GAS_ESTIMATE_TYPES.AVERAGE,
feeInSecondaryCurrency: showFiat
? getRenderableConvertedCurrencyFee(average, gasLimit, currentCurrency, conversionRate)
: '',
@ -318,7 +320,7 @@ function getRenderableEstimateDataForSmallButtonsFromGWEI (state) {
priceInHexWei: getGasPriceInHexWei(average, true),
},
{
labelKey: 'fast',
gasEstimateType: GAS_ESTIMATE_TYPES.FAST,
feeInSecondaryCurrency: showFiat
? getRenderableConvertedCurrencyFee(fast, gasLimit, currentCurrency, conversionRate)
: '',

View File

@ -77,21 +77,21 @@ describe('custom-gas selectors', () => {
{
expectedResult: [
{
labelKey: 'slow',
gasEstimateType: 'SLOW',
feeInSecondaryCurrency: '$0.01',
feeInPrimaryCurrency: '0.0000525 ETH',
timeEstimate: '~6 min 36 sec',
priceInHexWei: '0x9502f900',
},
{
labelKey: 'average',
gasEstimateType: 'AVERAGE',
feeInPrimaryCurrency: '0.000084 ETH',
feeInSecondaryCurrency: '$0.02',
priceInHexWei: '0xee6b2800',
timeEstimate: '~5 min 18 sec',
},
{
labelKey: 'fast',
gasEstimateType: 'FAST',
feeInSecondaryCurrency: '$0.03',
feeInPrimaryCurrency: '0.000105 ETH',
timeEstimate: '~3 min 18 sec',
@ -127,7 +127,7 @@ describe('custom-gas selectors', () => {
{
expectedResult: [
{
labelKey: 'slow',
gasEstimateType: 'SLOW',
feeInSecondaryCurrency: '$0.27',
feeInPrimaryCurrency: '0.000105 ETH',
timeEstimate: '~13 min 12 sec',
@ -136,12 +136,12 @@ describe('custom-gas selectors', () => {
{
feeInPrimaryCurrency: '0.000147 ETH',
feeInSecondaryCurrency: '$0.38',
labelKey: 'average',
gasEstimateType: 'AVERAGE',
priceInHexWei: '0x1a13b8600',
timeEstimate: '~10 min 6 sec',
},
{
labelKey: 'fast',
gasEstimateType: 'FAST',
feeInSecondaryCurrency: '$0.54',
feeInPrimaryCurrency: '0.00021 ETH',
timeEstimate: '~6 min 36 sec',
@ -180,21 +180,21 @@ describe('custom-gas selectors', () => {
{
expectedResult: [
{
labelKey: 'slow',
gasEstimateType: 'SLOW',
feeInSecondaryCurrency: '',
feeInPrimaryCurrency: '0.000105 ETH',
timeEstimate: '~13 min 12 sec',
priceInHexWei: '0x12a05f200',
},
{
labelKey: 'average',
gasEstimateType: 'AVERAGE',
feeInPrimaryCurrency: '0.000147 ETH',
feeInSecondaryCurrency: '',
timeEstimate: '~10 min 6 sec',
priceInHexWei: '0x1a13b8600',
},
{
labelKey: 'fast',
gasEstimateType: 'FAST',
feeInSecondaryCurrency: '',
feeInPrimaryCurrency: '0.00021 ETH',
timeEstimate: '~6 min 36 sec',
@ -233,21 +233,21 @@ describe('custom-gas selectors', () => {
{
expectedResult: [
{
labelKey: 'slow',
gasEstimateType: 'SLOW',
feeInSecondaryCurrency: '$0.27',
feeInPrimaryCurrency: '0.000105 ETH',
timeEstimate: '~13 min 12 sec',
priceInHexWei: '0x12a05f200',
},
{
labelKey: 'average',
gasEstimateType: 'AVERAGE',
feeInPrimaryCurrency: '0.000147 ETH',
feeInSecondaryCurrency: '$0.38',
priceInHexWei: '0x1a13b8600',
timeEstimate: '~10 min 6 sec',
},
{
labelKey: 'fast',
gasEstimateType: 'FAST',
feeInSecondaryCurrency: '$0.54',
feeInPrimaryCurrency: '0.00021 ETH',
timeEstimate: '~6 min 36 sec',
@ -286,21 +286,21 @@ describe('custom-gas selectors', () => {
{
expectedResult: [
{
labelKey: 'slow',
gasEstimateType: 'SLOW',
feeInSecondaryCurrency: '$0.27',
feeInPrimaryCurrency: '0.000105 ETH',
timeEstimate: '~13 min 12 sec',
priceInHexWei: '0x12a05f200',
},
{
labelKey: 'average',
gasEstimateType: 'AVERAGE',
feeInPrimaryCurrency: '0.000147 ETH',
feeInSecondaryCurrency: '$0.38',
priceInHexWei: '0x1a13b8600',
timeEstimate: '~10 min 6 sec',
},
{
labelKey: 'fast',
gasEstimateType: 'FAST',
feeInSecondaryCurrency: '$0.54',
feeInPrimaryCurrency: '0.00021 ETH',
timeEstimate: '~6 min 36 sec',
@ -355,19 +355,19 @@ describe('custom-gas selectors', () => {
{
feeInSecondaryCurrency: '$0.13',
feeInPrimaryCurrency: '0.00052 ETH',
labelKey: 'slow',
gasEstimateType: 'SLOW',
priceInHexWei: '0x5d21dba00',
},
{
feeInSecondaryCurrency: '$0.16',
feeInPrimaryCurrency: '0.00063 ETH',
labelKey: 'average',
gasEstimateType: 'AVERAGE',
priceInHexWei: '0x6fc23ac00',
},
{
feeInSecondaryCurrency: '$0.27',
feeInPrimaryCurrency: '0.00105 ETH',
labelKey: 'fast',
gasEstimateType: 'FAST',
priceInHexWei: '0xba43b7400',
},
],
@ -405,19 +405,19 @@ describe('custom-gas selectors', () => {
{
feeInSecondaryCurrency: '$2.68',
feeInPrimaryCurrency: '0.00105 ETH',
labelKey: 'slow',
gasEstimateType: 'SLOW',
priceInHexWei: '0xba43b7400',
},
{
feeInSecondaryCurrency: '$4.03',
feeInPrimaryCurrency: '0.00157 ETH',
labelKey: 'average',
gasEstimateType: 'AVERAGE',
priceInHexWei: '0x1176592e00',
},
{
feeInSecondaryCurrency: '$5.37',
feeInPrimaryCurrency: '0.0021 ETH',
labelKey: 'fast',
gasEstimateType: 'FAST',
priceInHexWei: '0x174876e800',
},
],
@ -455,19 +455,19 @@ describe('custom-gas selectors', () => {
{
feeInSecondaryCurrency: '',
feeInPrimaryCurrency: '0.00105 ETH',
labelKey: 'slow',
gasEstimateType: 'SLOW',
priceInHexWei: '0xba43b7400',
},
{
feeInSecondaryCurrency: '',
feeInPrimaryCurrency: '0.00157 ETH',
labelKey: 'average',
gasEstimateType: 'AVERAGE',
priceInHexWei: '0x1176592e00',
},
{
feeInSecondaryCurrency: '',
feeInPrimaryCurrency: '0.0021 ETH',
labelKey: 'fast',
gasEstimateType: 'FAST',
priceInHexWei: '0x174876e800',
},
],
@ -505,19 +505,19 @@ describe('custom-gas selectors', () => {
{
feeInSecondaryCurrency: '$2.68',
feeInPrimaryCurrency: '0.00105 ETH',
labelKey: 'slow',
gasEstimateType: 'SLOW',
priceInHexWei: '0xba43b7400',
},
{
feeInSecondaryCurrency: '$4.03',
feeInPrimaryCurrency: '0.00157 ETH',
labelKey: 'average',
gasEstimateType: 'AVERAGE',
priceInHexWei: '0x1176592e00',
},
{
feeInSecondaryCurrency: '$5.37',
feeInPrimaryCurrency: '0.0021 ETH',
labelKey: 'fast',
gasEstimateType: 'FAST',
priceInHexWei: '0x174876e800',
},
],
@ -555,19 +555,19 @@ describe('custom-gas selectors', () => {
{
feeInSecondaryCurrency: '$2.68',
feeInPrimaryCurrency: '0.00105 ETH',
labelKey: 'slow',
gasEstimateType: 'SLOW',
priceInHexWei: '0xba43b7400',
},
{
feeInSecondaryCurrency: '$4.03',
feeInPrimaryCurrency: '0.00157 ETH',
labelKey: 'average',
gasEstimateType: 'AVERAGE',
priceInHexWei: '0x1176592e00',
},
{
feeInSecondaryCurrency: '$5.37',
feeInPrimaryCurrency: '0.0021 ETH',
labelKey: 'fast',
gasEstimateType: 'FAST',
priceInHexWei: '0x174876e800',
},
],

View File

@ -350,6 +350,7 @@ var actions = {
createCancelTransaction,
createSpeedUpTransaction,
createRetryTransaction,
approveProviderRequestByOrigin,
rejectProviderRequestByOrigin,
@ -1860,6 +1861,28 @@ function createSpeedUpTransaction (txId, customGasPrice) {
}
}
function createRetryTransaction (txId, customGasPrice) {
log.debug('background.createRetryTransaction')
let newTx
return dispatch => {
return new Promise((resolve, reject) => {
background.createSpeedUpTransaction(txId, customGasPrice, (err, newState) => {
if (err) {
dispatch(actions.displayWarning(err.message))
return reject(err)
}
const { selectedAddressTxList } = newState
newTx = selectedAddressTxList[selectedAddressTxList.length - 1]
resolve(newState)
})
})
.then(newState => dispatch(actions.updateMetamaskState(newState)))
.then(() => newTx)
}
}
//
// config
//
@ -1988,11 +2011,11 @@ function addToAddressBook (recipient, nickname = '', memo = '') {
* @description Calls the addressBookController to remove an existing address.
* @param {String} addressToRemove - Address of the entry to remove from the address book
*/
function removeFromAddressBook (addressToRemove) {
function removeFromAddressBook (chainId, addressToRemove) {
log.debug(`background.removeFromAddressBook`)
return () => {
background.removeFromAddressBook(checksumAddress(addressToRemove))
background.removeFromAddressBook(chainId, checksumAddress(addressToRemove))
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

Some files were not shown because too many files have changed in this diff Show More