1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 17:33:23 +01:00

Merge branch 'develop' of github.com:MetaMask/metamask-extension into network-remove-provider-engine

This commit is contained in:
kumavis 2018-05-22 13:20:04 -07:00
commit 3a80f3dd83
121 changed files with 4306 additions and 16990 deletions

View File

@ -9,6 +9,9 @@ workflows:
- prep-build:
requires:
- prep-deps-npm
- prep-docs:
requires:
- prep-deps-npm
- prep-scss:
requires:
- prep-deps-npm
@ -25,7 +28,7 @@ workflows:
- test-e2e-firefox:
requires:
- prep-deps-npm
- prep-deps-firefox
- prep-deps-firefox
- prep-build
- test-unit:
requires:
@ -51,7 +54,6 @@ workflows:
- all-tests-pass:
requires:
- test-lint
- test-deps
- test-unit
- test-e2e-chrome
- test-e2e-firefox
@ -64,12 +66,22 @@ workflows:
- prep-deps-npm
- prep-build
- all-tests-pass
- job-publish:
- job-publish-prerelease:
requires:
- prep-deps-npm
- prep-build
- job-screens
- all-tests-pass
- job-publish-release:
filters:
branches:
only: master
requires:
- prep-deps-npm
- prep-build
- prep-docs
- job-screens
- all-tests-pass
jobs:
prep-deps-npm:
@ -125,6 +137,21 @@ jobs:
- dist
- builds
prep-docs:
docker:
- image: circleci/node:8-browsers
steps:
- checkout
- restore_cache:
key: dependency-cache-{{ .Revision }}
- run:
name: build:dist
command: npm run doc
- save_cache:
key: docs-cache-{{ .Revision }}
paths:
- docs/jsdoc
prep-scss:
docker:
- image: circleci/node:8-browsers
@ -181,7 +208,7 @@ jobs:
- store_artifacts:
path: test-artifacts
destination: test-artifacts
test-e2e-firefox:
environment:
browsers: '["Firefox"]'
@ -201,7 +228,7 @@ jobs:
- restore_cache:
key: dependency-cache-{{ .Revision }}
- restore_cache:
key: build-cache-{{ .Revision }}
key: build-cache-{{ .Revision }}
- run:
name: test:e2e:firefox
command: npm run test:e2e:firefox
@ -226,7 +253,7 @@ jobs:
paths:
- test-artifacts
job-publish:
job-publish-prerelease:
docker:
- image: circleci/node:8-browsers
steps:
@ -252,9 +279,29 @@ jobs:
- run:
name: build:announce
command: ./development/metamaskbot-build-announce.js
job-publish-release:
docker:
- image: circleci/node:8-browsers
steps:
- checkout
- restore_cache:
key: dependency-cache-{{ .Revision }}
- restore_cache:
key: build-cache-{{ .Revision }}
- restore_cache:
key: docs-cache-{{ .Revision }}
- restore_cache:
key: job-screens-{{ .Revision }}
- run:
name: sentry sourcemaps upload
command: npm run sentry:publish
- run:
name: github gh-pages docs publish
command: >
git config user.name metamaskbot
git config user.email admin@metamask.io
gh-pages -d docs/jsdocs
test-unit:
docker:
@ -368,4 +415,4 @@ jobs:
- run:
name: All Tests Passed
command: echo 'weew - everything passed!'

View File

@ -10,6 +10,7 @@
- Fix bug that prevents setting language locale in settings.
- Show checksum addresses throughout the UI
- Allow transactions with a 0 gwei gas price
- Made provider RPC errors contain useful messages
## 4.5.5 Fri Apr 06 2018

View File

@ -23,6 +23,9 @@
"addTokens": {
"message": "Add Tokens"
},
"addAcquiredTokens": {
"message": "Add the tokens you've acquired using MetaMask"
},
"amount": {
"message": "Amount"
},
@ -53,7 +56,7 @@
"message": "Back"
},
"balance": {
"message": "Balance:"
"message": "Balance"
},
"balances": {
"message": "Token balance(s)"
@ -717,6 +720,9 @@
"search": {
"message": "Search"
},
"searchResults": {
"message": "Search Results"
},
"secretPhrase": {
"message": "Enter your secret twelve word phrase here to restore your vault."
},
@ -832,6 +838,9 @@
"message": "$1 to ETH via ShapeShift",
"description": "system will fill in deposit type in start of message"
},
"token": {
"message": "Token"
},
"tokenAddress": {
"message": "Token Address"
},

14
app/images/search.svg Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="17px" height="17px" viewBox="0 0 17 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>search</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Add-Tokens" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Metamascara---add-from-token-list-Copy-3" transform="translate(-345.000000, -350.000000)" fill="#9B9B9B" fill-rule="nonzero">
<g id="search" transform="translate(345.000000, 350.000000)">
<path d="M2.01875,6.90625 C2.01875,4.25 4.25,2.01875 6.90625,2.01875 C9.5625,2.01875 11.6875,4.14375 11.6875,6.90625 C11.6875,9.5625 9.5625,11.6875 6.90625,11.6875 C4.14375,11.6875 2.01875,9.5625 2.01875,6.90625 Z M16.575,15.0875 L12.325,10.8375 C13.175,9.66875 13.6,8.2875 13.6,6.8 C13.70625,3.08125 10.625,0 6.90625,0 C3.08125,0 0,3.08125 0,6.90625 C0,10.73125 3.08125,13.8125 6.90625,13.8125 C8.18125,13.8125 9.45625,13.3875 10.4125,12.75 L14.6625,17 L16.575,15.0875 Z" id="Page-1"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="65px" height="58px" viewBox="0 0 65 58" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: sketchtool 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>7FDB75AD-BD4D-497C-B391-69EEB31A0561</title>
<desc>Created with sketchtool.</desc>
<defs></defs>
<g id="Add-Tokens" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Add-tokens" transform="translate(-267.000000, -284.000000)" fill="#B8BAC1">
<g id="tokensearch" transform="translate(267.000000, 284.000000)">
<path d="M28.5322581,2.80645161 C42.4391613,2.80645161 54.1925806,9.22854839 54.2552581,16.8433871 C54.1925806,24.4591613 42.4391613,30.8821935 28.5322581,30.8821935 C14.6253548,30.8821935 2.87193548,24.4600968 2.80925806,16.8443226 C2.87193548,9.22854839 14.6253548,2.80645161 28.5322581,2.80645161 M28.5322581,36.7289677 C15.7432581,36.7289677 4.78125806,31.2975484 3.05154839,24.5012581 C7.70932258,29.9981613 17.2559355,33.6886452 28.5322581,33.6886452 C39.8085806,33.6886452 49.3551935,29.9981613 54.0129677,24.5012581 C52.2832581,31.2975484 41.3212581,36.7289677 28.5322581,36.7289677 M28.5322581,54.2692903 C15.7432581,54.2692903 4.78125806,48.837871 3.05154839,42.0415806 C7.70932258,47.5384839 17.2559355,51.2289677 28.5322581,51.2289677 C33.2237097,51.2289677 37.6083226,50.5844194 41.471871,49.4403226 C42.1379355,49.243871 42.5270968,48.5675161 42.4110968,47.8818065 C42.4045484,47.8453226 42.398,47.8079032 42.3923871,47.7704839 C42.2642258,46.9603548 41.439129,46.458 40.6533226,46.6946774 C37.0180323,47.792 32.8822581,48.4225161 28.5322581,48.4225161 C15.7432581,48.4225161 4.78125806,42.9910968 3.05154839,36.1948065 C7.70932258,41.6917097 17.2559355,45.3821935 28.5322581,45.3821935 C33.3649677,45.3821935 37.8730645,44.6983548 41.8217419,43.4906452 C42.2763871,43.3503226 42.6066129,42.976129 42.7422581,42.5196129 L42.7534839,42.4812581 C43.0752903,41.4082581 42.0733871,40.4063548 41.004129,40.7403226 C37.2846452,41.9040645 33.0225806,42.5757419 28.5322581,42.5757419 C15.7432581,42.5757419 4.78125806,37.1443226 3.05154839,30.3480323 C7.70932258,35.8449355 17.2559355,39.5354194 28.5322581,39.5354194 C39.8085806,39.5354194 49.3551935,35.8449355 54.0129677,30.3480323 L54.0129677,33.5492581 C54.0129677,34.3846452 54.6902581,35.0619355 55.5256452,35.0619355 C56.3610323,35.0619355 57.0383226,34.3902581 57.0392581,33.5558065 C57.0467419,26.4900968 57.0645161,16.9257097 57.0645161,16.905129 C57.0645161,16.8845484 57.0617097,16.8649032 57.0617097,16.8443226 C57.0617097,16.8237419 57.0645161,16.8031613 57.0645161,16.7825806 L57.0598387,16.7825806 C56.9513226,7.36225806 44.4616774,0 28.5322581,0 C12.6028387,0 0.113193548,7.36225806 0.00467741935,16.7825806 L0,16.7825806 C0,16.8031613 0.00280645161,16.8237419 0.00280645161,16.8443226 C0.00280645161,16.8649032 0,16.8845484 0,16.905129 C0,16.9322581 0.00467741935,19.3420645 0.0102903226,22.6293548 L0,22.6293548 C0,22.7154194 0.00841935484,22.7996129 0.0102903226,22.8838065 C0.0140322581,24.5957419 0.0177741935,26.5247097 0.0196451613,28.476129 L0,28.476129 C0,28.650129 0.0130967742,28.8222581 0.0205806452,28.9953226 C0.0243225806,30.828871 0.0280645161,32.6586774 0.0308709677,34.3229032 L0,34.3229032 C0,34.5857742 0.0140322581,34.8467742 0.0318064516,35.1059032 C0.036483871,37.3108387 0.0392903226,39.1406452 0.0411612903,40.1696774 L0,40.1696774 C0,40.4905484 0.0177741935,40.8086129 0.0458387097,41.123871 L0.0495806452,41.2033871 C0.0645483871,41.3699032 0.0935483871,41.5345484 0.116935484,41.700129 C0.130032258,41.7861935 0.137516129,41.8731935 0.152483871,41.9583226 C0.183354839,42.1416774 0.225451613,42.3240968 0.266612903,42.5055806 C0.29,42.6103548 0.308709677,42.7160645 0.334903226,42.8199032 C0.358290323,42.9078387 0.387290323,42.9939032 0.411612903,43.0818387 C2.00006452,48.7134516 8.12841935,53.3160323 16.5777097,55.5705484 C16.6010968,55.5770968 16.6254194,55.5836452 16.6488065,55.5892581 C16.9350645,55.6650323 17.2213226,55.739871 17.5122581,55.8109677 C20.9099355,56.6538387 24.6322258,57.1215806 28.5322581,57.1215806 C32.4322903,57.1215806 36.1545806,56.6538387 39.5522581,55.8109677 C39.8431935,55.739871 40.1294516,55.6650323 40.4157097,55.5892581 C40.4390968,55.5836452 40.4634194,55.5770968 40.4868065,55.5705484 C41.5766452,55.2796129 42.6253226,54.9475161 43.6319032,54.579871 C44.4682258,54.2739677 44.7675806,53.2627097 44.2652258,52.5274194 C44.2437097,52.4956129 44.2212581,52.462871 44.1997419,52.430129 C43.8423871,51.8950323 43.1688387,51.6873548 42.5645161,51.9090645 C38.4998387,53.3955484 33.6624516,54.2692903 28.5322581,54.2692903" id="Fill-1"></path>
<path d="M64.3227484,54.3991355 L60.4535871,50.5299742 C61.4526839,49.1566839 61.9522323,47.5345548 61.9522323,45.7880065 C62.1009742,40.5661355 56.8996839,36.4144581 51.4654581,38.2367806 C48.6131677,39.1928452 46.4821355,41.7401677 46.0611677,44.7187484 C45.3530065,49.7460387 49.205329,54.0249419 54.0894903,54.0249419 C55.5872,54.0249419 57.0849097,53.5244581 58.2074903,52.7770065 L62.0766516,56.6452323 C62.6968774,57.2654581 63.7025226,57.2654581 64.3227484,56.6452323 C64.9429742,56.0250065 64.9429742,55.0193613 64.3227484,54.3991355 M48.3484258,45.9124258 C48.3484258,42.7925871 50.9696516,40.1713613 54.0894903,40.1713613 C57.209329,40.1713613 59.7052,42.6681677 59.7052,45.9124258 C59.7052,49.0332 57.209329,51.529071 54.0894903,51.529071 C50.8452323,51.529071 48.3484258,49.0332 48.3484258,45.9124258" id="Fill-3"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -322,7 +322,7 @@ function setupController (initState, initLangCode) {
/**
* A runtime.Port object, as provided by the browser:
* @link https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/Port
* @see https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/Port
* @typedef Port
* @type Object
*/

View File

@ -166,7 +166,7 @@ function documentElementCheck () {
/**
* Checks if the current domain is blacklisted
*
*
* @returns {boolean} {@code true} if the current domain is blacklisted
*/
function blacklistedDomainCheck () {
@ -175,6 +175,7 @@ function blacklistedDomainCheck () {
'dropbox.com',
'webbyawards.com',
'cdn.shopify.com/s/javascripts/tricorder/xtld-read-only-frame.html',
'adyen.com',
]
var currentUrl = window.location.href
var currentRegex

View File

@ -13,19 +13,17 @@ class AddressBookController {
* @param {object} opts Overrides the defaults for the initial state of this.store
* @property {array} opts.initState initializes the the state of the AddressBookController. Can contain an
* addressBook property to initialize the addressBook array
* @param {KeyringController} keyringController (Soon to be deprecated) The keyringController used in the current
* MetamaskController. Contains the identities used in this AddressBookController.
* @property {object} opts.preferencesStore the {@code PreferencesController} store
* @property {object} store The the store of the current users address book
* @property {array} store.addressBook An array of addresses and nicknames. These are set by the user when sending
* to a new address.
*
*/
constructor (opts = {}, keyringController) {
const initState = extend({
constructor ({initState, preferencesStore}) {
this.store = new ObservableStore(extend({
addressBook: [],
}, opts.initState)
this.store = new ObservableStore(initState)
this.keyringController = keyringController
}, initState))
this._preferencesStore = preferencesStore
}
//
@ -62,7 +60,7 @@ class AddressBookController {
*/
_addToAddressBook (address, name) {
const addressBook = this._getAddressBook()
const identities = this._getIdentities()
const {identities} = this._preferencesStore.getState()
const addressBookIndex = addressBook.findIndex((element) => { return element.address.toLowerCase() === address.toLowerCase() || element.name === name })
const identitiesIndex = Object.keys(identities).findIndex((element) => { return element.toLowerCase() === address.toLowerCase() })
@ -95,19 +93,6 @@ class AddressBookController {
_getAddressBook () {
return this.store.getState().addressBook
}
/**
* Retrieves identities from the keyring controller in order to avoid
* duplication
*
* @deprecated
* @returns {array} Returns the identies array from the keyringContoller's state
*
*/
_getIdentities () {
return this.keyringController.memStore.getState().identities
}
}
module.exports = AddressBookController

View File

@ -27,6 +27,7 @@ class PreferencesController {
useBlockie: false,
featureFlags: {},
currentLocale: opts.initLangCode,
identities: {},
}, opts.initState)
this.store = new ObservableStore(initState)
}
@ -62,6 +63,16 @@ class PreferencesController {
this.store.updateState({ currentLocale: key })
}
setAddresses (addresses) {
const oldIdentities = this.store.getState().identities
const identities = addresses.reduce((ids, address, index) => {
const oldId = oldIdentities[address] || {}
ids[address] = {name: `Account ${index + 1}`, address, ...oldId}
return ids
}, {})
this.store.updateState({ identities })
}
/**
* Setter for the `selectedAddress` property
*
@ -155,6 +166,21 @@ class PreferencesController {
return this.store.getState().tokens
}
/**
* Sets a custom label for an account
* @param {string} account the account to set a label for
* @param {string} label the custom label for the account
* @return {Promise<string>}
*/
setAccountLabel (account, label) {
const address = normalizeAddress(account)
const {identities} = this.store.getState()
identities[address] = identities[address] || {}
identities[address].name = label
this.store.updateState({ identities })
return Promise.resolve(label)
}
/**
* Gets an updated rpc list from this.addToFrequentRpcList() and sets the `frequentRpcList` to this update list.
*
@ -189,8 +215,8 @@ class PreferencesController {
* The returned list will have a max length of 2. If the _url currently exists it the list, it will be moved to the
* end of the list. The current list is modified and returned as a promise.
*
* @param {string} _url The rpc url to add to the frequentRpcList.
* @returns {Promise<array>} The updated frequentRpcList.
* @param {string} _url The rpc url to add to the frequentRpcList.
* @returns {Promise<array>} The updated frequentRpcList.
*
*/
addToFrequentRpcList (_url) {

View File

@ -112,21 +112,6 @@ class TransactionController extends EventEmitter {
this.txStateManager.wipeTransactions(address)
}
/**
Check if a txMeta in the list with the same nonce has been confirmed in a block
if the txParams dont have a nonce will return false
@returns {boolean} whether the nonce has been used in a transaction confirmed in a block
@param {object} txMeta - the txMeta object
*/
async isNonceTaken (txMeta) {
const { from, nonce } = txMeta.txParams
if ('nonce' in txMeta.txParams) {
const sameNonceTxList = this.txStateManager.getFilteredTxList({from, nonce, status: 'confirmed'})
return (sameNonceTxList.length >= 1)
}
return false
}
/**
add a new unapproved transaction to the pipeline

View File

@ -0,0 +1,66 @@
const log = require('loglevel')
/**
* JSON-RPC error object
*
* @typedef {Object} RpcError
* @property {number} code - Indicates the error type that occurred
* @property {Object} [data] - Contains additional information about the error
* @property {string} [message] - Short description of the error
*/
/**
* Middleware configuration object
*
* @typedef {Object} MiddlewareConfig
* @property {boolean} [override] - Use RPC_ERRORS message in place of provider message
*/
/**
* Map of standard and non-standard RPC error codes to messages
*/
const RPC_ERRORS = {
1: 'An unauthorized action was attempted.',
2: 'A disallowed action was attempted.',
3: 'An execution error occurred.',
[-32600]: 'The JSON sent is not a valid Request object.',
[-32601]: 'The method does not exist / is not available.',
[-32602]: 'Invalid method parameter(s).',
[-32603]: 'Internal JSON-RPC error.',
[-32700]: 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.',
internal: 'Internal server error.',
unknown: 'Unknown JSON-RPC error.',
}
/**
* Modifies a JSON-RPC error object in-place to add a human-readable message,
* optionally overriding any provider-supplied message
*
* @param {RpcError} error - JSON-RPC error object
* @param {boolean} override - Use RPC_ERRORS message in place of provider message
*/
function sanitizeRPCError (error, override) {
if (error.message && !override) { return error }
const message = error.code > -31099 && error.code < -32100 ? RPC_ERRORS.internal : RPC_ERRORS[error.code]
error.message = message || RPC_ERRORS.unknown
}
/**
* json-rpc-engine middleware that both logs standard and non-standard error
* messages and ends middleware stack traversal if an error is encountered
*
* @param {MiddlewareConfig} [config={override:true}] - Middleware configuration
* @returns {Function} json-rpc-engine middleware function
*/
function createErrorMiddleware ({ override = true } = {}) {
return (req, res, next) => {
next(done => {
const { error } = res
if (!error) { return done() }
sanitizeRPCError(error)
log.error(`MetaMask - RPC Error: ${error.message}`, error)
})
}
}
module.exports = createErrorMiddleware

View File

@ -1,5 +1,6 @@
const pump = require('pump')
const RpcEngine = require('json-rpc-engine')
const createErrorMiddleware = require('./createErrorMiddleware')
const createIdRemapMiddleware = require('json-rpc-engine/src/idRemapMiddleware')
const createStreamMiddleware = require('json-rpc-middleware-stream')
const LocalStorageStore = require('obs-store')
@ -44,6 +45,7 @@ function MetamaskInpageProvider (connectionStream) {
// handle sendAsync requests via dapp-side rpc engine
const rpcEngine = new RpcEngine()
rpcEngine.push(createIdRemapMiddleware())
rpcEngine.push(createErrorMiddleware())
rpcEngine.push(streamMiddleware)
self.rpcEngine = rpcEngine
}

View File

@ -145,7 +145,8 @@ module.exports = class MetamaskController extends EventEmitter {
// address book controller
this.addressBookController = new AddressBookController({
initState: initState.AddressBookController,
}, this.keyringController)
preferencesStore: this.preferencesController.store,
})
// tx mgmt
this.txController = new TransactionController({
@ -364,6 +365,7 @@ module.exports = class MetamaskController extends EventEmitter {
addToken: nodeify(preferencesController.addToken, preferencesController),
removeToken: nodeify(preferencesController.removeToken, preferencesController),
setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController),
setAccountLabel: nodeify(preferencesController.setAccountLabel, preferencesController),
setFeatureFlag: nodeify(preferencesController.setFeatureFlag, preferencesController),
// AddressController
@ -374,7 +376,6 @@ module.exports = class MetamaskController extends EventEmitter {
createNewVaultAndKeychain: nodeify(this.createNewVaultAndKeychain, this),
createNewVaultAndRestore: nodeify(this.createNewVaultAndRestore, this),
addNewKeyring: nodeify(keyringController.addNewKeyring, keyringController),
saveAccountLabel: nodeify(keyringController.saveAccountLabel, keyringController),
exportAccount: nodeify(keyringController.exportAccount, keyringController),
// txController
@ -382,7 +383,7 @@ module.exports = class MetamaskController extends EventEmitter {
updateTransaction: nodeify(txController.updateTransaction, txController),
updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController),
retryTransaction: nodeify(this.retryTransaction, this),
isNonceTaken: nodeify(txController.isNonceTaken, txController),
getFilteredTxList: nodeify(txController.getFilteredTxList, txController),
// messageManager
signMessage: nodeify(this.signMessage, this),
@ -434,7 +435,9 @@ module.exports = class MetamaskController extends EventEmitter {
} else {
vault = await this.keyringController.createNewVaultAndKeychain(password)
this.selectFirstIdentity(vault)
const accounts = await this.keyringController.getAccounts()
this.preferencesController.setAddresses(accounts)
this.selectFirstIdentity()
}
release()
} catch (err) {
@ -454,7 +457,9 @@ module.exports = class MetamaskController extends EventEmitter {
const release = await this.createVaultMutex.acquire()
try {
const vault = await this.keyringController.createNewVaultAndRestore(password, seed)
this.selectFirstIdentity(vault)
const accounts = await this.keyringController.getAccounts()
this.preferencesController.setAddresses(accounts)
this.selectFirstIdentity()
release()
return vault
} catch (err) {
@ -472,12 +477,10 @@ module.exports = class MetamaskController extends EventEmitter {
*/
/**
* Retrieves the first Identiy from the passed Vault and selects the related address
*
* @param {} vault
* Sets the first address in the state to the selected address
*/
selectFirstIdentity (vault) {
const { identities } = vault
selectFirstIdentity () {
const { identities } = this.preferencesController.store.getState()
const address = Object.keys(identities)[0]
this.preferencesController.setSelectedAddress(address)
}
@ -503,13 +506,15 @@ module.exports = class MetamaskController extends EventEmitter {
await this.verifySeedPhrase()
this.preferencesController.setAddresses(newAccounts)
newAccounts.forEach((address) => {
if (!oldAccounts.includes(address)) {
this.preferencesController.setSelectedAddress(address)
}
})
return keyState
const {identities} = this.preferencesController.store.getState()
return {...keyState, identities}
}
/**

View File

@ -0,0 +1,47 @@
const version = 26
/*
This migration moves the identities stored in the KeyringController
into the PreferencesController
*/
const clone = require('clone')
module.exports = {
version,
migrate (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
try {
const state = versionedData.data
versionedData.data = transformState(state)
} catch (err) {
console.warn(`MetaMask Migration #${version}` + err.stack)
return Promise.reject(err)
}
return Promise.resolve(versionedData)
},
}
function transformState (state) {
if (!state.KeyringController || !state.PreferencesController) {
return
}
if (!state.KeyringController.walletNicknames) {
return state
}
state.PreferencesController.identities = Object.keys(state.KeyringController.walletNicknames)
.reduce((identities, address) => {
identities[address] = {
name: state.KeyringController.walletNicknames[address],
address,
}
return identities
}, {})
delete state.KeyringController.walletNicknames
return state
}

View File

@ -36,4 +36,5 @@ module.exports = [
require('./023'),
require('./024'),
require('./025'),
require('./026'),
]

View File

@ -1,6 +1,16 @@
# Guide to Porting MetaMask to a New Environment
MetaMask has been under continuous development for nearly two years now, and weve gradually discovered some very useful abstractions, that have allowed us to grow more easily. A couple of those layers together allow MetaMask to be ported to new environments and contexts increasingly easily.
MetaMask has been under continuous development for nearly two years now, and weve gradually discovered some useful abstractions that have allowed us to grow more easily. A couple of those layers together allow MetaMask to be ported to new environments and contexts increasingly easily (although it still could be easier, and please let us know if you get stuck!)
Before we get started, it's worth becoming familiar with our basic architecture:
![metamask-architecture-diagram](./architecture.png)
The `metamask-background` describes the file at `app/scripts/background.js`, which is the web extension singleton. This context instantiates an instance of the `MetaMask Controller`, which represents the user's accounts, a connection to the blockchain, and the interaction with new Dapps.
When a new site is visited, the WebExtension creates a new `ContentScript` in that page's context, which can be seen at `app/scripts/contentscript.js`. This script represents a per-page setup process, which creates the per-page `web3` api, connects it to the background script via the Port API (wrapped in a [stream abstraction](https://github.com/substack/stream-handbook)), and injected into the DOM before anything loads.
The most confusing part about porting MetaMask to a new platform is the way we provide the Web3 API over a series of streams between contexts. Once you understand how we create the [InpageProvider](../app/scripts/lib/inpage-provider.js) in the [inpage.js script](../app/scripts/inpage.js), you will be able to understand how the [port-stream](../app/scripts/lib/port-stream.js) is just a thin wrapper around the [postMessage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage), and a similar stream API can be wrapped around any communication channel to communicate with the `MetaMaskController` via its `setupUntrustedCommunication(stream, domain)` method.
### The MetaMask Controller
@ -88,3 +98,4 @@ If streams seem new and confusing to you, that's ok, they can seem strange at fi
## Conclusion
I hope this has been helpful to you! If you have any other questionsm, or points you think need clarification in this guide, please [open an issue on our GitHub](https://github.com/MetaMask/metamask-plugin/issues/new)!

View File

@ -91,7 +91,7 @@ AccountDetailScreen.prototype.render = function () {
isEditingLabel: false,
},
saveText: (text) => {
props.dispatch(actions.saveAccountLabel(selected, text))
props.dispatch(actions.setAccountLabel(selected, text))
},
}, [

View File

@ -62,7 +62,7 @@ function mapStateToProps (state) {
isInitialized: state.metamask.isInitialized,
isUnlocked: state.metamask.isUnlocked,
currentView: state.appState.currentView,
activeAddress: state.appState.activeAddress,
selectedAddress: state.metamask.selectedAddress,
transForward: state.appState.transForward,
isMascara: state.metamask.isMascara,
isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized),
@ -197,7 +197,7 @@ App.prototype.renderAppBar = function () {
style: {},
enableAccountsSelector: true,
identities: this.props.identities,
selected: this.props.currentView.context,
selected: this.props.selectedAddress,
network: this.props.network,
keyrings: this.props.keyrings,
}, []),
@ -588,7 +588,7 @@ App.prototype.renderPrimary = function () {
},
}, [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
onClick: () => props.dispatch(actions.backToAccountDetail(props.activeAddress)),
onClick: () => props.dispatch(actions.backToAccountDetail(props.selectedAddress)),
style: {
marginLeft: '10px',
marginTop: '50px',

View File

@ -1,7 +1,6 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const extend = require('xtend')
const debounce = require('debounce')
const copyToClipboard = require('copy-to-clipboard')
const ENS = require('ethjs-ens')
@ -20,55 +19,61 @@ function EnsInput () {
EnsInput.prototype.render = function () {
const props = this.props
const opts = extend(props, {
list: 'addresses',
onChange: () => {
const network = this.props.network
const networkHasEnsSupport = getNetworkEnsSupport(network)
if (!networkHasEnsSupport) return
const recipient = document.querySelector('input[name="address"]').value
if (recipient.match(ensRE) === null) {
return this.setState({
loadingEns: false,
ensResolution: null,
ensFailure: null,
})
}
function onInputChange() {
const network = this.props.network
const networkHasEnsSupport = getNetworkEnsSupport(network)
if (!networkHasEnsSupport) return
this.setState({
loadingEns: true,
const recipient = document.querySelector('input[name="address"]').value
if (recipient.match(ensRE) === null) {
return this.setState({
loadingEns: false,
ensResolution: null,
ensFailure: null,
})
this.checkName()
},
})
return h('div', {
style: { width: '100%' },
}, [
h('input.large-input', opts),
// The address book functionality.
h('datalist#addresses',
[
// Corresponds to the addresses owned.
Object.keys(props.identities).map((key) => {
const identity = props.identities[key]
return h('option', {
value: identity.address,
label: identity.name,
key: identity.address,
})
}),
// Corresponds to previously sent-to addresses.
props.addressBook.map((identity) => {
return h('option', {
value: identity.address,
label: identity.name,
key: identity.address,
})
}),
]),
this.ensIcon(),
])
}
this.setState({
loadingEns: true,
})
this.checkName()
}
return (
h('div', {
style: { width: '100%' },
}, [
h('input.large-input', {
name: props.name,
placeholder: props.placeholder,
list: 'addresses',
onChange: onInputChange.bind(this),
}),
// The address book functionality.
h('datalist#addresses',
[
// Corresponds to the addresses owned.
Object.keys(props.identities).map((key) => {
const identity = props.identities[key]
return h('option', {
value: identity.address,
label: identity.name,
key: identity.address,
})
}),
// Corresponds to previously sent-to addresses.
props.addressBook.map((identity) => {
return h('option', {
value: identity.address,
label: identity.name,
key: identity.address,
})
}),
]),
this.ensIcon(),
])
)
}
EnsInput.prototype.componentDidMount = function () {

1722
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,8 +15,8 @@
"test:integration:build": "gulp build:scss",
"test:e2e:chrome": "shell-parallel -s 'npm run ganache:start' -x 'sleep 3 && npm run test:e2e:run:chrome'",
"test:e2e:firefox": "shell-parallel -s 'npm run ganache:start' -x 'sleep 3 && npm run test:e2e:run:firefox'",
"test:e2e:run:chrome": "SELENIUM_BROWSER=chrome mocha test/e2e/chrome/metamask.spec --bail --recursive",
"test:e2e:run:firefox": "SELENIUM_BROWSER=firefox mocha test/e2e/firefox/metamask.spec --bail --recursive",
"test:e2e:run:chrome": "SELENIUM_BROWSER=chrome mocha test/e2e/metamask.spec --bail --recursive",
"test:e2e:run:firefox": "SELENIUM_BROWSER=firefox mocha test/e2e/metamask.spec --bail --recursive",
"test:screens": "shell-parallel -s 'npm run ganache:start' -x 'sleep 3 && npm run test:screens:run'",
"test:screens:run": "node test/screens/new-ui.js",
"test:coverage": "nyc npm run test:unit && npm run test:coveralls-upload",
@ -63,6 +63,7 @@
]
},
"dependencies": {
"@material-ui/core": "^1.0.0",
"abi-decoder": "^1.0.9",
"asmcrypto.js": "0.22.0",
"async": "^2.5.0",
@ -97,7 +98,7 @@
"eth-json-rpc-filters": "^1.2.8",
"eth-json-rpc-infura": "^3.0.0",
"eth-json-rpc-middleware": "^2.1.0",
"eth-keyring-controller": "^2.2.0",
"eth-keyring-controller": "^3.1.1",
"eth-phishing-detect": "^1.1.4",
"eth-query": "^2.1.2",
"eth-sig-util": "^1.4.2",
@ -113,7 +114,7 @@
"ethjs-query": "^0.3.4",
"express": "^4.15.5",
"extension-link-enabler": "^1.0.0",
"extensionizer": "^1.0.0",
"extensionizer": "^1.0.1",
"fast-json-patch": "^2.0.4",
"fast-levenshtein": "^2.0.6",
"file-loader": "^1.1.11",
@ -138,7 +139,6 @@
"lodash.shuffle": "^4.2.0",
"lodash.uniqby": "^4.7.0",
"loglevel": "^1.4.1",
"material-ui": "1.0.0-beta.44",
"metamascara": "^2.0.0",
"metamask-logo": "^2.1.4",
"mkdirp": "^0.5.1",
@ -189,7 +189,6 @@
"sw-stream": "^2.0.2",
"swappable-obj-proxy": "^1.0.2",
"textarea-caret": "^3.0.1",
"through2": "^2.0.3",
"valid-url": "^1.0.9",
"vreme": "^3.0.2",
"web3": "^0.20.1",
@ -215,6 +214,7 @@
"browserify": "^16.1.1",
"chai": "^4.1.0",
"chromedriver": "2.36.0",
"clipboardy": "^1.2.3",
"compression": "^1.7.1",
"coveralls": "^3.0.0",
"cross-env": "^5.1.4",
@ -233,6 +233,7 @@
"ganache-cli": "^6.1.0",
"ganache-core": "^2.1.0",
"geckodriver": "^1.11.0",
"gh-pages": "^1.1.0",
"gifencoder": "^1.1.0",
"gulp": "github:gulpjs/gulp#6d71a658c61edb3090221579d8f97dbe086ba2ed",
"gulp-babel": "^7.0.0",
@ -291,6 +292,7 @@
"stylelint-config-standard": "^18.2.0",
"tape": "^4.5.1",
"testem": "^2.0.0",
"through2": "^2.0.3",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"watchify": "^3.9.0"

View File

@ -6,6 +6,9 @@ module.exports = function(config) {
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: process.cwd(),
// Uncomment to allow for longer timeouts
// browserNoActivityTimeout: 100000000,
browserConsoleLogOptions: {
terminal: false,
},

View File

@ -1,314 +0,0 @@
const fs = require('fs')
const mkdirp = require('mkdirp')
const path = require('path')
const assert = require('assert')
const pify = require('pify')
const webdriver = require('selenium-webdriver')
const until = require('selenium-webdriver/lib/until')
const By = webdriver.By
const { delay, buildChromeWebDriver } = require('../func')
describe('Metamask popup page', function () {
let driver, accountAddress, tokenAddress, extensionId
this.timeout(0)
before(async function () {
const extPath = path.resolve('dist/chrome')
driver = buildChromeWebDriver(extPath)
await driver.get('chrome://extensions')
await delay(500)
})
afterEach(async function () {
if (this.currentTest.state === 'failed') {
await verboseReportOnFailure(this.currentTest)
}
})
after(async function () {
await driver.quit()
})
describe('Setup', function () {
it('switches to Chrome extensions list', async function () {
const tabs = await driver.getAllWindowHandles()
await driver.switchTo().window(tabs[0])
await delay(300)
})
it(`selects MetaMask's extension id and opens it in the current tab`, async function () {
extensionId = await getExtensionId()
await driver.get(`chrome-extension://${extensionId}/popup.html`)
await delay(500)
})
it('sets provider type to localhost', async function () {
await driver.wait(until.elementLocated(By.css('#app-content')), 300)
await setProviderType('localhost')
})
})
describe('Account Creation', () => {
it('matches MetaMask title', async () => {
const title = await driver.getTitle()
assert.equal(title, 'MetaMask', 'title matches MetaMask')
})
it('shows privacy notice', async () => {
await driver.wait(async () => {
const privacyHeader = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-center.flex-grow > h3')).getText()
assert.equal(privacyHeader, 'PRIVACY NOTICE', 'shows privacy notice')
return privacyHeader === 'PRIVACY NOTICE'
}, 300)
await driver.findElement(By.css('button')).click()
})
it('show terms of use', async () => {
await driver.wait(async () => {
const terms = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-center.flex-grow > h3')).getText()
assert.equal(terms, 'TERMS OF USE', 'shows terms of use')
return terms === 'TERMS OF USE'
})
})
it('checks if the TOU button is disabled', async () => {
const button = await driver.findElement(By.css('button')).isEnabled()
assert.equal(button, false, 'disabled continue button')
const element = await driver.findElement(By.linkText('Attributions'))
await driver.executeScript('arguments[0].scrollIntoView(true)', element)
await delay(300)
})
it('allows the button to be clicked when scrolled to the bottom of TOU', async () => {
const button = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-center.flex-grow > button'))
const buttonEnabled = await button.isEnabled()
assert.equal(buttonEnabled, true, 'enabled continue button')
await button.click()
})
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'))
const button = await driver.findElements(By.css('button'))
await passwordBox.sendKeys('123456789')
await passwordBoxConfirm.sendKeys('123456789')
await button[0].click()
await delay(500)
})
it('shows value was created and seed phrase', async () => {
await delay(300)
const seedPhrase = await driver.findElement(By.css('.twelve-word-phrase')).getText()
assert.equal(seedPhrase.split(' ').length, 12)
const continueAfterSeedPhrase = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > button:nth-child(4)'))
assert.equal(await continueAfterSeedPhrase.getText(), `I'VE COPIED IT SOMEWHERE SAFE`)
await continueAfterSeedPhrase.click()
await delay(300)
})
it('shows account address', async function () {
accountAddress = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > div > div:nth-child(1) > flex-column > div.flex-row > div')).getText()
})
it('logs out of the vault', async () => {
await driver.findElement(By.css('.sandwich-expando')).click()
await delay(500)
const logoutButton = await driver.findElement(By.css('#app-content > div > div:nth-child(3) > span > div > li:nth-child(3)'))
assert.equal(await logoutButton.getText(), 'Log Out')
await logoutButton.click()
})
it('accepts account password after lock', async () => {
await delay(500)
await driver.findElement(By.id('password-box')).sendKeys('123456789')
await driver.findElement(By.css('button')).click()
await delay(500)
})
it('shows QR code option', async () => {
await delay(300)
await driver.findElement(By.css('.fa-ellipsis-h')).click()
await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div > div:nth-child(1) > flex-column > div.name-label > div > span > i > div > div > li:nth-child(3)')).click()
await delay(300)
})
it('checks QR code address is the same as account details address', async () => {
const QRaccountAddress = await driver.findElement(By.css('.ellip-address')).getText()
assert.equal(accountAddress.toLowerCase(), QRaccountAddress)
await driver.findElement(By.css('.fa-arrow-left')).click()
await delay(500)
})
})
describe('Import Ganache seed phrase', function () {
it('logs out', async function () {
await driver.findElement(By.css('.sandwich-expando')).click()
await delay(200)
const logOut = await driver.findElement(By.css('#app-content > div > div:nth-child(3) > span > div > li:nth-child(3)'))
assert.equal(await logOut.getText(), 'Log Out')
await logOut.click()
await delay(300)
})
it('restores from seed phrase', async function () {
const restoreSeedLink = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > div.flex-row.flex-center.flex-grow > p'))
assert.equal(await restoreSeedLink.getText(), 'Restore from seed phrase')
await restoreSeedLink.click()
await delay(100)
})
it('adds seed phrase', async function () {
const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'
const seedTextArea = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > textarea'))
await seedTextArea.sendKeys(testSeedPhrase)
await driver.findElement(By.id('password-box')).sendKeys('123456789')
await driver.findElement(By.id('password-box-confirm')).sendKeys('123456789')
await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > div > button:nth-child(2)')).click()
await delay(500)
})
it('balance renders', async function () {
await delay(200)
const balance = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div > div.flex-row > div.ether-balance.ether-balance-amount > div > div > div:nth-child(1) > div:nth-child(1)'))
assert.equal(await balance.getText(), '100.000')
await delay(200)
})
it('sends transaction', async function () {
const sendButton = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div > div.flex-row > button:nth-child(4)'))
assert.equal(await sendButton.getText(), 'SEND')
await sendButton.click()
await delay(200)
})
it('adds recipient address and amount', async function () {
const sendTranscationScreen = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > h3:nth-child(2)')).getText()
assert.equal(sendTranscationScreen, 'SEND TRANSACTION')
const inputAddress = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > section:nth-child(3) > div > input'))
const inputAmmount = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > section:nth-child(4) > input'))
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
await inputAmmount.sendKeys('10')
await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > section:nth-child(4) > button')).click()
await delay(300)
})
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()
await delay(500)
})
it('finds the transaction in the transactions list', async function () {
const tranasactionAmount = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > section > section > div > div > div > div.ether-balance.ether-balance-amount > div > div > div > div:nth-child(1)'))
assert.equal(await tranasactionAmount.getText(), '10.0')
})
})
describe('Token Factory', function () {
it('navigates to token factory', async function () {
await driver.get('http://tokenfactory.surge.sh/')
})
it('navigates to create token contract link', async function () {
const createToken = await driver.findElement(By.css('#bs-example-navbar-collapse-1 > ul > li:nth-child(3) > a'))
await createToken.click()
})
it('adds input for token', async function () {
const totalSupply = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > div > div:nth-child(5) > input'))
const tokenName = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > div > div:nth-child(6) > input'))
const tokenDecimal = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > div > div:nth-child(7) > input'))
const tokenSymbol = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > div > div:nth-child(8) > input'))
const createToken = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > div > button'))
await totalSupply.sendKeys('100')
await tokenName.sendKeys('Test')
await tokenDecimal.sendKeys('0')
await tokenSymbol.sendKeys('TST')
await createToken.click()
await delay(1000)
})
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'))
await metamaskSubmit.click()
await delay(1000)
})
it('switches back to Token Factory to grab the token contract address', async function () {
const windowHandles = await driver.getAllWindowHandles()
await driver.switchTo().window(windowHandles[0])
const tokenContactAddress = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > span:nth-child(3)'))
tokenAddress = await tokenContactAddress.getText()
await delay(500)
})
it('navigates back to MetaMask popup in the tab', async function () {
await driver.get(`chrome-extension://${extensionId}/popup.html`)
await delay(700)
})
})
describe('Add Token', function () {
it('switches to the add token screen', async function () {
const tokensTab = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > section > div > div.inactiveForm.pointer'))
assert.equal(await tokensTab.getText(), 'TOKENS')
await tokensTab.click()
await delay(300)
})
it('navigates to the add token screen', async function () {
const addTokenButton = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > section > div.full-flex-height > div > button'))
assert.equal(await addTokenButton.getText(), 'ADD TOKEN')
await addTokenButton.click()
})
it('checks add token screen rendered', async function () {
const addTokenScreen = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.section-title.flex-row.flex-center > h2'))
assert.equal(await addTokenScreen.getText(), 'ADD TOKEN')
})
it('adds token parameters', async function () {
const tokenContractAddress = await driver.findElement(By.css('#token-address'))
await tokenContractAddress.sendKeys(tokenAddress)
await delay(300)
await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-justify-center.flex-grow.select-none > div > button')).click()
await delay(100)
})
it('checks the token balance', async function () {
const tokenBalance = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > section > div.full-flex-height > ol > li:nth-child(2) > h3'))
assert.equal(await tokenBalance.getText(), '100 TST')
})
})
async function getExtensionId () {
const extension = await driver.executeScript('return document.querySelector("extensions-manager").shadowRoot.querySelector("extensions-view-manager extensions-item-list").shadowRoot.querySelector("extensions-item:nth-child(2)").getAttribute("id")')
return extension
}
async function setProviderType (type) {
await driver.executeScript('window.metamask.setProviderType(arguments[0])', type)
}
async function verboseReportOnFailure (test) {
const artifactDir = `./test-artifacts/chrome/${test.title}`
const filepathBase = `${artifactDir}/test-failure`
await pify(mkdirp)(artifactDir)
// capture screenshot
const screenshot = await driver.takeScreenshot()
await pify(fs.writeFile)(`${filepathBase}-screenshot.png`, screenshot, { encoding: 'base64' })
// capture dom source
const htmlSource = await driver.getPageSource()
await pify(fs.writeFile)(`${filepathBase}-dom.html`, htmlSource)
}
})

View File

@ -1,13 +1,24 @@
require('chromedriver')
require('geckodriver')
const path = require('path')
const webdriver = require('selenium-webdriver')
const Command = require('selenium-webdriver/lib/command').Command
const By = webdriver.By
exports.delay = function delay (time) {
module.exports = {
delay,
buildChromeWebDriver,
buildFirefoxWebdriver,
installWebExt,
getExtensionIdChrome,
getExtensionIdFirefox,
}
function delay (time) {
return new Promise(resolve => setTimeout(resolve, time))
}
exports.buildChromeWebDriver = function buildChromeWebDriver (extPath) {
function buildChromeWebDriver (extPath) {
return new webdriver.Builder()
.withCapabilities({
chromeOptions: {
@ -17,6 +28,29 @@ exports.buildChromeWebDriver = function buildChromeWebDriver (extPath) {
.build()
}
exports.buildFirefoxWebdriver = function buildFirefoxWebdriver (extPath) {
function buildFirefoxWebdriver () {
return new webdriver.Builder().build()
}
async function getExtensionIdChrome (driver) {
await driver.get('chrome://extensions')
const extensionId = await driver.executeScript('return document.querySelector("extensions-manager").shadowRoot.querySelector("extensions-view-manager extensions-item-list").shadowRoot.querySelector("extensions-item:nth-child(2)").getAttribute("id")')
return extensionId
}
async function getExtensionIdFirefox (driver) {
await driver.get('about:debugging#addons')
const extensionId = await driver.findElement(By.css('dd.addon-target-info-content:nth-child(6) > span:nth-child(1)')).getText()
return extensionId
}
async function installWebExt (driver, extension) {
const cmd = await new Command('moz-install-web-ext')
.setParameter('path', path.resolve(extension))
.setParameter('temporary', true)
await driver.getExecutor()
.defineCommand(cmd.getName(), 'POST', '/session/:sessionId/moz/addon/install')
return await driver.schedule(cmd, 'installWebExt(' + extension + ')')
}

View File

@ -4,23 +4,44 @@ const path = require('path')
const assert = require('assert')
const pify = require('pify')
const webdriver = require('selenium-webdriver')
const Command = require('selenium-webdriver/lib/command').Command
const By = webdriver.By
const { delay, buildFirefoxWebdriver } = require('../func')
const { By, Key } = webdriver
const { delay, buildChromeWebDriver, buildFirefoxWebdriver, installWebExt, getExtensionIdChrome, getExtensionIdFirefox } = require('./func')
describe('', function () {
describe('Metamask popup page', function () {
let driver, accountAddress, tokenAddress, extensionId
this.timeout(0)
before(async function () {
const extPath = path.resolve('dist/firefox')
driver = buildFirefoxWebdriver()
installWebExt(driver, extPath)
await delay(700)
if (process.env.SELENIUM_BROWSER === 'chrome') {
const extPath = path.resolve('dist/chrome')
driver = buildChromeWebDriver(extPath)
extensionId = await getExtensionIdChrome(driver)
await driver.get(`chrome-extension://${extensionId}/popup.html`)
} else if (process.env.SELENIUM_BROWSER === 'firefox') {
const extPath = path.resolve('dist/firefox')
driver = buildFirefoxWebdriver()
await installWebExt(driver, extPath)
await delay(700)
extensionId = await getExtensionIdFirefox(driver)
await driver.get(`moz-extension://${extensionId}/popup.html`)
}
})
afterEach(async function () {
// logs command not supported in firefox
// https://github.com/SeleniumHQ/selenium/issues/2910
if (process.env.SELENIUM_BROWSER === 'chrome') {
// check for console errors
const errors = await checkBrowserForConsoleErrors()
if (errors.length) {
const errorReports = errors.map(err => err.message)
const errorMessage = `Errors found in browser console:\n${errorReports.join('\n')}`
this.test.error(new Error(errorMessage))
}
}
// gather extra data if test failed
if (this.currentTest.state === 'failed') {
await verboseReportOnFailure(this.currentTest)
}
@ -32,23 +53,17 @@ describe('', function () {
describe('Setup', function () {
it('switches to Firefox addon list', async function () {
await driver.get('about:debugging#addons')
await delay(1000)
})
it(`selects MetaMask's extension id and opens it in the current tab`, async function () {
const tabs = await driver.getAllWindowHandles()
await driver.switchTo().window(tabs[0])
extensionId = await driver.findElement(By.css('dd.addon-target-info-content:nth-child(6) > span:nth-child(1)')).getText()
await driver.get(`moz-extension://${extensionId}/popup.html`)
await delay(500)
it('switches to Chrome extensions list', async function () {
await delay(300)
const windowHandles = await driver.getAllWindowHandles()
await driver.switchTo().window(windowHandles[0])
})
it('sets provider type to localhost', async function () {
await setProviderType('localhost')
await delay(300)
await setProviderType('localhost')
})
})
describe('Account Creation', () => {
@ -67,10 +82,9 @@ describe('', function () {
})
it('show terms of use', async () => {
await delay(300)
const terms = await driver.findElement(By.css('.terms-header')).getText()
assert.equal(terms, 'TERMS OF USE', 'shows terms of use')
await delay(300)
delay(300)
})
it('checks if the TOU button is disabled', async () => {
@ -78,15 +92,11 @@ describe('', function () {
assert.equal(button, false, 'disabled continue button')
const element = await driver.findElement(By.linkText('Attributions'))
await driver.executeScript('arguments[0].scrollIntoView(true)', element)
await delay(300)
await delay(700)
})
it('allows the button to be clicked when scrolled to the bottom of TOU', async () => {
const button = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-center.flex-grow > button'))
await delay(300)
const buttonEnabled = await button.isEnabled()
assert.equal(buttonEnabled, true, 'enabled continue button')
await delay(200)
await button.click()
})
@ -126,7 +136,7 @@ describe('', function () {
it('accepts account password after lock', async () => {
await delay(500)
await driver.findElement(By.id('password-box')).sendKeys('123456789')
await driver.findElement(By.id('password-box')).sendKeys(webdriver.Key.ENTER)
await driver.findElement(By.id('password-box')).sendKeys(Key.ENTER)
await delay(500)
})
@ -146,6 +156,7 @@ describe('', function () {
})
describe('Import Ganache seed phrase', function () {
it('logs out', async function () {
await driver.findElement(By.css('.sandwich-expando')).click()
await delay(200)
@ -236,7 +247,7 @@ describe('', function () {
await delay(1000)
})
// There is an issue with blank confirmation window, but the button is still there and the driver is able to clicked (?.?)
// There is an issue with blank confirmation window in Firefox, but the button is still there and the driver is able to clicked (?.?)
it('confirms transaction in MetaMask popup', async function () {
const windowHandles = await driver.getAllWindowHandles()
await driver.switchTo().window(windowHandles[windowHandles.length - 1])
@ -254,12 +265,17 @@ describe('', function () {
})
it('navigates back to MetaMask popup in the tab', async function () {
await driver.get(`moz-extension://${extensionId}/popup.html`)
if (process.env.SELENIUM_BROWSER === 'chrome') {
await driver.get(`chrome-extension://${extensionId}/popup.html`)
} else if (process.env.SELENIUM_BROWSER === 'firefox') {
await driver.get(`moz-extension://${extensionId}/popup.html`)
}
await delay(700)
})
})
describe('Add Token', function () {
it('switches to the add token screen', async function () {
const tokensTab = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > section > div > div.inactiveForm.pointer'))
assert.equal(await tokensTab.getText(), 'TOKENS')
@ -283,7 +299,7 @@ describe('', function () {
await tokenContractAddress.sendKeys(tokenAddress)
await delay(300)
await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-justify-center.flex-grow.select-none > div > button')).click()
await delay(100)
await delay(200)
})
it('checks the token balance', async function () {
@ -292,12 +308,37 @@ describe('', function () {
})
})
async function setProviderType(type) {
async function setProviderType (type) {
await driver.executeScript('window.metamask.setProviderType(arguments[0])', type)
}
async function verboseReportOnFailure(test) {
const artifactDir = `./test-artifacts/firefox/${test.title}`
async function checkBrowserForConsoleErrors() {
const ignoredLogTypes = ['WARNING']
const ignoredErrorMessages = [
// React throws error warnings on "dataset", but still sets the data-* properties correctly
'Warning: Unknown prop `dataset` on ',
// Third-party Favicon 404s show up as errors
'favicon.ico - Failed to load resource: the server responded with a status of 404 (Not Found)',
// React Development build - known issue blocked by test build sys
'Warning: It looks like you\'re using a minified copy of the development build of React.',
// Redux Development build - known issue blocked by test build sys
'This means that you are running a slower development build of Redux.',
]
const browserLogs = await driver.manage().logs().get('browser')
const errorEntries = browserLogs.filter(entry => !ignoredLogTypes.includes(entry.level.toString()))
const errorObjects = errorEntries.map(entry => entry.toJSON())
// ignore all errors that contain a message in `ignoredErrorMessages`
const matchedErrorObjects = errorObjects.filter(entry => !ignoredErrorMessages.some(message => entry.message.includes(message)))
return matchedErrorObjects
}
async function verboseReportOnFailure (test) {
let artifactDir
if (process.env.SELENIUM_BROWSER === 'chrome') {
artifactDir = `./test-artifacts/chrome/${test.title}`
} else if (process.env.SELENIUM_BROWSER === 'firefox') {
artifactDir = `./test-artifacts/firefox/${test.title}`
}
const filepathBase = `${artifactDir}/test-failure`
await pify(mkdirp)(artifactDir)
// capture screenshot
@ -309,15 +350,3 @@ describe('', function () {
}
})
async function installWebExt (driver, extension) {
const cmd = await new Command('moz-install-web-ext')
.setParameter('path', path.resolve(extension))
.setParameter('temporary', true)
await driver.getExecutor()
.defineCommand(cmd.getName(), 'POST', '/session/:sessionId/moz/addon/install')
return await driver.schedule(cmd, 'installWebExt(' + extension + ')')
}

View File

@ -22,6 +22,11 @@ async function runAddTokenFlowTest (assert, done) {
selectState.val('add token')
reactTriggerChange(selectState[0])
// Used to set values on TextField input component
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype, 'value'
).set
// Check that no tokens have been added
assert.ok($('.token-list-item').length === 0, 'no tokens added')
@ -31,14 +36,14 @@ async function runAddTokenFlowTest (assert, done) {
addTokenButton[0].click()
// Verify Add Token screen
let addTokenWrapper = await queryAsync($, '.add-token__wrapper')
let addTokenWrapper = await queryAsync($, '.page-container')
assert.ok(addTokenWrapper[0], 'add token wrapper renders')
let addTokenTitle = await queryAsync($, '.add-token__header__title')
let addTokenTitle = await queryAsync($, '.page-container__title')
assert.equal(addTokenTitle[0].textContent, 'Add Tokens', 'add token title is correct')
// Cancel Add Token
const cancelAddTokenButton = await queryAsync($, 'button.btn-secondary--lg.add-token__cancel-button')
const cancelAddTokenButton = await queryAsync($, 'button.btn-secondary--lg.page-container__footer-button')
assert.ok(cancelAddTokenButton[0], 'cancel add token button present')
cancelAddTokenButton.click()
@ -50,20 +55,22 @@ async function runAddTokenFlowTest (assert, done) {
addTokenButton[0].click()
// Verify Add Token Screen
addTokenWrapper = await queryAsync($, '.add-token__wrapper')
addTokenTitle = await queryAsync($, '.add-token__header__title')
addTokenWrapper = await queryAsync($, '.page-container')
addTokenTitle = await queryAsync($, '.page-container__title')
assert.ok(addTokenWrapper[0], 'add token wrapper renders')
assert.equal(addTokenTitle[0].textContent, 'Add Tokens', 'add token title is correct')
// Search for token
const searchInput = await queryAsync($, 'input.add-token__input')
searchInput.val('a')
reactTriggerChange(searchInput[0])
const searchInput = (await findAsync(addTokenWrapper, '#search-tokens'))[0]
searchInput.focus()
await timeout(1000)
nativeInputValueSetter.call(searchInput, 'a')
searchInput.dispatchEvent(new Event('input', { bubbles: true}))
// Click token to add
const tokenWrapper = await queryAsync($, 'div.add-token__token-wrapper')
const tokenWrapper = await queryAsync($, 'div.token-list__token')
assert.ok(tokenWrapper[0], 'token found')
const tokenImageProp = tokenWrapper.find('.add-token__token-icon').css('background-image')
const tokenImageProp = tokenWrapper.find('.token-list__token-icon').css('background-image')
const tokenImageUrl = tokenImageProp.slice(5, -2)
tokenWrapper[0].click()
@ -73,11 +80,8 @@ async function runAddTokenFlowTest (assert, done) {
nextButton[0].click()
// Confirm Add token
assert.equal(
$('.add-token__description')[0].textContent,
'Token balance(s)',
'confirm add token rendered'
)
const confirmAddToken = await queryAsync($, '.confirm-add-token')
assert.ok(confirmAddToken[0], 'confirm add token rendered')
assert.ok($('button.btn-primary--lg')[0], 'confirm add token button found')
$('button.btn-primary--lg')[0].click()
@ -91,39 +95,46 @@ async function runAddTokenFlowTest (assert, done) {
assert.ok(addTokenButton[0], 'add token button present')
addTokenButton[0].click()
const addTokenTabs = await queryAsync($, '.add-token__header__tabs__tab')
addTokenWrapper = await queryAsync($, '.page-container')
const addTokenTabs = await queryAsync($, '.page-container__tab')
assert.equal(addTokenTabs.length, 2, 'expected number of tabs')
assert.equal(addTokenTabs[1].textContent, 'Custom Token', 'Custom Token tab present')
assert.ok(addTokenTabs[1], 'add custom token tab present')
addTokenTabs[1].click()
await timeout(1000)
// Input token contract address
const customInput = await queryAsync($, 'input.add-token__add-custom-input')
customInput.val('0x177af043D3A1Aed7cc5f2397C70248Fc6cDC056c')
reactTriggerChange(customInput[0])
const customInput = (await findAsync(addTokenWrapper, '#custom-address'))[0]
customInput.focus()
await timeout(1000)
nativeInputValueSetter.call(customInput, '0x177af043D3A1Aed7cc5f2397C70248Fc6cDC056c')
customInput.dispatchEvent(new Event('input', { bubbles: true}))
// Click Next button
nextButton = await queryAsync($, 'button.btn-primary--lg')
assert.equal(nextButton[0].textContent, 'Next', 'next button rendered')
nextButton[0].click()
// nextButton = await queryAsync($, 'button.btn-primary--lg')
// assert.equal(nextButton[0].textContent, 'Next', 'next button rendered')
// nextButton[0].click()
// Verify symbol length error since contract address won't return symbol
const errorMessage = await queryAsync($, '.add-token__add-custom-error-message')
// // Verify symbol length error since contract address won't return symbol
const errorMessage = await queryAsync($, '#custom-symbol-helper-text')
assert.ok(errorMessage[0], 'error rendered')
$('button.btn-secondary--lg')[0].click()
// // Confirm Add token
// await timeout(100000)
// Confirm Add token
// assert.equal(
// $('.add-token__description')[0].textContent,
// $('.page-container__subtitle')[0].textContent,
// 'Would you like to add these tokens?',
// 'confirm add token rendered'
// )
// assert.ok($('button.btn-primary--lg')[0], 'confirm add token button found')
// $('button.btn-primary--lg')[0].click()
// // Verify added token image
// heroBalance = await queryAsync($, '.hero-balance')
// assert.ok(heroBalance, 'rendered hero balance')
// assert.ok(heroBalance.find('.identicon')[0], 'token added')
// Verify added token image
heroBalance = await queryAsync($, '.hero-balance')
assert.ok(heroBalance, 'rendered hero balance')
assert.ok(heroBalance.find('.identicon')[0], 'token added')
}

View File

@ -5,29 +5,44 @@ const mkdirp = require('mkdirp')
const rimraf = require('rimraf')
const webdriver = require('selenium-webdriver')
const endOfStream = require('end-of-stream')
const clipboardy = require('clipboardy')
const Ethjs = require('ethjs')
const GIFEncoder = require('gifencoder')
const pngFileStream = require('png-file-stream')
const sizeOfPng = require('image-size/lib/types/png')
const By = webdriver.By
const { delay, buildWebDriver } = require('./func')
const localesIndex = require('../../app/_locales/index.json')
// const localesIndex = []
const eth = new Ethjs(new Ethjs.HttpProvider('http://localhost:8545'))
let driver
let screenshotCount = 0
captureAllScreens().catch((err) => {
captureAllScreens()
.then(async () => {
// build screenshots into gif
console.log('building gif...')
await generateGif()
await driver.quit()
process.exit()
})
.catch(async (err) => {
try {
console.error(err)
verboseReportOnFailure()
driver.quit()
verboseReportOnFailure({ title: 'something broke' })
} catch (err) {
console.error(err)
}
await driver.quit()
process.exit(1)
})
async function captureAllScreens() {
let screenshotCount = 0
async function captureAllScreens() {
// common names
let button
let tabs
@ -74,10 +89,11 @@ async function captureAllScreens() {
await driver.findElement(By.css('button')).click()
await captureLanguageScreenShots('create password')
const password = '123456789'
const passwordBox = await driver.findElement(By.css('input#create-password'))
const passwordBoxConfirm = await driver.findElement(By.css('input#confirm-password'))
passwordBox.sendKeys('123456789')
passwordBoxConfirm.sendKeys('123456789')
passwordBox.sendKeys(password)
passwordBoxConfirm.sendKeys(password)
await delay(500)
await captureLanguageScreenShots('choose-password-filled')
@ -111,109 +127,123 @@ async function captureAllScreens() {
await delay(300)
await captureLanguageScreenShots('secret backup phrase - reveal')
const seedPhrase = await driver.findElement(By.css('.backup-phrase__secret-words')).getText()
const seedPhraseWords = seedPhrase.split(' ')
await driver.findElement(By.css('button')).click()
await delay(300)
await captureLanguageScreenShots('confirm secret backup phrase')
// finish up
console.log('building gif...')
await generateGif()
await driver.quit()
return
// enter seed phrase
const seedPhraseButtons = await driver.findElements(By.css('.backup-phrase__confirm-seed-options > button'))
const seedPhraseButtonWords = await Promise.all(seedPhraseButtons.map(button => button.getText()))
for (let targetWord of seedPhraseWords) {
const wordIndex = seedPhraseButtonWords.indexOf(targetWord)
if (wordIndex === -1) throw new Error(`Captured seed phrase word "${targetWord}" not in found seed phrase button options ${seedPhraseButtonWords.join(' ')}`)
await driver.findElement(By.css(`.backup-phrase__confirm-seed-options > button:nth-child(${wordIndex+1})`)).click()
await delay(100)
}
await captureLanguageScreenShots('confirm secret backup phrase - words selected correctly')
//
// await button.click()
// await delay(700)
// this.seedPhase = await driver.findElement(By.css('.twelve-word-phrase')).getText()
// await captureScreenShot('seed phrase')
//
// const continueAfterSeedPhrase = await driver.findElement(By.css('button'))
// await continueAfterSeedPhrase.click()
await driver.findElement(By.css('.backup-phrase__content-wrapper .first-time-flow__button')).click()
await delay(300)
await captureLanguageScreenShots('metamask post-initialize greeter screen deposit ether')
await driver.findElement(By.css('.page-container__header-close')).click()
await delay(300)
await captureLanguageScreenShots('metamask account main screen')
// account details + export private key
await driver.findElement(By.css('.wallet-view__name-container > .wallet-view__details-button')).click()
await delay(300)
await captureLanguageScreenShots('metamask account detail screen')
await driver.findElement(By.css('.account-modal__button:nth-of-type(2)')).click()
await delay(300)
await captureLanguageScreenShots('metamask account detail export private key screen - initial')
await driver.findElement(By.css('.private-key-password > input')).sendKeys(password)
await delay(300)
await captureLanguageScreenShots('metamask account detail export private key screen - password entered')
await driver.findElement(By.css('.btn-primary--lg.export-private-key__button')).click()
await delay(300)
await captureLanguageScreenShots('metamask account detail export private key screen - reveal key')
await driver.findElement(By.css('.export-private-key__button')).click()
await delay(300)
await captureLanguageScreenShots('metamask account detail export private key screen - done')
// get eth from Ganache
// const viewAddressButton = await driver.findElement(By.css('.wallet-view__address'))
// await driver.actions({ bridge: true }).move({ origin: viewAddressButton }).perform()
// console.log('driver.actions', driver.actions({ bridge: true }))
// await delay(300)
// await captureScreenShot('main screen')
//
// await driver.findElement(By.css('.sandwich-expando')).click()
// await delay(500)
// await captureScreenShot('menu')
// await captureLanguageScreenShots('metamask home - hover copy address')
// await driver.findElement(By.css('#app-content > div > div:nth-child(3) > span > div > li:nth-child(3)')).click()
// await captureScreenShot('main screen')
// it('should accept account password after lock', async () => {
// await delay(500)
// await driver.findElement(By.id('password-box')).sendKeys('123456789')
// await driver.findElement(By.css('button')).click()
// await delay(500)
// })
//
// it('should show QR code option', async () => {
// await delay(300)
// await driver.findElement(By.css('.fa-ellipsis-h')).click()
// await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div > div:nth-child(1) > flex-column > div.name-label > div > span > i > div > div > li:nth-child(3)')).click()
// await delay(300)
// })
//
// it('should show the account address', async () => {
// this.accountAddress = await driver.findElement(By.css('.ellip-address')).getText()
// await driver.findElement(By.css('.fa-arrow-left')).click()
// await delay(500)
// })
await driver.findElement(By.css('.wallet-view__address')).click()
await delay(100)
await captureLanguageScreenShots('metamask home - hover copy address')
async function captureLanguageScreenShots(label) {
const nonEnglishLocales = localesIndex.filter(localeMeta => localeMeta.code !== 'en')
// take english shot
await captureScreenShot(`${label} (en)`)
for (let localeMeta of nonEnglishLocales) {
// set locale and take shot
await setLocale(localeMeta.code)
await delay(300)
await captureScreenShot(`${label} (${localeMeta.code})`)
}
// return locale to english
await setLocale('en')
const primaryAddress = clipboardy.readSync()
await requestEther(primaryAddress)
// wait for block polling
await delay(10000)
await captureLanguageScreenShots('metamask home - has ether')
}
async function captureLanguageScreenShots(label) {
const nonEnglishLocales = localesIndex.filter(localeMeta => localeMeta.code !== 'en')
// take english shot
await captureScreenShot(`${label} (en)`)
for (let localeMeta of nonEnglishLocales) {
// set locale and take shot
await setLocale(localeMeta.code)
await delay(300)
await captureScreenShot(`${label} (${localeMeta.code})`)
}
// return locale to english
await setLocale('en')
await delay(300)
}
async function setLocale(code) {
await driver.executeScript('window.metamask.updateCurrentLocale(arguments[0])', code)
}
async function setLocale(code) {
await driver.executeScript('window.metamask.updateCurrentLocale(arguments[0])', code)
}
async function setProviderType(type) {
await driver.executeScript('window.metamask.setProviderType(arguments[0])', type)
}
async function setProviderType(type) {
await driver.executeScript('window.metamask.setProviderType(arguments[0])', type)
}
// cleanup
await driver.quit()
async function cleanScreenShotDir() {
await pify(rimraf)(`./test-artifacts/screens/`)
}
async function cleanScreenShotDir() {
await pify(rimraf)(`./test-artifacts/screens/`)
}
async function captureScreenShot(label) {
const shotIndex = screenshotCount.toString().padStart(4, '0')
screenshotCount++
const artifactDir = `./test-artifacts/screens/`
await pify(mkdirp)(artifactDir)
// capture screenshot
const screenshot = await driver.takeScreenshot()
await pify(fs.writeFile)(`${artifactDir}/${shotIndex} - ${label}.png`, screenshot, { encoding: 'base64' })
}
async function captureScreenShot(label) {
const shotIndex = screenshotCount.toString().padStart(4, '0')
screenshotCount++
const artifactDir = `./test-artifacts/screens/`
await pify(mkdirp)(artifactDir)
// capture screenshot
const screenshot = await driver.takeScreenshot()
await pify(fs.writeFile)(`${artifactDir}/${shotIndex} - ${label}.png`, screenshot, { encoding: 'base64' })
}
async function generateGif(){
// calculate screenshot size
const screenshot = await driver.takeScreenshot()
const pngBuffer = Buffer.from(screenshot, 'base64')
const size = sizeOfPng.calculate(pngBuffer)
async function generateGif(){
// calculate screenshot size
const screenshot = await driver.takeScreenshot()
const pngBuffer = Buffer.from(screenshot, 'base64')
const size = sizeOfPng.calculate(pngBuffer)
// read only the english pngs into gif
const encoder = new GIFEncoder(size.width, size.height)
const stream = pngFileStream('./test-artifacts/screens/* (en).png')
.pipe(encoder.createWriteStream({ repeat: 0, delay: 1000, quality: 10 }))
.pipe(fs.createWriteStream('./test-artifacts/screens/walkthrough (en).gif'))
// wait for end
await pify(endOfStream)(stream)
}
// read only the english pngs into gif
const encoder = new GIFEncoder(size.width, size.height)
const stream = pngFileStream('./test-artifacts/screens/* (en).png')
.pipe(encoder.createWriteStream({ repeat: 0, delay: 1000, quality: 10 }))
.pipe(fs.createWriteStream('./test-artifacts/screens/walkthrough (en).gif'))
// wait for end
await pify(endOfStream)(stream)
}
async function verboseReportOnFailure(test) {
@ -227,3 +257,8 @@ async function verboseReportOnFailure(test) {
const htmlSource = await driver.getPageSource()
await pify(fs.writeFile)(`${filepathBase}-dom.html`, htmlSource)
}
async function requestEther(address) {
const accounts = await eth.accounts()
await eth.sendTransaction({ from: accounts[0], to: address, value: 1 * 1e18, data: '0x0' })
}

File diff suppressed because it is too large Load Diff

View File

@ -1,35 +0,0 @@
// var jsdom = require('mocha-jsdom')
var assert = require('assert')
var freeze = require('deep-freeze-strict')
var path = require('path')
var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js'))
var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js'))
describe('SAVE_ACCOUNT_LABEL', function () {
it('updates the state.metamask.identities[:i].name property of the state to the action.value.label', function () {
var initialState = {
metamask: {
identities: {
foo: {
name: 'bar',
},
},
},
}
freeze(initialState)
const action = {
type: actions.SAVE_ACCOUNT_LABEL,
value: {
account: 'foo',
label: 'baz',
},
}
freeze(action)
var resultingState = reducers(initialState, action)
assert.equal(resultingState.metamask.identities.foo.name, action.value.label)
})
})

View File

@ -0,0 +1,34 @@
const assert = require('assert')
const freeze = require('deep-freeze-strict')
const path = require('path')
const actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js'))
const reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js'))
describe('SET_ACCOUNT_LABEL', function () {
it('updates the state.metamask.identities[:i].name property of the state to the action.value.label', function () {
const initialState = {
metamask: {
identities: {
foo: {
name: 'bar',
},
},
},
}
freeze(initialState)
const action = {
type: actions.SET_ACCOUNT_LABEL,
value: {
account: 'foo',
label: 'baz',
},
}
freeze(action)
const resultingState = reducers(initialState, action)
assert.equal(resultingState.metamask.identities.foo.name, action.value.label)
})
})

View File

@ -9,7 +9,7 @@ var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'redu
describe('tx confirmation screen', function () {
beforeEach(function () {
this.sinon = sinon.sandbox.create()
this.sinon = sinon.createSandbox()
})
afterEach(function () {

View File

@ -1,5 +1,5 @@
const assert = require('assert')
const ComposableObservableStore = require('../../app/scripts/lib/ComposableObservableStore')
const ComposableObservableStore = require('../../../app/scripts/lib/ComposableObservableStore')
const ObservableStore = require('obs-store')
describe('ComposableObservableStore', () => {

View File

@ -0,0 +1,31 @@
const assert = require('assert')
const path = require('path')
const accountImporter = require('../../../app/scripts/account-import-strategies/index')
const ethUtil = require('ethereumjs-util')
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))
})
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')
}
})
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')
})
})

View File

@ -0,0 +1,48 @@
const assert = require('assert')
const getBuyEthUrl = require('../../../app/scripts/lib/buy-eth-url')
describe('', function () {
const mainnet = {
network: '1',
amount: 5,
address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
}
const ropsten = {
network: '3',
}
const rinkeby = {
network: '4',
}
const kovan = {
network: '42',
}
it('returns coinbase url with amount and address for network 1', function () {
const coinbaseUrl = getBuyEthUrl(mainnet)
const coinbase = coinbaseUrl.match(/(https:\/\/buy.coinbase.com)/)
const amount = coinbaseUrl.match(/(amount)\D\d/)
const address = coinbaseUrl.match(/(address)(.*)(?=&)/)
assert.equal(coinbase[0], 'https://buy.coinbase.com')
assert.equal(amount[0], 'amount=5')
assert.equal(address[0], 'address=0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc')
})
it('returns metamask ropsten faucet for network 3', function () {
const ropstenUrl = getBuyEthUrl(ropsten)
assert.equal(ropstenUrl, 'https://faucet.metamask.io/')
})
it('returns rinkeby dapp for network 4', function () {
const rinkebyUrl = getBuyEthUrl(rinkeby)
assert.equal(rinkebyUrl, 'https://www.rinkeby.io/')
})
it('returns kovan github test faucet for network 42', function () {
const kovanUrl = getBuyEthUrl(kovan)
assert.equal(kovanUrl, 'https://github.com/kovan-testnet/faucet')
})
})

View File

@ -1,26 +1,26 @@
const assert = require('assert')
const AddressBookController = require('../../app/scripts/controllers/address-book')
const AddressBookController = require('../../../../app/scripts/controllers/address-book')
const mockKeyringController = {
memStore: {
getState: function () {
return {
identities: {
'0x0aaa': {
address: '0x0aaa',
name: 'owned',
},
const stubPreferencesStore = {
getState: function () {
return {
identities: {
'0x0aaa': {
address: '0x0aaa',
name: 'owned',
},
}
},
},
}
},
}
};
describe('address-book-controller', function () {
var addressBookController
beforeEach(function () {
addressBookController = new AddressBookController({}, mockKeyringController)
addressBookController = new AddressBookController({
preferencesStore: stubPreferencesStore,
})
})
describe('addres book management', function () {

View File

@ -1,5 +1,5 @@
const assert = require('assert')
const BlacklistController = require('../../app/scripts/controllers/blacklist')
const BlacklistController = require('../../../../app/scripts/controllers/blacklist')
describe('blacklist controller', function () {
let blacklistController

View File

@ -3,7 +3,7 @@ global.fetch = global.fetch || require('isomorphic-fetch')
const assert = require('assert')
const nock = require('nock')
const CurrencyController = require('../../app/scripts/controllers/currency')
const CurrencyController = require('../../../../app/scripts/controllers/currency')
describe('currency-controller', function () {
var currencyController
@ -45,7 +45,6 @@ describe('currency-controller', function () {
currencyController.updateConversionRate()
.then(function () {
var result = currencyController.getConversionRate()
console.log('currencyController.getConversionRate:', result)
assert.equal(typeof result, 'number')
done()
}).catch(function (err) {

View File

@ -1,6 +1,6 @@
const assert = require('assert')
const sinon = require('sinon')
const InfuraController = require('../../app/scripts/controllers/infura')
const InfuraController = require('../../../../app/scripts/controllers/infura')
describe('infura-controller', function () {
let infuraController, sandbox, networkStatus
@ -8,7 +8,7 @@ describe('infura-controller', function () {
before(async function () {
infuraController = new InfuraController()
sandbox = sinon.sandbox.create()
sandbox = sinon.createSandbox()
sinon.stub(infuraController, 'checkInfuraNetworkStatus').resolves(response)
networkStatus = await infuraController.checkInfuraNetworkStatus()
})

View File

@ -0,0 +1,550 @@
const assert = require('assert')
const sinon = require('sinon')
const clone = require('clone')
const nock = require('nock')
const createThoughStream = require('through2').obj
const MetaMaskController = require('../../../../app/scripts/metamask-controller')
const blacklistJSON = require('eth-phishing-detect/src/config')
const firstTimeState = require('../../../../app/scripts/first-time-state')
const currentNetworkId = 42
const DEFAULT_LABEL = 'Account 1'
const TEST_SEED = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium'
const TEST_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'
const TEST_SEED_ALT = 'setup olympic issue mobile velvet surge alcohol burger horse view reopen gentle'
const TEST_ADDRESS_ALT = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'
describe('MetaMaskController', function () {
let metamaskController
const sandbox = sinon.createSandbox()
const noop = () => {}
beforeEach(function () {
nock('https://api.infura.io')
.persist()
.get('/v2/blacklist')
.reply(200, blacklistJSON)
nock('https://api.infura.io')
.get('/v1/ticker/ethusd')
.reply(200, '{"base": "ETH", "quote": "USD", "bid": 288.45, "ask": 288.46, "volume": 112888.17569277, "exchange": "bitfinex", "total_volume": 272175.00106721005, "num_exchanges": 8, "timestamp": 1506444677}')
nock('https://api.infura.io')
.get('/v1/ticker/ethjpy')
.reply(200, '{"base": "ETH", "quote": "JPY", "bid": 32300.0, "ask": 32400.0, "volume": 247.4616071, "exchange": "kraken", "total_volume": 247.4616071, "num_exchanges": 1, "timestamp": 1506444676}')
nock('https://api.infura.io')
.persist()
.get(/.*/)
.reply(200)
metamaskController = new MetaMaskController({
showUnapprovedTx: noop,
showUnconfirmedMessage: noop,
encryptor: {
encrypt: function (password, object) {
this.object = object
return Promise.resolve()
},
decrypt: function () {
return Promise.resolve(this.object)
},
},
initState: clone(firstTimeState),
})
sandbox.spy(metamaskController.keyringController, 'createNewVaultAndKeychain')
sandbox.spy(metamaskController.keyringController, 'createNewVaultAndRestore')
})
afterEach(function () {
nock.cleanAll()
sandbox.restore()
})
describe('#getGasPrice', function () {
it('gives the 50th percentile lowest accepted gas price from recentBlocksController', async function () {
const realRecentBlocksController = metamaskController.recentBlocksController
metamaskController.recentBlocksController = {
store: {
getState: () => {
return {
recentBlocks: [
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] },
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] },
{ gasPrices: [ '0x174876e800', '0x174876e800' ]},
{ gasPrices: [ '0x174876e800', '0x174876e800' ]},
],
}
},
},
}
const gasPrice = metamaskController.getGasPrice()
assert.equal(gasPrice, '0x3b9aca00', 'accurately estimates 50th percentile accepted gas price')
metamaskController.recentBlocksController = realRecentBlocksController
})
})
describe('#createNewVaultAndKeychain', function () {
it('can only create new vault on keyringController once', async function () {
const selectStub = sandbox.stub(metamaskController, 'selectFirstIdentity')
const password = 'a-fake-password'
await metamaskController.createNewVaultAndKeychain(password)
await metamaskController.createNewVaultAndKeychain(password)
assert(metamaskController.keyringController.createNewVaultAndKeychain.calledOnce)
selectStub.reset()
})
})
describe('#createNewVaultAndRestore', function () {
it('should be able to call newVaultAndRestore despite a mistake.', async function () {
const password = 'what-what-what'
await metamaskController.createNewVaultAndRestore(password, TEST_SEED.slice(0, -1)).catch((e) => null)
await metamaskController.createNewVaultAndRestore(password, TEST_SEED)
assert(metamaskController.keyringController.createNewVaultAndRestore.calledTwice)
})
it('should clear previous identities after vault restoration', async () => {
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED)
assert.deepEqual(metamaskController.getState().identities, {
[TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL },
})
await metamaskController.preferencesController.setAccountLabel(TEST_ADDRESS, 'Account Foo')
assert.deepEqual(metamaskController.getState().identities, {
[TEST_ADDRESS]: { address: TEST_ADDRESS, name: 'Account Foo' },
})
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT)
assert.deepEqual(metamaskController.getState().identities, {
[TEST_ADDRESS_ALT]: { address: TEST_ADDRESS_ALT, name: DEFAULT_LABEL },
})
})
})
describe('#getApi', function () {
let getApi, state
beforeEach(function () {
getApi = metamaskController.getApi()
})
it('getState', function (done) {
getApi.getState((err, res) => {
if (err) {
done(err)
} else {
state = res
}
})
assert.deepEqual(state, metamaskController.getState())
done()
})
})
describe('preferencesController', function () {
it('defaults useBlockie to false', function () {
assert.equal(metamaskController.preferencesController.store.getState().useBlockie, false)
})
it('setUseBlockie to true', function () {
metamaskController.setUseBlockie(true, noop)
assert.equal(metamaskController.preferencesController.store.getState().useBlockie, true)
})
})
describe('#selectFirstIdentity', function () {
let identities, address
beforeEach(function () {
address = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'
identities = {
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': {
'address': address,
'name': 'Account 1',
},
'0xc42edfcc21ed14dda456aa0756c153f7985d8813': {
'address': '0xc42edfcc21ed14dda456aa0756c153f7985d8813',
'name': 'Account 2',
},
}
metamaskController.preferencesController.store.updateState({ identities })
metamaskController.selectFirstIdentity()
})
it('changes preferences controller select address', function () {
const preferenceControllerState = metamaskController.preferencesController.store.getState()
assert.equal(preferenceControllerState.selectedAddress, address)
})
it('changes metamask controller selected address', function () {
const metamaskState = metamaskController.getState()
assert.equal(metamaskState.selectedAddress, address)
})
})
describe('#setCustomRpc', function () {
const customRPC = 'https://custom.rpc/'
let rpcTarget
beforeEach(function () {
nock('https://custom.rpc')
.post('/')
.reply(200)
rpcTarget = metamaskController.setCustomRpc(customRPC)
})
afterEach(function () {
nock.cleanAll()
})
it('returns custom RPC that when called', async function () {
assert.equal(await rpcTarget, customRPC)
})
it('changes the network controller rpc', function () {
const networkControllerState = metamaskController.networkController.store.getState()
assert.equal(networkControllerState.provider.rpcTarget, customRPC)
})
})
describe('#setCurrentCurrency', function () {
let defaultMetaMaskCurrency
beforeEach(function () {
defaultMetaMaskCurrency = metamaskController.currencyController.getCurrentCurrency()
})
it('defaults to usd', function () {
assert.equal(defaultMetaMaskCurrency, 'usd')
})
it('sets currency to JPY', function () {
metamaskController.setCurrentCurrency('JPY', noop)
assert.equal(metamaskController.currencyController.getCurrentCurrency(), 'JPY')
})
})
describe('#createShapeshifttx', function () {
let depositAddress, depositType, shapeShiftTxList
beforeEach(function () {
nock('https://shapeshift.io')
.get('/txStat/3EevLFfB4H4XMWQwYCgjLie1qCAGpd2WBc')
.reply(200, '{"status": "no_deposits", "address": "3EevLFfB4H4XMWQwYCgjLie1qCAGpd2WBc"}')
depositAddress = '3EevLFfB4H4XMWQwYCgjLie1qCAGpd2WBc'
depositType = 'ETH'
shapeShiftTxList = metamaskController.shapeshiftController.store.getState().shapeShiftTxList
})
it('creates a shapeshift tx', async function () {
metamaskController.createShapeShiftTx(depositAddress, depositType)
assert.equal(shapeShiftTxList[0].depositAddress, depositAddress)
})
})
describe('#addNewAccount', function () {
let addNewAccount
beforeEach(function () {
addNewAccount = metamaskController.addNewAccount()
})
it('errors when an primary keyring is does not exist', async function () {
try {
await addNewAccount
assert.equal(1 === 0)
} catch (e) {
assert.equal(e.message, 'MetamaskController - No HD Key Tree found')
}
})
})
describe('#verifyseedPhrase', function () {
let seedPhrase, getConfigSeed
it('errors when no keying is provided', async function () {
try {
await metamaskController.verifySeedPhrase()
} catch (error) {
assert.equal(error.message, 'MetamaskController - No HD Key Tree found')
}
})
beforeEach(async function () {
await metamaskController.createNewVaultAndKeychain('password')
seedPhrase = await metamaskController.verifySeedPhrase()
})
it('#placeSeedWords should match the initially created vault seed', function () {
metamaskController.placeSeedWords((err, result) => {
if (err) {
console.log(err)
} else {
getConfigSeed = metamaskController.configManager.getSeedWords()
assert.equal(result, seedPhrase)
assert.equal(result, getConfigSeed)
}
})
assert.equal(getConfigSeed, undefined)
})
it('#addNewAccount', async function () {
await metamaskController.addNewAccount()
const getAccounts = await metamaskController.keyringController.getAccounts()
assert.equal(getAccounts.length, 2)
})
})
describe('#resetAccount', function () {
beforeEach(function () {
const selectedAddressStub = sinon.stub(metamaskController.preferencesController, 'getSelectedAddress')
const getNetworkstub = sinon.stub(metamaskController.txController.txStateManager, 'getNetwork')
selectedAddressStub.returns('0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc')
getNetworkstub.returns(42)
metamaskController.txController.txStateManager._saveTxList([
{ id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'} },
{ id: 2, status: 'rejected', metamaskNetworkId: 32, txParams: {} },
{ id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {from: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4'} },
])
})
it('wipes transactions from only the correct network id and with the selected address', async function () {
await metamaskController.resetAccount()
assert.equal(metamaskController.txController.txStateManager.getTx(1), undefined)
})
})
describe('#clearSeedWordCache', function () {
it('should have set seed words', function () {
metamaskController.configManager.setSeedWords('test words')
const getConfigSeed = metamaskController.configManager.getSeedWords()
assert.equal(getConfigSeed, 'test words')
})
it('should clear config seed phrase', function () {
metamaskController.configManager.setSeedWords('test words')
metamaskController.clearSeedWordCache((err, result) => {
if (err) console.log(err)
})
const getConfigSeed = metamaskController.configManager.getSeedWords()
assert.equal(getConfigSeed, null)
})
})
describe('#setCurrentLocale', function () {
it('checks the default currentLocale', function () {
const preferenceCurrentLocale = metamaskController.preferencesController.store.getState().currentLocale
assert.equal(preferenceCurrentLocale, undefined)
})
it('sets current locale in preferences controller', function () {
metamaskController.setCurrentLocale('ja', noop)
const preferenceCurrentLocale = metamaskController.preferencesController.store.getState().currentLocale
assert.equal(preferenceCurrentLocale, 'ja')
})
})
describe('#newUnsignedMessage', function () {
let msgParams, metamaskMsgs, messages, msgId
const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'
const data = '0x43727970746f6b697474696573'
beforeEach(async function () {
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT)
msgParams = {
'from': address,
'data': data,
}
metamaskController.newUnsignedMessage(msgParams, noop)
metamaskMsgs = metamaskController.messageManager.getUnapprovedMsgs()
messages = metamaskController.messageManager.messages
msgId = Object.keys(metamaskMsgs)[0]
messages[0].msgParams.metamaskId = parseInt(msgId)
})
it('persists address from msg params', function () {
assert.equal(metamaskMsgs[msgId].msgParams.from, address)
})
it('persists data from msg params', function () {
assert.equal(metamaskMsgs[msgId].msgParams.data, data)
})
it('sets the status to unapproved', function () {
assert.equal(metamaskMsgs[msgId].status, 'unapproved')
})
it('sets the type to eth_sign', function () {
assert.equal(metamaskMsgs[msgId].type, 'eth_sign')
})
it('rejects the message', function () {
const msgIdInt = parseInt(msgId)
metamaskController.cancelMessage(msgIdInt, noop)
assert.equal(messages[0].status, 'rejected')
})
it('errors when signing a message', async function () {
try {
await metamaskController.signMessage(messages[0].msgParams)
} catch (error) {
assert.equal(error.message, 'message length is invalid')
}
})
})
describe('#newUnsignedPersonalMessage', function () {
it('errors with no from in msgParams', function () {
const msgParams = {
'data': data,
}
metamaskController.newUnsignedPersonalMessage(msgParams, function (error) {
assert.equal(error.message, 'MetaMask Message Signature: from field is required.')
})
})
let msgParams, metamaskPersonalMsgs, personalMessages, msgId
const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'
const data = '0x43727970746f6b697474696573'
beforeEach(async function () {
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT)
msgParams = {
'from': address,
'data': data,
}
metamaskController.newUnsignedPersonalMessage(msgParams, noop)
metamaskPersonalMsgs = metamaskController.personalMessageManager.getUnapprovedMsgs()
personalMessages = metamaskController.personalMessageManager.messages
msgId = Object.keys(metamaskPersonalMsgs)[0]
personalMessages[0].msgParams.metamaskId = parseInt(msgId)
})
it('persists address from msg params', function () {
assert.equal(metamaskPersonalMsgs[msgId].msgParams.from, address)
})
it('persists data from msg params', function () {
assert.equal(metamaskPersonalMsgs[msgId].msgParams.data, data)
})
it('sets the status to unapproved', function () {
assert.equal(metamaskPersonalMsgs[msgId].status, 'unapproved')
})
it('sets the type to personal_sign', function () {
assert.equal(metamaskPersonalMsgs[msgId].type, 'personal_sign')
})
it('rejects the message', function () {
const msgIdInt = parseInt(msgId)
metamaskController.cancelPersonalMessage(msgIdInt, noop)
assert.equal(personalMessages[0].status, 'rejected')
})
it('errors when signing a message', async function () {
await metamaskController.signPersonalMessage(personalMessages[0].msgParams)
assert.equal(metamaskPersonalMsgs[msgId].status, 'signed')
assert.equal(metamaskPersonalMsgs[msgId].rawSig, '0x6a1b65e2b8ed53cf398a769fad24738f9fbe29841fe6854e226953542c4b6a173473cb152b6b1ae5f06d601d45dd699a129b0a8ca84e78b423031db5baa734741b')
})
})
describe('#setupUntrustedCommunication', function () {
let streamTest
const phishingUrl = 'decentral.market'
afterEach(function () {
streamTest.end()
})
it('sets up phishing stream for untrusted communication ', async function () {
await metamaskController.blacklistController.updatePhishingList()
streamTest = createThoughStream((chunk, enc, cb) => {
assert.equal(chunk.name, 'phishing')
assert.equal(chunk.data.hostname, phishingUrl)
cb()
})
// console.log(streamTest)
metamaskController.setupUntrustedCommunication(streamTest, phishingUrl)
})
})
describe('#setupTrustedCommunication', function () {
let streamTest
afterEach(function () {
streamTest.end()
})
it('sets up controller dnode api for trusted communication', function (done) {
streamTest = createThoughStream((chunk, enc, cb) => {
assert.equal(chunk.name, 'controller')
cb()
done()
})
metamaskController.setupTrustedCommunication(streamTest, 'mycrypto.com')
})
})
describe('#markAccountsFound', function () {
it('adds lost accounts to config manager data', function () {
metamaskController.markAccountsFound(noop)
const configManagerData = metamaskController.configManager.getData()
assert.deepEqual(configManagerData.lostAccounts, [])
})
})
describe('#markPasswordForgotten', function () {
it('adds and sets forgottenPassword to config data to true', function () {
metamaskController.markPasswordForgotten(noop)
const configManagerData = metamaskController.configManager.getData()
assert.equal(configManagerData.forgottenPassword, true)
})
})
describe('#unMarkPasswordForgotten', function () {
it('adds and sets forgottenPassword to config data to false', function () {
metamaskController.unMarkPasswordForgotten(noop)
const configManagerData = metamaskController.configManager.getData()
assert.equal(configManagerData.forgottenPassword, false)
})
})
})

View File

@ -1,11 +1,11 @@
const assert = require('assert')
const nock = require('nock')
const NetworkController = require('../../app/scripts/controllers/network')
const NetworkController = require('../../../../app/scripts/controllers/network')
const {
getNetworkDisplayName,
} = require('../../app/scripts/controllers/network/util')
} = require('../../../../app/scripts/controllers/network/util')
const { createTestProviderTools } = require('../stub/provider')
const { createTestProviderTools } = require('../../../stub/provider')
const providerResultStub = {}
describe('# Network Controller', function () {

View File

@ -1,6 +1,6 @@
const assert = require('assert')
const configManagerGen = require('../lib/mock-config-manager')
const NoticeController = require('../../app/scripts/notice-controller')
const configManagerGen = require('../../../lib/mock-config-manager')
const NoticeController = require('../../../../app/scripts/notice-controller')
describe('notice-controller', function () {
var noticeController

View File

@ -0,0 +1,162 @@
const assert = require('assert')
const PreferencesController = require('../../../../app/scripts/controllers/preferences')
describe('preferences controller', function () {
let preferencesController
beforeEach(() => {
preferencesController = new PreferencesController()
})
describe('setAddresses', function () {
it('should keep a map of addresses to names and addresses in the store', function () {
preferencesController.setAddresses([
'0xda22le',
'0x7e57e2',
])
const {identities} = preferencesController.store.getState()
assert.deepEqual(identities, {
'0xda22le': {
name: 'Account 1',
address: '0xda22le',
},
'0x7e57e2': {
name: 'Account 2',
address: '0x7e57e2',
},
})
})
it('should replace its list of addresses', function () {
preferencesController.setAddresses([
'0xda22le',
'0x7e57e2',
])
preferencesController.setAddresses([
'0xda22le77',
'0x7e57e277',
])
const {identities} = preferencesController.store.getState()
assert.deepEqual(identities, {
'0xda22le77': {
name: 'Account 1',
address: '0xda22le77',
},
'0x7e57e277': {
name: 'Account 2',
address: '0x7e57e277',
},
})
})
})
describe('setAccountLabel', function () {
it('should update a label for the given account', function () {
preferencesController.setAddresses([
'0xda22le',
'0x7e57e2',
])
assert.deepEqual(preferencesController.store.getState().identities['0xda22le'], {
name: 'Account 1',
address: '0xda22le',
})
preferencesController.setAccountLabel('0xda22le', 'Dazzle')
assert.deepEqual(preferencesController.store.getState().identities['0xda22le'], {
name: 'Dazzle',
address: '0xda22le',
})
})
})
describe('getTokens', function () {
it('should return an empty list initially', async function () {
await preferencesController.setSelectedAddress('0x7e57e2')
const tokens = preferencesController.getTokens()
assert.equal(tokens.length, 0, 'empty list of tokens')
})
})
describe('addToken', function () {
it('should add that token to its state', async function () {
const address = '0xabcdef1234567'
const symbol = 'ABBR'
const decimals = 5
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken(address, symbol, decimals)
const tokens = preferencesController.getTokens()
assert.equal(tokens.length, 1, 'one token added')
const added = tokens[0]
assert.equal(added.address, address, 'set address correctly')
assert.equal(added.symbol, symbol, 'set symbol correctly')
assert.equal(added.decimals, decimals, 'set decimals correctly')
})
it('should allow updating a token value', async function () {
const address = '0xabcdef1234567'
const symbol = 'ABBR'
const decimals = 5
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken(address, symbol, decimals)
const newDecimals = 6
await preferencesController.addToken(address, symbol, newDecimals)
const tokens = preferencesController.getTokens()
assert.equal(tokens.length, 1, 'one token added')
const added = tokens[0]
assert.equal(added.address, address, 'set address correctly')
assert.equal(added.symbol, symbol, 'set symbol correctly')
assert.equal(added.decimals, newDecimals, 'updated decimals correctly')
})
it('should allow adding tokens to two separate addresses', async function () {
const address = '0xabcdef1234567'
const symbol = 'ABBR'
const decimals = 5
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken(address, symbol, decimals)
assert.equal(preferencesController.getTokens().length, 1, 'one token added for 1st address')
await preferencesController.setSelectedAddress('0xda22le')
await preferencesController.addToken(address, symbol, decimals)
assert.equal(preferencesController.getTokens().length, 1, 'one token added for 2nd address')
})
})
describe('removeToken', function () {
it('should remove the only token from its state', async function () {
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken('0xa', 'A', 5)
await preferencesController.removeToken('0xa')
const tokens = preferencesController.getTokens()
assert.equal(tokens.length, 0, 'one token removed')
})
it('should remove a token from its state', async function () {
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
await preferencesController.removeToken('0xa')
const tokens = preferencesController.getTokens()
assert.equal(tokens.length, 1, 'one token removed')
const [token1] = tokens
assert.deepEqual(token1, {address: '0xb', symbol: 'B', decimals: 5})
})
})
})

View File

@ -1,6 +1,6 @@
const assert = require('assert')
const sinon = require('sinon')
const TokenRatesController = require('../../app/scripts/controllers/token-rates')
const TokenRatesController = require('../../../../app/scripts/controllers/token-rates')
const ObservableStore = require('obs-store')
describe('TokenRatesController', () => {

View File

@ -1,6 +1,6 @@
const assert = require('assert')
const NonceTracker = require('../../app/scripts/controllers/transactions/nonce-tracker')
const MockTxGen = require('../lib/mock-tx-gen')
const NonceTracker = require('../../../../../app/scripts/controllers/transactions/nonce-tracker')
const MockTxGen = require('../../../../lib/mock-tx-gen')
let providerResultStub = {}
describe('Nonce Tracker', function () {

View File

@ -3,9 +3,9 @@ const ethUtil = require('ethereumjs-util')
const EthTx = require('ethereumjs-tx')
const ObservableStore = require('obs-store')
const clone = require('clone')
const { createTestProviderTools } = require('../stub/provider')
const PendingTransactionTracker = require('../../app/scripts/controllers/transactions/pending-tx-tracker')
const MockTxGen = require('../lib/mock-tx-gen')
const { createTestProviderTools } = require('../../../../stub/provider')
const PendingTransactionTracker = require('../../../../../app/scripts/controllers/transactions/pending-tx-tracker')
const MockTxGen = require('../../../../lib/mock-tx-gen')
const sinon = require('sinon')
const noop = () => true
const currentNetworkId = 42
@ -294,7 +294,7 @@ describe('PendingTransactionTracker', function () {
})
afterEach(() => {
pendingTxTracker.publishTransaction.reset()
pendingTxTracker.publishTransaction.restore()
})
it('should publish the transaction', function (done) {

View File

@ -4,9 +4,9 @@ const EthTx = require('ethereumjs-tx')
const EthjsQuery = require('ethjs-query')
const ObservableStore = require('obs-store')
const sinon = require('sinon')
const TransactionController = require('../../app/scripts/controllers/transactions')
const TxGasUtils = require('../../app/scripts/controllers/transactions/tx-gas-utils')
const { createTestProviderTools, getTestAccounts } = require('../stub/provider')
const TransactionController = require('../../../../../app/scripts/controllers/transactions')
const TxGasUtils = require('../../../../../app/scripts/controllers/transactions/tx-gas-utils')
const { createTestProviderTools, getTestAccounts } = require('../../../../stub/provider')
const noop = () => true
const currentNetworkId = 42
@ -40,36 +40,6 @@ describe('Transaction Controller', function () {
txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop })
})
describe('#isNonceTaken', function () {
it('should return true', function (done) {
txController.txStateManager._saveTxList([
{ id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} },
{ id: 2, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} },
{ id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} },
])
txController.isNonceTaken({txParams: {nonce:0, from:'0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'}})
.then((isNonceTaken) => {
assert(isNonceTaken)
done()
}).catch(done)
})
it('should return false', function (done) {
txController.txStateManager._saveTxList([
{ id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} },
{ id: 2, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} },
{ id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} },
])
txController.isNonceTaken({txParams: {nonce:0, from:'0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'}})
.then((isNonceTaken) => {
assert(!isNonceTaken)
done()
}).catch(done)
})
})
describe('#getState', function () {
it('should return a state object with the right keys and datat types', function () {
const exposedState = txController.getState()

View File

@ -3,8 +3,8 @@ const Transaction = require('ethereumjs-tx')
const BN = require('bn.js')
const { hexToBn, bnToHex } = require('../../app/scripts/lib/util')
const TxUtils = require('../../app/scripts/controllers/transactions/tx-gas-utils')
const { hexToBn, bnToHex } = require('../../../../../app/scripts/lib/util')
const TxUtils = require('../../../../../app/scripts/controllers/transactions/tx-gas-utils')
describe('txUtils', function () {

View File

@ -1,5 +1,5 @@
const assert = require('assert')
const txHelper = require('../../ui/lib/tx-helper')
const txHelper = require('../../../../../ui/lib/tx-helper')
describe('txHelper', function () {
it('always shows the oldest tx first', function () {

View File

@ -1,6 +1,6 @@
const assert = require('assert')
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
const testVault = require('../data/v17-long-history.json')
const txStateHistoryHelper = require('../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
const testVault = require('../../../../data/v17-long-history.json')
describe ('Transaction state history helper', function () {

View File

@ -1,8 +1,8 @@
const assert = require('assert')
const clone = require('clone')
const ObservableStore = require('obs-store')
const TxStateManager = require('../../app/scripts/controllers/transactions/tx-state-manager')
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
const TxStateManager = require('../../../../../app/scripts/controllers/transactions/tx-state-manager')
const txStateHistoryHelper = require('../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
const noop = () => true
describe('TransactionStateManager', function () {

View File

@ -1,5 +1,5 @@
const assert = require('assert')
const txUtils = require('../../app/scripts/controllers/transactions/lib/util')
const txUtils = require('../../../../../app/scripts/controllers/transactions/lib/util')
describe('txUtils', function () {

View File

@ -1,6 +1,6 @@
const assert = require('assert')
const EdgeEncryptor = require('../../app/scripts/edge-encryptor')
const EdgeEncryptor = require('../../../app/scripts/edge-encryptor')
var password = 'passw0rd1'
var data = 'some random data'

View File

@ -1,5 +1,5 @@
const assert = require('assert')
const MessageManager = require('../../app/scripts/lib/message-manager')
const MessageManager = require('../../../app/scripts/lib/message-manager')
describe('Message Manager', function () {
let messageManager

View File

@ -1,5 +1,5 @@
const assert = require('assert')
const nodeify = require('../../app/scripts/lib/nodeify')
const nodeify = require('../../../app/scripts/lib/nodeify')
describe('nodeify', function () {
var obj = {

View File

@ -1,6 +1,6 @@
const assert = require('assert')
const PendingBalanceCalculator = require('../../app/scripts/lib/pending-balance-calculator')
const MockTxGen = require('../lib/mock-tx-gen')
const PendingBalanceCalculator = require('../../../app/scripts/lib/pending-balance-calculator')
const MockTxGen = require('../../lib/mock-tx-gen')
const BN = require('ethereumjs-util').BN
let providerResultStub = {}

View File

@ -1,6 +1,6 @@
const assert = require('assert')
const PersonalMessageManager = require('../../app/scripts/lib/personal-message-manager')
const PersonalMessageManager = require('../../../app/scripts/lib/personal-message-manager')
describe('Personal Message Manager', function () {
let messageManager

View File

@ -1,9 +1,9 @@
const assert = require('assert')
const clone = require('clone')
const KeyringController = require('eth-keyring-controller')
const firstTimeState = require('../../app/scripts/first-time-state')
const seedPhraseVerifier = require('../../app/scripts/lib/seed-phrase-verifier')
const mockEncryptor = require('../lib/mock-encryptor')
const firstTimeState = require('../../../app/scripts/first-time-state')
const seedPhraseVerifier = require('../../../app/scripts/lib/seed-phrase-verifier')
const mockEncryptor = require('../../lib/mock-encryptor')
describe('SeedPhraseVerifier', function () {

View File

@ -1,5 +1,5 @@
const assert = require('assert')
const { sufficientBalance } = require('../../app/scripts/lib/util')
const { sufficientBalance } = require('../../../app/scripts/lib/util')
describe('SufficientBalance', function () {

View File

@ -1,120 +0,0 @@
const assert = require('assert')
const sinon = require('sinon')
const clone = require('clone')
const nock = require('nock')
const MetaMaskController = require('../../app/scripts/metamask-controller')
const blacklistJSON = require('../stub/blacklist')
const firstTimeState = require('../../app/scripts/first-time-state')
const DEFAULT_LABEL = 'Account 1'
const TEST_SEED = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium'
const TEST_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'
const TEST_SEED_ALT = 'setup olympic issue mobile velvet surge alcohol burger horse view reopen gentle'
const TEST_ADDRESS_ALT = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'
describe('MetaMaskController', function () {
let metamaskController
const sandbox = sinon.sandbox.create()
const noop = () => { }
beforeEach(function () {
nock('https://api.infura.io')
.persist()
.get('/v2/blacklist')
.reply(200, blacklistJSON)
nock('https://api.infura.io')
.persist()
.get(/.*/)
.reply(200)
metamaskController = new MetaMaskController({
showUnapprovedTx: noop,
encryptor: {
encrypt: function (password, object) {
this.object = object
return Promise.resolve()
},
decrypt: function () {
return Promise.resolve(this.object)
},
},
initState: clone(firstTimeState),
})
sandbox.spy(metamaskController.keyringController, 'createNewVaultAndKeychain')
sandbox.spy(metamaskController.keyringController, 'createNewVaultAndRestore')
})
afterEach(function () {
nock.cleanAll()
sandbox.restore()
})
describe('#getGasPrice', function () {
it('gives the 50th percentile lowest accepted gas price from recentBlocksController', async function () {
const realRecentBlocksController = metamaskController.recentBlocksController
metamaskController.recentBlocksController = {
store: {
getState: () => {
return {
recentBlocks: [
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] },
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] },
{ gasPrices: [ '0x174876e800', '0x174876e800' ]},
{ gasPrices: [ '0x174876e800', '0x174876e800' ]},
],
}
},
},
}
const gasPrice = metamaskController.getGasPrice()
assert.equal(gasPrice, '0x3b9aca00', 'accurately estimates 50th percentile accepted gas price')
metamaskController.recentBlocksController = realRecentBlocksController
})
})
describe('#createNewVaultAndKeychain', function () {
it('can only create new vault on keyringController once', async function () {
const selectStub = sandbox.stub(metamaskController, 'selectFirstIdentity')
const password = 'a-fake-password'
await metamaskController.createNewVaultAndKeychain(password)
await metamaskController.createNewVaultAndKeychain(password)
assert(metamaskController.keyringController.createNewVaultAndKeychain.calledOnce)
selectStub.reset()
})
})
describe('#createNewVaultAndRestore', function () {
it('should be able to call newVaultAndRestore despite a mistake.', async function () {
const password = 'what-what-what'
await metamaskController.createNewVaultAndRestore(password, TEST_SEED.slice(0, -1)).catch((e) => null)
await metamaskController.createNewVaultAndRestore(password, TEST_SEED)
assert(metamaskController.keyringController.createNewVaultAndRestore.calledTwice)
})
it('should clear previous identities after vault restoration', async () => {
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED)
assert.deepEqual(metamaskController.getState().identities, {
[TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL },
})
await metamaskController.keyringController.saveAccountLabel(TEST_ADDRESS, 'Account Foo')
assert.deepEqual(metamaskController.getState().identities, {
[TEST_ADDRESS]: { address: TEST_ADDRESS, name: 'Account Foo' },
})
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT)
assert.deepEqual(metamaskController.getState().identities, {
[TEST_ADDRESS_ALT]: { address: TEST_ADDRESS_ALT, name: DEFAULT_LABEL },
})
})
})
})

View File

@ -0,0 +1,41 @@
const assert = require('assert')
const migration26 = require('../../../app/scripts/migrations/026')
const oldStorage = {
'meta': {'version': 25},
'data': {
'PreferencesController': {},
'KeyringController': {
'walletNicknames': {
'0x1e77e2': 'Test Account 1',
'0x7e57e2': 'Test Account 2',
},
},
},
}
describe('migration #26', () => {
it('should move the identities from KeyringController', (done) => {
migration26.migrate(oldStorage)
.then((newStorage) => {
const identities = newStorage.data.PreferencesController.identities
assert.deepEqual(identities, {
'0x1e77e2': {name: 'Test Account 1', address: '0x1e77e2'},
'0x7e57e2': {name: 'Test Account 2', address: '0x7e57e2'},
})
assert.strictEqual(newStorage.data.KeyringController.walletNicknames, undefined)
done()
})
.catch(done)
})
it('should successfully migrate first time state', (done) => {
migration26.migrate({
meta: {},
data: require('../../../app/scripts/first-time-state'),
})
.then((migratedData) => {
assert.equal(migratedData.meta.version, migration26.version)
done()
}).catch(done)
})
})

View File

@ -1,22 +1,22 @@
const assert = require('assert')
const path = require('path')
const wallet1 = require(path.join('..', 'lib', 'migrations', '001.json'))
const vault4 = require(path.join('..', 'lib', 'migrations', '004.json'))
const wallet1 = require(path.join('..', '..', 'lib', 'migrations', '001.json'))
const vault4 = require(path.join('..', '..', 'lib', 'migrations', '004.json'))
let vault5, vault6, vault7, vault8, vault9 // vault10, vault11
const migration2 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '002'))
const migration3 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '003'))
const migration4 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '004'))
const migration5 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '005'))
const migration6 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '006'))
const migration7 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '007'))
const migration8 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '008'))
const migration9 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '009'))
const migration10 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '010'))
const migration11 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '011'))
const migration12 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '012'))
const migration13 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '013'))
const migration2 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '002'))
const migration3 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '003'))
const migration4 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '004'))
const migration5 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '005'))
const migration6 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '006'))
const migration7 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '007'))
const migration8 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '008'))
const migration9 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '009'))
const migration10 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '010'))
const migration11 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '011'))
const migration12 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '012'))
const migration13 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '013'))
const oldTestRpc = 'https://rawtestrpc.metamask.io/'

View File

@ -1,7 +1,7 @@
const assert = require('assert')
const clone = require('clone')
const Migrator = require('../../app/scripts/lib/migrator/')
const liveMigrations = require('../../app/scripts/migrations/')
const Migrator = require('../../../app/scripts/lib/migrator/')
const liveMigrations = require('../../../app/scripts/migrations/')
const stubMigrations = [
{
version: 1,
@ -33,7 +33,7 @@ const versionedData = {meta: {version: 0}, data: {hello: 'world'}}
const firstTimeState = {
meta: { version: 0 },
data: require('../../app/scripts/first-time-state'),
data: require('../../../app/scripts/first-time-state'),
}
describe('Migrator', () => {

View File

@ -6,7 +6,7 @@ var contractNamer = require(path.join(__dirname, '..', '..', 'old-ui', 'lib', 'c
describe('contractNamer', function () {
beforeEach(function () {
this.sinon = sinon.sandbox.create()
this.sinon = sinon.createSandbox()
})
afterEach(function () {

View File

@ -1,48 +0,0 @@
const assert = require('assert')
const PreferencesController = require('../../app/scripts/controllers/preferences')
describe('preferences controller', function () {
let preferencesController
before(() => {
preferencesController = new PreferencesController()
})
describe('addToken', function () {
it('should add that token to its state', async function () {
const address = '0xabcdef1234567'
const symbol = 'ABBR'
const decimals = 5
await preferencesController.addToken(address, symbol, decimals)
const tokens = preferencesController.getTokens()
assert.equal(tokens.length, 1, 'one token added')
const added = tokens[0]
assert.equal(added.address, address, 'set address correctly')
assert.equal(added.symbol, symbol, 'set symbol correctly')
assert.equal(added.decimals, decimals, 'set decimals correctly')
})
it('should allow updating a token value', async function () {
const address = '0xabcdef1234567'
const symbol = 'ABBR'
const decimals = 5
await preferencesController.addToken(address, symbol, decimals)
const newDecimals = 6
await preferencesController.addToken(address, symbol, newDecimals)
const tokens = preferencesController.getTokens()
assert.equal(tokens.length, 1, 'one token added')
const added = tokens[0]
assert.equal(added.address, address, 'set address correctly')
assert.equal(added.symbol, symbol, 'set symbol correctly')
assert.equal(added.decimals, newDecimals, 'updated decimals correctly')
})
})
})

View File

@ -10,7 +10,7 @@ var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'redu
describe('#unlockMetamask(selectedAccount)', function () {
beforeEach(function () {
// sinon allows stubbing methods that are easily verified
this.sinon = sinon.sandbox.create()
this.sinon = sinon.createSandbox()
})
afterEach(function () {

View File

@ -10,7 +10,7 @@ describe('util', function () {
for (var i = 0; i < 18; i++) { ethInWei += '0' }
beforeEach(function () {
this.sinon = sinon.sandbox.create()
this.sinon = sinon.createSandbox()
})
afterEach(function () {

View File

@ -24,7 +24,7 @@ function mapDispatchToProps (dispatch) {
dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' }))
},
hideModal: () => dispatch(actions.hideModal()),
saveAccountLabel: (address, label) => dispatch(actions.saveAccountLabel(address, label)),
setAccountLabel: (address, label) => dispatch(actions.setAccountLabel(address, label)),
}
}

View File

@ -124,8 +124,8 @@ var actions = {
SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY',
showPrivateKey: showPrivateKey,
exportAccountComplete,
SAVE_ACCOUNT_LABEL: 'SAVE_ACCOUNT_LABEL',
saveAccountLabel: saveAccountLabel,
SET_ACCOUNT_LABEL: 'SET_ACCOUNT_LABEL',
setAccountLabel,
// tx conf screen
COMPLETED_TX: 'COMPLETED_TX',
TRANSACTION_ERROR: 'TRANSACTION_ERROR',
@ -275,6 +275,10 @@ var actions = {
UPDATE_NETWORK_ENDPOINT_TYPE: 'UPDATE_NETWORK_ENDPOINT_TYPE',
retryTransaction,
SET_PENDING_TOKENS: 'SET_PENDING_TOKENS',
CLEAR_PENDING_TOKENS: 'CLEAR_PENDING_TOKENS',
setPendingTokens,
clearPendingTokens,
}
module.exports = actions
@ -1598,13 +1602,13 @@ function showPrivateKey (key) {
}
}
function saveAccountLabel (account, label) {
function setAccountLabel (account, label) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
log.debug(`background.saveAccountLabel`)
log.debug(`background.setAccountLabel`)
return new Promise((resolve, reject) => {
background.saveAccountLabel(account, label, (err) => {
background.setAccountLabel(account, label, (err) => {
dispatch(actions.hideLoadingIndication())
if (err) {
@ -1613,7 +1617,7 @@ function saveAccountLabel (account, label) {
}
dispatch({
type: actions.SAVE_ACCOUNT_LABEL,
type: actions.SET_ACCOUNT_LABEL,
value: { account, label },
})
@ -1929,3 +1933,22 @@ function updateNetworkEndpointType (networkEndpointType) {
value: networkEndpointType,
}
}
function setPendingTokens (pendingTokens) {
const { customToken = {}, selectedTokens = {} } = pendingTokens
const { address, symbol, decimals } = customToken
const tokens = address && symbol && decimals
? { ...selectedTokens, [address]: { ...customToken, isCustom: true } }
: selectedTokens
return {
type: actions.SET_PENDING_TOKENS,
payload: tokens,
}
}
function clearPendingTokens () {
return {
type: actions.CLEAR_PENDING_TOKENS,
}
}

View File

@ -26,6 +26,7 @@ const UnlockPage = require('./components/pages/unlock-page')
const RestoreVaultPage = require('./components/pages/keychains/restore-vault')
const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
const AddTokenPage = require('./components/pages/add-token')
const ConfirmAddTokenPage = require('./components/pages/confirm-add-token')
const CreateAccountPage = require('./components/pages/create-account')
const NoticeScreen = require('./components/pages/notice')
@ -47,6 +48,7 @@ const {
REVEAL_SEED_ROUTE,
RESTORE_VAULT_ROUTE,
ADD_TOKEN_ROUTE,
CONFIRM_ADD_TOKEN_ROUTE,
NEW_ACCOUNT_ROUTE,
SEND_ROUTE,
CONFIRM_TRANSACTION_ROUTE,
@ -77,6 +79,7 @@ class App extends Component {
h(Authenticated, { path: CONFIRM_TRANSACTION_ROUTE, component: ConfirmTxScreen }),
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen2 }),
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }),
h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }),
h(Authenticated, { path: DEFAULT_ROUTE, exact, component: Home }),
])

View File

@ -1,7 +1,6 @@
const { Component } = require('react')
const h = require('react-hyperscript')
const PropTypes = require('prop-types')
const classnames = require('classnames')
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
const SECONDARY = 'secondary'
const CLASSNAME_PRIMARY = 'btn-primary'
@ -24,10 +23,12 @@ class Button extends Component {
const { type, large, className, ...buttonProps } = this.props
return (
h('button', {
className: classnames(getClassName(type, large), className),
...buttonProps,
}, this.props.children)
<button
className={classnames(getClassName(type, large), className)}
{ ...buttonProps }
>
{ this.props.children }
</button>
)
}
}
@ -39,5 +40,5 @@ Button.propTypes = {
children: PropTypes.string,
}
module.exports = Button
export default Button

View File

@ -1,2 +1,2 @@
const Button = require('./button.component')
import Button from './button.component'
module.exports = Button

View File

@ -0,0 +1,5 @@
@import './export-text-container/index';
@import './info-box/index';
@import './pages/index';

View File

@ -0,0 +1,2 @@
import InfoBox from './info-box.component'
module.exports = InfoBox

View File

@ -0,0 +1,24 @@
.info-box {
border-radius: 4px;
background-color: $alabaster;
position: relative;
padding: 16px;
display: flex;
flex-flow: column;
color: $mid-gray;
&__close::after {
content: '\00D7';
font-size: 29px;
font-weight: 200;
color: $dusty-gray;
position: absolute;
right: 12px;
top: 0;
cursor: pointer;
}
&__description {
font-size: .75rem;
}
}

View File

@ -0,0 +1,49 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class InfoBox extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
onClose: PropTypes.func,
title: PropTypes.string,
description: PropTypes.string,
}
constructor (props) {
super(props)
this.state = {
isShowing: true,
}
}
handleClose () {
const { onClose } = this.props
if (onClose) {
onClose()
} else {
this.setState({ isShowing: false })
}
}
render () {
const { title, description } = this.props
return !this.state.isShowing
? null
: (
<div className="info-box">
<div
className="info-box__close"
onClick={() => this.handleClose()}
/>
<div className="info-box__title">{ title }</div>
<div className="info-box__description">{ description }</div>
</div>
)
}
}

View File

@ -25,7 +25,7 @@ function mapDispatchToProps (dispatch) {
dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' }))
},
hideModal: () => dispatch(actions.hideModal()),
saveAccountLabel: (address, label) => dispatch(actions.saveAccountLabel(address, label)),
setAccountLabel: (address, label) => dispatch(actions.setAccountLabel(address, label)),
}
}
@ -49,7 +49,7 @@ AccountDetailsModal.prototype.render = function () {
selectedIdentity,
network,
showExportPrivateKeyModal,
saveAccountLabel,
setAccountLabel,
} = this.props
const { name, address } = selectedIdentity
@ -57,7 +57,7 @@ AccountDetailsModal.prototype.render = function () {
h(EditableLabel, {
className: 'account-modal__name',
defaultValue: name,
onSubmit: label => saveAccountLabel(address, label),
onSubmit: label => setAccountLabel(address, label),
}),
h(QrView, {

View File

@ -18,8 +18,8 @@ function mapDispatchToProps (dispatch) {
hideModal: () => {
dispatch(actions.hideModal())
},
saveAccountLabel: (account, label) => {
dispatch(actions.saveAccountLabel(account, label))
setAccountLabel: (account, label) => {
dispatch(actions.setAccountLabel(account, label))
},
}
}
@ -41,7 +41,7 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(EditAccountNameMod
EditAccountNameModal.prototype.render = function () {
const { hideModal, saveAccountLabel, identity } = this.props
const { hideModal, setAccountLabel, identity } = this.props
return h('div', {}, [
h('div.flex-column.edit-account-name-modal-content', {
@ -69,7 +69,7 @@ EditAccountNameModal.prototype.render = function () {
h('button.btn-clear.edit-account-name-modal-save-button.allcaps', {
onClick: () => {
if (this.state.inputText.length !== 0) {
saveAccountLabel(identity.address, this.state.inputText)
setAccountLabel(identity.address, this.state.inputText)
hideModal()
}
},

View File

@ -95,7 +95,7 @@ const mapDispatchToProps = dispatch => {
dispatch(actions.addNewAccount())
.then((newAccountAddress) => {
if (newAccountName) {
dispatch(actions.saveAccountLabel(newAccountAddress, newAccountName))
dispatch(actions.setAccountLabel(newAccountAddress, newAccountName))
}
dispatch(actions.hideModal())
})

View File

@ -1,431 +0,0 @@
const inherits = require('util').inherits
const Component = require('react').Component
const classnames = require('classnames')
const h = require('react-hyperscript')
const PropTypes = require('prop-types')
const connect = require('react-redux').connect
const R = require('ramda')
const Fuse = require('fuse.js')
const contractMap = require('eth-contract-metadata')
const TokenBalance = require('../../components/token-balance')
const Identicon = require('../../components/identicon')
const contractList = Object.entries(contractMap)
.map(([ _, tokenData]) => tokenData)
.filter(tokenData => Boolean(tokenData.erc20))
const fuse = new Fuse(contractList, {
shouldSort: true,
threshold: 0.45,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
{ name: 'name', weight: 0.5 },
{ name: 'symbol', weight: 0.5 },
],
})
const actions = require('../../actions')
const ethUtil = require('ethereumjs-util')
const { tokenInfoGetter } = require('../../token-util')
const { DEFAULT_ROUTE } = require('../../routes')
const emptyAddr = '0x0000000000000000000000000000000000000000'
AddTokenScreen.contextTypes = {
t: PropTypes.func,
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(AddTokenScreen)
function mapStateToProps (state) {
const { identities, tokens } = state.metamask
return {
identities,
tokens,
}
}
function mapDispatchToProps (dispatch) {
return {
addTokens: tokens => dispatch(actions.addTokens(tokens)),
}
}
inherits(AddTokenScreen, Component)
function AddTokenScreen () {
this.state = {
isShowingConfirmation: false,
isShowingInfoBox: true,
customAddress: '',
customSymbol: '',
customDecimals: '',
searchQuery: '',
selectedTokens: {},
errors: {},
autoFilled: false,
displayedTab: 'SEARCH',
}
this.tokenAddressDidChange = this.tokenAddressDidChange.bind(this)
this.tokenSymbolDidChange = this.tokenSymbolDidChange.bind(this)
this.tokenDecimalsDidChange = this.tokenDecimalsDidChange.bind(this)
this.onNext = this.onNext.bind(this)
Component.call(this)
}
AddTokenScreen.prototype.componentWillMount = function () {
this.tokenInfoGetter = tokenInfoGetter()
}
AddTokenScreen.prototype.toggleToken = function (address, token) {
const { selectedTokens = {}, errors } = this.state
const selectedTokensCopy = { ...selectedTokens }
if (address in selectedTokensCopy) {
delete selectedTokensCopy[address]
} else {
selectedTokensCopy[address] = token
}
this.setState({
selectedTokens: selectedTokensCopy,
errors: {
...errors,
tokenSelector: null,
},
})
}
AddTokenScreen.prototype.onNext = function () {
const { isValid, errors } = this.validate()
return !isValid
? this.setState({ errors })
: this.setState({ isShowingConfirmation: true })
}
AddTokenScreen.prototype.tokenAddressDidChange = function (e) {
const customAddress = e.target.value.trim()
this.setState({ customAddress })
if (ethUtil.isValidAddress(customAddress) && customAddress !== emptyAddr) {
this.attemptToAutoFillTokenParams(customAddress)
} else {
this.setState({
customSymbol: '',
customDecimals: 0,
})
}
}
AddTokenScreen.prototype.tokenSymbolDidChange = function (e) {
const customSymbol = e.target.value.trim()
this.setState({ customSymbol })
}
AddTokenScreen.prototype.tokenDecimalsDidChange = function (e) {
const customDecimals = e.target.value.trim()
this.setState({ customDecimals })
}
AddTokenScreen.prototype.checkExistingAddresses = function (address) {
if (!address) return false
const tokensList = this.props.tokens
const matchesAddress = existingToken => {
return existingToken.address.toLowerCase() === address.toLowerCase()
}
return R.any(matchesAddress)(tokensList)
}
AddTokenScreen.prototype.validate = function () {
const errors = {}
const identitiesList = Object.keys(this.props.identities)
const { customAddress, customSymbol, customDecimals, selectedTokens } = this.state
const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase()
if (customAddress) {
const validAddress = ethUtil.isValidAddress(customAddress)
if (!validAddress) {
errors.customAddress = this.context.t('invalidAddress')
}
const validDecimals = customDecimals !== null
&& customDecimals !== ''
&& customDecimals >= 0
&& customDecimals < 36
if (!validDecimals) {
errors.customDecimals = this.context.t('decimalsMustZerotoTen')
}
const symbolLen = customSymbol.trim().length
const validSymbol = symbolLen > 0 && symbolLen < 10
if (!validSymbol) {
errors.customSymbol = this.context.t('symbolBetweenZeroTen')
}
const ownAddress = identitiesList.includes(standardAddress)
if (ownAddress) {
errors.customAddress = this.context.t('personalAddressDetected')
}
const tokenAlreadyAdded = this.checkExistingAddresses(customAddress)
if (tokenAlreadyAdded) {
errors.customAddress = this.context.t('tokenAlreadyAdded')
}
} else if (
Object.entries(selectedTokens)
.reduce((isEmpty, [ symbol, isSelected ]) => (
isEmpty && !isSelected
), true)
) {
errors.tokenSelector = this.context.t('mustSelectOne')
}
return {
isValid: !Object.keys(errors).length,
errors,
}
}
AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) {
const { symbol, decimals } = await this.tokenInfoGetter(address)
if (symbol && decimals) {
this.setState({
customSymbol: symbol,
customDecimals: decimals,
autoFilled: true,
})
}
}
AddTokenScreen.prototype.renderCustomForm = function () {
const { autoFilled, customAddress, customSymbol, customDecimals, errors } = this.state
return (
h('div.add-token__add-custom-form', [
h('div', {
className: classnames('add-token__add-custom-field', {
'add-token__add-custom-field--error': errors.customAddress,
}),
}, [
h('div.add-token__add-custom-label', this.context.t('tokenAddress')),
h('input.add-token__add-custom-input', {
type: 'text',
onChange: this.tokenAddressDidChange,
value: customAddress,
}),
h('div.add-token__add-custom-error-message', errors.customAddress),
]),
h('div', {
className: classnames('add-token__add-custom-field', {
'add-token__add-custom-field--error': errors.customSymbol,
}),
}, [
h('div.add-token__add-custom-label', this.context.t('tokenSymbol')),
h('input.add-token__add-custom-input', {
type: 'text',
onChange: this.tokenSymbolDidChange,
value: customSymbol,
disabled: autoFilled,
}),
h('div.add-token__add-custom-error-message', errors.customSymbol),
]),
h('div', {
className: classnames('add-token__add-custom-field', {
'add-token__add-custom-field--error': errors.customDecimals,
}),
}, [
h('div.add-token__add-custom-label', this.context.t('decimal')),
h('input.add-token__add-custom-input', {
type: 'number',
onChange: this.tokenDecimalsDidChange,
value: customDecimals,
disabled: autoFilled,
}),
h('div.add-token__add-custom-error-message', errors.customDecimals),
]),
])
)
}
AddTokenScreen.prototype.renderTokenList = function () {
const { searchQuery = '', selectedTokens } = this.state
const fuseSearchResult = fuse.search(searchQuery)
const addressSearchResult = contractList.filter(token => {
return token.address.toLowerCase() === searchQuery.toLowerCase()
})
const results = [...addressSearchResult, ...fuseSearchResult]
return h('div', [
results.length > 0 && h('div.add-token__token-icons-title', this.context.t('popularTokens')),
h('div.add-token__token-icons-container', Array(6).fill(undefined)
.map((_, i) => {
const { logo, symbol, name, address } = results[i] || {}
const tokenAlreadyAdded = this.checkExistingAddresses(address)
return Boolean(logo || symbol || name) && (
h('div.add-token__token-wrapper', {
className: classnames({
'add-token__token-wrapper--selected': selectedTokens[address],
'add-token__token-wrapper--disabled': tokenAlreadyAdded,
}),
onClick: () => !tokenAlreadyAdded && this.toggleToken(address, results[i]),
}, [
h('div.add-token__token-icon', {
style: {
backgroundImage: logo && `url(images/contract/${logo})`,
},
}),
h('div.add-token__token-data', [
h('div.add-token__token-symbol', symbol),
h('div.add-token__token-name', name),
]),
// tokenAlreadyAdded && (
// h('div.add-token__token-message', 'Already added')
// ),
])
)
})),
])
}
AddTokenScreen.prototype.renderConfirmation = function () {
const {
customAddress: address,
customSymbol: symbol,
customDecimals: decimals,
selectedTokens,
} = this.state
const { addTokens, history } = this.props
const customToken = {
address,
symbol,
decimals,
}
const tokens = address && symbol && decimals
? { ...selectedTokens, [address]: customToken }
: selectedTokens
return (
h('div.add-token', [
h('div.add-token__wrapper', [
h('div.add-token__content-container.add-token__confirmation-content', [
h('div.add-token__description.add-token__confirmation-description', this.context.t('balances')),
h('div.add-token__confirmation-token-list',
Object.entries(tokens)
.map(([ address, token ]) => (
h('span.add-token__confirmation-token-list-item', [
h(Identicon, {
className: 'add-token__confirmation-token-icon',
diameter: 75,
address,
}),
h(TokenBalance, { token }),
])
))
),
]),
]),
h('div.add-token__buttons', [
h('button.btn-secondary--lg.add-token__cancel-button', {
onClick: () => this.setState({ isShowingConfirmation: false }),
}, this.context.t('back')),
h('button.btn-primary--lg', {
onClick: () => addTokens(tokens).then(() => history.push(DEFAULT_ROUTE)),
}, this.context.t('addTokens')),
]),
])
)
}
AddTokenScreen.prototype.displayTab = function (selectedTab) {
this.setState({ displayedTab: selectedTab })
}
AddTokenScreen.prototype.renderTabs = function () {
const { isShowingInfoBox, displayedTab, errors } = this.state
return displayedTab === 'CUSTOM_TOKEN'
? this.renderCustomForm()
: h('div', [
h('div.add-token__wrapper', [
h('div.add-token__content-container', [
isShowingInfoBox && h('div.add-token__info-box', [
h('div.add-token__info-box__close', {
onClick: () => this.setState({ isShowingInfoBox: false }),
}),
h('div.add-token__info-box__title', this.context.t('whatsThis')),
h('div.add-token__info-box__copy', this.context.t('keepTrackTokens')),
h('a.add-token__info-box__copy--blue', {
href: 'http://metamask.helpscoutdocs.com/article/16-managing-erc20-tokens',
target: '_blank',
}, this.context.t('learnMore')),
]),
h('div.add-token__input-container', [
h('input.add-token__input', {
type: 'text',
placeholder: this.context.t('searchTokens'),
onChange: e => this.setState({ searchQuery: e.target.value }),
}),
h('div.add-token__search-input-error-message', errors.tokenSelector),
]),
this.renderTokenList(),
]),
]),
])
}
AddTokenScreen.prototype.render = function () {
const {
isShowingConfirmation,
displayedTab,
} = this.state
const { history } = this.props
return h('div.add-token', [
h('div.add-token__header', [
h('div.add-token__header__cancel', {
onClick: () => history.push(DEFAULT_ROUTE),
}, [
h('i.fa.fa-angle-left.fa-lg'),
h('span', this.context.t('cancel')),
]),
h('div.add-token__header__title', this.context.t('addTokens')),
isShowingConfirmation && h('div.add-token__header__subtitle', this.context.t('likeToAddTokens')),
!isShowingConfirmation && h('div.add-token__header__tabs', [
h('div.add-token__header__tabs__tab', {
className: classnames('add-token__header__tabs__tab', {
'add-token__header__tabs__selected': displayedTab === 'SEARCH',
'add-token__header__tabs__unselected': displayedTab !== 'SEARCH',
}),
onClick: () => this.displayTab('SEARCH'),
}, this.context.t('search')),
h('div.add-token__header__tabs__tab', {
className: classnames('add-token__header__tabs__tab', {
'add-token__header__tabs__selected': displayedTab === 'CUSTOM_TOKEN',
'add-token__header__tabs__unselected': displayedTab !== 'CUSTOM_TOKEN',
}),
onClick: () => this.displayTab('CUSTOM_TOKEN'),
}, this.context.t('customToken')),
]),
]),
isShowingConfirmation
? this.renderConfirmation()
: this.renderTabs(),
!isShowingConfirmation && h('div.add-token__buttons', [
h('button.btn-secondary--lg.add-token__cancel-button', {
onClick: () => history.push(DEFAULT_ROUTE),
}, this.context.t('cancel')),
h('button.btn-primary--lg.add-token__confirm-button', {
onClick: this.onNext,
}, this.context.t('next')),
]),
])
}

View File

@ -0,0 +1,351 @@
import React, { Component } from 'react'
import classnames from 'classnames'
import PropTypes from 'prop-types'
import ethUtil from 'ethereumjs-util'
import { checkExistingAddresses } from './util'
import { tokenInfoGetter } from '../../../token-util'
import { DEFAULT_ROUTE, CONFIRM_ADD_TOKEN_ROUTE } from '../../../routes'
import Button from '../../button'
import TextField from '../../text-field'
import TokenList from './token-list'
import TokenSearch from './token-search'
const emptyAddr = '0x0000000000000000000000000000000000000000'
const SEARCH_TAB = 'SEARCH'
const CUSTOM_TOKEN_TAB = 'CUSTOM_TOKEN'
class AddToken extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
history: PropTypes.object,
setPendingTokens: PropTypes.func,
pendingTokens: PropTypes.object,
clearPendingTokens: PropTypes.func,
tokens: PropTypes.array,
identities: PropTypes.object,
}
constructor (props) {
super(props)
this.state = {
customAddress: '',
customSymbol: '',
customDecimals: 0,
searchResults: [],
selectedTokens: {},
tokenSelectorError: null,
customAddressError: null,
customSymbolError: null,
customDecimalsError: null,
autoFilled: false,
displayedTab: SEARCH_TAB,
}
}
componentDidMount () {
this.tokenInfoGetter = tokenInfoGetter()
const { pendingTokens = {} } = this.props
const pendingTokenKeys = Object.keys(pendingTokens)
if (pendingTokenKeys.length > 0) {
let selectedTokens = {}
let customToken = {}
pendingTokenKeys.forEach(tokenAddress => {
const token = pendingTokens[tokenAddress]
const { isCustom } = token
if (isCustom) {
customToken = { ...token }
} else {
selectedTokens = { ...selectedTokens, [tokenAddress]: { ...token } }
}
})
const {
address: customAddress = '',
symbol: customSymbol = '',
decimals: customDecimals = 0,
} = customToken
const displayedTab = Object.keys(selectedTokens).length > 0 ? SEARCH_TAB : CUSTOM_TOKEN_TAB
this.setState({ selectedTokens, customAddress, customSymbol, customDecimals, displayedTab })
}
}
handleToggleToken (token) {
const { address } = token
const { selectedTokens = {} } = this.state
const selectedTokensCopy = { ...selectedTokens }
if (address in selectedTokensCopy) {
delete selectedTokensCopy[address]
} else {
selectedTokensCopy[address] = token
}
this.setState({
selectedTokens: selectedTokensCopy,
tokenSelectorError: null,
})
}
hasError () {
const {
tokenSelectorError,
customAddressError,
customSymbolError,
customDecimalsError,
} = this.state
return tokenSelectorError || customAddressError || customSymbolError || customDecimalsError
}
hasSelected () {
const { customAddress = '', selectedTokens = {} } = this.state
return customAddress || Object.keys(selectedTokens).length > 0
}
handleNext () {
if (this.hasError()) {
return
}
if (!this.hasSelected()) {
this.setState({ tokenSelectorError: this.context.t('mustSelectOne') })
return
}
const { setPendingTokens, history } = this.props
const {
customAddress: address,
customSymbol: symbol,
customDecimals: decimals,
selectedTokens,
} = this.state
const customToken = {
address,
symbol,
decimals,
}
setPendingTokens({ customToken, selectedTokens })
history.push(CONFIRM_ADD_TOKEN_ROUTE)
}
async attemptToAutoFillTokenParams (address) {
const { symbol = '', decimals = 0 } = await this.tokenInfoGetter(address)
const autoFilled = Boolean(symbol && decimals)
this.setState({ autoFilled })
this.handleCustomSymbolChange(symbol || '')
this.handleCustomDecimalsChange(decimals)
}
handleCustomAddressChange (value) {
const customAddress = value.trim()
this.setState({
customAddress,
customAddressError: null,
tokenSelectorError: null,
autoFilled: false,
})
const isValidAddress = ethUtil.isValidAddress(customAddress)
const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase()
switch (true) {
case !isValidAddress:
this.setState({
customAddressError: this.context.t('invalidAddress'),
customSymbol: '',
customDecimals: 0,
customSymbolError: null,
customDecimalsError: null,
})
break
case Boolean(this.props.identities[standardAddress]):
this.setState({
customAddressError: this.context.t('personalAddressDetected'),
})
break
case checkExistingAddresses(customAddress, this.props.tokens):
this.setState({
customAddressError: this.context.t('tokenAlreadyAdded'),
})
break
default:
if (customAddress !== emptyAddr) {
this.attemptToAutoFillTokenParams(customAddress)
}
}
}
handleCustomSymbolChange (value) {
const customSymbol = value.trim()
const symbolLength = customSymbol.length
let customSymbolError = null
if (symbolLength <= 0 || symbolLength >= 10) {
customSymbolError = this.context.t('symbolBetweenZeroTen')
}
this.setState({ customSymbol, customSymbolError })
}
handleCustomDecimalsChange (value) {
const customDecimals = value.trim()
const validDecimals = customDecimals !== null &&
customDecimals !== '' &&
customDecimals >= 0 &&
customDecimals < 36
let customDecimalsError = null
if (!validDecimals) {
customDecimalsError = this.context.t('decimalsMustZerotoTen')
}
this.setState({ customDecimals, customDecimalsError })
}
renderCustomTokenForm () {
const {
customAddress,
customSymbol,
customDecimals,
customAddressError,
customSymbolError,
customDecimalsError,
autoFilled,
} = this.state
return (
<div className="add-token__custom-token-form">
<TextField
id="custom-address"
label="Token Address"
type="text"
value={customAddress}
onChange={e => this.handleCustomAddressChange(e.target.value)}
error={customAddressError}
fullWidth
margin="normal"
/>
<TextField
id="custom-symbol"
label="Token Symbol"
type="text"
value={customSymbol}
onChange={e => this.handleCustomSymbolChange(e.target.value)}
error={customSymbolError}
fullWidth
margin="normal"
disabled={autoFilled}
/>
<TextField
id="custom-decimals"
label="Decimals of Precision"
type="number"
value={customDecimals}
onChange={e => this.handleCustomDecimalsChange(e.target.value)}
error={customDecimalsError}
fullWidth
margin="normal"
disabled={autoFilled}
/>
</div>
)
}
renderSearchToken () {
const { tokenSelectorError, selectedTokens, searchResults } = this.state
return (
<div className="add-token__search-token">
<TokenSearch
onSearch={({ results = [] }) => this.setState({ searchResults: results })}
error={tokenSelectorError}
/>
<div className="add-token__token-list">
<TokenList
results={searchResults}
selectedTokens={selectedTokens}
onToggleToken={token => this.handleToggleToken(token)}
/>
</div>
</div>
)
}
render () {
const { displayedTab } = this.state
const { history, clearPendingTokens } = this.props
return (
<div className="page-container">
<div className="page-container__header page-container__header--no-padding-bottom">
<div className="page-container__title">
{ this.context.t('addTokens') }
</div>
<div className="page-container__tabs">
<div
className={classnames('page-container__tab', {
'page-container__tab--selected': displayedTab === SEARCH_TAB,
})}
onClick={() => this.setState({ displayedTab: SEARCH_TAB })}
>
{ this.context.t('search') }
</div>
<div
className={classnames('page-container__tab', {
'page-container__tab--selected': displayedTab === CUSTOM_TOKEN_TAB,
})}
onClick={() => this.setState({ displayedTab: CUSTOM_TOKEN_TAB })}
>
{ this.context.t('customToken') }
</div>
</div>
</div>
<div className="page-container__content">
{
displayedTab === CUSTOM_TOKEN_TAB
? this.renderCustomTokenForm()
: this.renderSearchToken()
}
</div>
<div className="page-container__footer">
<Button
type="secondary"
large
className="page-container__footer-button"
onClick={() => {
clearPendingTokens()
history.push(DEFAULT_ROUTE)
}}
>
{ this.context.t('cancel') }
</Button>
<Button
type="primary"
large
className="page-container__footer-button"
onClick={() => this.handleNext()}
disabled={this.hasError() || !this.hasSelected()}
>
{ this.context.t('next') }
</Button>
</div>
</div>
)
}
}
export default AddToken

View File

@ -0,0 +1,22 @@
import { connect } from 'react-redux'
import AddToken from './add-token.component'
const { setPendingTokens, clearPendingTokens } = require('../../../actions')
const mapStateToProps = ({ metamask }) => {
const { identities, tokens, pendingTokens } = metamask
return {
identities,
tokens,
pendingTokens,
}
}
const mapDispatchToProps = dispatch => {
return {
setPendingTokens: tokens => dispatch(setPendingTokens(tokens)),
clearPendingTokens: () => dispatch(clearPendingTokens()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(AddToken)

View File

@ -0,0 +1,2 @@
import AddToken from './add-token.container'
module.exports = AddToken

View File

@ -0,0 +1,25 @@
@import './token-list/index';
.add-token {
&__custom-token-form {
padding: 8px 16px 16px;
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
display: none;
}
input[type="number"]:hover::-webkit-inner-spin-button {
-webkit-appearance: none;
display: none;
}
}
&__search-token {
padding: 16px;
}
&__token-list {
margin-top: 16px;
}
}

View File

@ -0,0 +1,2 @@
import TokenList from './token-list.container'
module.exports = TokenList

View File

@ -0,0 +1,65 @@
@import './token-list-placeholder/index';
.token-list {
&__title {
font-size: .75rem;
}
&__tokens-container {
display: flex;
flex-direction: column;
}
&__token {
transition: 200ms ease-in-out;
display: flex;
flex-flow: row nowrap;
align-items: center;
padding: 8px;
margin-top: 8px;
box-sizing: border-box;
border-radius: 10px;
cursor: pointer;
border: 2px solid transparent;
position: relative;
&:hover {
border: 2px solid rgba($malibu-blue, .5);
}
&--selected {
border: 2px solid $malibu-blue !important;
}
&--disabled {
opacity: .4;
pointer-events: none;
}
}
&__token-icon {
width: 48px;
height: 48px;
background-repeat: no-repeat;
background-size: contain;
background-position: center;
border-radius: 50%;
background-color: $white;
box-shadow: 0 2px 4px 0 rgba($black, .24);
margin-right: 12px;
flex: 0 0 auto;
}
&__token-data {
display: flex;
flex-direction: row;
align-items: center;
min-width: 0;
}
&__token-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}

View File

@ -0,0 +1,2 @@
import TokenListPlaceholder from './token-list-placeholder.component'
module.exports = TokenListPlaceholder

View File

@ -0,0 +1,19 @@
.token-list-placeholder {
display: flex;
align-items: center;
padding-top: 36px;
flex-direction: column;
line-height: 22px;
opacity: .5;
&__text {
color: $silver-chalice;
width: 50%;
text-align: center;
margin-top: 8px;
}
&__link {
color: $curious-blue;
}
}

View File

@ -0,0 +1,27 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class TokenListPlaceholder extends Component {
static contextTypes = {
t: PropTypes.func,
}
render () {
return (
<div className="token-list-placeholder">
<img src="images/tokensearch.svg" />
<div className="token-list-placeholder__text">
{ this.context.t('addAcquiredTokens') }
</div>
<a
className="token-list-placeholder__link"
href="http://metamask.helpscoutdocs.com/article/16-managing-erc20-tokens"
target="_blank"
rel="noopener noreferrer"
>
{ this.context.t('learnMore') }
</a>
</div>
)
}
}

View File

@ -0,0 +1,60 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { checkExistingAddresses } from '../util'
import TokenListPlaceholder from './token-list-placeholder'
export default class InfoBox extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
tokens: PropTypes.array,
results: PropTypes.array,
selectedTokens: PropTypes.object,
onToggleToken: PropTypes.func,
}
render () {
const { results = [], selectedTokens = {}, onToggleToken, tokens = [] } = this.props
return results.length === 0
? <TokenListPlaceholder />
: (
<div className="token-list">
<div className="token-list__title">
{ this.context.t('searchResults') }
</div>
<div className="token-list__tokens-container">
{
Array(6).fill(undefined)
.map((_, i) => {
const { logo, symbol, name, address } = results[i] || {}
const tokenAlreadyAdded = checkExistingAddresses(address, tokens)
return Boolean(logo || symbol || name) && (
<div
className={classnames('token-list__token', {
'token-list__token--selected': selectedTokens[address],
'token-list__token--disabled': tokenAlreadyAdded,
})}
onClick={() => !tokenAlreadyAdded && onToggleToken(results[i])}
key={i}
>
<div
className="token-list__token-icon"
style={{ backgroundImage: logo && `url(images/contract/${logo})` }}>
</div>
<div className="token-list__token-data">
<span className="token-list__token-name">{ `${name} (${symbol})` }</span>
</div>
</div>
)
})
}
</div>
</div>
)
}
}

View File

@ -0,0 +1,11 @@
import { connect } from 'react-redux'
import TokenList from './token-list.component'
const mapStateToProps = ({ metamask }) => {
const { tokens } = metamask
return {
tokens,
}
}
export default connect(mapStateToProps)(TokenList)

View File

@ -0,0 +1,2 @@
import TokenSearch from './token-search.component'
module.exports = TokenSearch

View File

@ -0,0 +1,85 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import contractMap from 'eth-contract-metadata'
import Fuse from 'fuse.js'
import InputAdornment from '@material-ui/core/InputAdornment'
import TextField from '../../../text-field'
const contractList = Object.entries(contractMap)
.map(([ _, tokenData]) => tokenData)
.filter(tokenData => Boolean(tokenData.erc20))
const fuse = new Fuse(contractList, {
shouldSort: true,
threshold: 0.45,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
{ name: 'name', weight: 0.5 },
{ name: 'symbol', weight: 0.5 },
],
})
export default class TokenSearch extends Component {
static contextTypes = {
t: PropTypes.func,
}
static defaultProps = {
error: null,
}
static propTypes = {
onSearch: PropTypes.func,
error: PropTypes.string,
}
constructor (props) {
super(props)
this.state = {
searchQuery: '',
}
}
handleSearch (searchQuery) {
this.setState({ searchQuery })
const fuseSearchResult = fuse.search(searchQuery)
const addressSearchResult = contractList.filter(token => {
return token.address.toLowerCase() === searchQuery.toLowerCase()
})
const results = [...addressSearchResult, ...fuseSearchResult]
this.props.onSearch({ searchQuery, results })
}
renderAdornment () {
return (
<InputAdornment
position="start"
style={{ marginRight: '12px' }}
>
<img src="images/search.svg" />
</InputAdornment>
)
}
render () {
const { error } = this.props
const { searchQuery } = this.state
return (
<TextField
id="search-tokens"
placeholder={this.context.t('searchTokens')}
type="text"
value={searchQuery}
onChange={e => this.handleSearch(e.target.value)}
error={error}
fullWidth
startAdornment={this.renderAdornment()}
/>
)
}
}

View File

@ -0,0 +1,13 @@
import R from 'ramda'
export function checkExistingAddresses (address, tokenList = []) {
if (!address) {
return false
}
const matchesAddress = existingToken => {
return existingToken.address.toLowerCase() === address.toLowerCase()
}
return R.any(matchesAddress)(tokenList)
}

View File

@ -0,0 +1,115 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { DEFAULT_ROUTE, ADD_TOKEN_ROUTE } from '../../../routes'
import Button from '../../button'
import Identicon from '../../../components/identicon'
import TokenBalance from './token-balance'
export default class ConfirmAddToken extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
history: PropTypes.object,
clearPendingTokens: PropTypes.func,
addTokens: PropTypes.func,
pendingTokens: PropTypes.object,
}
componentDidMount () {
const { pendingTokens = {}, history } = this.props
if (Object.keys(pendingTokens).length === 0) {
history.push(DEFAULT_ROUTE)
}
}
getTokenName (name, symbol) {
return typeof name === 'undefined'
? symbol
: `${name} (${symbol})`
}
render () {
const { history, addTokens, clearPendingTokens, pendingTokens } = this.props
return (
<div className="page-container">
<div className="page-container__header">
<div className="page-container__title">
{ this.context.t('addTokens') }
</div>
<div className="page-container__subtitle">
{ this.context.t('likeToAddTokens') }
</div>
</div>
<div className="page-container__content">
<div className="confirm-add-token">
<div className="confirm-add-token__header">
<div className="confirm-add-token__token">
{ this.context.t('token') }
</div>
<div className="confirm-add-token__balance">
{ this.context.t('balance') }
</div>
</div>
<div className="confirm-add-token__token-list">
{
Object.entries(pendingTokens)
.map(([ address, token ]) => {
const { name, symbol } = token
return (
<div
className="confirm-add-token__token-list-item"
key={address}
>
<div className="confirm-add-token__token confirm-add-token__data">
<Identicon
className="confirm-add-token__token-icon"
diameter={48}
address={address}
/>
<div className="confirm-add-token__name">
{ this.getTokenName(name, symbol) }
</div>
</div>
<div className="confirm-add-token__balance">
<TokenBalance token={token} />
</div>
</div>
)
})
}
</div>
</div>
</div>
<div className="page-container__footer">
<Button
type="secondary"
large
className="page-container__footer-button"
onClick={() => history.push(ADD_TOKEN_ROUTE)}
>
{ this.context.t('back') }
</Button>
<Button
type="primary"
large
className="page-container__footer-button"
onClick={() => {
addTokens(pendingTokens)
.then(() => {
clearPendingTokens()
history.push(DEFAULT_ROUTE)
})
}}
>
{ this.context.t('addTokens') }
</Button>
</div>
</div>
)
}
}

View File

@ -0,0 +1,20 @@
import { connect } from 'react-redux'
import ConfirmAddToken from './confirm-add-token.component'
const { addTokens, clearPendingTokens } = require('../../../actions')
const mapStateToProps = ({ metamask }) => {
const { pendingTokens } = metamask
return {
pendingTokens,
}
}
const mapDispatchToProps = dispatch => {
return {
addTokens: tokens => dispatch(addTokens(tokens)),
clearPendingTokens: () => dispatch(clearPendingTokens()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ConfirmAddToken)

View File

@ -0,0 +1,2 @@
import ConfirmAddToken from './confirm-add-token.container'
module.exports = ConfirmAddToken

View File

@ -0,0 +1,69 @@
.confirm-add-token {
padding: 16px;
&__header {
font-size: .75rem;
display: flex;
}
&__token {
flex: 1;
min-width: 0;
}
&__balance {
flex: 0 0 30%;
min-width: 0;
}
&__token-list {
display: flex;
flex-flow: column nowrap;
.token-balance {
display: flex;
flex-flow: row nowrap;
align-items: flex-start;
&__amount {
color: $scorpion;
font-size: 43px;
line-height: 43px;
margin-right: 8px;
}
&__symbol {
color: $scorpion;
font-size: 16px;
font-weight: 400;
line-height: 24px;
}
}
}
&__token-list-item {
display: flex;
flex-flow: row nowrap;
align-items: center;
margin-top: 8px;
box-sizing: border-box;
}
&__data {
display: flex;
align-items: center;
padding: 8px;
}
&__name {
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__token-icon {
margin-right: 12px;
flex: 0 0 auto;
}
}

View File

@ -0,0 +1,2 @@
import TokenBalance from './token-balance.container'
module.exports = TokenBalance

View File

@ -0,0 +1,16 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class TokenBalance extends Component {
static propTypes = {
string: PropTypes.string,
symbol: PropTypes.string,
error: PropTypes.string,
}
render () {
return (
<div className="hide-text-overflow">{ this.props.string }</div>
)
}
}

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