Merge branch 'develop' of github.com:MetaMask/metamask-extension into network-remove-provider-engine
This commit is contained in:
commit
a89902c170
|
@ -97,7 +97,7 @@ workflows:
|
|||
jobs:
|
||||
prep-deps-npm:
|
||||
docker:
|
||||
- image: circleci/node:8-browsers
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -116,7 +116,7 @@ jobs:
|
|||
|
||||
prep-deps-firefox:
|
||||
docker:
|
||||
- image: circleci/node:8-browsers
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
|
@ -129,7 +129,7 @@ jobs:
|
|||
|
||||
prep-build:
|
||||
docker:
|
||||
- image: circleci/node:8-browsers
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -148,7 +148,7 @@ jobs:
|
|||
|
||||
prep-docs:
|
||||
docker:
|
||||
- image: circleci/node:8-browsers
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -163,7 +163,7 @@ jobs:
|
|||
|
||||
prep-scss:
|
||||
docker:
|
||||
- image: circleci/node:8-browsers
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -182,7 +182,7 @@ jobs:
|
|||
|
||||
test-lint:
|
||||
docker:
|
||||
- image: circleci/node:8-browsers
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -193,7 +193,7 @@ jobs:
|
|||
|
||||
test-deps:
|
||||
docker:
|
||||
- image: circleci/node:8-browsers
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -204,7 +204,7 @@ jobs:
|
|||
|
||||
test-e2e-chrome:
|
||||
docker:
|
||||
- image: circleci/node:8-browsers
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -220,7 +220,7 @@ jobs:
|
|||
|
||||
test-e2e-firefox:
|
||||
docker:
|
||||
- image: circleci/node:8-browsers
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -241,7 +241,7 @@ jobs:
|
|||
|
||||
test-e2e-beta-chrome:
|
||||
docker:
|
||||
- image: circleci/node:8-browsers
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -257,7 +257,7 @@ jobs:
|
|||
|
||||
test-e2e-beta-firefox:
|
||||
docker:
|
||||
- image: circleci/node:8-browsers
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -278,7 +278,7 @@ jobs:
|
|||
|
||||
job-screens:
|
||||
docker:
|
||||
- image: circleci/node:8-browsers
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -295,7 +295,7 @@ jobs:
|
|||
|
||||
job-publish-prerelease:
|
||||
docker:
|
||||
- image: circleci/node:8-browsers
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -322,7 +322,7 @@ jobs:
|
|||
|
||||
job-publish-release:
|
||||
docker:
|
||||
- image: circleci/node:8-browsers
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -345,7 +345,7 @@ jobs:
|
|||
|
||||
test-unit:
|
||||
docker:
|
||||
- image: circleci/node:8-browsers
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -358,7 +358,7 @@ jobs:
|
|||
environment:
|
||||
browsers: '["Firefox"]'
|
||||
docker:
|
||||
- image: circleci/node:8-browsers
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -382,7 +382,7 @@ jobs:
|
|||
environment:
|
||||
browsers: '["Chrome"]'
|
||||
docker:
|
||||
- image: circleci/node:8-browsers
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -401,7 +401,7 @@ jobs:
|
|||
environment:
|
||||
browsers: '["Firefox"]'
|
||||
docker:
|
||||
- image: circleci/node:8-browsers
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -425,7 +425,7 @@ jobs:
|
|||
environment:
|
||||
browsers: '["Chrome"]'
|
||||
docker:
|
||||
- image: circleci/node:8-browsers
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -442,7 +442,7 @@ jobs:
|
|||
|
||||
all-tests-pass:
|
||||
docker:
|
||||
- image: circleci/node:8-browsers
|
||||
- image: circleci/node:8.11.3-browsers
|
||||
steps:
|
||||
- run:
|
||||
name: All Tests Passed
|
||||
|
|
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -2,6 +2,16 @@
|
|||
|
||||
## Current Master
|
||||
|
||||
## 4.8.0 Thur Jun 14 2018
|
||||
|
||||
- [#4513](https://github.com/MetaMask/metamask-extension/pull/4513): Attempting to import an empty private key will now show a clear error.
|
||||
- [#4570](https://github.com/MetaMask/metamask-extension/pull/4570): Fix bug where metamask data would stop being written to disk after prolonged use.
|
||||
- [#4523](https://github.com/MetaMask/metamask-extension/pull/4523): Fix bug where account reset did not work with custom RPC providers.
|
||||
- [#4524](https://github.com/MetaMask/metamask-extension/pull/4524): Fix for Brave i18n getAcceptLanguages.
|
||||
- [#4557](https://github.com/MetaMask/metamask-extension/pull/4557): Fix bug where nonce mutex was never released.
|
||||
- [#4566](https://github.com/MetaMask/metamask-extension/pull/4566): Add phishing notice.
|
||||
- [#4591](https://github.com/MetaMask/metamask-extension/pull/4591): Allow Copying Token Addresses and link to Token on Etherscan.
|
||||
|
||||
## 4.7.4 Tue Jun 05 2018
|
||||
|
||||
- Add diagnostic reporting for users with multiple HD keyrings
|
||||
|
|
1
LICENSE
1
LICENSE
|
@ -18,3 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
|
@ -146,6 +146,9 @@
|
|||
"copy": {
|
||||
"message": "Copy"
|
||||
},
|
||||
"copyContractAddress": {
|
||||
"message": "Copy Contract Address"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Copy to clipboard"
|
||||
},
|
||||
|
@ -262,6 +265,9 @@
|
|||
"encryptNewDen": {
|
||||
"message": "Encrypt your new DEN"
|
||||
},
|
||||
"ensNameNotFound": {
|
||||
"message": "ENS name not found"
|
||||
},
|
||||
"enterPassword": {
|
||||
"message": "Enter password"
|
||||
},
|
||||
|
@ -955,6 +961,9 @@
|
|||
"viewAccount": {
|
||||
"message": "View Account"
|
||||
},
|
||||
"viewOnEtherscan": {
|
||||
"message": "View on Etherscan"
|
||||
},
|
||||
"visitWebSite": {
|
||||
"message": "Visit our web site"
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "__MSG_appName__",
|
||||
"short_name": "__MSG_appName__",
|
||||
"version": "4.7.4",
|
||||
"version": "4.8.0",
|
||||
"manifest_version": 2,
|
||||
"author": "https://metamask.io",
|
||||
"description": "__MSG_appDescription__",
|
||||
|
|
|
@ -16,7 +16,18 @@ const accountImporter = {
|
|||
|
||||
strategies: {
|
||||
'Private Key': (privateKey) => {
|
||||
const stripped = ethUtil.stripHexPrefix(privateKey)
|
||||
if (!privateKey) {
|
||||
throw new Error('Cannot import an empty key.')
|
||||
}
|
||||
|
||||
const prefixed = ethUtil.addHexPrefix(privateKey)
|
||||
const buffer = ethUtil.toBuffer(prefixed)
|
||||
|
||||
if (!ethUtil.isValidPrivate(buffer)) {
|
||||
throw new Error('Cannot import invalid private key.')
|
||||
}
|
||||
|
||||
const stripped = ethUtil.stripHexPrefix(prefixed)
|
||||
return stripped
|
||||
},
|
||||
'JSON File': (input, password) => {
|
||||
|
|
|
@ -16,6 +16,7 @@ const ExtensionPlatform = require('./platforms/extension')
|
|||
const Migrator = require('./lib/migrator/')
|
||||
const migrations = require('./migrations/')
|
||||
const PortStream = require('./lib/port-stream.js')
|
||||
const createStreamSink = require('./lib/createStreamSink')
|
||||
const NotificationManager = require('./lib/notification-manager.js')
|
||||
const MetamaskController = require('./metamask-controller')
|
||||
const rawFirstTimeState = require('./first-time-state')
|
||||
|
@ -276,7 +277,7 @@ function setupController (initState, initLangCode) {
|
|||
asStream(controller.store),
|
||||
debounce(1000),
|
||||
storeTransform(versionifyData),
|
||||
storeTransform(persistData),
|
||||
createStreamSink(persistData),
|
||||
(error) => {
|
||||
log.error('MetaMask - Persistence pipeline failed', error)
|
||||
}
|
||||
|
@ -292,7 +293,7 @@ function setupController (initState, initLangCode) {
|
|||
return versionedData
|
||||
}
|
||||
|
||||
function persistData (state) {
|
||||
async function persistData (state) {
|
||||
if (!state) {
|
||||
throw new Error('MetaMask - updated state is missing', state)
|
||||
}
|
||||
|
@ -300,12 +301,13 @@ function setupController (initState, initLangCode) {
|
|||
throw new Error('MetaMask - updated state does not have data', state)
|
||||
}
|
||||
if (localStore.isSupported) {
|
||||
localStore.set(state)
|
||||
.catch((err) => {
|
||||
try {
|
||||
await localStore.set(state)
|
||||
} catch (err) {
|
||||
// log error so we dont break the pipeline
|
||||
log.error('error setting state in local store:', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
//
|
||||
|
|
|
@ -98,14 +98,21 @@ module.exports = class NetworkController extends EventEmitter {
|
|||
type: 'rpc',
|
||||
rpcTarget,
|
||||
}
|
||||
this.providerStore.updateState(providerConfig)
|
||||
this._switchNetwork(providerConfig)
|
||||
this.providerConfig = providerConfig
|
||||
}
|
||||
|
||||
async setProviderType (type) {
|
||||
assert.notEqual(type, 'rpc', `NetworkController - cannot call "setProviderType" with type 'rpc'. use "setRpcTarget"`)
|
||||
assert(INFURA_PROVIDER_TYPES.includes(type) || type === LOCALHOST, `NetworkController - Unknown rpc type "${type}"`)
|
||||
const providerConfig = { type }
|
||||
this.providerConfig = providerConfig
|
||||
}
|
||||
|
||||
resetConnection () {
|
||||
this.providerConfig = this.getProviderConfig()
|
||||
}
|
||||
|
||||
set providerConfig (providerConfig) {
|
||||
this.providerStore.updateState(providerConfig)
|
||||
this._switchNetwork(providerConfig)
|
||||
}
|
||||
|
|
|
@ -170,7 +170,7 @@ class TransactionController extends EventEmitter {
|
|||
// add default tx params
|
||||
txMeta = await this.addTxGasDefaults(txMeta)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
log.warn(error)
|
||||
this.txStateManager.setTxStatusFailed(txMeta.id, error)
|
||||
throw error
|
||||
}
|
||||
|
@ -269,7 +269,12 @@ class TransactionController extends EventEmitter {
|
|||
// must set transaction to submitted/failed before releasing lock
|
||||
nonceLock.releaseLock()
|
||||
} catch (err) {
|
||||
this.txStateManager.setTxStatusFailed(txId, err)
|
||||
// this is try-catch wrapped so that we can guarantee that the nonceLock is released
|
||||
try {
|
||||
this.txStateManager.setTxStatusFailed(txId, err)
|
||||
} catch (err) {
|
||||
log.error(err)
|
||||
}
|
||||
// must set transaction to submitted/failed before releasing lock
|
||||
if (nonceLock) nonceLock.releaseLock()
|
||||
// continue with error chain
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const Config = require('./recipient-blacklist-config.json')
|
||||
const Config = require('./recipient-blacklist.js')
|
||||
|
||||
/** @module*/
|
||||
module.exports = {
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"blacklist": [
|
||||
"0x627306090abab3a6e1400e9345bc60c78a8bef57",
|
||||
"0xf17f52151ebef6c7334fad080c5704d77216b732",
|
||||
"0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef",
|
||||
"0x821aea9a577a9b44299b9c15c88cf3087f3b5544",
|
||||
"0x0d1d4e623d10f9fba5db95830f7d3839406c6af2",
|
||||
"0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e",
|
||||
"0x2191ef87e392377ec08e7c08eb105ef5448eced5",
|
||||
"0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5",
|
||||
"0x6330a553fc93768f612722bb8c2ec78ac90b3bbc",
|
||||
"0x5aeda56215b167893e80b4fe645ba6d5bab767de"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
module.exports = {
|
||||
'blacklist': [
|
||||
// IDEX phisher
|
||||
'0x9bcb0A9d99d815Bb87ee3191b1399b1Bcc46dc77',
|
||||
// Ganache default seed phrases
|
||||
'0x627306090abab3a6e1400e9345bc60c78a8bef57',
|
||||
'0xf17f52151ebef6c7334fad080c5704d77216b732',
|
||||
'0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef',
|
||||
'0x821aea9a577a9b44299b9c15c88cf3087f3b5544',
|
||||
'0x0d1d4e623d10f9fba5db95830f7d3839406c6af2',
|
||||
'0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e',
|
||||
'0x2191ef87e392377ec08e7c08eb105ef5448eced5',
|
||||
'0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5',
|
||||
'0x6330a553fc93768f612722bb8c2ec78ac90b3bbc',
|
||||
'0x5aeda56215b167893e80b4fe645ba6d5bab767de',
|
||||
],
|
||||
}
|
|
@ -83,8 +83,8 @@ class NonceTracker {
|
|||
|
||||
async _globalMutexFree () {
|
||||
const globalMutex = this._lookupMutex('global')
|
||||
const release = await globalMutex.acquire()
|
||||
release()
|
||||
const releaseLock = await globalMutex.acquire()
|
||||
releaseLock()
|
||||
}
|
||||
|
||||
async _takeMutex (lockId) {
|
||||
|
|
|
@ -38,9 +38,30 @@ web3.setProvider = function () {
|
|||
log.debug('MetaMask - overrode web3.setProvider')
|
||||
}
|
||||
log.debug('MetaMask - injected web3')
|
||||
// export global web3, with usage-detection
|
||||
|
||||
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
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
const WritableStream = require('readable-stream').Writable
|
||||
const promiseToCallback = require('promise-to-callback')
|
||||
|
||||
module.exports = createStreamSink
|
||||
|
||||
|
||||
function createStreamSink(asyncWriteFn, _opts) {
|
||||
return new AsyncWritableStream(asyncWriteFn, _opts)
|
||||
}
|
||||
|
||||
class AsyncWritableStream extends WritableStream {
|
||||
|
||||
constructor (asyncWriteFn, _opts) {
|
||||
const opts = Object.assign({ objectMode: true }, _opts)
|
||||
super(opts)
|
||||
this._asyncWriteFn = asyncWriteFn
|
||||
}
|
||||
|
||||
// write from incomming stream to state
|
||||
_write (chunk, encoding, callback) {
|
||||
promiseToCallback(this._asyncWriteFn(chunk, encoding))(callback)
|
||||
}
|
||||
|
||||
}
|
|
@ -2,8 +2,7 @@ const extension = require('extensionizer')
|
|||
const promisify = require('pify')
|
||||
const allLocales = require('../../_locales/index.json')
|
||||
|
||||
const isSupported = extension.i18n && extension.i18n.getAcceptLanguages
|
||||
const getPreferredLocales = isSupported ? promisify(
|
||||
const getPreferredLocales = extension.i18n ? promisify(
|
||||
extension.i18n.getAcceptLanguages,
|
||||
{ errorFirst: false }
|
||||
) : async () => []
|
||||
|
@ -18,7 +17,21 @@ const existingLocaleCodes = allLocales.map(locale => locale.code.toLowerCase().r
|
|||
*
|
||||
*/
|
||||
async function getFirstPreferredLangCode () {
|
||||
const userPreferredLocaleCodes = await getPreferredLocales()
|
||||
let userPreferredLocaleCodes
|
||||
|
||||
try {
|
||||
userPreferredLocaleCodes = await getPreferredLocales()
|
||||
} catch (e) {
|
||||
// Brave currently throws when calling getAcceptLanguages, so this handles that.
|
||||
userPreferredLocaleCodes = []
|
||||
}
|
||||
|
||||
// safeguard for Brave Browser until they implement chrome.i18n.getAcceptLanguages
|
||||
// https://github.com/MetaMask/metamask-extension/issues/4270
|
||||
if (!userPreferredLocaleCodes){
|
||||
userPreferredLocaleCodes = []
|
||||
}
|
||||
|
||||
const firstPreferredLangCode = userPreferredLocaleCodes
|
||||
.map(code => code.toLowerCase())
|
||||
.find(code => existingLocaleCodes.includes(code))
|
||||
|
@ -26,3 +39,4 @@ async function getFirstPreferredLangCode () {
|
|||
}
|
||||
|
||||
module.exports = getFirstPreferredLangCode
|
||||
|
||||
|
|
|
@ -32,6 +32,8 @@ class NotificationManager {
|
|||
type: 'popup',
|
||||
width,
|
||||
height,
|
||||
}).then((currentPopup) => {
|
||||
this._popupId = currentPopup.id
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -84,7 +86,7 @@ class NotificationManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* Given an array of windows, returns the first that has a 'popup' type, or null if no such window exists.
|
||||
* Given an array of windows, returns the 'popup' that has been opened by MetaMask, or null if no such window exists.
|
||||
*
|
||||
* @private
|
||||
* @param {array} windows An array of objects containing data about the open MetaMask extension windows.
|
||||
|
@ -93,7 +95,7 @@ class NotificationManager {
|
|||
_getPopupIn (windows) {
|
||||
return windows ? windows.find((win) => {
|
||||
// Returns notification popup
|
||||
return (win && win.type === 'popup')
|
||||
return (win && win.type === 'popup' && win.id === this._popupId)
|
||||
}) : null
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,6 @@ const BN = require('ethereumjs-util').BN
|
|||
const GWEI_BN = new BN('1000000000')
|
||||
const percentile = require('percentile')
|
||||
const seedPhraseVerifier = require('./lib/seed-phrase-verifier')
|
||||
const DiagnosticsReporter = require('./lib/diagnostics-reporter')
|
||||
const log = require('loglevel')
|
||||
|
||||
module.exports = class MetamaskController extends EventEmitter {
|
||||
|
@ -64,12 +63,6 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
const initState = opts.initState || {}
|
||||
this.recordFirstTimeInfo(initState)
|
||||
|
||||
// metamask diagnostics reporter
|
||||
this.diagnostics = opts.diagnostics || new DiagnosticsReporter({
|
||||
firstTimeInfo: initState.firstTimeInfo,
|
||||
version,
|
||||
})
|
||||
|
||||
// platform-specific api
|
||||
this.platform = opts.platform
|
||||
|
||||
|
@ -91,7 +84,6 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
this.preferencesController = new PreferencesController({
|
||||
initState: initState.PreferencesController,
|
||||
initLangCode: opts.initLangCode,
|
||||
diagnostics: this.diagnostics,
|
||||
})
|
||||
|
||||
// currency controller
|
||||
|
@ -189,9 +181,6 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
version,
|
||||
firstVersion: initState.firstTimeInfo.version,
|
||||
})
|
||||
this.noticeController.updateNoticesList()
|
||||
// to be uncommented when retrieving notices from a remote server.
|
||||
// this.noticeController.startPolling()
|
||||
|
||||
this.shapeshiftController = new ShapeShiftController({
|
||||
initState: initState.ShapeShiftController,
|
||||
|
@ -430,28 +419,24 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
* @returns {Object} vault
|
||||
*/
|
||||
async createNewVaultAndKeychain (password) {
|
||||
const release = await this.createVaultMutex.acquire()
|
||||
let vault
|
||||
|
||||
const releaseLock = await this.createVaultMutex.acquire()
|
||||
try {
|
||||
let vault
|
||||
const accounts = await this.keyringController.getAccounts()
|
||||
|
||||
if (accounts.length > 0) {
|
||||
vault = await this.keyringController.fullUpdate()
|
||||
|
||||
} else {
|
||||
vault = await this.keyringController.createNewVaultAndKeychain(password)
|
||||
const accounts = await this.keyringController.getAccounts()
|
||||
this.preferencesController.setAddresses(accounts)
|
||||
this.selectFirstIdentity()
|
||||
}
|
||||
release()
|
||||
releaseLock()
|
||||
return vault
|
||||
} catch (err) {
|
||||
release()
|
||||
releaseLock()
|
||||
throw err
|
||||
}
|
||||
|
||||
return vault
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -460,7 +445,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
* @param {} seed
|
||||
*/
|
||||
async createNewVaultAndRestore (password, seed) {
|
||||
const release = await this.createVaultMutex.acquire()
|
||||
const releaseLock = await this.createVaultMutex.acquire()
|
||||
try {
|
||||
// clear known identities
|
||||
this.preferencesController.setAddresses([])
|
||||
|
@ -470,10 +455,10 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
const accounts = await this.keyringController.getAccounts()
|
||||
this.preferencesController.setAddresses(accounts)
|
||||
this.selectFirstIdentity()
|
||||
release()
|
||||
releaseLock()
|
||||
return vault
|
||||
} catch (err) {
|
||||
release()
|
||||
releaseLock()
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
@ -624,10 +609,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
async resetAccount () {
|
||||
const selectedAddress = this.preferencesController.getSelectedAddress()
|
||||
this.txController.wipeTransactions(selectedAddress)
|
||||
|
||||
const networkController = this.networkController
|
||||
const oldType = networkController.getProviderConfig().type
|
||||
await networkController.setProviderType(oldType, true)
|
||||
this.networkController.resetConnection()
|
||||
|
||||
return selectedAddress
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ const EventEmitter = require('events').EventEmitter
|
|||
const semver = require('semver')
|
||||
const extend = require('xtend')
|
||||
const ObservableStore = require('obs-store')
|
||||
const hardCodedNotices = require('../../notices/notices.json')
|
||||
const hardCodedNotices = require('../../notices/notices.js')
|
||||
const uniqBy = require('lodash.uniqby')
|
||||
|
||||
module.exports = class NoticeController extends EventEmitter {
|
||||
|
@ -16,8 +16,12 @@ module.exports = class NoticeController extends EventEmitter {
|
|||
noticesList: [],
|
||||
}, opts.initState)
|
||||
this.store = new ObservableStore(initState)
|
||||
// setup memStore
|
||||
this.memStore = new ObservableStore({})
|
||||
this.store.subscribe(() => this._updateMemstore())
|
||||
this._updateMemstore()
|
||||
// pull in latest notices
|
||||
this.updateNoticesList()
|
||||
}
|
||||
|
||||
getNoticesList () {
|
||||
|
@ -29,9 +33,9 @@ module.exports = class NoticeController extends EventEmitter {
|
|||
return notices.filter((notice) => notice.read === false)
|
||||
}
|
||||
|
||||
getLatestUnreadNotice () {
|
||||
getNextUnreadNotice () {
|
||||
const unreadNotices = this.getUnreadNotices()
|
||||
return unreadNotices[unreadNotices.length - 1]
|
||||
return unreadNotices[0]
|
||||
}
|
||||
|
||||
async setNoticesList (noticesList) {
|
||||
|
@ -47,7 +51,7 @@ module.exports = class NoticeController extends EventEmitter {
|
|||
notices[index].read = true
|
||||
notices[index].body = ''
|
||||
this.setNoticesList(notices)
|
||||
const latestNotice = this.getLatestUnreadNotice()
|
||||
const latestNotice = this.getNextUnreadNotice()
|
||||
cb(null, latestNotice)
|
||||
} catch (err) {
|
||||
cb(err)
|
||||
|
@ -64,15 +68,6 @@ module.exports = class NoticeController extends EventEmitter {
|
|||
return result
|
||||
}
|
||||
|
||||
startPolling () {
|
||||
if (this.noticePoller) {
|
||||
clearInterval(this.noticePoller)
|
||||
}
|
||||
this.noticePoller = setInterval(() => {
|
||||
this.noticeController.updateNoticesList()
|
||||
}, 300000)
|
||||
}
|
||||
|
||||
_mergeNotices (oldNotices, newNotices) {
|
||||
return uniqBy(oldNotices.concat(newNotices), 'id')
|
||||
}
|
||||
|
@ -91,19 +86,15 @@ module.exports = class NoticeController extends EventEmitter {
|
|||
})
|
||||
}
|
||||
|
||||
_mapNoticeIds (notices) {
|
||||
return notices.map((notice) => notice.id)
|
||||
}
|
||||
|
||||
async _retrieveNoticeData () {
|
||||
// Placeholder for the API.
|
||||
// Placeholder for remote notice API.
|
||||
return hardCodedNotices
|
||||
}
|
||||
|
||||
_updateMemstore () {
|
||||
const lastUnreadNotice = this.getLatestUnreadNotice()
|
||||
const noActiveNotices = !lastUnreadNotice
|
||||
this.memStore.updateState({ lastUnreadNotice, noActiveNotices })
|
||||
const nextUnreadNotice = this.getNextUnreadNotice()
|
||||
const noActiveNotices = !nextUnreadNotice
|
||||
this.memStore.updateState({ nextUnreadNotice, noActiveNotices })
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
"conversionRate": 12.7200827,
|
||||
"conversionDate": 1487363041,
|
||||
"noActiveNotices": true,
|
||||
"lastUnreadNotice": {
|
||||
"nextUnreadNotice": {
|
||||
"read": true,
|
||||
"date": "Thu Feb 09 2017",
|
||||
"title": "Terms of Use",
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"conversionRate": 12.7527416,
|
||||
"conversionDate": 1487624341,
|
||||
"noActiveNotices": false,
|
||||
"lastUnreadNotice": {
|
||||
"nextUnreadNotice": {
|
||||
"read": false,
|
||||
"date": "Thu Feb 09 2017",
|
||||
"title": "Terms of Use",
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
"conversionRate": 8.3533002,
|
||||
"conversionDate": 1481671082,
|
||||
"noActiveNotices": false,
|
||||
"lastUnreadNotice": {
|
||||
"nextUnreadNotice": {
|
||||
"read": false,
|
||||
"date": "Tue Dec 13 2016",
|
||||
"title": "MultiVault Support",
|
||||
|
|
|
@ -14,7 +14,7 @@ import LoadingScreen from './loading-screen'
|
|||
class NoticeScreen extends Component {
|
||||
static propTypes = {
|
||||
address: PropTypes.string.isRequired,
|
||||
lastUnreadNotice: PropTypes.shape({
|
||||
nextUnreadNotice: PropTypes.shape({
|
||||
title: PropTypes.string,
|
||||
date: PropTypes.string,
|
||||
body: PropTypes.string,
|
||||
|
@ -31,7 +31,7 @@ class NoticeScreen extends Component {
|
|||
};
|
||||
|
||||
static defaultProps = {
|
||||
lastUnreadNotice: {},
|
||||
nextUnreadNotice: {},
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -47,8 +47,8 @@ class NoticeScreen extends Component {
|
|||
}
|
||||
|
||||
acceptTerms = () => {
|
||||
const { markNoticeRead, lastUnreadNotice, history } = this.props
|
||||
markNoticeRead(lastUnreadNotice)
|
||||
const { markNoticeRead, nextUnreadNotice, history } = this.props
|
||||
markNoticeRead(nextUnreadNotice)
|
||||
.then(hasActiveNotices => {
|
||||
if (!hasActiveNotices) {
|
||||
history.push(INITIALIZE_BACKUP_PHRASE_ROUTE)
|
||||
|
@ -72,7 +72,7 @@ class NoticeScreen extends Component {
|
|||
render () {
|
||||
const {
|
||||
address,
|
||||
lastUnreadNotice: { title, body },
|
||||
nextUnreadNotice: { title, body },
|
||||
isLoading,
|
||||
} = this.props
|
||||
const { atBottom } = this.state
|
||||
|
@ -113,12 +113,12 @@ class NoticeScreen extends Component {
|
|||
}
|
||||
|
||||
const mapStateToProps = ({ metamask, appState }) => {
|
||||
const { selectedAddress, lastUnreadNotice, noActiveNotices } = metamask
|
||||
const { selectedAddress, nextUnreadNotice, noActiveNotices } = metamask
|
||||
const { isLoading } = appState
|
||||
|
||||
return {
|
||||
address: selectedAddress,
|
||||
lastUnreadNotice,
|
||||
nextUnreadNotice,
|
||||
noActiveNotices,
|
||||
isLoading,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
Dear MetaMask Users,
|
||||
|
||||
There have been several instances of high-profile legitimate websites such as BTC Manager and Games Workshop that have had their websites temporarily compromised. This involves showing a fake MetaMask window on the page asking for user's seed phrases. MetaMask will never open itself in this way and users are encouraged to report these instances immediately to either [our phishing blacklist](https://github.com/MetaMask/eth-phishing-detect/issues) or our support email at [support@metamask.io](mailto:support@metamask.io).
|
||||
|
||||
Please read our full article on this ongoing issue at [https://medium.com/metamask/new-phishing-strategy-becoming-common-1b1123837168](https://medium.com/metamask/new-phishing-strategy-becoming-common-1b1123837168).
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
var fs = require('fs')
|
||||
var path = require('path')
|
||||
var prompt = require('prompt')
|
||||
var open = require('open')
|
||||
var extend = require('extend')
|
||||
var notices = require('./notices.json')
|
||||
|
||||
|
||||
console.log('List of Notices')
|
||||
console.log(`ID \t DATE \t\t\t TITLE`)
|
||||
notices.forEach((notice) => {
|
||||
console.log(`${(' ' + notice.id).slice(-2)} \t ${notice.date} \t ${notice.title}`)
|
||||
})
|
||||
prompt.get(['id'], (error, res) => {
|
||||
prompt.start()
|
||||
if (error) {
|
||||
console.log("Exiting...")
|
||||
process.exit()
|
||||
}
|
||||
var index = notices.findIndex((notice) => { return notice.id == res.id})
|
||||
if (index === -1) {
|
||||
console.log('Notice not found. Exiting...')
|
||||
}
|
||||
notices.splice(index, 1)
|
||||
fs.unlink(`notices/archive/notice_${res.id}.md`)
|
||||
fs.writeFile(`notices/notices.json`, JSON.stringify(notices))
|
||||
})
|
|
@ -1,33 +0,0 @@
|
|||
var fsp = require('fs-promise')
|
||||
var path = require('path')
|
||||
var prompt = require('prompt')
|
||||
var open = require('open')
|
||||
var extend = require('extend')
|
||||
var notices = require('./notices.json')
|
||||
var id = Number(require('./notice-nonce.json'))
|
||||
|
||||
var date = new Date().toDateString()
|
||||
|
||||
var notice = {
|
||||
read: false,
|
||||
date: date,
|
||||
}
|
||||
|
||||
fsp.writeFile(`notices/archive/notice_${id}.md`,'Message goes here. Please write out your notice and save before proceeding at the command line.')
|
||||
.then(() => {
|
||||
open(`notices/archive/notice_${id}.md`)
|
||||
prompt.start()
|
||||
prompt.get(['title'], (err, result) => {
|
||||
notice.title = result.title
|
||||
fsp.readFile(`notices/archive/notice_${id}.md`)
|
||||
.then((body) => {
|
||||
notice.body = body.toString()
|
||||
notice.id = id
|
||||
notices.push(notice)
|
||||
return fsp.writeFile(`notices/notices.json`, JSON.stringify(notices))
|
||||
}).then((completion) => {
|
||||
id += 1
|
||||
return fsp.writeFile(`notices/notice-nonce.json`, id)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1 +0,0 @@
|
|||
4
|
|
@ -0,0 +1,34 @@
|
|||
// fs.readFileSync is inlined by browserify transform "brfs"
|
||||
const fs = require('fs')
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
id: 0,
|
||||
read: false,
|
||||
date: 'Thu Feb 09 2017',
|
||||
title: 'Terms of Use',
|
||||
body: fs.readFileSync(__dirname + '/archive/notice_0.md', 'utf8'),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
read: false,
|
||||
date: 'Mon May 08 2017',
|
||||
title: 'Privacy Notice',
|
||||
body: fs.readFileSync(__dirname + '/archive/notice_2.md', 'utf8'),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
read: false,
|
||||
date: 'Tue Nov 28 2017',
|
||||
title: 'Seed Phrase Alert',
|
||||
firstVersion: '<=3.12.0',
|
||||
body: fs.readFileSync(__dirname + '/archive/notice_3.md', 'utf8'),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
read: false,
|
||||
date: 'Wed Jun 13 2018',
|
||||
title: 'Phishing Warning',
|
||||
body: fs.readFileSync(__dirname + '/archive/notice_4.md', 'utf8'),
|
||||
}
|
||||
]
|
File diff suppressed because one or more lines are too long
|
@ -73,7 +73,7 @@ function mapStateToProps (state) {
|
|||
network: state.metamask.network,
|
||||
provider: state.metamask.provider,
|
||||
forgottenPassword: state.appState.forgottenPassword,
|
||||
lastUnreadNotice: state.metamask.lastUnreadNotice,
|
||||
nextUnreadNotice: state.metamask.nextUnreadNotice,
|
||||
lostAccounts: state.metamask.lostAccounts,
|
||||
frequentRpcList: state.metamask.frequentRpcList || [],
|
||||
featureFlags,
|
||||
|
@ -460,9 +460,9 @@ App.prototype.renderPrimary = function () {
|
|||
}, [
|
||||
|
||||
h(NoticeScreen, {
|
||||
notice: props.lastUnreadNotice,
|
||||
notice: props.nextUnreadNotice,
|
||||
key: 'NoticeScreen',
|
||||
onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)),
|
||||
onConfirm: () => props.dispatch(actions.markNoticeRead(props.nextUnreadNotice)),
|
||||
}),
|
||||
|
||||
!props.isInitialized && h('.flex-row.flex-center.flex-grow', [
|
||||
|
|
|
@ -34,9 +34,13 @@ TypedMessageRenderer.prototype.render = function () {
|
|||
|
||||
function renderTypedData (values) {
|
||||
return values.map(function (value) {
|
||||
let v = value.value
|
||||
if (typeof v === 'boolean') {
|
||||
v = v.toString()
|
||||
}
|
||||
return h('div', {}, [
|
||||
h('strong', {style: {display: 'block', fontWeight: 'bold'}}, String(value.name) + ':'),
|
||||
h('div', {}, value.value),
|
||||
h('div', {}, v),
|
||||
])
|
||||
})
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
|
@ -9,6 +9,7 @@
|
|||
"dist": "gulp dist",
|
||||
"doc": "jsdoc -c development/tools/.jsdoc.json",
|
||||
"test": "npm run test:unit && npm run test:integration && npm run lint",
|
||||
"watch:test:unit": "nodemon --exec \"npm run test:unit\" ./test ./app ./ui",
|
||||
"test:unit": "cross-env METAMASK_ENV=test mocha --exit --require test/setup.js --recursive \"test/unit/**/*.js\" \"ui/app/**/*.test.js\" && dot-only-hunter",
|
||||
"test:single": "cross-env METAMASK_ENV=test mocha --require test/helper.js",
|
||||
"test:integration": "npm run test:integration:build && npm run test:flat && npm run test:mascara",
|
||||
|
@ -45,8 +46,6 @@
|
|||
"disc": "gulp disc --debug",
|
||||
"announce": "node development/announcer.js",
|
||||
"version:bump": "node development/run-version-bump.js",
|
||||
"generateNotice": "node notices/notice-generator.js",
|
||||
"deleteNotice": "node notices/notice-delete.js",
|
||||
"storybook": "start-storybook -p 6006 -c .storybook"
|
||||
},
|
||||
"browserify": {
|
||||
|
@ -111,7 +110,7 @@
|
|||
"ethereumjs-wallet": "^0.6.0",
|
||||
"etherscan-link": "^1.0.2",
|
||||
"ethjs": "^0.4.0",
|
||||
"ethjs-contract": "^0.2.0",
|
||||
"ethjs-contract": "^0.2.3",
|
||||
"ethjs-ens": "^2.0.0",
|
||||
"ethjs-query": "^0.3.4",
|
||||
"express": "^4.15.5",
|
||||
|
@ -187,6 +186,7 @@
|
|||
"semaphore": "^1.0.5",
|
||||
"semver": "^5.4.1",
|
||||
"shallow-copy": "0.0.1",
|
||||
"superstatic": "^5.0.2",
|
||||
"sw-controller": "^1.0.3",
|
||||
"sw-stream": "^2.0.2",
|
||||
"swappable-obj-proxy": "^1.0.2",
|
||||
|
@ -212,7 +212,7 @@
|
|||
"babel-register": "^6.7.2",
|
||||
"babelify": "^8.0.0",
|
||||
"beefy": "^2.1.5",
|
||||
"brfs": "^1.4.3",
|
||||
"brfs": "^1.6.1",
|
||||
"browserify": "^16.1.1",
|
||||
"chai": "^4.1.0",
|
||||
"chromedriver": "2.36.0",
|
||||
|
@ -235,9 +235,9 @@
|
|||
"fs-extra": "^6.0.1",
|
||||
"fs-promise": "^2.0.3",
|
||||
"ganache-cli": "^6.1.0",
|
||||
"ganache-core": "^2.1.0",
|
||||
"ganache-core": "^2.1.3",
|
||||
"geckodriver": "^1.11.0",
|
||||
"gh-pages": "^1.1.0",
|
||||
"gh-pages": "^1.2.0",
|
||||
"gifencoder": "^1.1.0",
|
||||
"gulp": "github:gulpjs/gulp#6d71a658c61edb3090221579d8f97dbe086ba2ed",
|
||||
"gulp-babel": "^7.0.0",
|
||||
|
@ -254,13 +254,14 @@
|
|||
"gulp-util": "^3.0.7",
|
||||
"gulp-watch": "^5.0.0",
|
||||
"gulp-zip": "^4.0.0",
|
||||
"http-server": "^0.11.1",
|
||||
"image-size": "^0.6.2",
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"jsdoc": "^3.5.5",
|
||||
"jsdom": "^11.2.0",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"jshint-stylish": "~2.2.1",
|
||||
"karma": "^2.0.0",
|
||||
"karma": "^2.0.4",
|
||||
"karma-chrome-launcher": "^2.2.0",
|
||||
"karma-cli": "^1.0.1",
|
||||
"karma-firefox-launcher": "^1.0.1",
|
||||
|
@ -271,9 +272,9 @@
|
|||
"mocha-jsdom": "^1.1.0",
|
||||
"mocha-sinon": "^2.0.0",
|
||||
"nock": "^9.0.14",
|
||||
"node-sass": "^4.7.2",
|
||||
"node-sass": "^4.9.0",
|
||||
"nsp": "^3.2.1",
|
||||
"nyc": "^11.0.3",
|
||||
"nyc": "^13.0.0",
|
||||
"open": "0.0.5",
|
||||
"path": "^0.12.7",
|
||||
"png-file-stream": "^1.0.0",
|
||||
|
@ -297,11 +298,11 @@
|
|||
"style-loader": "^0.21.0",
|
||||
"stylelint-config-standard": "^18.2.0",
|
||||
"tape": "^4.5.1",
|
||||
"testem": "^2.0.0",
|
||||
"testem": "^2.8.0",
|
||||
"through2": "^2.0.3",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"vinyl-source-stream": "^2.0.0",
|
||||
"watchify": "^3.9.0"
|
||||
"watchify": "^3.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
The `piggybankContract` is compiled from:
|
||||
|
||||
pragma solidity ^0.4.0;
|
||||
contract PiggyBank {
|
||||
|
||||
uint private balance;
|
||||
address public owner;
|
||||
|
||||
function PiggyBank() public {
|
||||
owner = msg.sender;
|
||||
balance = 0;
|
||||
}
|
||||
|
||||
function deposit() public payable returns (uint) {
|
||||
balance += msg.value;
|
||||
return balance;
|
||||
}
|
||||
|
||||
function withdraw(uint withdrawAmount) public returns (uint remainingBal) {
|
||||
require(msg.sender == owner);
|
||||
balance -= withdrawAmount;
|
||||
|
||||
msg.sender.transfer(withdrawAmount);
|
||||
|
||||
return balance;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
var piggybankContract = web3.eth.contract([{"constant":false,"inputs":[{"name":"withdrawAmount","type":"uint256"}],"name":"withdraw","outputs":[{"name":"remainingBal","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"deposit","outputs":[{"name":"","type":"uint256"}],"payable":true,"stateMutability":"payable","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]);
|
||||
|
||||
deployButton.addEventListener('click', function (event) {
|
||||
|
||||
var piggybank = piggybankContract.new(
|
||||
{
|
||||
from: web3.eth.accounts[0],
|
||||
data: '0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506000808190555061023b806100686000396000f300608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632e1a7d4d1461005c5780638da5cb5b1461009d578063d0e30db0146100f4575b600080fd5b34801561006857600080fd5b5061008760048036038101908080359060200190929190505050610112565b6040518082815260200191505060405180910390f35b3480156100a957600080fd5b506100b26101d0565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100fc6101f6565b6040518082815260200191505060405180910390f35b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561017057600080fd5b8160008082825403925050819055503373ffffffffffffffffffffffffffffffffffffffff166108fc839081150290604051600060405180830381858888f193505050501580156101c5573d6000803e3d6000fd5b506000549050919050565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60003460008082825401925050819055506000549050905600a165627a7a72305820f237db3ec816a52589d82512117bc85bc08d3537683ffeff9059108caf3e5d400029',
|
||||
gas: '4700000'
|
||||
}, function (e, contract){
|
||||
console.log(e, contract);
|
||||
if (typeof contract.address !== 'undefined') {
|
||||
console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);
|
||||
|
||||
console.log(`contract`, contract);
|
||||
|
||||
depositButton.addEventListener('click', function (event) {
|
||||
contract.deposit({ from: web3.eth.accounts[0], value: '0x29a2241af62c0000' }, function (result) {
|
||||
console.log(result)
|
||||
})
|
||||
})
|
||||
|
||||
withdrawButton.addEventListener('click', function (event) {
|
||||
contract.withdraw('0xde0b6b3a7640000', { from: web3.eth.accounts[0] }, function (result) {
|
||||
console.log(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
})
|
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<body>
|
||||
<button id="deployButton">Deploy Contract</button>
|
||||
<button id="depositButton">Deposit</button>
|
||||
<button id="withdrawButton">Withdraw</button>
|
||||
</body>
|
||||
<script src="contract.js"></script>
|
||||
</html>
|
|
@ -23,6 +23,7 @@ describe('Using MetaMask with an existing account', function () {
|
|||
|
||||
const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'
|
||||
const testAddress = '0xE18035BF8712672935FDB4e5e431b1a0183d2DFC'
|
||||
const testPrivateKey2 = '14abe6f4aab7f9f626fe981c864d0adeb5685f289ac9270c27b8fd790b4235d6'
|
||||
const regularDelayMs = 1000
|
||||
const largeDelayMs = regularDelayMs * 2
|
||||
const waitingNewPageDelayMs = regularDelayMs * 10
|
||||
|
@ -109,27 +110,39 @@ describe('Using MetaMask with an existing account', function () {
|
|||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('clicks through the privacy notice', async () => {
|
||||
const [nextScreen] = await findElements(driver, By.css('.tou button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
it('clicks through the ToS', async () => {
|
||||
// terms of use
|
||||
const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled()
|
||||
assert.equal(canClickThrough, false, 'disabled continue button')
|
||||
const element = await findElement(driver, By.linkText('Attributions'))
|
||||
await driver.executeScript('arguments[0].scrollIntoView(true)', element)
|
||||
const bottomOfTos = await findElement(driver, By.linkText('Attributions'))
|
||||
await driver.executeScript('arguments[0].scrollIntoView(true)', bottomOfTos)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const acceptTos = await findElement(driver, By.xpath(`//button[contains(text(), 'Accept')]`))
|
||||
const acceptTos = await findElement(driver, By.css('.tou button'))
|
||||
await acceptTos.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('clicks through the privacy notice', async () => {
|
||||
// privacy notice
|
||||
const nextScreen = await findElement(driver, By.css('.tou button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('clicks through the phishing notice', async () => {
|
||||
// phishing notice
|
||||
const noticeElement = await driver.findElement(By.css('.markdown'))
|
||||
await driver.executeScript('arguments[0].scrollTop = arguments[0].scrollHeight', noticeElement)
|
||||
await delay(regularDelayMs)
|
||||
const nextScreen = await findElement(driver, By.css('.tou button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Show account information', () => {
|
||||
it('shows the correct account address', async () => {
|
||||
const detailsButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Details')]`))
|
||||
detailsButton.click()
|
||||
await driver.findElement(By.css('.wallet-view__details-button')).click()
|
||||
await driver.findElement(By.css('.qr-wrapper')).isDisplayed()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
|
@ -225,8 +238,10 @@ describe('Using MetaMask with an existing account', function () {
|
|||
await configureGas.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const gasModal = await driver.findElement(By.css('span .modal'))
|
||||
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
|
||||
await save.click()
|
||||
await driver.wait(until.stalenessOf(gasModal))
|
||||
await delay(regularDelayMs)
|
||||
|
||||
// Continue to next screen
|
||||
|
@ -303,15 +318,8 @@ describe('Using MetaMask with an existing account', function () {
|
|||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const addTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Tokens')]`))
|
||||
await addTokens.click()
|
||||
await delay(largeDelayMs)
|
||||
})
|
||||
|
||||
it('renders the balance for the new token', async () => {
|
||||
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
|
||||
const tokenAmount = await balance.getText()
|
||||
assert.equal(tokenAmount, '0BAT')
|
||||
const [importAccount] = await findElements(driver, By.xpath(`//div[contains(text(), 'Import Account')]`))
|
||||
await importAccount.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
})
|
||||
|
@ -343,8 +351,9 @@ describe('Using MetaMask with an existing account', function () {
|
|||
await driver.get(extensionUri)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
await confirmButton.click()
|
||||
it('enter private key', async () => {
|
||||
const privateKeyInput = await findElement(driver, By.css('#private-key-box'))
|
||||
await privateKeyInput.sendKeys(testPrivateKey2)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
await driver.switchTo().window(tokenFactory)
|
||||
|
@ -357,36 +366,17 @@ describe('Using MetaMask with an existing account', function () {
|
|||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('clicks on the Add Token button', async () => {
|
||||
const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`))
|
||||
await addToken.click()
|
||||
it('should show the correct account name', async () => {
|
||||
const [accountName] = await findElements(driver, By.css('.account-name'))
|
||||
assert.equal(await accountName.getText(), 'Account 3')
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('picks the new Test token', async () => {
|
||||
const addCustomToken = await findElement(driver, By.xpath("//div[contains(text(), 'Custom Token')]"))
|
||||
await addCustomToken.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const newTokenAddress = await findElement(driver, By.css('#custom-address'))
|
||||
await newTokenAddress.sendKeys(tokenAddress)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const addTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Tokens')]`))
|
||||
await addTokens.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('renders the balance for the new token', async () => {
|
||||
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
|
||||
await driver.wait(until.elementTextIs(balance, '100TST'))
|
||||
const tokenAmount = await balance.getText()
|
||||
assert.equal(tokenAmount, '100TST')
|
||||
it('should show the imported label', async () => {
|
||||
const [importedLabel] = await findElements(driver, By.css('.wallet-view__keyring-label'))
|
||||
assert.equal(await importedLabel.getText(), 'IMPORTED')
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
const { delay } = require('../func')
|
||||
const { until } = require('selenium-webdriver')
|
||||
|
||||
module.exports = {
|
||||
checkBrowserForConsoleErrors,
|
||||
findElement,
|
||||
findElements,
|
||||
openNewPage,
|
||||
}
|
||||
|
||||
async function checkBrowserForConsoleErrors (driver) {
|
||||
|
@ -31,3 +33,15 @@ async function findElement (driver, by, timeout = 10000) {
|
|||
async function findElements (driver, by, timeout = 10000) {
|
||||
return driver.wait(until.elementsLocated(by), timeout)
|
||||
}
|
||||
|
||||
async function openNewPage (driver, url) {
|
||||
await driver.executeScript('window.open()')
|
||||
await delay(1000)
|
||||
|
||||
const handles = await driver.getAllWindowHandles()
|
||||
const secondHandle = handles[1]
|
||||
await driver.switchTo().window(secondHandle)
|
||||
|
||||
await driver.get(url)
|
||||
await delay(1000)
|
||||
}
|
||||
|
|
|
@ -12,6 +12,9 @@ const {
|
|||
findElement,
|
||||
findElements,
|
||||
checkBrowserForConsoleErrors,
|
||||
loadExtension,
|
||||
verboseReportOnFailure,
|
||||
openNewPage,
|
||||
} = require('./helpers')
|
||||
|
||||
describe('MetaMask', function () {
|
||||
|
@ -50,8 +53,7 @@ describe('MetaMask', function () {
|
|||
}
|
||||
}
|
||||
if (this.currentTest.state === 'failed') {
|
||||
await verboseReportOnFailure({ browser, driver, title: this.currentTest.title })
|
||||
await delay(1000000)
|
||||
await verboseReportOnFailure(driver, this.currentTest)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -60,10 +62,26 @@ describe('MetaMask', function () {
|
|||
})
|
||||
|
||||
describe('New UI setup', async function () {
|
||||
let networkSelector
|
||||
it('switches to first tab', async function () {
|
||||
const [firstTab] = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(firstTab)
|
||||
await delay(regularDelayMs)
|
||||
try {
|
||||
networkSelector = await findElement(driver, By.css('#network_component'))
|
||||
} catch (e) {
|
||||
await loadExtension(driver, extensionId)
|
||||
}
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('use the local network', async function () {
|
||||
await networkSelector.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const localhost = await findElement(driver, By.xpath(`//li[contains(text(), 'Localhost')]`))
|
||||
await localhost.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('selects the new UI option', async () => {
|
||||
|
@ -72,15 +90,21 @@ describe('MetaMask', function () {
|
|||
await delay(regularDelayMs)
|
||||
|
||||
// Close all other tabs
|
||||
let [oldUi, infoPage, newUi] = await driver.getAllWindowHandles()
|
||||
newUi = newUi || infoPage
|
||||
let [oldUi, tab1, tab2] = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(oldUi)
|
||||
await driver.close()
|
||||
if (infoPage !== newUi) {
|
||||
await driver.switchTo().window(infoPage)
|
||||
|
||||
await driver.switchTo().window(tab1)
|
||||
const tab1Url = await driver.getCurrentUrl()
|
||||
if (tab1Url.match(/metamask.io/)) {
|
||||
await driver.switchTo().window(tab1)
|
||||
await driver.close()
|
||||
await driver.switchTo().window(tab2)
|
||||
} else if (tab2) {
|
||||
await driver.switchTo().window(tab2)
|
||||
await driver.close()
|
||||
await driver.switchTo().window(tab1)
|
||||
}
|
||||
await driver.switchTo().window(newUi)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const continueBtn = await findElement(driver, By.css('.welcome-screen__button'))
|
||||
|
@ -107,27 +131,43 @@ describe('MetaMask', function () {
|
|||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('clicks through the privacy notice', async () => {
|
||||
const nextScreen = await findElement(driver, By.css('.tou button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
it('clicks through the ToS', async () => {
|
||||
// terms of use
|
||||
const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled()
|
||||
assert.equal(canClickThrough, false, 'disabled continue button')
|
||||
const bottomOfTos = await findElement(driver, By.linkText('Attributions'))
|
||||
await driver.executeScript('arguments[0].scrollIntoView(true)', bottomOfTos)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const acceptTos = await findElement(driver, By.css('.tou button'))
|
||||
driver.wait(until.elementIsEnabled(acceptTos))
|
||||
await acceptTos.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('clicks through the privacy notice', async () => {
|
||||
// privacy notice
|
||||
const nextScreen = await findElement(driver, By.css('.tou button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('clicks through the phishing notice', async () => {
|
||||
// phishing notice
|
||||
const noticeElement = await driver.findElement(By.css('.markdown'))
|
||||
await driver.executeScript('arguments[0].scrollTop = arguments[0].scrollHeight', noticeElement)
|
||||
await delay(regularDelayMs)
|
||||
const nextScreen = await findElement(driver, By.css('.tou button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
let seedPhrase
|
||||
|
||||
it('reveals the seed phrase', async () => {
|
||||
const revealSeedPhrase = await findElement(driver, By.css('.backup-phrase__secret-blocker'))
|
||||
await revealSeedPhrase.click()
|
||||
const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button')
|
||||
await driver.wait(until.elementLocated(byRevealButton, 10000))
|
||||
const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000)
|
||||
await revealSeedPhraseButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
seedPhrase = await driver.findElement(By.css('.backup-phrase__secret-words')).getText()
|
||||
|
@ -139,56 +179,76 @@ describe('MetaMask', function () {
|
|||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
async function retypeSeedPhrase (words) {
|
||||
try {
|
||||
const word0 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[0]}')]`), 10000)
|
||||
|
||||
await word0.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word1 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[1]}')]`), 10000)
|
||||
|
||||
await word1.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word2 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[2]}')]`), 10000)
|
||||
|
||||
await word2.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word3 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[3]}')]`), 10000)
|
||||
|
||||
await word3.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word4 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[4]}')]`), 10000)
|
||||
|
||||
await word4.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word5 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[5]}')]`), 10000)
|
||||
|
||||
await word5.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word6 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[6]}')]`), 10000)
|
||||
|
||||
await word6.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word7 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[7]}')]`), 10000)
|
||||
|
||||
await word7.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word8 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[8]}')]`), 10000)
|
||||
|
||||
await word8.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word9 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[9]}')]`), 10000)
|
||||
|
||||
await word9.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word10 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[10]}')]`), 10000)
|
||||
|
||||
await word10.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word11 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[11]}')]`), 10000)
|
||||
await word11.click()
|
||||
await delay(tinyDelayMs)
|
||||
} catch (e) {
|
||||
await loadExtension(driver, extensionId)
|
||||
await retypeSeedPhrase(words)
|
||||
}
|
||||
}
|
||||
|
||||
it('can retype the seed phrase', async () => {
|
||||
const words = seedPhrase.split(' ')
|
||||
|
||||
const word0 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[0]}')]`))
|
||||
await word0.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word1 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[1]}')]`))
|
||||
await word1.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word2 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[2]}')]`))
|
||||
await word2.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word3 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[3]}')]`))
|
||||
await word3.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word4 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[4]}')]`))
|
||||
await word4.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word5 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[5]}')]`))
|
||||
await word5.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word6 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[6]}')]`))
|
||||
await word6.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word7 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[7]}')]`))
|
||||
await word7.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word8 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[8]}')]`))
|
||||
await word8.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word9 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[9]}')]`))
|
||||
await word9.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word10 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[10]}')]`))
|
||||
await word10.click()
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const word11 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[11]}')]`))
|
||||
await word11.click()
|
||||
await delay(tinyDelayMs)
|
||||
await retypeSeedPhrase(words)
|
||||
|
||||
const confirm = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
await confirm.click()
|
||||
|
@ -196,7 +256,8 @@ describe('MetaMask', function () {
|
|||
})
|
||||
|
||||
it('clicks through the deposit modal', async () => {
|
||||
const buyModal = await driver.findElement(By.css('span .modal'))
|
||||
const byBuyModal = By.css('span .modal')
|
||||
const buyModal = await driver.wait(until.elementLocated(byBuyModal))
|
||||
const closeModal = await findElement(driver, By.css('.page-container__header-close'))
|
||||
await closeModal.click()
|
||||
await driver.wait(until.stalenessOf(buyModal))
|
||||
|
@ -210,8 +271,12 @@ describe('MetaMask', function () {
|
|||
await driver.findElement(By.css('.qr-wrapper')).isDisplayed()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
let accountModal = await driver.findElement(By.css('span .modal'))
|
||||
|
||||
await driver.executeScript("document.querySelector('.account-modal-close').click()")
|
||||
await delay(regularDelayMs * 4)
|
||||
|
||||
await driver.wait(until.stalenessOf(accountModal))
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -311,8 +376,11 @@ describe('MetaMask', function () {
|
|||
await configureGas.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const gasModal = await driver.findElement(By.css('span .modal'))
|
||||
|
||||
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
|
||||
await save.click()
|
||||
await driver.wait(until.stalenessOf(gasModal))
|
||||
await delay(regularDelayMs)
|
||||
|
||||
// Continue to next screen
|
||||
|
@ -331,19 +399,20 @@ describe('MetaMask', function () {
|
|||
const transactions = await findElements(driver, By.css('.tx-list-item'))
|
||||
assert.equal(transactions.length, 1)
|
||||
|
||||
const txValues = await findElements(driver, By.css('.tx-list-value'))
|
||||
assert.equal(txValues.length, 1)
|
||||
assert.equal(await txValues[0].getText(), '1 ETH')
|
||||
const txValues = await findElement(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /1\sETH/), 10000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Send ETH from Faucet', () => {
|
||||
it('starts a send transaction inside Faucet', async () => {
|
||||
await driver.executeScript('window.open("https://faucet.metamask.io")')
|
||||
await delay(waitingNewPageDelayMs)
|
||||
await openNewPage(driver, 'https://faucet.metamask.io')
|
||||
|
||||
const [extension, faucet] = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(faucet)
|
||||
|
||||
const faucetPageTitle = await findElement(driver, By.css('.container-fluid'))
|
||||
await driver.wait(until.elementTextMatches(faucetPageTitle, /MetaMask/))
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const send1eth = await findElement(driver, By.xpath(`//button[contains(text(), '10 ether')]`), 14000)
|
||||
|
@ -369,6 +438,380 @@ describe('MetaMask', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Deploy contract and call contract methods', () => {
|
||||
let extension
|
||||
let contractTestPage
|
||||
it('confirms a deploy contract transaction', async () => {
|
||||
await openNewPage(driver, 'http://127.0.0.1:8080/');
|
||||
|
||||
[extension, contractTestPage] = await driver.getAllWindowHandles()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const deployContractButton = await findElement(driver, By.css('#deployButton'))
|
||||
await deployContractButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
await driver.switchTo().window(extension)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const txListItem = await findElement(driver, By.css('.tx-list-item'))
|
||||
await txListItem.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
|
||||
|
||||
const txAccounts = await findElements(driver, By.css('.tx-list-account'))
|
||||
assert.equal(await txAccounts[0].getText(), 'Contract Deployment')
|
||||
})
|
||||
|
||||
it('calls and confirms a contract method where ETH is sent', async () => {
|
||||
await driver.switchTo().window(contractTestPage)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const depositButton = await findElement(driver, By.css('#depositButton'))
|
||||
await depositButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
await driver.switchTo().window(extension)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const txListItem = await findElement(driver, By.css('.tx-list-item'))
|
||||
await txListItem.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
// Set the gas limit
|
||||
const configureGas = await findElement(driver, By.css('.sliders-icon-container'))
|
||||
await configureGas.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
let gasModal = await driver.findElement(By.css('span .modal'))
|
||||
await driver.wait(until.elementLocated(By.css('.send-v2__customize-gas__title')))
|
||||
|
||||
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input'))
|
||||
await gasPriceInput.clear()
|
||||
await gasPriceInput.sendKeys('10')
|
||||
await gasLimitInput.clear()
|
||||
await gasLimitInput.sendKeys('60001')
|
||||
|
||||
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
|
||||
await save.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
await driver.wait(until.stalenessOf(gasModal))
|
||||
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
|
||||
|
||||
const txValues = await findElement(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /3\sETH/), 10000)
|
||||
|
||||
const txAccounts = await findElements(driver, By.css('.tx-list-account'))
|
||||
const firstTxAddress = await txAccounts[0].getText()
|
||||
assert(firstTxAddress.match(/^0x\w{8}\.{3}\w{4}$/))
|
||||
})
|
||||
|
||||
it('calls and confirms a contract method where ETH is received', async () => {
|
||||
await driver.switchTo().window(contractTestPage)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const withdrawButton = await findElement(driver, By.css('#withdrawButton'))
|
||||
await withdrawButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
await driver.switchTo().window(extension)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const txListItem = await findElement(driver, By.css('.tx-list-item'))
|
||||
await txListItem.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
|
||||
|
||||
const txValues = await findElement(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /0\sETH/), 10000)
|
||||
|
||||
await driver.switchTo().window(contractTestPage)
|
||||
await driver.close()
|
||||
await driver.switchTo().window(extension)
|
||||
})
|
||||
|
||||
it('renders the correct ETH balance', async () => {
|
||||
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
|
||||
await driver.wait(until.elementTextMatches(balance, /^86.*ETH.*$/), 10000)
|
||||
const tokenAmount = await balance.getText()
|
||||
assert.ok(/^86.*ETH.*$/.test(tokenAmount))
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Add a custom token from TokenFactory', () => {
|
||||
it('creates a new token', async () => {
|
||||
openNewPage(driver, 'https://tokenfactory.surge.sh/#/factory')
|
||||
|
||||
await delay(regularDelayMs * 10)
|
||||
const [extension, tokenFactory] = await driver.getAllWindowHandles()
|
||||
|
||||
const [
|
||||
totalSupply,
|
||||
tokenName,
|
||||
tokenDecimal,
|
||||
tokenSymbol,
|
||||
] = await findElements(driver, By.css('.form-control'))
|
||||
|
||||
await totalSupply.sendKeys('100')
|
||||
await tokenName.sendKeys('Test')
|
||||
await tokenDecimal.sendKeys('0')
|
||||
await tokenSymbol.sendKeys('TST')
|
||||
|
||||
const createToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Create Token')]`))
|
||||
await createToken.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
await driver.switchTo().window(extension)
|
||||
await driver.get(extensionUri)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
await driver.switchTo().window(tokenFactory)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const tokenContactAddress = await driver.findElement(By.css('div > div > div:nth-child(2) > span:nth-child(3)'))
|
||||
tokenAddress = await tokenContactAddress.getText()
|
||||
|
||||
await driver.close()
|
||||
await driver.switchTo().window(extension)
|
||||
await loadExtension(driver, extensionId)
|
||||
await driver.switchTo().window(extension)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
})
|
||||
|
||||
it('clicks on the Add Token button', async () => {
|
||||
const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`))
|
||||
await addToken.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('picks the newly created Test token', async () => {
|
||||
const addCustomToken = await findElement(driver, By.xpath("//div[contains(text(), 'Custom Token')]"))
|
||||
await addCustomToken.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const newTokenAddress = await findElement(driver, By.css('#custom-address'))
|
||||
await newTokenAddress.sendKeys(tokenAddress)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const addTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Tokens')]`))
|
||||
await addTokens.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('renders the balance for the new token', async () => {
|
||||
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
|
||||
await driver.wait(until.elementTextMatches(balance, /^100\s*TST\s*$/))
|
||||
const tokenAmount = await balance.getText()
|
||||
assert.ok(/^100\s*TST\s*$/.test(tokenAmount))
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Send token from inside MetaMask', () => {
|
||||
let gasModal
|
||||
it('starts to send a transaction', async function () {
|
||||
const sendButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`))
|
||||
await sendButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const inputAddress = await findElement(driver, By.css('input[placeholder="Recipient Address"]'))
|
||||
const inputAmount = await findElement(driver, By.css('.currency-display__input'))
|
||||
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
|
||||
await inputAmount.sendKeys('50')
|
||||
|
||||
// Set the gas limit
|
||||
const configureGas = await findElement(driver, By.css('.send-v2__gas-fee-display button'))
|
||||
await configureGas.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
gasModal = await driver.findElement(By.css('span .modal'))
|
||||
})
|
||||
|
||||
it('customizes gas', async () => {
|
||||
await driver.wait(until.elementLocated(By.css('.send-v2__customize-gas__title')))
|
||||
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
|
||||
await save.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('transitions to the confirm screen', async () => {
|
||||
await driver.wait(until.stalenessOf(gasModal))
|
||||
|
||||
// Continue to next screen
|
||||
const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('submits the transaction', async function () {
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
const transactions = await findElements(driver, By.css('.tx-list-item'))
|
||||
assert.equal(transactions.length, 1)
|
||||
|
||||
const txValues = await findElements(driver, By.css('.tx-list-value'))
|
||||
assert.equal(txValues.length, 1)
|
||||
|
||||
// test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved,
|
||||
// or possibly until we use latest version of firefox in the tests
|
||||
if (process.env.SELENIUM_BROWSER !== 'firefox') {
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /50\sTST/), 10000)
|
||||
}
|
||||
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
const tx = await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed|Failed/), 10000)
|
||||
assert.equal(await tx.getText(), 'Confirmed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Send a custom token from TokenFactory', () => {
|
||||
let gasModal
|
||||
it('sends an already created token', async () => {
|
||||
openNewPage(driver, `https://tokenfactory.surge.sh/#/token/${tokenAddress}`)
|
||||
|
||||
const [extension, tokenFactory] = await driver.getAllWindowHandles()
|
||||
|
||||
const [
|
||||
transferToAddress,
|
||||
transferToAmount,
|
||||
] = await findElements(driver, By.css('.form-control'))
|
||||
|
||||
await transferToAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
|
||||
await transferToAmount.sendKeys('26')
|
||||
|
||||
const transferAmountButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Transfer Amount')]`))
|
||||
await transferAmountButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [,, popup] = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(popup)
|
||||
await driver.close()
|
||||
await driver.switchTo().window(extension)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [txListItem] = await findElements(driver, By.css('.tx-list-item'))
|
||||
await txListItem.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
// Set the gas limit
|
||||
const configureGas = await driver.wait(until.elementLocated(By.css('.send-v2__gas-fee-display button')))
|
||||
await configureGas.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
gasModal = await driver.findElement(By.css('span .modal'))
|
||||
})
|
||||
|
||||
it('customizes gas', async () => {
|
||||
await driver.wait(until.elementLocated(By.css('.send-v2__customize-gas__title')))
|
||||
|
||||
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input'))
|
||||
await gasPriceInput.clear()
|
||||
await delay(tinyDelayMs)
|
||||
await gasPriceInput.sendKeys('10')
|
||||
await delay(tinyDelayMs)
|
||||
await gasLimitInput.clear()
|
||||
await delay(tinyDelayMs)
|
||||
await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'a'))
|
||||
await gasLimitInput.sendKeys('60000')
|
||||
await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'e'))
|
||||
|
||||
// Needed for different behaviour of input in different versions of firefox
|
||||
const gasLimitInputValue = await gasLimitInput.getAttribute('value')
|
||||
if (gasLimitInputValue === '600001') {
|
||||
await gasLimitInput.sendKeys(Key.BACK_SPACE)
|
||||
}
|
||||
|
||||
const save = await findElement(driver, By.css('.send-v2__customize-gas__save'))
|
||||
await save.click()
|
||||
await driver.wait(until.stalenessOf(gasModal))
|
||||
|
||||
const gasFeeInput = await findElement(driver, By.css('.currency-display__input'))
|
||||
assert.equal(await gasFeeInput.getAttribute('value'), 0.0006)
|
||||
})
|
||||
|
||||
it('submits the transaction', async function () {
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
const transactions = await findElements(driver, By.css('.tx-list-item'))
|
||||
assert.equal(transactions.length, 2)
|
||||
|
||||
const txValues = await findElements(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /26\sTST/))
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
|
||||
|
||||
const walletBalance = await findElement(driver, By.css('.wallet-balance'))
|
||||
await walletBalance.click()
|
||||
|
||||
const tokenListItems = await findElements(driver, By.css('.token-list-item'))
|
||||
await tokenListItems[0].click()
|
||||
|
||||
// test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved,
|
||||
// or possibly until we use latest version of firefox in the tests
|
||||
if (process.env.SELENIUM_BROWSER !== 'firefox') {
|
||||
const tokenBalanceAmount = await findElement(driver, By.css('.token-balance__amount'))
|
||||
assert.equal(await tokenBalanceAmount.getText(), '24')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Hide token', () => {
|
||||
it('hides the token when clicked', async () => {
|
||||
const [hideTokenEllipsis] = await findElements(driver, By.css('.token-list-item__ellipsis'))
|
||||
await hideTokenEllipsis.click()
|
||||
|
||||
const byTokenMenuDropdownOption = By.css('.menu__item--clickable')
|
||||
const tokenMenuDropdownOption = await driver.wait(until.elementLocated(byTokenMenuDropdownOption))
|
||||
|
||||
await tokenMenuDropdownOption.click()
|
||||
|
||||
const confirmHideModal = await findElement(driver, By.css('span .modal'))
|
||||
|
||||
const byHideTokenConfirmationButton = By.css('.hide-token-confirmation__button')
|
||||
const hideTokenConfirmationButton = await driver.wait(until.elementLocated(byHideTokenConfirmationButton))
|
||||
await hideTokenConfirmationButton.click()
|
||||
|
||||
await driver.wait(until.stalenessOf(confirmHideModal))
|
||||
})
|
||||
})
|
||||
|
||||
describe('Add existing token using search', () => {
|
||||
it('clicks on the Add Token button', async () => {
|
||||
const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`))
|
||||
|
@ -396,83 +839,7 @@ describe('MetaMask', function () {
|
|||
|
||||
it('renders the balance for the chosen token', async () => {
|
||||
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
|
||||
await driver.wait(until.elementTextIs(balance, '0BAT'))
|
||||
const tokenAmount = await balance.getText()
|
||||
assert.equal(tokenAmount, '0BAT')
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Add a custom token from TokenFactory', () => {
|
||||
it('creates a new token', async () => {
|
||||
await driver.executeScript('window.open("https://tokenfactory.surge.sh/#/factory")')
|
||||
await delay(waitingNewPageDelayMs)
|
||||
|
||||
const [extension, tokenFactory] = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(tokenFactory)
|
||||
const [
|
||||
totalSupply,
|
||||
tokenName,
|
||||
tokenDecimal,
|
||||
tokenSymbol,
|
||||
] = await findElements(driver, By.css('.form-control'))
|
||||
|
||||
await totalSupply.sendKeys('100')
|
||||
await tokenName.sendKeys('Test')
|
||||
await tokenDecimal.sendKeys('0')
|
||||
await tokenSymbol.sendKeys('TST')
|
||||
|
||||
const createToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Create Token')]`))
|
||||
await createToken.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
await driver.switchTo().window(extension)
|
||||
await driver.get(extensionUri)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
await driver.switchTo().window(tokenFactory)
|
||||
await delay(regularDelayMs)
|
||||
const tokenContactAddress = await driver.findElement(By.css('div > div > div:nth-child(2) > span:nth-child(3)'))
|
||||
tokenAddress = await tokenContactAddress.getText()
|
||||
await driver.close()
|
||||
await driver.switchTo().window(extension)
|
||||
await driver.get(extensionUri)
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('clicks on the Add Token button', async () => {
|
||||
const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`))
|
||||
await addToken.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('picks the newly created Test token', async () => {
|
||||
const addCustomToken = await findElement(driver, By.xpath("//div[contains(text(), 'Custom Token')]"))
|
||||
await addCustomToken.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const newTokenAddress = await findElement(driver, By.css('#custom-address'))
|
||||
await newTokenAddress.sendKeys(tokenAddress)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const addTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Tokens')]`))
|
||||
await addTokens.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('renders the balance for the new token', async () => {
|
||||
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
|
||||
await driver.wait(until.elementTextIs(balance, '100TST'))
|
||||
const tokenAmount = await balance.getText()
|
||||
assert.equal(tokenAmount, '100TST')
|
||||
await driver.wait(until.elementTextMatches(balance, /0\sBAT/))
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -6,5 +6,5 @@ set -o pipefail
|
|||
|
||||
export PATH="$PATH:./node_modules/.bin"
|
||||
|
||||
shell-parallel -s 'npm run ganache:start' -x 'sleep 5 && mocha test/e2e/beta/metamask-beta-ui.spec'
|
||||
shell-parallel -s 'npm run ganache:start' -x 'sleep 5 && mocha test/e2e/beta/from-import-beta-ui.spec'
|
||||
shell-parallel -s 'npm run ganache:start' -x 'sleep 5 && superstatic test/e2e/beta/contract-test/ --port 8080 --host 127.0.0.1' -x 'sleep 5 && mocha test/e2e/beta/metamask-beta-ui.spec'
|
||||
shell-parallel -s 'npm run ganache:start -- -d' -x 'sleep 5 && superstatic test/e2e/beta/contract-test/ --port 8080 --host 127.0.0.1' -x 'sleep 5 && mocha test/e2e/beta/from-import-beta-ui.spec'
|
||||
|
|
|
@ -57,7 +57,7 @@ async function setupBrowserAndExtension ({ browser, extPath }) {
|
|||
}
|
||||
|
||||
function buildChromeWebDriver (extPath) {
|
||||
const tmpProfile = path.join(os.tmpdir(), fs.mkdtempSync('mm-chrome-profile'));
|
||||
const tmpProfile = fs.mkdtempSync(path.join(os.tmpdir(), 'mm-chrome-profile'))
|
||||
return new webdriver.Builder()
|
||||
.withCapabilities({
|
||||
chromeOptions: {
|
||||
|
|
|
@ -59,13 +59,6 @@ describe('Metamask popup page', function () {
|
|||
it('matches MetaMask title', async () => {
|
||||
const title = await driver.getTitle()
|
||||
assert.equal(title, 'MetaMask', 'title matches MetaMask')
|
||||
})
|
||||
|
||||
it('shows privacy notice', async () => {
|
||||
await delay(300)
|
||||
const privacy = await driver.findElement(By.css('.terms-header')).getText()
|
||||
assert.equal(privacy, 'PRIVACY NOTICE', 'shows privacy notice')
|
||||
await driver.findElement(By.css('button')).click()
|
||||
await delay(300)
|
||||
})
|
||||
|
||||
|
@ -88,6 +81,24 @@ describe('Metamask popup page', function () {
|
|||
await button.click()
|
||||
})
|
||||
|
||||
it('shows privacy notice', async () => {
|
||||
const privacy = await driver.findElement(By.css('.terms-header')).getText()
|
||||
assert.equal(privacy, 'PRIVACY NOTICE', 'shows privacy notice')
|
||||
await driver.findElement(By.css('button')).click()
|
||||
await delay(300)
|
||||
})
|
||||
|
||||
it('shows phishing notice', async () => {
|
||||
await delay(300)
|
||||
const noticeHeader = await driver.findElement(By.css('.terms-header')).getText()
|
||||
assert.equal(noticeHeader, 'PHISHING WARNING', 'shows phishing warning')
|
||||
const element = await driver.findElement(By.css('.markdown'))
|
||||
await driver.executeScript('arguments[0].scrollTop = arguments[0].scrollHeight', element)
|
||||
await delay(300)
|
||||
await driver.findElement(By.css('button')).click()
|
||||
await delay(300)
|
||||
})
|
||||
|
||||
it('accepts password with length of eight', async () => {
|
||||
const passwordBox = await driver.findElement(By.id('password-box'))
|
||||
const passwordBoxConfirm = await driver.findElement(By.id('password-box-confirm'))
|
||||
|
@ -206,7 +217,11 @@ describe('Metamask popup page', function () {
|
|||
|
||||
it('confirms transaction', async function () {
|
||||
await delay(300)
|
||||
await driver.findElement(By.css('#pending-tx-form > div.flex-row.flex-space-around.conf-buttons > input')).click()
|
||||
const bySubmitButton = By.css('#pending-tx-form > div.flex-row.flex-space-around.conf-buttons > input')
|
||||
const submitButton = await driver.wait(until.elementLocated(bySubmitButton))
|
||||
|
||||
submitButton.click()
|
||||
|
||||
await delay(500)
|
||||
})
|
||||
|
||||
|
@ -246,7 +261,8 @@ describe('Metamask popup page', function () {
|
|||
it('confirms transaction in MetaMask popup', async function () {
|
||||
const windowHandles = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(windowHandles[windowHandles.length - 1])
|
||||
const metamaskSubmit = await driver.findElement(By.css('#pending-tx-form > div.flex-row.flex-space-around.conf-buttons > input'))
|
||||
const byMetamaskSubmit = By.css('#pending-tx-form > div.flex-row.flex-space-around.conf-buttons > input')
|
||||
const metamaskSubmit = await driver.wait(until.elementLocated(byMetamaskSubmit))
|
||||
await metamaskSubmit.click()
|
||||
await delay(1000)
|
||||
})
|
||||
|
|
|
@ -117,12 +117,12 @@ async function runSendFlowTest(assert, done) {
|
|||
const sendGasField = await queryAsync($, '.send-v2__gas-fee-display')
|
||||
assert.equal(
|
||||
sendGasField.find('.currency-display__input-wrapper > input').val(),
|
||||
'0.000198264',
|
||||
'0.000021',
|
||||
'send gas field should show estimated gas total'
|
||||
)
|
||||
assert.equal(
|
||||
sendGasField.find('.currency-display__converted-value')[0].textContent,
|
||||
'$0.24 USD',
|
||||
'$0.03 USD',
|
||||
'send gas field should show estimated gas total converted to USD'
|
||||
)
|
||||
|
||||
|
|
|
@ -1,31 +1,59 @@
|
|||
const assert = require('assert')
|
||||
const path = require('path')
|
||||
const accountImporter = require('../../../app/scripts/account-import-strategies/index')
|
||||
const ethUtil = require('ethereumjs-util')
|
||||
const accountImporter = require('../../../app/scripts/account-import-strategies/index')
|
||||
const { assertRejects } = require('../test-utils')
|
||||
|
||||
describe('Account Import Strategies', function () {
|
||||
const privkey = '0x4cfd3e90fc78b0f86bf7524722150bb8da9c60cd532564d7ff43f5716514f553'
|
||||
const json = '{"version":3,"id":"dbb54385-0a99-437f-83c0-647de9f244c3","address":"a7f92ce3fba24196cf6f4bd2e1eb3db282ba998c","Crypto":{"ciphertext":"bde13d9ade5c82df80281ca363320ce254a8a3a06535bbf6ffdeaf0726b1312c","cipherparams":{"iv":"fbf93718a57f26051b292f072f2e5b41"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"7ffe00488319dec48e4c49a120ca49c6afbde9272854c64d9541c83fc6acdffe","n":8192,"r":8,"p":1},"mac":"2adfd9c4bc1cdac4c85bddfb31d9e21a684e0e050247a70c5698facf6b7d4681"}}'
|
||||
|
||||
it('imports a private key and strips 0x prefix', async function () {
|
||||
const importPrivKey = await accountImporter.importAccount('Private Key', [ privkey ])
|
||||
assert.equal(importPrivKey, ethUtil.stripHexPrefix(privkey))
|
||||
describe('private key import', function () {
|
||||
it('imports a private key and strips 0x prefix', async function () {
|
||||
const importPrivKey = await accountImporter.importAccount('Private Key', [ privkey ])
|
||||
assert.equal(importPrivKey, ethUtil.stripHexPrefix(privkey))
|
||||
})
|
||||
|
||||
it('throws an error for empty string private key', async () => {
|
||||
assertRejects(async function() {
|
||||
await accountImporter.importAccount('Private Key', [ '' ])
|
||||
}, Error, 'no empty strings')
|
||||
})
|
||||
|
||||
it('throws an error for undefined string private key', async () => {
|
||||
assertRejects(async function () {
|
||||
await accountImporter.importAccount('Private Key', [ undefined ])
|
||||
})
|
||||
})
|
||||
|
||||
it('throws an error for undefined string private key', async () => {
|
||||
assertRejects(async function () {
|
||||
await accountImporter.importAccount('Private Key', [])
|
||||
})
|
||||
})
|
||||
|
||||
it('throws an error for invalid private key', async () => {
|
||||
assertRejects(async function () {
|
||||
await accountImporter.importAccount('Private Key', [ 'popcorn' ])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('fails when password is incorrect for keystore', async function () {
|
||||
const wrongPassword = 'password2'
|
||||
describe('JSON keystore import', function () {
|
||||
it('fails when password is incorrect for keystore', async function () {
|
||||
const wrongPassword = 'password2'
|
||||
|
||||
try {
|
||||
await accountImporter.importAccount('JSON File', [ json, wrongPassword])
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'Key derivation failed - possibly wrong passphrase')
|
||||
}
|
||||
try {
|
||||
await accountImporter.importAccount('JSON File', [ json, wrongPassword])
|
||||
} catch (error) {
|
||||
assert.equal(error.message, 'Key derivation failed - possibly wrong passphrase')
|
||||
}
|
||||
})
|
||||
|
||||
it('imports json string and password to return a private key', async function () {
|
||||
const fileContentsPassword = 'password1'
|
||||
const importJson = await accountImporter.importAccount('JSON File', [ json, fileContentsPassword])
|
||||
assert.equal(importJson, '0x5733876abe94146069ce8bcbabbde2677f2e35fa33e875e92041ed2ac87e5bc7')
|
||||
})
|
||||
})
|
||||
|
||||
it('imports json string and password to return a private key', async function () {
|
||||
const fileContentsPassword = 'password1'
|
||||
const importJson = await accountImporter.importAccount('JSON File', [ json, fileContentsPassword])
|
||||
assert.equal(importJson, '0x5733876abe94146069ce8bcbabbde2677f2e35fa33e875e92041ed2ac87e5bc7')
|
||||
})
|
||||
|
||||
})
|
||||
|
|
|
@ -14,18 +14,6 @@ describe('notice-controller', function () {
|
|||
})
|
||||
|
||||
describe('notices', function () {
|
||||
describe('#getNoticesList', function () {
|
||||
it('should return an empty array when new', function (done) {
|
||||
// const testList = [{
|
||||
// id: 0,
|
||||
// read: false,
|
||||
// title: 'Futuristic Notice',
|
||||
// }]
|
||||
var result = noticeController.getNoticesList()
|
||||
assert.equal(result.length, 0)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
describe('#setNoticesList', function () {
|
||||
it('should set data appropriately', function (done) {
|
||||
|
@ -41,36 +29,6 @@ describe('notice-controller', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('#updateNoticeslist', function () {
|
||||
it('should integrate the latest changes from the source', function (done) {
|
||||
var testList = [{
|
||||
id: 55,
|
||||
read: false,
|
||||
title: 'Futuristic Notice',
|
||||
}]
|
||||
noticeController.setNoticesList(testList)
|
||||
noticeController.updateNoticesList().then(() => {
|
||||
var newList = noticeController.getNoticesList()
|
||||
assert.ok(newList[0].id === 55)
|
||||
assert.ok(newList[1])
|
||||
done()
|
||||
})
|
||||
})
|
||||
it('should not overwrite any existing fields', function (done) {
|
||||
var testList = [{
|
||||
id: 0,
|
||||
read: false,
|
||||
title: 'Futuristic Notice',
|
||||
}]
|
||||
noticeController.setNoticesList(testList)
|
||||
var newList = noticeController.getNoticesList()
|
||||
assert.equal(newList[0].id, 0)
|
||||
assert.equal(newList[0].title, 'Futuristic Notice')
|
||||
assert.equal(newList.length, 1)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
describe('#markNoticeRead', function () {
|
||||
it('should mark a notice as read', function (done) {
|
||||
var testList = [{
|
||||
|
@ -86,7 +44,7 @@ describe('notice-controller', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('#getLatestUnreadNotice', function () {
|
||||
describe('#getNextUnreadNotice', function () {
|
||||
it('should retrieve the latest unread notice', function (done) {
|
||||
var testList = [
|
||||
{id: 0, read: true, title: 'Past Notice'},
|
||||
|
@ -94,8 +52,8 @@ describe('notice-controller', function () {
|
|||
{id: 2, read: false, title: 'Future Notice'},
|
||||
]
|
||||
noticeController.setNoticesList(testList)
|
||||
var latestUnread = noticeController.getLatestUnreadNotice()
|
||||
assert.equal(latestUnread.id, 2)
|
||||
var latestUnread = noticeController.getNextUnreadNotice()
|
||||
assert.equal(latestUnread.id, 1)
|
||||
done()
|
||||
})
|
||||
it('should return undefined if no unread notices exist.', function (done) {
|
||||
|
@ -105,7 +63,7 @@ describe('notice-controller', function () {
|
|||
{id: 2, read: true, title: 'Future Notice'},
|
||||
]
|
||||
noticeController.setNoticesList(testList)
|
||||
var latestUnread = noticeController.getLatestUnreadNotice()
|
||||
var latestUnread = noticeController.getNextUnreadNotice()
|
||||
assert.ok(!latestUnread)
|
||||
done()
|
||||
})
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
const assert = require('assert')
|
||||
|
||||
module.exports = {
|
||||
assertRejects,
|
||||
}
|
||||
|
||||
// assert.rejects added in node v10
|
||||
async function assertRejects (asyncFn, regExp) {
|
||||
let f = () => {}
|
||||
try {
|
||||
await asyncFn()
|
||||
} catch (error) {
|
||||
f = () => { throw error }
|
||||
} finally {
|
||||
assert.throws(f, regExp)
|
||||
}
|
||||
}
|
|
@ -175,6 +175,8 @@ var actions = {
|
|||
CLEAR_SEND: 'CLEAR_SEND',
|
||||
OPEN_FROM_DROPDOWN: 'OPEN_FROM_DROPDOWN',
|
||||
CLOSE_FROM_DROPDOWN: 'CLOSE_FROM_DROPDOWN',
|
||||
GAS_LOADING_STARTED: 'GAS_LOADING_STARTED',
|
||||
GAS_LOADING_FINISHED: 'GAS_LOADING_FINISHED',
|
||||
setGasLimit,
|
||||
setGasPrice,
|
||||
updateGasData,
|
||||
|
@ -190,6 +192,8 @@ var actions = {
|
|||
updateSendErrors,
|
||||
clearSend,
|
||||
setSelectedAddress,
|
||||
gasLoadingStarted,
|
||||
gasLoadingFinished,
|
||||
// app messages
|
||||
confirmSeedWords: confirmSeedWords,
|
||||
showAccountDetail: showAccountDetail,
|
||||
|
@ -740,8 +744,9 @@ function updateGasData ({
|
|||
to,
|
||||
value,
|
||||
}) {
|
||||
const estimatedGasPrice = estimateGasPriceFromRecentBlocks(recentBlocks)
|
||||
return (dispatch) => {
|
||||
dispatch(actions.gasLoadingStarted())
|
||||
const estimatedGasPrice = estimateGasPriceFromRecentBlocks(recentBlocks)
|
||||
return Promise.all([
|
||||
Promise.resolve(estimatedGasPrice),
|
||||
estimateGas({
|
||||
|
@ -762,14 +767,28 @@ function updateGasData ({
|
|||
.then((gasEstimate) => {
|
||||
dispatch(actions.setGasTotal(gasEstimate))
|
||||
dispatch(updateSendErrors({ gasLoadingError: null }))
|
||||
dispatch(actions.gasLoadingFinished())
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err)
|
||||
dispatch(updateSendErrors({ gasLoadingError: 'gasLoadingError' }))
|
||||
dispatch(actions.gasLoadingFinished())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function gasLoadingStarted () {
|
||||
return {
|
||||
type: actions.GAS_LOADING_STARTED,
|
||||
}
|
||||
}
|
||||
|
||||
function gasLoadingFinished () {
|
||||
return {
|
||||
type: actions.GAS_LOADING_FINISHED,
|
||||
}
|
||||
}
|
||||
|
||||
function updateSendTokenBalance ({
|
||||
selectedToken,
|
||||
tokenContract,
|
||||
|
|
|
@ -314,7 +314,7 @@ function mapStateToProps (state) {
|
|||
noActiveNotices,
|
||||
seedWords,
|
||||
unapprovedTxs,
|
||||
lastUnreadNotice,
|
||||
nextUnreadNotice,
|
||||
lostAccounts,
|
||||
unapprovedMsgCount,
|
||||
unapprovedPersonalMsgCount,
|
||||
|
@ -348,7 +348,7 @@ function mapStateToProps (state) {
|
|||
network: state.metamask.network,
|
||||
provider: state.metamask.provider,
|
||||
forgottenPassword: state.appState.forgottenPassword,
|
||||
lastUnreadNotice,
|
||||
nextUnreadNotice,
|
||||
lostAccounts,
|
||||
frequentRpcList: state.metamask.frequentRpcList || [],
|
||||
currentCurrency: state.metamask.currentCurrency,
|
||||
|
|
|
@ -33,6 +33,7 @@ const {
|
|||
const {
|
||||
getGasPrice,
|
||||
getGasLimit,
|
||||
getGasIsLoading,
|
||||
getForceGasMin,
|
||||
conversionRateSelector,
|
||||
getSendAmount,
|
||||
|
@ -51,6 +52,7 @@ function mapStateToProps (state) {
|
|||
return {
|
||||
gasPrice: getGasPrice(state),
|
||||
gasLimit: getGasLimit(state),
|
||||
gasIsLoading: getGasIsLoading(state),
|
||||
forceGasMin: getForceGasMin(state),
|
||||
conversionRate,
|
||||
amount: getSendAmount(state),
|
||||
|
@ -73,7 +75,7 @@ function mapDispatchToProps (dispatch) {
|
|||
}
|
||||
}
|
||||
|
||||
function getOriginalState (props) {
|
||||
function getFreshState (props) {
|
||||
const gasPrice = props.gasPrice || MIN_GAS_PRICE_DEC
|
||||
const gasLimit = props.gasLimit || MIN_GAS_LIMIT_DEC
|
||||
|
||||
|
@ -97,7 +99,11 @@ inherits(CustomizeGasModal, Component)
|
|||
function CustomizeGasModal (props) {
|
||||
Component.call(this)
|
||||
|
||||
this.state = getOriginalState(props)
|
||||
const originalState = getFreshState(props)
|
||||
this.state = {
|
||||
...originalState,
|
||||
originalState,
|
||||
}
|
||||
}
|
||||
|
||||
CustomizeGasModal.contextTypes = {
|
||||
|
@ -106,6 +112,36 @@ CustomizeGasModal.contextTypes = {
|
|||
|
||||
module.exports = connect(mapStateToProps, mapDispatchToProps)(CustomizeGasModal)
|
||||
|
||||
CustomizeGasModal.prototype.componentWillReceiveProps = function (nextProps) {
|
||||
const currentState = getFreshState(this.props)
|
||||
const {
|
||||
gasPrice: currentGasPrice,
|
||||
gasLimit: currentGasLimit,
|
||||
} = currentState
|
||||
const newState = getFreshState(nextProps)
|
||||
const {
|
||||
gasPrice: newGasPrice,
|
||||
gasLimit: newGasLimit,
|
||||
gasTotal: newGasTotal,
|
||||
} = newState
|
||||
const gasPriceChanged = currentGasPrice !== newGasPrice
|
||||
const gasLimitChanged = currentGasLimit !== newGasLimit
|
||||
|
||||
if (gasPriceChanged) {
|
||||
this.setState({
|
||||
gasPrice: newGasPrice,
|
||||
gasTotal: newGasTotal,
|
||||
priceSigZeros: '',
|
||||
priceSigDec: '',
|
||||
})
|
||||
}
|
||||
if (gasLimitChanged) {
|
||||
this.setState({ gasLimit: newGasLimit, gasTotal: newGasTotal })
|
||||
}
|
||||
if (gasLimitChanged || gasPriceChanged) {
|
||||
this.validate({ gasLimit: newGasLimit, gasTotal: newGasTotal })
|
||||
}
|
||||
}
|
||||
|
||||
CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
|
||||
const {
|
||||
|
@ -137,7 +173,7 @@ CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
|
|||
}
|
||||
|
||||
CustomizeGasModal.prototype.revert = function () {
|
||||
this.setState(getOriginalState(this.props))
|
||||
this.setState(this.state.originalState)
|
||||
}
|
||||
|
||||
CustomizeGasModal.prototype.validate = function ({ gasTotal, gasLimit }) {
|
||||
|
@ -233,7 +269,7 @@ CustomizeGasModal.prototype.convertAndSetGasPrice = function (newGasPrice) {
|
|||
}
|
||||
|
||||
CustomizeGasModal.prototype.render = function () {
|
||||
const { hideModal, forceGasMin } = this.props
|
||||
const { hideModal, forceGasMin, gasIsLoading } = this.props
|
||||
const { gasPrice, gasLimit, gasTotal, error, priceSigZeros, priceSigDec } = this.state
|
||||
|
||||
let convertedGasPrice = conversionUtil(gasPrice, {
|
||||
|
@ -266,7 +302,7 @@ CustomizeGasModal.prototype.render = function () {
|
|||
toNumericBase: 'dec',
|
||||
})
|
||||
|
||||
return h('div.send-v2__customize-gas', {}, [
|
||||
return !gasIsLoading && h('div.send-v2__customize-gas', {}, [
|
||||
h('div.send-v2__customize-gas__content', {
|
||||
}, [
|
||||
h('div.send-v2__customize-gas__header', {}, [
|
||||
|
@ -288,6 +324,7 @@ CustomizeGasModal.prototype.render = function () {
|
|||
onChange: value => this.convertAndSetGasPrice(value),
|
||||
title: this.context.t('gasPrice'),
|
||||
copy: this.context.t('gasPriceCalculation'),
|
||||
gasIsLoading,
|
||||
}),
|
||||
|
||||
h(GasModalCard, {
|
||||
|
@ -297,6 +334,7 @@ CustomizeGasModal.prototype.render = function () {
|
|||
onChange: value => this.convertAndSetGasLimit(value),
|
||||
title: this.context.t('gasLimit'),
|
||||
copy: this.context.t('gasLimitCalculation'),
|
||||
gasIsLoading,
|
||||
}),
|
||||
|
||||
]),
|
||||
|
|
|
@ -4,14 +4,21 @@ const h = require('react-hyperscript')
|
|||
const inherits = require('util').inherits
|
||||
const connect = require('react-redux').connect
|
||||
const actions = require('../../actions')
|
||||
|
||||
const genAccountLink = require('etherscan-link').createAccountLink
|
||||
const copyToClipboard = require('copy-to-clipboard')
|
||||
const { Menu, Item, CloseArea } = require('./components/menu')
|
||||
|
||||
TokenMenuDropdown.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
module.exports = connect(null, mapDispatchToProps)(TokenMenuDropdown)
|
||||
module.exports = connect(mapStateToProps, mapDispatchToProps)(TokenMenuDropdown)
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
network: state.metamask.network,
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
|
@ -37,22 +44,34 @@ TokenMenuDropdown.prototype.onClose = function (e) {
|
|||
TokenMenuDropdown.prototype.render = function () {
|
||||
const { showHideTokenConfirmationModal } = this.props
|
||||
|
||||
return h('div.token-menu-dropdown', {}, [
|
||||
h('div.token-menu-dropdown__close-area', {
|
||||
return h(Menu, { className: 'token-menu-dropdown', isShowing: true }, [
|
||||
h(CloseArea, {
|
||||
onClick: this.onClose,
|
||||
}),
|
||||
h('div.token-menu-dropdown__container', {}, [
|
||||
h('div.token-menu-dropdown__options', {}, [
|
||||
|
||||
h('div.token-menu-dropdown__option', {
|
||||
onClick: (e) => {
|
||||
e.stopPropagation()
|
||||
showHideTokenConfirmationModal(this.props.token)
|
||||
this.props.onClose()
|
||||
},
|
||||
}, this.context.t('hideToken')),
|
||||
|
||||
]),
|
||||
]),
|
||||
h(Item, {
|
||||
onClick: (e) => {
|
||||
e.stopPropagation()
|
||||
showHideTokenConfirmationModal(this.props.token)
|
||||
this.props.onClose()
|
||||
},
|
||||
text: this.context.t('hideToken'),
|
||||
}),
|
||||
h(Item, {
|
||||
onClick: (e) => {
|
||||
e.stopPropagation()
|
||||
copyToClipboard(this.props.token.address)
|
||||
this.props.onClose()
|
||||
},
|
||||
text: this.context.t('copyContractAddress'),
|
||||
}),
|
||||
h(Item, {
|
||||
onClick: (e) => {
|
||||
e.stopPropagation()
|
||||
const url = genAccountLink(this.props.token.address, this.props.network)
|
||||
global.platform.openWindow({ url })
|
||||
this.props.onClose()
|
||||
},
|
||||
text: this.context.t('viewOnEtherscan'),
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
|
|||
const connect = require('react-redux').connect
|
||||
const ToAutoComplete = require('./send/to-autocomplete')
|
||||
const log = require('loglevel')
|
||||
const { isValidENSAddress } = require('../util')
|
||||
|
||||
EnsInput.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
|
@ -25,31 +26,34 @@ function EnsInput () {
|
|||
Component.call(this)
|
||||
}
|
||||
|
||||
EnsInput.prototype.onChange = function (recipient) {
|
||||
const network = this.props.network
|
||||
const networkHasEnsSupport = getNetworkEnsSupport(network)
|
||||
|
||||
this.props.onChange({ toAddress: recipient })
|
||||
|
||||
if (!networkHasEnsSupport) return
|
||||
|
||||
if (recipient.match(ensRE) === null) {
|
||||
return this.setState({
|
||||
loadingEns: false,
|
||||
ensResolution: null,
|
||||
ensFailure: null,
|
||||
toError: null,
|
||||
})
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loadingEns: true,
|
||||
})
|
||||
this.checkName(recipient)
|
||||
}
|
||||
|
||||
EnsInput.prototype.render = function () {
|
||||
const props = this.props
|
||||
const opts = extend(props, {
|
||||
list: 'addresses',
|
||||
onChange: (recipient) => {
|
||||
const network = this.props.network
|
||||
const networkHasEnsSupport = getNetworkEnsSupport(network)
|
||||
|
||||
props.onChange(recipient)
|
||||
|
||||
if (!networkHasEnsSupport) return
|
||||
|
||||
if (recipient.match(ensRE) === null) {
|
||||
return this.setState({
|
||||
loadingEns: false,
|
||||
ensResolution: null,
|
||||
ensFailure: null,
|
||||
})
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loadingEns: true,
|
||||
})
|
||||
this.checkName(recipient)
|
||||
},
|
||||
onChange: this.onChange.bind(this),
|
||||
})
|
||||
return h('div', {
|
||||
style: { width: '100%', position: 'relative' },
|
||||
|
@ -85,17 +89,27 @@ EnsInput.prototype.lookupEnsName = function (recipient) {
|
|||
nickname: recipient.trim(),
|
||||
hoverText: address + '\n' + this.context.t('clickCopy'),
|
||||
ensFailure: false,
|
||||
toError: null,
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((reason) => {
|
||||
log.error(reason)
|
||||
return this.setState({
|
||||
const setStateObj = {
|
||||
loadingEns: false,
|
||||
ensResolution: ZERO_ADDRESS,
|
||||
ensResolution: recipient,
|
||||
ensFailure: true,
|
||||
hoverText: reason.message,
|
||||
})
|
||||
toError: null,
|
||||
}
|
||||
if (isValidENSAddress(recipient) && reason.message === 'ENS name not defined.') {
|
||||
setStateObj.hoverText = this.context.t('ensNameNotFound')
|
||||
setStateObj.toError = 'ensNameNotFound'
|
||||
setStateObj.ensFailure = false
|
||||
} else {
|
||||
log.error(reason)
|
||||
setStateObj.hoverText = reason.message
|
||||
}
|
||||
|
||||
return this.setState(setStateObj)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -105,9 +119,14 @@ EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) {
|
|||
// If an address is sent without a nickname, meaning not from ENS or from
|
||||
// the user's own accounts, a default of a one-space string is used.
|
||||
const nickname = state.nickname || ' '
|
||||
if (prevProps.network !== this.props.network) {
|
||||
const provider = global.ethereumProvider
|
||||
this.ens = new ENS({ provider, network: this.props.network })
|
||||
this.onChange(ensResolution)
|
||||
}
|
||||
if (prevState && ensResolution && this.props.onChange &&
|
||||
ensResolution !== prevState.ensResolution) {
|
||||
this.props.onChange(ensResolution, nickname)
|
||||
this.props.onChange({ toAddress: ensResolution, nickname, toError: state.toError })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,7 +143,9 @@ EnsInput.prototype.ensIcon = function (recipient) {
|
|||
}
|
||||
|
||||
EnsInput.prototype.ensIconContents = function (recipient) {
|
||||
const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: ZERO_ADDRESS}
|
||||
const { loadingEns, ensFailure, ensResolution, toError } = this.state || { ensResolution: ZERO_ADDRESS }
|
||||
|
||||
if (toError) return
|
||||
|
||||
if (loadingEns) {
|
||||
return h('img', {
|
||||
|
|
|
@ -36,6 +36,7 @@ IdenticonComponent.prototype.render = function () {
|
|||
key: 'identicon-' + address,
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexShrink: 0,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: diameter,
|
||||
|
|
|
@ -22,12 +22,16 @@ function isValidInput (text) {
|
|||
return re.test(text)
|
||||
}
|
||||
|
||||
function removeLeadingZeroes (str) {
|
||||
return str.replace(/^0*(?=\d)/, '')
|
||||
}
|
||||
|
||||
InputNumber.prototype.setValue = function (newValue) {
|
||||
newValue = removeLeadingZeroes(newValue)
|
||||
if (newValue && !isValidInput(newValue)) return
|
||||
const { fixed, min = -1, max = Infinity, onChange } = this.props
|
||||
|
||||
newValue = fixed ? newValue.toFixed(4) : newValue
|
||||
|
||||
const newValueGreaterThanMin = conversionGTE(
|
||||
{ value: newValue || '0', fromNumericBase: 'dec' },
|
||||
{ value: min, fromNumericBase: 'hex' },
|
||||
|
@ -47,7 +51,7 @@ InputNumber.prototype.setValue = function (newValue) {
|
|||
}
|
||||
|
||||
InputNumber.prototype.render = function () {
|
||||
const { unitLabel, step = 1, placeholder, value = 0 } = this.props
|
||||
const { unitLabel, step = 1, placeholder, value } = this.props
|
||||
|
||||
return h('div.customize-gas-input-wrapper', {}, [
|
||||
h('input', {
|
||||
|
@ -63,11 +67,11 @@ InputNumber.prototype.render = function () {
|
|||
h('span.gas-tooltip-input-detail', {}, [unitLabel]),
|
||||
h('div.gas-tooltip-input-arrows', {}, [
|
||||
h('i.fa.fa-angle-up', {
|
||||
onClick: () => this.setValue(addCurrencies(value, step)),
|
||||
onClick: () => this.setValue(addCurrencies(value, step, { toNumericBase: 'dec' })),
|
||||
}),
|
||||
h('i.fa.fa-angle-down', {
|
||||
style: { cursor: 'pointer' },
|
||||
onClick: () => this.setValue(subtractCurrencies(value, step)),
|
||||
onClick: () => this.setValue(subtractCurrencies(value, step, { toNumericBase: 'dec' })),
|
||||
}),
|
||||
]),
|
||||
])
|
||||
|
|
|
@ -86,9 +86,9 @@ class Home extends Component {
|
|||
// if (!props.noActiveNotices) {
|
||||
// log.debug('rendering notice screen for unread notices.')
|
||||
// return h(NoticeScreen, {
|
||||
// notice: props.lastUnreadNotice,
|
||||
// notice: props.nextUnreadNotice,
|
||||
// key: 'NoticeScreen',
|
||||
// onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)),
|
||||
// onConfirm: () => props.dispatch(actions.markNoticeRead(props.nextUnreadNotice)),
|
||||
// })
|
||||
// } else if (props.lostAccounts && props.lostAccounts.length > 0) {
|
||||
// log.debug('rendering notice screen for lost accounts view.')
|
||||
|
@ -279,7 +279,7 @@ function mapStateToProps (state) {
|
|||
noActiveNotices,
|
||||
seedWords,
|
||||
unapprovedTxs,
|
||||
lastUnreadNotice,
|
||||
nextUnreadNotice,
|
||||
lostAccounts,
|
||||
unapprovedMsgCount,
|
||||
unapprovedPersonalMsgCount,
|
||||
|
@ -313,7 +313,7 @@ function mapStateToProps (state) {
|
|||
network: state.metamask.network,
|
||||
provider: state.metamask.provider,
|
||||
forgottenPassword: state.appState.forgottenPassword,
|
||||
lastUnreadNotice,
|
||||
nextUnreadNotice,
|
||||
lostAccounts,
|
||||
frequentRpcList: state.metamask.frequentRpcList || [],
|
||||
currentCurrency: state.metamask.currentCurrency,
|
||||
|
|
|
@ -154,11 +154,11 @@ class Notice extends Component {
|
|||
|
||||
const mapStateToProps = state => {
|
||||
const { metamask } = state
|
||||
const { noActiveNotices, lastUnreadNotice, lostAccounts } = metamask
|
||||
const { noActiveNotices, nextUnreadNotice, lostAccounts } = metamask
|
||||
|
||||
return {
|
||||
noActiveNotices,
|
||||
lastUnreadNotice,
|
||||
nextUnreadNotice,
|
||||
lostAccounts,
|
||||
}
|
||||
}
|
||||
|
@ -171,21 +171,21 @@ Notice.propTypes = {
|
|||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
markNoticeRead: lastUnreadNotice => dispatch(actions.markNoticeRead(lastUnreadNotice)),
|
||||
markNoticeRead: nextUnreadNotice => dispatch(actions.markNoticeRead(nextUnreadNotice)),
|
||||
markAccountsFound: () => dispatch(actions.markAccountsFound()),
|
||||
}
|
||||
}
|
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
const { noActiveNotices, lastUnreadNotice, lostAccounts } = stateProps
|
||||
const { noActiveNotices, nextUnreadNotice, lostAccounts } = stateProps
|
||||
const { markNoticeRead, markAccountsFound } = dispatchProps
|
||||
|
||||
let notice
|
||||
let onConfirm
|
||||
|
||||
if (!noActiveNotices) {
|
||||
notice = lastUnreadNotice
|
||||
onConfirm = () => markNoticeRead(lastUnreadNotice)
|
||||
notice = nextUnreadNotice
|
||||
onConfirm = () => markNoticeRead(nextUnreadNotice)
|
||||
} else if (lostAccounts && lostAccounts.length > 0) {
|
||||
notice = generateLostAccountsNotice(lostAccounts)
|
||||
onConfirm = () => markAccountsFound()
|
||||
|
|
|
@ -20,7 +20,7 @@ const {
|
|||
calcGasTotal,
|
||||
isBalanceSufficient,
|
||||
} = require('../send_/send.utils')
|
||||
const GasFeeDisplay = require('../send/gas-fee-display-v2')
|
||||
const GasFeeDisplay = require('../send_/send-content/send-gas-row/gas-fee-display/gas-fee-display.component').default
|
||||
const SenderToRecipient = require('../sender-to-recipient')
|
||||
const NetworkDisplay = require('../network-display')
|
||||
const currencyFormatter = require('currency-formatter')
|
||||
|
@ -647,7 +647,7 @@ ConfirmSendEther.prototype.gatherTxMeta = function () {
|
|||
const state = this.state
|
||||
const txData = clone(state.txData) || clone(props.txData)
|
||||
|
||||
const { gasPrice: sendGasPrice, gas: sendGasLimit } = props.send
|
||||
const { gasPrice: sendGasPrice, gasLimit: sendGasLimit } = props.send
|
||||
const {
|
||||
lastGasPrice,
|
||||
txParams: {
|
||||
|
|
|
@ -11,7 +11,7 @@ abiDecoder.addABI(tokenAbi)
|
|||
const actions = require('../../actions')
|
||||
const clone = require('clone')
|
||||
const Identicon = require('../identicon')
|
||||
const GasFeeDisplay = require('../send/gas-fee-display-v2.js')
|
||||
const GasFeeDisplay = require('../send_/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js').default
|
||||
const NetworkDisplay = require('../network-display')
|
||||
const ethUtil = require('ethereumjs-util')
|
||||
const BN = ethUtil.BN
|
||||
|
@ -651,7 +651,7 @@ ConfirmSendToken.prototype.gatherTxMeta = function () {
|
|||
const state = this.state
|
||||
const txData = clone(state.txData) || clone(props.txData)
|
||||
|
||||
const { gasPrice: sendGasPrice, gas: sendGasLimit } = props.send
|
||||
const { gasPrice: sendGasPrice, gasLimit: sendGasLimit } = props.send
|
||||
const {
|
||||
lastGasPrice,
|
||||
txParams: {
|
||||
|
|
|
@ -57,6 +57,7 @@ CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversi
|
|||
return selectedToken
|
||||
? conversionUtil(ethUtil.addHexPrefix(value), {
|
||||
fromNumericBase: 'hex',
|
||||
toNumericBase: 'dec',
|
||||
toCurrency: symbol,
|
||||
conversionRate: multiplier,
|
||||
invertConversionRate: true,
|
||||
|
@ -91,8 +92,12 @@ CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValu
|
|||
: convertedValue
|
||||
}
|
||||
|
||||
function removeLeadingZeroes (str) {
|
||||
return str.replace(/^0*(?=\d)/, '')
|
||||
}
|
||||
|
||||
CurrencyDisplay.prototype.handleChange = function (newVal) {
|
||||
this.setState({ valueToRender: newVal })
|
||||
this.setState({ valueToRender: removeLeadingZeroes(newVal) })
|
||||
this.props.onChange(this.getAmount(newVal))
|
||||
}
|
||||
|
||||
|
@ -113,6 +118,7 @@ CurrencyDisplay.prototype.render = function () {
|
|||
readOnly = false,
|
||||
inError = false,
|
||||
onBlur,
|
||||
step,
|
||||
} = this.props
|
||||
const { valueToRender } = this.state
|
||||
|
||||
|
@ -147,6 +153,7 @@ CurrencyDisplay.prototype.render = function () {
|
|||
width: this.getInputWidth(valueToRender, readOnly),
|
||||
},
|
||||
min: 0,
|
||||
step,
|
||||
}),
|
||||
|
||||
h('span.currency-display__currency-symbol', primaryCurrency),
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
const Component = require('react').Component
|
||||
const PropTypes = require('prop-types')
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const CurrencyDisplay = require('./currency-display')
|
||||
const connect = require('react-redux').connect
|
||||
|
||||
GasFeeDisplay.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
module.exports = connect()(GasFeeDisplay)
|
||||
|
||||
|
||||
inherits(GasFeeDisplay, Component)
|
||||
function GasFeeDisplay () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
GasFeeDisplay.prototype.render = function () {
|
||||
const {
|
||||
conversionRate,
|
||||
gasTotal,
|
||||
onClick,
|
||||
primaryCurrency = 'ETH',
|
||||
convertedCurrency,
|
||||
gasLoadingError,
|
||||
} = this.props
|
||||
|
||||
return h('div.send-v2__gas-fee-display', [
|
||||
|
||||
gasTotal
|
||||
? h(CurrencyDisplay, {
|
||||
primaryCurrency,
|
||||
convertedCurrency,
|
||||
value: gasTotal,
|
||||
conversionRate,
|
||||
convertedPrefix: '$',
|
||||
readOnly: true,
|
||||
})
|
||||
: gasLoadingError
|
||||
? h('div.currency-display.currency-display--message', this.context.t('setGasPrice'))
|
||||
: h('div.currency-display', this.context.t('loading')),
|
||||
|
||||
h('button.sliders-icon-container', {
|
||||
onClick,
|
||||
disabled: !gasTotal && !gasLoadingError,
|
||||
}, [
|
||||
h('i.fa.fa-sliders.sliders-icon'),
|
||||
]),
|
||||
|
||||
])
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { connect } from 'react-redux'
|
||||
import {
|
||||
getConversionRate,
|
||||
getConvertedCurrency,
|
||||
getCurrentCurrency,
|
||||
} from '../send.selectors.js'
|
||||
import AccountListItem from './account-list-item.component'
|
||||
|
||||
|
@ -10,6 +10,6 @@ export default connect(mapStateToProps)(AccountListItem)
|
|||
function mapStateToProps (state) {
|
||||
return {
|
||||
conversionRate: getConversionRate(state),
|
||||
currentCurrency: getConvertedCurrency(state),
|
||||
currentCurrency: getCurrentCurrency(state),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ proxyquire('../account-list-item.container.js', {
|
|||
},
|
||||
'../send.selectors.js': {
|
||||
getConversionRate: (s) => `mockConversionRate:${s}`,
|
||||
getConvertedCurrency: (s) => `mockCurrentCurrency:${s}`,
|
||||
getCurrentCurrency: (s) => `mockCurrentCurrency:${s}`,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ export default class SendAmountRow extends Component {
|
|||
tokenBalance: PropTypes.string,
|
||||
updateSendAmount: PropTypes.func,
|
||||
updateSendAmountError: PropTypes.func,
|
||||
updateGas: PropTypes.func,
|
||||
}
|
||||
|
||||
validateAmount (amount) {
|
||||
|
@ -56,6 +57,14 @@ export default class SendAmountRow extends Component {
|
|||
updateSendAmount(amount)
|
||||
}
|
||||
|
||||
updateGas (amount) {
|
||||
const { selectedToken, updateGas } = this.props
|
||||
|
||||
if (selectedToken) {
|
||||
updateGas({ amount })
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
amount,
|
||||
|
@ -77,12 +86,16 @@ export default class SendAmountRow extends Component {
|
|||
<CurrencyDisplay
|
||||
conversionRate={amountConversionRate}
|
||||
convertedCurrency={convertedCurrency}
|
||||
onBlur={newAmount => this.updateAmount(newAmount)}
|
||||
onBlur={newAmount => {
|
||||
this.updateGas(newAmount)
|
||||
this.updateAmount(newAmount)
|
||||
}}
|
||||
onChange={newAmount => this.validateAmount(newAmount)}
|
||||
inError={inError}
|
||||
primaryCurrency={primaryCurrency || 'ETH'}
|
||||
selectedToken={selectedToken}
|
||||
value={amount || '0x0'}
|
||||
value={amount}
|
||||
step="any"
|
||||
/>
|
||||
</SendRowWrapper>
|
||||
)
|
||||
|
|
|
@ -2,7 +2,7 @@ import { connect } from 'react-redux'
|
|||
import {
|
||||
getAmountConversionRate,
|
||||
getConversionRate,
|
||||
getConvertedCurrency,
|
||||
getCurrentCurrency,
|
||||
getGasTotal,
|
||||
getPrimaryCurrency,
|
||||
getSelectedToken,
|
||||
|
@ -31,7 +31,7 @@ function mapStateToProps (state) {
|
|||
amountConversionRate: getAmountConversionRate(state),
|
||||
balance: getSendFromBalance(state),
|
||||
conversionRate: getConversionRate(state),
|
||||
convertedCurrency: getConvertedCurrency(state),
|
||||
convertedCurrency: getCurrentCurrency(state),
|
||||
gasTotal: getGasTotal(state),
|
||||
inError: sendAmountIsInError(state),
|
||||
primaryCurrency: getPrimaryCurrency(state),
|
||||
|
|
|
@ -12,10 +12,12 @@ const propsMethodSpies = {
|
|||
setMaxModeTo: sinon.spy(),
|
||||
updateSendAmount: sinon.spy(),
|
||||
updateSendAmountError: sinon.spy(),
|
||||
updateGas: sinon.spy(),
|
||||
}
|
||||
|
||||
sinon.spy(SendAmountRow.prototype, 'updateAmount')
|
||||
sinon.spy(SendAmountRow.prototype, 'validateAmount')
|
||||
sinon.spy(SendAmountRow.prototype, 'updateGas')
|
||||
|
||||
describe('SendAmountRow Component', function () {
|
||||
let wrapper
|
||||
|
@ -36,6 +38,7 @@ describe('SendAmountRow Component', function () {
|
|||
tokenBalance={'mockTokenBalance'}
|
||||
updateSendAmount={propsMethodSpies.updateSendAmount}
|
||||
updateSendAmountError={propsMethodSpies.updateSendAmountError}
|
||||
updateGas={propsMethodSpies.updateGas}
|
||||
/>, { context: { t: str => str + '_t' } })
|
||||
instance = wrapper.instance()
|
||||
})
|
||||
|
@ -139,8 +142,14 @@ describe('SendAmountRow Component', function () {
|
|||
assert.equal(primaryCurrency, 'mockPrimaryCurrency')
|
||||
assert.deepEqual(selectedToken, { address: 'mockTokenAddress' })
|
||||
assert.equal(value, 'mockAmount')
|
||||
assert.equal(SendAmountRow.prototype.updateGas.callCount, 0)
|
||||
assert.equal(SendAmountRow.prototype.updateAmount.callCount, 0)
|
||||
onBlur('mockNewAmount')
|
||||
assert.equal(SendAmountRow.prototype.updateGas.callCount, 1)
|
||||
assert.deepEqual(
|
||||
SendAmountRow.prototype.updateGas.getCall(0).args,
|
||||
['mockNewAmount']
|
||||
)
|
||||
assert.equal(SendAmountRow.prototype.updateAmount.callCount, 1)
|
||||
assert.deepEqual(
|
||||
SendAmountRow.prototype.updateAmount.getCall(0).args,
|
||||
|
|
|
@ -24,7 +24,7 @@ proxyquire('../send-amount-row.container.js', {
|
|||
'../../send.selectors': {
|
||||
getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`,
|
||||
getConversionRate: (s) => `mockConversionRate:${s}`,
|
||||
getConvertedCurrency: (s) => `mockConvertedCurrency:${s}`,
|
||||
getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`,
|
||||
getGasTotal: (s) => `mockGasTotal:${s}`,
|
||||
getPrimaryCurrency: (s) => `mockPrimaryCurrency:${s}`,
|
||||
getSelectedToken: (s) => `mockSelectedToken:${s}`,
|
||||
|
|
|
@ -18,7 +18,7 @@ export default class SendContent extends Component {
|
|||
<div className="send-v2__form">
|
||||
<SendFromRow />
|
||||
<SendToRow updateGas={(updateData) => this.props.updateGas(updateData)} />
|
||||
<SendAmountRow />
|
||||
<SendAmountRow updateGas={(updateData) => this.props.updateGas(updateData)} />
|
||||
<SendGasRow />
|
||||
</div>
|
||||
</PageContainerContent>
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import CurrencyDisplay from '../../../../send/currency-display'
|
||||
|
||||
|
||||
export default class GasFeeDisplay extends Component {
|
||||
|
||||
static propTypes = {
|
||||
conversionRate: PropTypes.number,
|
||||
primaryCurrency: PropTypes.string,
|
||||
convertedCurrency: PropTypes.string,
|
||||
gasLoadingError: PropTypes.bool,
|
||||
gasTotal: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
conversionRate,
|
||||
gasTotal,
|
||||
onClick,
|
||||
primaryCurrency = 'ETH',
|
||||
convertedCurrency,
|
||||
gasLoadingError,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<div className="send-v2__gas-fee-display">
|
||||
{gasTotal
|
||||
? <CurrencyDisplay
|
||||
primaryCurrency={primaryCurrency}
|
||||
convertedCurrency={convertedCurrency}
|
||||
value={gasTotal}
|
||||
conversionRate={conversionRate}
|
||||
gasLoadingError={gasLoadingError}
|
||||
convertedPrefix={'$'}
|
||||
readOnly
|
||||
/>
|
||||
: gasLoadingError
|
||||
? <div className="currency-display.currency-display--message">
|
||||
{this.context.t('setGasPrice')}
|
||||
</div>
|
||||
: <div className="currency-display">
|
||||
{this.context.t('loading')}
|
||||
</div>
|
||||
}
|
||||
<button
|
||||
className="sliders-icon-container"
|
||||
onClick={onClick}
|
||||
disabled={!gasTotal && !gasLoadingError}
|
||||
>
|
||||
<i className="fa fa-sliders sliders-icon" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
GasFeeDisplay.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { default } from './gas-fee-display.component'
|
|
@ -0,0 +1,55 @@
|
|||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import {shallow} from 'enzyme'
|
||||
import GasFeeDisplay from '../gas-fee-display.component'
|
||||
import CurrencyDisplay from '../../../../../send/currency-display'
|
||||
import sinon from 'sinon'
|
||||
|
||||
|
||||
const propsMethodSpies = {
|
||||
showCustomizeGasModal: sinon.spy(),
|
||||
}
|
||||
|
||||
describe('SendGasRow Component', function() {
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<GasFeeDisplay
|
||||
conversionRate={20}
|
||||
gasTotal={'mockGasTotal'}
|
||||
onClick={propsMethodSpies.showCustomizeGasModal}
|
||||
primaryCurrency={'mockPrimaryCurrency'}
|
||||
convertedCurrency={'mockConvertedCurrency'}
|
||||
/>, {context: {t: str => str + '_t'}})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
propsMethodSpies.showCustomizeGasModal.resetHistory()
|
||||
})
|
||||
|
||||
describe('render', () => {
|
||||
it('should render a CurrencyDisplay component', () => {
|
||||
assert.equal(wrapper.find(CurrencyDisplay).length, 1)
|
||||
})
|
||||
|
||||
it('should render the CurrencyDisplay with the correct props', () => {
|
||||
const {
|
||||
conversionRate,
|
||||
convertedCurrency,
|
||||
value,
|
||||
} = wrapper.find(CurrencyDisplay).props()
|
||||
assert.equal(conversionRate, 20)
|
||||
assert.equal(convertedCurrency, 'mockConvertedCurrency')
|
||||
assert.equal(value, 'mockGasTotal')
|
||||
})
|
||||
|
||||
it('should render the Button with the correct props', () => {
|
||||
const {
|
||||
onClick,
|
||||
} = wrapper.find('button').props()
|
||||
assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0)
|
||||
onClick()
|
||||
assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,7 +1,7 @@
|
|||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import SendRowWrapper from '../send-row-wrapper/'
|
||||
import GasFeeDisplay from '../../../send/gas-fee-display-v2'
|
||||
import GasFeeDisplay from './gas-fee-display/gas-fee-display.component'
|
||||
|
||||
export default class SendGasRow extends Component {
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { connect } from 'react-redux'
|
||||
import {
|
||||
getConversionRate,
|
||||
getConvertedCurrency,
|
||||
getCurrentCurrency,
|
||||
getGasTotal,
|
||||
} from '../../send.selectors.js'
|
||||
import { sendGasIsInError } from './send-gas-row.selectors.js'
|
||||
|
@ -13,7 +13,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(SendGasRow)
|
|||
function mapStateToProps (state) {
|
||||
return {
|
||||
conversionRate: getConversionRate(state),
|
||||
convertedCurrency: getConvertedCurrency(state),
|
||||
convertedCurrency: getCurrentCurrency(state),
|
||||
gasTotal: getGasTotal(state),
|
||||
gasLoadingError: sendGasIsInError(state),
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import sinon from 'sinon'
|
|||
import SendGasRow from '../send-gas-row.component.js'
|
||||
|
||||
import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component'
|
||||
import GasFeeDisplay from '../../../../send/gas-fee-display-v2'
|
||||
import GasFeeDisplay from '../gas-fee-display/gas-fee-display.component'
|
||||
|
||||
const propsMethodSpies = {
|
||||
showCustomizeGasModal: sinon.spy(),
|
||||
|
|
|
@ -19,7 +19,7 @@ proxyquire('../send-gas-row.container.js', {
|
|||
},
|
||||
'../../send.selectors.js': {
|
||||
getConversionRate: (s) => `mockConversionRate:${s}`,
|
||||
getConvertedCurrency: (s) => `mockConvertedCurrency:${s}`,
|
||||
getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`,
|
||||
getGasTotal: (s) => `mockGasTotal:${s}`,
|
||||
},
|
||||
'./send-gas-row.selectors.js': { sendGasIsInError: (s) => `mockGasLoadingError:${s}` },
|
||||
|
|
|
@ -19,9 +19,9 @@ export default class SendToRow extends Component {
|
|||
updateSendToError: PropTypes.func,
|
||||
};
|
||||
|
||||
handleToChange (to, nickname = '') {
|
||||
handleToChange (to, nickname = '', toError) {
|
||||
const { updateSendTo, updateSendToError, updateGas } = this.props
|
||||
const toErrorObject = getToErrorObject(to)
|
||||
const toErrorObject = getToErrorObject(to, toError)
|
||||
updateSendTo(to, nickname)
|
||||
updateSendToError(toErrorObject)
|
||||
if (toErrorObject.to === null) {
|
||||
|
@ -53,7 +53,7 @@ export default class SendToRow extends Component {
|
|||
inError={inError}
|
||||
name={'address'}
|
||||
network={network}
|
||||
onChange={(newTo, newNickname) => this.handleToChange(newTo, newNickname)}
|
||||
onChange={({ toAddress, nickname, toError }) => this.handleToChange(toAddress, nickname, toError)}
|
||||
openDropdown={() => openToDropdown()}
|
||||
placeholder={this.context.t('recipientAddress')}
|
||||
to={to}
|
||||
|
|
|
@ -4,12 +4,10 @@ const {
|
|||
} = require('../../send.constants')
|
||||
const { isValidAddress } = require('../../../../util')
|
||||
|
||||
function getToErrorObject (to) {
|
||||
let toError = null
|
||||
|
||||
function getToErrorObject (to, toError = null) {
|
||||
if (!to) {
|
||||
toError = REQUIRED_ERROR
|
||||
} else if (!isValidAddress(to)) {
|
||||
} else if (!isValidAddress(to) && !toError) {
|
||||
toError = INVALID_RECIPIENT_ADDRESS_ERROR
|
||||
}
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@ import proxyquire from 'proxyquire'
|
|||
|
||||
const SendToRow = proxyquire('../send-to-row.component.js', {
|
||||
'./send-to-row.utils.js': {
|
||||
getToErrorObject: (to) => ({
|
||||
to: to === false ? null : `mockToErrorObject:${to}`,
|
||||
getToErrorObject: (to, toError) => ({
|
||||
to: to === false ? null : `mockToErrorObject:${to}${toError}`,
|
||||
}),
|
||||
},
|
||||
}).default
|
||||
|
@ -67,11 +67,11 @@ describe('SendToRow Component', function () {
|
|||
|
||||
it('should call updateSendToError', () => {
|
||||
assert.equal(propsMethodSpies.updateSendToError.callCount, 0)
|
||||
instance.handleToChange('mockTo2')
|
||||
instance.handleToChange('mockTo2', '', 'mockToError')
|
||||
assert.equal(propsMethodSpies.updateSendToError.callCount, 1)
|
||||
assert.deepEqual(
|
||||
propsMethodSpies.updateSendToError.getCall(0).args,
|
||||
[{ to: 'mockToErrorObject:mockTo2' }]
|
||||
[{ to: 'mockToErrorObject:mockTo2mockToError' }]
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -138,11 +138,11 @@ describe('SendToRow Component', function () {
|
|||
openDropdown()
|
||||
assert.equal(propsMethodSpies.openToDropdown.callCount, 1)
|
||||
assert.equal(SendToRow.prototype.handleToChange.callCount, 0)
|
||||
onChange('mockNewTo', 'mockNewNickname')
|
||||
onChange({ toAddress: 'mockNewTo', nickname: 'mockNewNickname', toError: 'mockToError' })
|
||||
assert.equal(SendToRow.prototype.handleToChange.callCount, 1)
|
||||
assert.deepEqual(
|
||||
SendToRow.prototype.handleToChange.getCall(0).args,
|
||||
['mockNewTo', 'mockNewNickname']
|
||||
['mockNewTo', 'mockNewNickname', 'mockToError']
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -40,6 +40,12 @@ describe('send-to-row utils', () => {
|
|||
to: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('should return the passed error if to is truthy but invalid if to is truthy and valid', () => {
|
||||
assert.deepEqual(getToErrorObject('invalid #$ 345878', 'someExplicitError'), {
|
||||
to: 'someExplicitError',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
|
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
|
|||
import PersistentForm from '../../../lib/persistent-form'
|
||||
import {
|
||||
getAmountErrorObject,
|
||||
getToAddressForGasUpdate,
|
||||
doesAmountErrorRequireUpdate,
|
||||
} from './send.utils'
|
||||
|
||||
|
@ -38,7 +39,7 @@ export default class SendTransactionScreen extends PersistentForm {
|
|||
updateSendTokenBalance: PropTypes.func,
|
||||
};
|
||||
|
||||
updateGas ({ to } = {}) {
|
||||
updateGas ({ to: updatedToAddress, amount: value } = {}) {
|
||||
const {
|
||||
amount,
|
||||
blockGasLimit,
|
||||
|
@ -48,6 +49,7 @@ export default class SendTransactionScreen extends PersistentForm {
|
|||
recentBlocks,
|
||||
selectedAddress,
|
||||
selectedToken = {},
|
||||
to: currentToAddress,
|
||||
updateAndSetGasTotal,
|
||||
} = this.props
|
||||
|
||||
|
@ -59,8 +61,8 @@ export default class SendTransactionScreen extends PersistentForm {
|
|||
recentBlocks,
|
||||
selectedAddress,
|
||||
selectedToken,
|
||||
to: to && to.toLowerCase(),
|
||||
value: amount,
|
||||
to: getToAddressForGasUpdate(updatedToAddress, currentToAddress),
|
||||
value: value || amount,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ const ONE_GWEI_IN_WEI_HEX = ethUtil.addHexPrefix(conversionUtil('0x1', {
|
|||
}))
|
||||
|
||||
const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send.
|
||||
const BASE_TOKEN_GAS_COST = '0x186a0' // Hex for 100000, a base estimate for token transfers.
|
||||
|
||||
module.exports = {
|
||||
INSUFFICIENT_FUNDS_ERROR,
|
||||
|
@ -52,4 +53,5 @@ module.exports = {
|
|||
REQUIRED_ERROR,
|
||||
SIMPLE_GAS_COST,
|
||||
TOKEN_TRANSFER_FUNCTION_SIGNATURE,
|
||||
BASE_TOKEN_GAS_COST,
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
getSendAmount,
|
||||
getSendEditingTransactionId,
|
||||
getSendFromObject,
|
||||
getSendTo,
|
||||
getTokenBalance,
|
||||
} from './send.selectors'
|
||||
import {
|
||||
|
@ -54,6 +55,7 @@ function mapStateToProps (state) {
|
|||
recentBlocks: getRecentBlocks(state),
|
||||
selectedAddress: getSelectedAddress(state),
|
||||
selectedToken: getSelectedToken(state),
|
||||
to: getSendTo(state),
|
||||
tokenBalance: getTokenBalance(state),
|
||||
tokenContract: getSelectedTokenContract(state),
|
||||
tokenToFiatRate: getSelectedTokenToFiatRate(state),
|
||||
|
|
|
@ -14,7 +14,6 @@ const selectors = {
|
|||
getAmountConversionRate,
|
||||
getBlockGasLimit,
|
||||
getConversionRate,
|
||||
getConvertedCurrency,
|
||||
getCurrentAccountWithSendEtherInfo,
|
||||
getCurrentCurrency,
|
||||
getCurrentNetwork,
|
||||
|
@ -98,10 +97,6 @@ function getConversionRate (state) {
|
|||
return state.metamask.conversionRate
|
||||
}
|
||||
|
||||
function getConvertedCurrency (state) {
|
||||
return state.metamask.currentCurrency
|
||||
}
|
||||
|
||||
function getCurrentAccountWithSendEtherInfo (state) {
|
||||
const currentAddress = getSelectedAddress(state)
|
||||
const accounts = accountsWithSendEtherInfoSelector(state)
|
||||
|
|
|
@ -4,11 +4,13 @@ const {
|
|||
conversionGTE,
|
||||
multiplyCurrencies,
|
||||
conversionGreaterThan,
|
||||
conversionLessThan,
|
||||
} = require('../../conversion-util')
|
||||
const {
|
||||
calcTokenAmount,
|
||||
} = require('../../token-util')
|
||||
const {
|
||||
BASE_TOKEN_GAS_COST,
|
||||
INSUFFICIENT_FUNDS_ERROR,
|
||||
INSUFFICIENT_TOKENS_ERROR,
|
||||
NEGATIVE_ETH_ERROR,
|
||||
|
@ -20,6 +22,7 @@ const abi = require('ethereumjs-abi')
|
|||
const ethUtil = require('ethereumjs-util')
|
||||
|
||||
module.exports = {
|
||||
addGasBuffer,
|
||||
calcGasTotal,
|
||||
calcTokenBalance,
|
||||
doesAmountErrorRequireUpdate,
|
||||
|
@ -27,6 +30,7 @@ module.exports = {
|
|||
estimateGasPriceFromRecentBlocks,
|
||||
generateTokenTransferData,
|
||||
getAmountErrorObject,
|
||||
getToAddressForGasUpdate,
|
||||
isBalanceSufficient,
|
||||
isTokenBalanceSufficient,
|
||||
}
|
||||
|
@ -175,12 +179,13 @@ async function estimateGas ({ selectedAddress, selectedToken, blockGasLimit, to,
|
|||
}
|
||||
|
||||
// if recipient has no code, gas is 21k max:
|
||||
const hasRecipient = Boolean(to)
|
||||
if (hasRecipient && !selectedToken) {
|
||||
const code = await global.eth.getCode(to)
|
||||
if (!selectedToken) {
|
||||
const code = Boolean(to) && await global.eth.getCode(to)
|
||||
if (!code || code === '0x') {
|
||||
return SIMPLE_GAS_COST
|
||||
}
|
||||
} else if (selectedToken && !to) {
|
||||
return BASE_TOKEN_GAS_COST
|
||||
}
|
||||
|
||||
paramsForGasEstimate.to = selectedToken ? selectedToken.address : to
|
||||
|
@ -201,16 +206,46 @@ async function estimateGas ({ selectedAddress, selectedToken, blockGasLimit, to,
|
|||
err.message.includes('gas required exceeds allowance or always failing transaction')
|
||||
)
|
||||
if (simulationFailed) {
|
||||
return resolve(paramsForGasEstimate.gas)
|
||||
const estimateWithBuffer = addGasBuffer(paramsForGasEstimate.gas, blockGasLimit, 1.5)
|
||||
return resolve(ethUtil.addHexPrefix(estimateWithBuffer))
|
||||
} else {
|
||||
return reject(err)
|
||||
}
|
||||
}
|
||||
return resolve(estimatedGas.toString(16))
|
||||
const estimateWithBuffer = addGasBuffer(estimatedGas.toString(16), blockGasLimit, 1.5)
|
||||
return resolve(ethUtil.addHexPrefix(estimateWithBuffer))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function addGasBuffer (initialGasLimitHex, blockGasLimitHex, bufferMultiplier = 1.5) {
|
||||
const upperGasLimit = multiplyCurrencies(blockGasLimitHex, 0.9, {
|
||||
toNumericBase: 'hex',
|
||||
multiplicandBase: 16,
|
||||
multiplierBase: 10,
|
||||
numberOfDecimals: '0',
|
||||
})
|
||||
const bufferedGasLimit = multiplyCurrencies(initialGasLimitHex, bufferMultiplier, {
|
||||
toNumericBase: 'hex',
|
||||
multiplicandBase: 16,
|
||||
multiplierBase: 10,
|
||||
numberOfDecimals: '0',
|
||||
})
|
||||
|
||||
// if initialGasLimit is above blockGasLimit, dont modify it
|
||||
if (conversionGreaterThan(
|
||||
{ value: initialGasLimitHex, fromNumericBase: 'hex' },
|
||||
{ value: upperGasLimit, fromNumericBase: 'hex' },
|
||||
)) return initialGasLimitHex
|
||||
// if bufferedGasLimit is below blockGasLimit, use bufferedGasLimit
|
||||
if (conversionLessThan(
|
||||
{ value: bufferedGasLimit, fromNumericBase: 'hex' },
|
||||
{ value: upperGasLimit, fromNumericBase: 'hex' },
|
||||
)) return bufferedGasLimit
|
||||
// otherwise use blockGasLimit
|
||||
return upperGasLimit
|
||||
}
|
||||
|
||||
function generateTokenTransferData ({ toAddress = '0x0', amount = '0x0', selectedToken }) {
|
||||
if (!selectedToken) return
|
||||
return TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call(
|
||||
|
@ -237,3 +272,7 @@ function estimateGasPriceFromRecentBlocks (recentBlocks) {
|
|||
|
||||
return lowestPrices[Math.floor(lowestPrices.length / 2)]
|
||||
}
|
||||
|
||||
function getToAddressForGasUpdate (...addresses) {
|
||||
return [...addresses, ''].find(str => str !== undefined && str !== null).toLowerCase()
|
||||
}
|
||||
|
|
|
@ -201,7 +201,7 @@ describe('Send Component', function () {
|
|||
})
|
||||
|
||||
describe('updateGas', () => {
|
||||
it('should call updateAndSetGasTotal with the correct params', () => {
|
||||
it('should call updateAndSetGasTotal with the correct params if no to prop is passed', () => {
|
||||
propsMethodSpies.updateAndSetGasTotal.resetHistory()
|
||||
wrapper.instance().updateGas()
|
||||
assert.equal(propsMethodSpies.updateAndSetGasTotal.callCount, 1)
|
||||
|
@ -215,12 +215,22 @@ describe('Send Component', function () {
|
|||
recentBlocks: ['mockBlock'],
|
||||
selectedAddress: 'mockSelectedAddress',
|
||||
selectedToken: 'mockSelectedToken',
|
||||
to: undefined,
|
||||
to: '',
|
||||
value: 'mockAmount',
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should call updateAndSetGasTotal with the correct params if a to prop is passed', () => {
|
||||
propsMethodSpies.updateAndSetGasTotal.resetHistory()
|
||||
wrapper.setProps({ to: 'someAddress' })
|
||||
wrapper.instance().updateGas()
|
||||
assert.equal(
|
||||
propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0].to,
|
||||
'someaddress',
|
||||
)
|
||||
})
|
||||
|
||||
it('should call updateAndSetGasTotal with to set to lowercase if passed', () => {
|
||||
propsMethodSpies.updateAndSetGasTotal.resetHistory()
|
||||
wrapper.instance().updateGas({ to: '0xABC' })
|
||||
|
|
|
@ -39,6 +39,7 @@ proxyquire('../send.container.js', {
|
|||
getSelectedTokenContract: (s) => `mockTokenContract:${s}`,
|
||||
getSelectedTokenToFiatRate: (s) => `mockTokenToFiatRate:${s}`,
|
||||
getSendAmount: (s) => `mockAmount:${s}`,
|
||||
getSendTo: (s) => `mockTo:${s}`,
|
||||
getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`,
|
||||
getSendFromObject: (s) => `mockFrom:${s}`,
|
||||
getTokenBalance: (s) => `mockTokenBalance:${s}`,
|
||||
|
@ -70,6 +71,7 @@ describe('send container', () => {
|
|||
recentBlocks: 'mockRecentBlocks:mockState',
|
||||
selectedAddress: 'mockSelectedAddress:mockState',
|
||||
selectedToken: 'mockSelectedToken:mockState',
|
||||
to: 'mockTo:mockState',
|
||||
tokenBalance: 'mockTokenBalance:mockState',
|
||||
tokenContract: 'mockTokenContract:mockState',
|
||||
tokenToFiatRate: 'mockTokenToFiatRate:mockState',
|
||||
|
|
|
@ -8,7 +8,6 @@ const {
|
|||
getBlockGasLimit,
|
||||
getAmountConversionRate,
|
||||
getConversionRate,
|
||||
getConvertedCurrency,
|
||||
getCurrentAccountWithSendEtherInfo,
|
||||
getCurrentCurrency,
|
||||
getCurrentNetwork,
|
||||
|
@ -154,15 +153,6 @@ describe('send selectors', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('getConvertedCurrency()', () => {
|
||||
it('should return the currently selected currency', () => {
|
||||
assert.equal(
|
||||
getConvertedCurrency(mockState),
|
||||
'USD'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getCurrentAccountWithSendEtherInfo()', () => {
|
||||
it('should return the currently selected account with identity info', () => {
|
||||
assert.deepEqual(
|
||||
|
|
|
@ -2,6 +2,7 @@ import assert from 'assert'
|
|||
import sinon from 'sinon'
|
||||
import proxyquire from 'proxyquire'
|
||||
import {
|
||||
BASE_TOKEN_GAS_COST,
|
||||
ONE_GWEI_IN_WEI_HEX,
|
||||
SIMPLE_GAS_COST,
|
||||
} from '../send.constants'
|
||||
|
@ -18,10 +19,12 @@ const {
|
|||
const stubs = {
|
||||
addCurrencies: sinon.stub().callsFake((a, b, obj) => a + b),
|
||||
conversionUtil: sinon.stub().callsFake((val, obj) => parseInt(val, 16)),
|
||||
conversionGTE: sinon.stub().callsFake((obj1, obj2) => obj1.value > obj2.value),
|
||||
conversionGTE: sinon.stub().callsFake((obj1, obj2) => obj1.value >= obj2.value),
|
||||
multiplyCurrencies: sinon.stub().callsFake((a, b) => `${a}x${b}`),
|
||||
calcTokenAmount: sinon.stub().callsFake((a, d) => 'calc:' + a + d),
|
||||
rawEncode: sinon.stub().returns([16, 1100]),
|
||||
conversionGreaterThan: sinon.stub().callsFake((obj1, obj2) => obj1.value > obj2.value),
|
||||
conversionLessThan: sinon.stub().callsFake((obj1, obj2) => obj1.value < obj2.value),
|
||||
}
|
||||
|
||||
const sendUtils = proxyquire('../send.utils.js', {
|
||||
|
@ -30,6 +33,8 @@ const sendUtils = proxyquire('../send.utils.js', {
|
|||
conversionUtil: stubs.conversionUtil,
|
||||
conversionGTE: stubs.conversionGTE,
|
||||
multiplyCurrencies: stubs.multiplyCurrencies,
|
||||
conversionGreaterThan: stubs.conversionGreaterThan,
|
||||
conversionLessThan: stubs.conversionLessThan,
|
||||
},
|
||||
'../../token-util': { calcTokenAmount: stubs.calcTokenAmount },
|
||||
'ethereumjs-abi': {
|
||||
|
@ -44,6 +49,7 @@ const {
|
|||
estimateGasPriceFromRecentBlocks,
|
||||
generateTokenTransferData,
|
||||
getAmountErrorObject,
|
||||
getToAddressForGasUpdate,
|
||||
calcTokenBalance,
|
||||
isBalanceSufficient,
|
||||
isTokenBalanceSufficient,
|
||||
|
@ -255,7 +261,7 @@ describe('send utils', () => {
|
|||
estimateGasMethod: sinon.stub().callsFake(
|
||||
(data, cb) => cb(
|
||||
data.to.match(/willFailBecauseOf:/) ? { message: data.to.match(/:(.+)$/)[1] } : null,
|
||||
{ toString: (n) => `mockToString:${n}` }
|
||||
{ toString: (n) => `0xabc${n}` }
|
||||
)
|
||||
),
|
||||
}
|
||||
|
@ -279,13 +285,23 @@ describe('send utils', () => {
|
|||
})
|
||||
|
||||
it('should call ethQuery.estimateGas with the expected params', async () => {
|
||||
const result = await estimateGas(baseMockParams)
|
||||
const result = await sendUtils.estimateGas(baseMockParams)
|
||||
assert.equal(baseMockParams.estimateGasMethod.callCount, 1)
|
||||
assert.deepEqual(
|
||||
baseMockParams.estimateGasMethod.getCall(0).args[0],
|
||||
Object.assign({ gasPrice: undefined, value: undefined }, baseExpectedCall)
|
||||
)
|
||||
assert.equal(result, 'mockToString:16')
|
||||
assert.equal(result, '0xabc16')
|
||||
})
|
||||
|
||||
it('should call ethQuery.estimateGas with the expected params when initialGasLimitHex is lower than the upperGasLimit', async () => {
|
||||
const result = await estimateGas(Object.assign({}, baseMockParams, { blockGasLimit: '0xbcd' }))
|
||||
assert.equal(baseMockParams.estimateGasMethod.callCount, 1)
|
||||
assert.deepEqual(
|
||||
baseMockParams.estimateGasMethod.getCall(0).args[0],
|
||||
Object.assign({ gasPrice: undefined, value: undefined }, baseExpectedCall, { gas: '0xbcdx0.95' })
|
||||
)
|
||||
assert.equal(result, '0xabc16x1.5')
|
||||
})
|
||||
|
||||
it('should call ethQuery.estimateGas with a value of 0x0 and the expected data and to if passed a selectedToken', async () => {
|
||||
|
@ -300,7 +316,7 @@ describe('send utils', () => {
|
|||
to: 'mockAddress',
|
||||
})
|
||||
)
|
||||
assert.equal(result, 'mockToString:16')
|
||||
assert.equal(result, '0xabc16')
|
||||
})
|
||||
|
||||
it(`should return ${SIMPLE_GAS_COST} if ethQuery.getCode does not return '0x'`, async () => {
|
||||
|
@ -309,12 +325,23 @@ describe('send utils', () => {
|
|||
assert.equal(result, SIMPLE_GAS_COST)
|
||||
})
|
||||
|
||||
it(`should return ${SIMPLE_GAS_COST} if not passed a selectedToken or truthy to address`, async () => {
|
||||
assert.equal(baseMockParams.estimateGasMethod.callCount, 0)
|
||||
const result = await estimateGas(Object.assign({}, baseMockParams, { to: null }))
|
||||
assert.equal(result, SIMPLE_GAS_COST)
|
||||
})
|
||||
|
||||
it(`should not return ${SIMPLE_GAS_COST} if passed a selectedToken`, async () => {
|
||||
assert.equal(baseMockParams.estimateGasMethod.callCount, 0)
|
||||
const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123', selectedToken: { address: '' } }))
|
||||
assert.notEqual(result, SIMPLE_GAS_COST)
|
||||
})
|
||||
|
||||
it(`should return ${BASE_TOKEN_GAS_COST} if passed a selectedToken but no to address`, async () => {
|
||||
const result = await estimateGas(Object.assign({}, baseMockParams, { to: null, selectedToken: { address: '' } }))
|
||||
assert.equal(result, BASE_TOKEN_GAS_COST)
|
||||
})
|
||||
|
||||
it(`should return the adjusted blockGasLimit if it fails with a 'Transaction execution error.'`, async () => {
|
||||
const result = await estimateGas(Object.assign({}, baseMockParams, {
|
||||
to: 'isContract willFailBecauseOf:Transaction execution error.',
|
||||
|
@ -401,4 +428,15 @@ describe('send utils', () => {
|
|||
assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), '0x5')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getToAddressForGasUpdate()', () => {
|
||||
it('should return empty string if all params are undefined or null', () => {
|
||||
assert.equal(getToAddressForGasUpdate(undefined, null), '')
|
||||
})
|
||||
|
||||
it('should return the first string that is not defined or null in lower case', () => {
|
||||
assert.equal(getToAddressForGasUpdate('A', null), 'a')
|
||||
assert.equal(getToAddressForGasUpdate(undefined, 'B'), 'b')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -181,7 +181,7 @@ ShapeshiftForm.prototype.render = function () {
|
|||
return h('div.shapeshift-form-wrapper', [
|
||||
showQrCode
|
||||
? this.renderQrCode()
|
||||
: h('div.shapeshift-form', [
|
||||
: h('div.modal-shapeshift-form', [
|
||||
h('div.shapeshift-form__selectors', [
|
||||
|
||||
h('div.shapeshift-form__selector', [
|
||||
|
|
|
@ -178,7 +178,14 @@ SignatureRequest.prototype.renderBody = function () {
|
|||
rows = data
|
||||
} else if (type === 'eth_sign') {
|
||||
rows = [{ name: this.context.t('message'), value: data }]
|
||||
notice = this.context.t('signNotice')
|
||||
notice = [this.context.t('signNotice'),
|
||||
h('span.request-signature__help-link', {
|
||||
onClick: () => {
|
||||
global.platform.openWindow({
|
||||
url: 'https://consensys.zendesk.com/hc/en-us/articles/360004427792',
|
||||
})
|
||||
},
|
||||
}, this.context.t('learnMore'))]
|
||||
}
|
||||
|
||||
return h('div.request-signature__body', {}, [
|
||||
|
@ -197,6 +204,9 @@ SignatureRequest.prototype.renderBody = function () {
|
|||
h('div.request-signature__rows', [
|
||||
|
||||
...rows.map(({ name, value }) => {
|
||||
if (typeof value === 'boolean') {
|
||||
value = value.toString()
|
||||
}
|
||||
return h('div.request-signature__row', [
|
||||
h('div.request-signature__row-title', [`${name}:`]),
|
||||
h('div.request-signature__row-value', value),
|
||||
|
|
|
@ -34,7 +34,7 @@ TokenBalance.prototype.render = function () {
|
|||
return isLoading
|
||||
? h('span', '')
|
||||
: h('span.token-balance', [
|
||||
h('span.token-balance__amount', string),
|
||||
h('span.hide-text-overflow.token-balance__amount', string),
|
||||
!balanceOnly && h('span.token-balance__symbol', symbol),
|
||||
])
|
||||
}
|
||||
|
|
|
@ -190,6 +190,16 @@ const conversionGreaterThan = (
|
|||
return firstValue.gt(secondValue)
|
||||
}
|
||||
|
||||
const conversionLessThan = (
|
||||
{ ...firstProps },
|
||||
{ ...secondProps },
|
||||
) => {
|
||||
const firstValue = converter({ ...firstProps })
|
||||
const secondValue = converter({ ...secondProps })
|
||||
|
||||
return firstValue.lt(secondValue)
|
||||
}
|
||||
|
||||
const conversionMax = (
|
||||
{ ...firstProps },
|
||||
{ ...secondProps },
|
||||
|
@ -229,6 +239,7 @@ module.exports = {
|
|||
addCurrencies,
|
||||
multiplyCurrencies,
|
||||
conversionGreaterThan,
|
||||
conversionLessThan,
|
||||
conversionGTE,
|
||||
conversionLTE,
|
||||
conversionMax,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
.currency-display {
|
||||
height: 54px;
|
||||
width: 100%ß;
|
||||
border: 1px solid $alto;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
|
@ -21,7 +20,7 @@
|
|||
line-height: 22px;
|
||||
border: none;
|
||||
outline: 0 !important;
|
||||
max-width: 100%;
|
||||
max-width: 22ch;
|
||||
}
|
||||
|
||||
&__primary-currency {
|
||||
|
@ -47,14 +46,22 @@
|
|||
&__input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -67,12 +74,14 @@
|
|||
.react-numeric-input {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,25 +27,37 @@
|
|||
@media screen and (max-width: $break-small) {
|
||||
flex-direction: column;
|
||||
flex: 0 0 auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $break-large) {
|
||||
flex-direction: row;
|
||||
flex-grow: 3;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.balance-display {
|
||||
.token-amount {
|
||||
color: $black;
|
||||
max-width: 100%;
|
||||
|
||||
.token-balance {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
max-width: 100%;
|
||||
text-align: center;
|
||||
|
||||
.token-amount {
|
||||
font-size: 1.75rem;
|
||||
margin-top: 1rem;
|
||||
|
||||
.token-balance {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.fiat-amount {
|
||||
|
@ -56,9 +68,10 @@
|
|||
}
|
||||
|
||||
@media screen and (min-width: $break-large) {
|
||||
margin-left: .8em;
|
||||
margin: 0 .8em;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
|
||||
.token-amount {
|
||||
font-size: 1.5rem;
|
||||
|
|
|
@ -642,10 +642,31 @@
|
|||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.shapeshift-form-wrapper {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 1 0 auto;
|
||||
|
||||
.shapeshift-form, .modal-shapeshift-form {
|
||||
border-radius: 8px;
|
||||
background-color: rgba(0, 0, 0, .05);
|
||||
padding: 17px 15px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&__caret {
|
||||
width: auto;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__logo {
|
||||
|
@ -773,17 +794,15 @@
|
|||
margin-top: 28px;
|
||||
flex: 1 0 auto;
|
||||
|
||||
.shapeshift-form {
|
||||
width: auto;
|
||||
.shapeshift-form, .modal-shapeshift-form {
|
||||
border-radius: 8px;
|
||||
background-color: rgba(0, 0, 0, .05);
|
||||
padding: 17px 15px;
|
||||
|
||||
&__caret {
|
||||
width: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,14 +26,16 @@ $wallet-view-bg: $alabaster;
|
|||
//Account and transaction details
|
||||
.account-and-transaction-details {
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// tx view
|
||||
|
||||
.tx-view {
|
||||
flex: 63.5 0 66.5%;
|
||||
flex: 1 1 66.5%;
|
||||
background: $tx-view-bg;
|
||||
min-width: 0;
|
||||
|
||||
// No title on mobile
|
||||
@media screen and (max-width: 575px) {
|
||||
|
@ -286,7 +288,7 @@ $wallet-view-bg: $alabaster;
|
|||
}
|
||||
|
||||
.token-balance__amount {
|
||||
padding-right: 6px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
// first time
|
||||
|
|
|
@ -183,6 +183,12 @@
|
|||
padding: 6px 18px 15px;
|
||||
}
|
||||
|
||||
&__help-link {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
color: $curious-blue;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
|
|
@ -81,13 +81,9 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and (
|
|||
}
|
||||
|
||||
.token-menu-dropdown {
|
||||
height: 55px;
|
||||
width: 80%;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(0, 0, 0, .82);
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .5);
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
top: 52px;
|
||||
right: 25px;
|
||||
z-index: 2000;
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ function reduceApp (state, action) {
|
|||
warning: null,
|
||||
buyView: {},
|
||||
isMouseUser: false,
|
||||
gasIsLoading: false,
|
||||
}, state.appState)
|
||||
|
||||
switch (action.type) {
|
||||
|
@ -675,6 +676,16 @@ function reduceApp (state, action) {
|
|||
isMouseUser: action.value,
|
||||
})
|
||||
|
||||
case actions.GAS_LOADING_STARTED:
|
||||
return extend(appState, {
|
||||
gasIsLoading: true,
|
||||
})
|
||||
|
||||
case actions.GAS_LOADING_FINISHED:
|
||||
return extend(appState, {
|
||||
gasIsLoading: false,
|
||||
})
|
||||
|
||||
default:
|
||||
return appState
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ function reduceMetamask (state, action) {
|
|||
identities: {},
|
||||
unapprovedTxs: {},
|
||||
noActiveNotices: true,
|
||||
lastUnreadNotice: undefined,
|
||||
nextUnreadNotice: undefined,
|
||||
frequentRpcList: [],
|
||||
addressBook: [],
|
||||
selectedTokenAddress: null,
|
||||
|
@ -65,7 +65,7 @@ function reduceMetamask (state, action) {
|
|||
case actions.SHOW_NOTICE:
|
||||
return extend(metamaskState, {
|
||||
noActiveNotices: false,
|
||||
lastUnreadNotice: action.value,
|
||||
nextUnreadNotice: action.value,
|
||||
})
|
||||
|
||||
case actions.CLEAR_NOTICES:
|
||||
|
|
|
@ -16,6 +16,7 @@ const selectors = {
|
|||
transactionsSelector,
|
||||
accountsWithSendEtherInfoSelector,
|
||||
getCurrentAccountWithSendEtherInfo,
|
||||
getGasIsLoading,
|
||||
getGasPrice,
|
||||
getGasLimit,
|
||||
getForceGasMin,
|
||||
|
@ -117,6 +118,10 @@ function transactionsSelector (state) {
|
|||
.sort((a, b) => b.time - a.time)
|
||||
}
|
||||
|
||||
function getGasIsLoading (state) {
|
||||
return state.appState.gasIsLoading
|
||||
}
|
||||
|
||||
function getGasPrice (state) {
|
||||
return state.metamask.send.gasPrice
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ module.exports = {
|
|||
miniAddressSummary: miniAddressSummary,
|
||||
isAllOneCase: isAllOneCase,
|
||||
isValidAddress: isValidAddress,
|
||||
isValidENSAddress,
|
||||
numericBalance: numericBalance,
|
||||
parseBalance: parseBalance,
|
||||
formatBalance: formatBalance,
|
||||
|
@ -87,6 +88,10 @@ function isValidAddress (address) {
|
|||
return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed)
|
||||
}
|
||||
|
||||
function isValidENSAddress (address) {
|
||||
return address.match(/^.{7,}\.(eth|test)$/)
|
||||
}
|
||||
|
||||
function isInvalidChecksumAddress (address) {
|
||||
var prefixed = ethUtil.addHexPrefix(address)
|
||||
if (address === '0x0000000000000000000000000000000000000000') return false
|
||||
|
|
Loading…
Reference in New Issue