Merge branch 'master' of github.com:MetaMask/metamask-plugin into library

feature/default_network_editable
kumavis 8 years ago
commit 850b6d1440
  1. 30
      CHANGELOG.md
  2. 7
      README.md
  3. 6
      app/manifest.json
  4. 16
      app/notification.html
  5. 50
      app/scripts/background.js
  6. 6
      app/scripts/chromereload.js
  7. 4
      app/scripts/config.js
  8. 8
      app/scripts/contentscript.js
  9. 19
      app/scripts/lib/extension-instance.js
  10. 7
      app/scripts/lib/idStore.js
  11. 27
      app/scripts/lib/inpage-provider.js
  12. 8
      app/scripts/lib/is-popup-or-notification.js
  13. 168
      app/scripts/lib/notifications.js
  14. 32
      app/scripts/metamask-controller.js
  15. 24
      app/scripts/popup.js
  16. 7
      circle.yml
  17. 2
      development/states.js
  18. 2
      development/states/custom-rpc.json
  19. 84
      development/states/locked.json
  20. 39
      development/states/restore-vault.json
  21. 76
      development/states/send.json
  22. 348
      development/states/shapeshift.json
  23. 28
      docs/form_persisting_architecture.md
  24. 66
      gulpfile.js
  25. 4
      package.json
  26. 29
      test/unit/extension-test.js
  27. 97
      test/unit/metamask-controller-test.js
  28. 6
      ui/app/account-detail.js
  29. 4
      ui/app/accounts/account-list-item.js
  30. 1
      ui/app/accounts/index.js
  31. 23
      ui/app/actions.js
  32. 121
      ui/app/app.js
  33. 140
      ui/app/components/account-eth-balance.js
  34. 2
      ui/app/components/account-info-link.js
  35. 2
      ui/app/components/buy-button-subview.js
  36. 25
      ui/app/components/eth-balance.js
  37. 71
      ui/app/components/fiat-value.js
  38. 27
      ui/app/components/mascot.js
  39. 3
      ui/app/components/network.js
  40. 11
      ui/app/components/pending-tx-details.js
  41. 15
      ui/app/components/shapeshift-form.js
  42. 8
      ui/app/components/tooltip.js
  43. 1
      ui/app/components/transaction-list-item.js
  44. 10
      ui/app/conf-tx.js
  45. 7
      ui/app/eth-store-warning.js
  46. 2
      ui/app/first-time/init-menu.js
  47. 17
      ui/app/first-time/restore-vault.js
  48. 2
      ui/app/info.js
  49. 35
      ui/app/reducers/app.js
  50. 25
      ui/app/send.js
  51. 79
      ui/app/unlock.js
  52. 5
      ui/index.js
  53. 61
      ui/lib/persistent-form.js
  54. 4
      ui/lib/tx-helper.js

@ -2,11 +2,37 @@
## Current Master ## Current Master
- Fix bug where pending transactions from Test net (or other networks) show up In Main net.
- Add fiat conversion values to more views.
- On fresh install, open a new tab with the MetaMask Introduction video. Does not open on update.
- Block negative values from transactions.
- Fixed a memory leak.
- MetaMask logo now renders as super lightweight SVG, improving compatibility and performance.
- Now showing loading indication during vault unlocking, to clarify behavior for users who are experience slow unlocks.
- Now only initially creates one wallet when restoring a vault, to reduce some users' confusion.
## 2.10.2 2016-09-02
- Fix bug where notification popup would not display.
## 2.10.1 2016-09-02
- Fix bug where provider menu did not allow switching to custom network from a custom network.
- Sending a transaction from within MetaMask no longer triggers a popup.
- The ability to build without livereload features (such as for production) can be enabled with the gulp --disableLiveReload flag.
- Fix Ethereum JSON RPC Filters bug.
## 2.10.0 2016-08-29
- Changed transaction approval from notifications system to popup system.
- Add a back button to locked screen to allow restoring vault from seed words when password is forgotten.
- Forms now retain their values even when closing the popup and reopening it.
- Fixed a spelling error in provider menu.
## 2.9.2 2016-08-24 ## 2.9.2 2016-08-24
- Fixed shortcut bug from preventing installation. - Fixed shortcut bug from preventing installation.
## 2.9.1 2016-08-24 ## 2.9.1 2016-08-24
- Added static image as fallback for when WebGL isn't supported. - Added static image as fallback for when WebGL isn't supported.
@ -27,7 +53,7 @@
## 2.8.0 2016-08-15 ## 2.8.0 2016-08-15
- Integrate ShapeShift - Integrate ShapeShift
- Add a for for Coinbase to specify amount to buy - Add a form for Coinbase to specify amount to buy
- Fix various typos. - Fix various typos.
- Make dapp-metamask connection more reliable - Make dapp-metamask connection more reliable
- Remove Ethereum Classic from provider menu. - Remove Ethereum Classic from provider menu.

@ -1,5 +1,12 @@
# MetaMask Plugin [![Build Status](https://circleci.com/gh/MetaMask/metamask-plugin.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-plugin) # MetaMask Plugin [![Build Status](https://circleci.com/gh/MetaMask/metamask-plugin.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-plugin)
## Developing Compatible Dapps
If you're a web dapp developer, we've got two types of guides for you:
- If you've never built a Dapp before, we've got a gentle introduction on [Developing Dapps with Truffle and MetaMask](https://blog.metamask.io/developing-for-metamask-with-truffle/).
- If you have a Dapp, and you want to ensure compatibility, [here is our guide on building MetaMask-compatible Dapps](https://github.com/MetaMask/faq/blob/master/DEVELOPERS.md)
## Building locally ## Building locally
- Install [Node.js](https://nodejs.org/en/) version 6.3.1 or later. - Install [Node.js](https://nodejs.org/en/) version 6.3.1 or later.

@ -1,8 +1,9 @@
{ {
"name": "MetaMask", "name": "MetaMask",
"short_name": "Metamask", "short_name": "Metamask",
"version": "2.9.2", "version": "2.10.2",
"manifest_version": 2, "manifest_version": 2,
"author": "https://metamask.io",
"description": "Ethereum Browser Extension", "description": "Ethereum Browser Extension",
"commands": { "commands": {
"_execute_browser_action": { "_execute_browser_action": {
@ -28,7 +29,8 @@
"scripts": [ "scripts": [
"scripts/chromereload.js", "scripts/chromereload.js",
"scripts/background.js" "scripts/background.js"
] ],
"persistent": true
}, },
"browser_action": { "browser_action": {
"default_icon": { "default_icon": {

@ -0,0 +1,16 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>MetaMask Notification</title>
<style>
body {
overflow: hidden;
}
</style>
</head>
<body>
<div id="app-content"></div>
<script src="./scripts/popup.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>

@ -3,9 +3,7 @@ const extend = require('xtend')
const Dnode = require('dnode') const Dnode = require('dnode')
const eos = require('end-of-stream') const eos = require('end-of-stream')
const PortStream = require('./lib/port-stream.js') const PortStream = require('./lib/port-stream.js')
const createUnlockRequestNotification = require('./lib/notifications.js').createUnlockRequestNotification const notification = require('./lib/notifications.js')
const createTxNotification = require('./lib/notifications.js').createTxNotification
const createMsgNotification = require('./lib/notifications.js').createMsgNotification
const messageManager = require('./lib/message-manager') const messageManager = require('./lib/message-manager')
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
const MetamaskController = require('./metamask-controller') const MetamaskController = require('./metamask-controller')
@ -26,50 +24,32 @@ const controller = new MetamaskController({
const idStore = controller.idStore const idStore = controller.idStore
function unlockAccountMessage () { function unlockAccountMessage () {
createUnlockRequestNotification({ notification.show()
title: 'Account Unlock Request',
})
} }
function showUnconfirmedMessage (msgParams, msgId) { function showUnconfirmedMessage (msgParams, msgId) {
var controllerState = controller.getState() notification.show()
createMsgNotification({
imageifyIdenticons: false,
txData: {
msgParams: msgParams,
time: (new Date()).getTime(),
},
identities: controllerState.identities,
accounts: controllerState.accounts,
onConfirm: idStore.approveMessage.bind(idStore, msgId, noop),
onCancel: idStore.cancelMessage.bind(idStore, msgId),
})
} }
function showUnconfirmedTx (txParams, txData, onTxDoneCb) { function showUnconfirmedTx (txParams, txData, onTxDoneCb) {
var controllerState = controller.getState() notification.show()
createTxNotification({
imageifyIdenticons: false,
txData: {
txParams: txParams,
time: (new Date()).getTime(),
},
identities: controllerState.identities,
accounts: controllerState.accounts,
onConfirm: idStore.approveTransaction.bind(idStore, txData.id, noop),
onCancel: idStore.cancelTransaction.bind(idStore, txData.id),
})
} }
// On first install, open a window to MetaMask website to how-it-works.
extension.runtime.onInstalled.addListener(function (details) {
if (details.reason === 'install') {
extension.tabs.create({url: 'https://metamask.io/#how-it-works'})
}
})
// //
// connect to other contexts // connect to other contexts
// //
extension.runtime.onConnect.addListener(connectRemote) extension.runtime.onConnect.addListener(connectRemote)
function connectRemote (remotePort) { function connectRemote (remotePort) {
var isMetaMaskInternalProcess = (remotePort.name === 'popup') var isMetaMaskInternalProcess = remotePort.name === 'popup' || remotePort.name === 'notification'
var portStream = new PortStream(remotePort) var portStream = new PortStream(remotePort)
if (isMetaMaskInternalProcess) { if (isMetaMaskInternalProcess) {
// communication with popup // communication with popup
@ -109,7 +89,7 @@ function setupControllerConnection (stream) {
dnode.on('remote', (remote) => { dnode.on('remote', (remote) => {
// push updates to popup // push updates to popup
controller.ethStore.on('update', controller.sendUpdate.bind(controller)) controller.ethStore.on('update', controller.sendUpdate.bind(controller))
controller.remote = remote controller.listeners.push(remote)
idStore.on('update', controller.sendUpdate.bind(controller)) idStore.on('update', controller.sendUpdate.bind(controller))
// teardown on disconnect // teardown on disconnect
@ -188,5 +168,3 @@ function getOldStyleData () {
function setData (data) { function setData (data) {
window.localStorage[STORAGE_KEY] = JSON.stringify(data) window.localStorage[STORAGE_KEY] = JSON.stringify(data)
} }
function noop () {}

@ -324,13 +324,13 @@ window.LiveReloadOptions = { host: 'localhost' };
this.pluginIdentifiers = {} this.pluginIdentifiers = {}
this.console = this.window.console && this.window.console.log && this.window.console.error ? this.window.location.href.match(/LR-verbose/) ? this.window.console : { this.console = this.window.console && this.window.console.log && this.window.console.error ? this.window.location.href.match(/LR-verbose/) ? this.window.console : {
log: function () {}, log: function () {},
error: this.window.console.error.bind(this.window.console), error: console.error,
} : { } : {
log: function () {}, log: function () {},
error: function () {}, error: function () {},
} }
if (!(this.WebSocket = this.window.WebSocket || this.window.MozWebSocket)) { if (!(this.WebSocket = this.window.WebSocket || this.window.MozWebSocket)) {
this.console.error('LiveReload disabled because the browser does not seem to support web sockets') console.error('LiveReload disabled because the browser does not seem to support web sockets')
return return
} }
if ('LiveReloadOptions' in window) { if ('LiveReloadOptions' in window) {
@ -344,7 +344,7 @@ window.LiveReloadOptions = { host: 'localhost' };
} else { } else {
this.options = Options.extract(this.window.document) this.options = Options.extract(this.window.document)
if (!this.options) { if (!this.options) {
this.console.error('LiveReload disabled because it could not find its own <SCRIPT> tag') console.error('LiveReload disabled because it could not find its own <SCRIPT> tag')
return return
} }
} }

@ -1,5 +1,5 @@
const MAINET_RPC_URL = 'https://mainnet.infura.io/' const MAINET_RPC_URL = 'https://mainnet.infura.io/metamask'
const TESTNET_RPC_URL = 'https://morden.infura.io/' const TESTNET_RPC_URL = 'https://morden.infura.io/metamask'
const DEFAULT_RPC_URL = TESTNET_RPC_URL const DEFAULT_RPC_URL = TESTNET_RPC_URL
global.METAMASK_DEBUG = false global.METAMASK_DEBUG = false

@ -43,20 +43,20 @@ function setupStreams(){
name: 'contentscript', name: 'contentscript',
target: 'inpage', target: 'inpage',
}) })
pageStream.on('error', console.error.bind(console)) pageStream.on('error', console.error)
var pluginPort = extension.runtime.connect({name: 'contentscript'}) var pluginPort = extension.runtime.connect({name: 'contentscript'})
var pluginStream = new PortStream(pluginPort) var pluginStream = new PortStream(pluginPort)
pluginStream.on('error', console.error.bind(console)) pluginStream.on('error', console.error)
// forward communication plugin->inpage // forward communication plugin->inpage
pageStream.pipe(pluginStream).pipe(pageStream) pageStream.pipe(pluginStream).pipe(pageStream)
// connect contentscript->inpage reload stream // connect contentscript->inpage reload stream
var mx = ObjectMultiplex() var mx = ObjectMultiplex()
mx.on('error', console.error.bind(console)) mx.on('error', console.error)
mx.pipe(pageStream) mx.pipe(pageStream)
var reloadStream = mx.createStream('reload') var reloadStream = mx.createStream('reload')
reloadStream.on('error', console.error.bind(console)) reloadStream.on('error', console.error)
// if we lose connection with the plugin, trigger tab refresh // if we lose connection with the plugin, trigger tab refresh
pluginStream.on('close', function () { pluginStream.on('close', function () {

@ -41,11 +41,28 @@ function Extension () {
} }
} catch (e) {} } catch (e) {}
try {
if (browser[api]) {
_this[api] = browser[api]
}
} catch (e) {}
try { try {
_this.api = browser.extension[api] _this.api = browser.extension[api]
} catch (e) {} } catch (e) {}
}) })
try {
if (browser && browser.runtime) {
this.runtime = browser.runtime
}
} catch (e) {}
try {
if (browser && browser.browserAction) {
this.browserAction = browser.browserAction
}
} catch (e) {}
} }
module.exports = Extension module.exports = Extension

@ -45,7 +45,11 @@ function IdentityStore (opts = {}) {
IdentityStore.prototype.createNewVault = function (password, entropy, cb) { IdentityStore.prototype.createNewVault = function (password, entropy, cb) {
delete this._keyStore delete this._keyStore
var serializedKeystore = this.configManager.getWallet()
if (serializedKeystore) {
this.configManager.setData({})
}
this._createIdmgmt(password, null, entropy, (err) => { this._createIdmgmt(password, null, entropy, (err) => {
if (err) return cb(err) if (err) return cb(err)
@ -437,6 +441,7 @@ IdentityStore.prototype.tryPassword = function (password, cb) {
IdentityStore.prototype._createIdmgmt = function (password, seed, entropy, cb) { IdentityStore.prototype._createIdmgmt = function (password, seed, entropy, cb) {
const configManager = this.configManager const configManager = this.configManager
var keyStore = null var keyStore = null
LightwalletKeyStore.deriveKeyFromPassword(password, (err, derivedKey) => { LightwalletKeyStore.deriveKeyFromPassword(password, (err, derivedKey) => {
if (err) return cb(err) if (err) return cb(err)
@ -478,7 +483,7 @@ IdentityStore.prototype._restoreFromSeed = function (password, seed, derivedKey)
keyStore.addHdDerivationPath(this.hdPathString, derivedKey, {curve: 'secp256k1', purpose: 'sign'}) keyStore.addHdDerivationPath(this.hdPathString, derivedKey, {curve: 'secp256k1', purpose: 'sign'})
keyStore.setDefaultHdDerivationPath(this.hdPathString) keyStore.setDefaultHdDerivationPath(this.hdPathString)
keyStore.generateNewAddress(derivedKey, 3) keyStore.generateNewAddress(derivedKey, 1)
configManager.setWallet(keyStore.serialize()) configManager.setWallet(keyStore.serialize())
if (global.METAMASK_DEBUG) { if (global.METAMASK_DEBUG) {
console.log('restored from seed. saved to keystore') console.log('restored from seed. saved to keystore')

@ -33,15 +33,29 @@ function MetamaskInpageProvider (connectionStream) {
}) })
asyncProvider.on('error', console.error.bind(console)) asyncProvider.on('error', console.error.bind(console))
self.asyncProvider = asyncProvider self.asyncProvider = asyncProvider
self.idMap = {}
// handle sendAsync requests via asyncProvider // handle sendAsync requests via asyncProvider
self.sendAsync = function(payload, cb){ self.sendAsync = function(payload, cb){
// rewrite request ids // rewrite request ids
var request = jsonrpcMessageTransform(payload, (message) => { var request = eachJsonMessage(payload, (message) => {
message.id = createRandomId() var newId = createRandomId()
self.idMap[newId] = message.id
message.id = newId
return message return message
}) })
// forward to asyncProvider // forward to asyncProvider
asyncProvider.sendAsync(request, cb) asyncProvider.sendAsync(request, function(err, res){
if (err) return cb(err)
// transform messages to original ids
eachJsonMessage(res, (message) => {
var oldId = self.idMap[message.id]
delete self.idMap[message.id]
message.id = oldId
return message
})
cb(null, res)
})
} }
} }
@ -66,7 +80,8 @@ MetamaskInpageProvider.prototype.send = function (payload) {
// throw not-supported Error // throw not-supported Error
default: default:
var message = 'The MetaMask Web3 object does not support synchronous methods. See https://github.com/MetaMask/faq/blob/master/DEVELOPERS.md#all-async---think-of-metamask-as-a-light-client for details.' var message = 'The MetaMask Web3 object does not support synchronous methods like ' + payload.method +
'. See https://github.com/MetaMask/faq/blob/master/DEVELOPERS.md#all-async---think-of-metamask-as-a-light-client for details.'
throw new Error(message) throw new Error(message)
} }
@ -111,10 +126,10 @@ function createRandomId(){
return datePart + extraPart return datePart + extraPart
} }
function jsonrpcMessageTransform(payload, transformFn){ function eachJsonMessage(payload, transformFn){
if (Array.isArray(payload)) { if (Array.isArray(payload)) {
return payload.map(transformFn) return payload.map(transformFn)
} else { } else {
return transformFn(payload) return transformFn(payload)
} }
} }

@ -0,0 +1,8 @@
module.exports = function isPopupOrNotification() {
const url = window.location.href
if (url.match(/popup.html$/)) {
return 'popup'
} else {
return 'notification'
}
}

@ -1,159 +1,63 @@
const createId = require('hat')
const extend = require('xtend')
const unmountComponentAtNode = require('react-dom').unmountComponentAtNode
const findDOMNode = require('react-dom').findDOMNode
const render = require('react-dom').render
const h = require('react-hyperscript')
const PendingTxDetails = require('../../../ui/app/components/pending-tx-details')
const PendingMsgDetails = require('../../../ui/app/components/pending-msg-details')
const MetaMaskUiCss = require('../../../ui/css')
const extension = require('./extension') const extension = require('./extension')
var notificationHandlers = {}
const notifications = { const notifications = {
createUnlockRequestNotification: createUnlockRequestNotification, show,
createTxNotification: createTxNotification, getPopup,
createMsgNotification: createMsgNotification, closePopup,
} }
module.exports = notifications module.exports = notifications
window.METAMASK_NOTIFIER = notifications window.METAMASK_NOTIFIER = notifications
setupListeners() function show () {
getPopup((err, popup) => {
if (err) throw err
function setupListeners () { if (popup) {
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
if (!extension.notifications) return console.error('Chrome notifications API missing...')
// notification button press // bring focus to existing popup
extension.notifications.onButtonClicked.addListener(function (notificationId, buttonIndex) { extension.windows.update(popup.id, { focused: true })
var handlers = notificationHandlers[notificationId]
if (buttonIndex === 0) {
handlers.confirm()
} else {
handlers.cancel()
}
extension.notifications.clear(notificationId)
})
// notification teardown } else {
extension.notifications.onClosed.addListener(function (notificationId) {
delete notificationHandlers[notificationId]
})
}
// creation helper // create new popup
function createUnlockRequestNotification (opts) { extension.windows.create({
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236 url: 'notification.html',
if (!extension.notifications) return console.error('Chrome notifications API missing...') type: 'popup',
var message = 'An Ethereum app has requested a signature. Please unlock your account.' focused: true,
width: 360,
height: 500,
})
var id = createId() }
extension.notifications.create(id, {
type: 'basic',
iconUrl: '/images/icon-128.png',
title: opts.title,
message: message,
}) })
} }
function createTxNotification (state) { function getWindows(cb) {
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236 // Ignore in test environment
if (!extension.notifications) return console.error('Chrome notifications API missing...') if (!extension.windows) {
return cb()
renderTxNotificationSVG(state, function (err, notificationSvgSource) { }
if (err) throw err
showNotification(extend(state, { extension.windows.getAll({}, (windows) => {
title: 'New Unsigned Transaction', cb(null, windows)
imageUrl: toSvgUri(notificationSvgSource),
}))
}) })
} }
function createMsgNotification (state) { function getPopup(cb) {
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236 getWindows((err, windows) => {
if (!extension.notifications) return console.error('Chrome notifications API missing...')
renderMsgNotificationSVG(state, function (err, notificationSvgSource) {
if (err) throw err if (err) throw err
cb(null, getPopupIn(windows))
showNotification(extend(state, {
title: 'New Unsigned Message',
imageUrl: toSvgUri(notificationSvgSource),
}))
}) })
} }
function showNotification (state) { function getPopupIn(windows) {
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236 return windows ? windows.find((win) => win.type === 'popup') : null
if (!extension.notifications) return console.error('Chrome notifications API missing...')
var id = createId()
extension.notifications.create(id, {
type: 'image',
requireInteraction: true,
iconUrl: '/images/icon-128.png',
imageUrl: state.imageUrl,
title: state.title,
message: '',
buttons: [{
title: 'Approve',
}, {
title: 'Reject',
}],
})
notificationHandlers[id] = {
confirm: state.onConfirm,
cancel: state.onCancel,
}
}
function renderTxNotificationSVG (state, cb) {
var content = h(PendingTxDetails, state)
renderNotificationSVG(content, cb)
}
function renderMsgNotificationSVG (state, cb) {
var content = h(PendingMsgDetails, state)
renderNotificationSVG(content, cb)
} }
function renderNotificationSVG (content, cb) { function closePopup() {
var container = document.createElement('div') getPopup((err, popup) => {
var confirmView = h('div.app-primary', { if (err) throw err
style: { if (!popup) return
width: '360px', extension.windows.remove(popup.id, console.error)
height: '240px',
padding: '16px',
// background: '#F7F7F7',
background: 'white',
},
}, [
h('style', MetaMaskUiCss()),
content,
])
render(confirmView, container, function ready() {
var rootElement = findDOMNode(this)
var viewSource = rootElement.outerHTML
unmountComponentAtNode(container)
var svgSource = svgWrapper(viewSource)
// insert content into svg wrapper
cb(null, svgSource)
}) })
} }
function svgWrapper (content) {
var wrapperSource = `
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="240">
<foreignObject x="0" y="0" width="100%" height="100%">
<body xmlns="http://www.w3.org/1999/xhtml" height="100%">{{content}}</body>
</foreignObject>
</svg>
`
return wrapperSource.split('{{content}}').join(content)
}
function toSvgUri (content) {
return 'data:image/svg+xml;utf8,' + encodeURIComponent(content)
}

@ -12,6 +12,7 @@ module.exports = class MetamaskController {
constructor (opts) { constructor (opts) {
this.opts = opts this.opts = opts
this.listeners = []
this.configManager = new ConfigManager(opts) this.configManager = new ConfigManager(opts)
this.idStore = new IdentityStore({ this.idStore = new IdentityStore({
configManager: this.configManager, configManager: this.configManager,
@ -112,9 +113,9 @@ module.exports = class MetamaskController {
} }
sendUpdate () { sendUpdate () {
if (this.remote) { this.listeners.forEach((remote) => {
this.remote.sendUpdate(this.getState()) remote.sendUpdate(this.getState())
} })
} }
initializeProvider (opts) { initializeProvider (opts) {
@ -130,10 +131,17 @@ module.exports = class MetamaskController {
}, },
// tx signing // tx signing
approveTransaction: this.newUnsignedTransaction.bind(this), approveTransaction: this.newUnsignedTransaction.bind(this),
signTransaction: idStore.signTransaction.bind(idStore), signTransaction: (...args) => {
idStore.signTransaction(...args)
this.sendUpdate()
},
// msg signing // msg signing
approveMessage: this.newUnsignedMessage.bind(this), approveMessage: this.newUnsignedMessage.bind(this),
signMessage: idStore.signMessage.bind(idStore), signMessage: (...args) => {
idStore.signMessage(...args)
this.sendUpdate()
},
} }
var provider = MetaMaskProvider(providerOpts) var provider = MetaMaskProvider(providerOpts)
@ -191,8 +199,13 @@ module.exports = class MetamaskController {
const idStore = this.idStore const idStore = this.idStore
var state = idStore.getState() var state = idStore.getState()
let err = this.enforceTxValidations(txParams)
if (err) return onTxDoneCb(err)
// It's locked // It's locked
if (!state.isUnlocked) { if (!state.isUnlocked) {
// Allow the environment to define an unlock message.
this.opts.unlockAccountMessage() this.opts.unlockAccountMessage()
idStore.addUnconfirmedTransaction(txParams, onTxDoneCb, noop) idStore.addUnconfirmedTransaction(txParams, onTxDoneCb, noop)
@ -200,11 +213,19 @@ module.exports = class MetamaskController {
} else { } else {
idStore.addUnconfirmedTransaction(txParams, onTxDoneCb, (err, txData) => { idStore.addUnconfirmedTransaction(txParams, onTxDoneCb, (err, txData) => {
if (err) return onTxDoneCb(err) if (err) return onTxDoneCb(err)
this.sendUpdate()
this.opts.showUnconfirmedTx(txParams, txData, onTxDoneCb) this.opts.showUnconfirmedTx(txParams, txData, onTxDoneCb)
}) })
} }
} }
enforceTxValidations (txParams) {
if (('value' in txParams) && txParams.value.indexOf('-') === 0) {
const msg = `Invalid transaction value of ${txParams.value} not a positive number.`
return new Error(msg)
}
}
newUnsignedMessage (msgParams, cb) { newUnsignedMessage (msgParams, cb) {
var state = this.idStore.getState() var state = this.idStore.getState()
if (!state.isUnlocked) { if (!state.isUnlocked) {
@ -212,6 +233,7 @@ module.exports = class MetamaskController {
this.opts.unlockAccountMessage() this.opts.unlockAccountMessage()
} else { } else {
this.addUnconfirmedMessage(msgParams, cb) this.addUnconfirmedMessage(msgParams, cb)
this.sendUpdate()
} }
} }

@ -9,7 +9,9 @@ const injectCss = require('inject-css')
const PortStream = require('./lib/port-stream.js') const PortStream = require('./lib/port-stream.js')
const StreamProvider = require('web3-stream-provider') const StreamProvider = require('web3-stream-provider')
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
const isPopupOrNotification = require('./lib/is-popup-or-notification')
const extension = require('./lib/extension') const extension = require('./lib/extension')
const notification = require('./lib/notifications')
// setup app // setup app
var css = MetaMaskUiCss() var css = MetaMaskUiCss()
@ -22,7 +24,11 @@ async.parallel({
function connectToAccountManager (cb) { function connectToAccountManager (cb) {
// setup communication with background // setup communication with background
var pluginPort = extension.runtime.connect({name: 'popup'})
var name = isPopupOrNotification()
closePopupIfOpen(name)
window.METAMASK_UI_TYPE = name
var pluginPort = extension.runtime.connect({ name })
var portStream = new PortStream(pluginPort) var portStream = new PortStream(pluginPort)
// setup multiplexing // setup multiplexing
var mx = setupMultiplex(portStream) var mx = setupMultiplex(portStream)
@ -68,22 +74,12 @@ function getCurrentDomain (cb) {
}) })
} }
function clearNotifications(){
extension.notifications.getAll(function (object) {
for (let notification in object){
extension.notifications.clear(notification)
}
})
}
function setupApp (err, opts) { function setupApp (err, opts) {
if (err) { if (err) {
alert(err.stack) alert(err.stack)
throw err throw err
} }
clearNotifications()
var container = document.getElementById('app-content') var container = document.getElementById('app-content')
MetaMaskUi({ MetaMaskUi({
@ -93,3 +89,9 @@ function setupApp (err, opts) {
networkVersion: opts.networkVersion, networkVersion: opts.networkVersion,
}) })
} }
function closePopupIfOpen(name) {
if (name !== 'notification') {
notification.closePopup()
}
}

@ -4,9 +4,4 @@ machine:
dependencies: dependencies:
pre: pre:
- "npm i -g testem" - "npm i -g testem"
override: - "npm i -g mocha"
- sudo apt-get install libxss1 libappindicator1 libindicator7 lsb-base
- curl -L -o google-chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
- sudo dpkg -i google-chrome.deb
- sudo sed -i 's|HERE/chrome\"|HERE/chrome\" --disable-setuid-sandbox|g' /opt/google/chrome/google-chrome
- rm google-chrome.deb

File diff suppressed because one or more lines are too long

@ -158,7 +158,7 @@
} }
], ],
"selectedAddress": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", "selectedAddress": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"network": "loading", "network": "166",
"seedWords": null, "seedWords": null,
"isConfirmed": true, "isConfirmed": true,
"isEthConfirmed": true, "isEthConfirmed": true,

@ -1,84 +1,44 @@
{ {
"metamask": { "metamask": {
"isInitialized": true, "isInitialized": true,
"isUnlocked": true, "isUnlocked": false,
"isEthConfirmed": true,
"currentDomain": "example.com", "currentDomain": "example.com",
"rpcTarget": "https://rawtestrpc.metamask.io/", "rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": { "identities": {},
"0x5f11b68b7d41633e74c6b18d8b8d147da52aedd6": {
"name": "Wallet 1",
"address": "0x5f11b68b7d41633e74c6b18d8b8d147da52aedd6",
"mayBeFauceting": false
},
"0x843963b837841dad3b0f5969ff271108776616df": {
"name": "Wallet 2",
"address": "0x843963b837841dad3b0f5969ff271108776616df",
"mayBeFauceting": false
},
"0x2cb215323857bec1c91e5db10fe87379a5cf129a": {
"name": "Wallet 3",
"address": "0x2cb215323857bec1c91e5db10fe87379a5cf129a",
"mayBeFauceting": false
},
"0xc5091450b7548b0dce3a76b8d325929c39e648d1": {
"name": "Wallet 4",
"address": "0xc5091450b7548b0dce3a76b8d325929c39e648d1",
"mayBeFauceting": false
}
},
"unconfTxs": {}, "unconfTxs": {},
"accounts": { "currentFiat": "USD",
"0x5f11b68b7d41633e74c6b18d8b8d147da52aedd6": { "conversionRate": 11.4379398,
"balance": "0x0", "conversionDate": 1473358355,
"nonce": "0x0", "accounts": {},
"code": "0x",
"address": "0x5f11b68b7d41633e74c6b18d8b8d147da52aedd6"
},
"0x843963b837841dad3b0f5969ff271108776616df": {
"balance": "0x0",
"nonce": "0x0",
"code": "0x",
"address": "0x843963b837841dad3b0f5969ff271108776616df"
},
"0x2cb215323857bec1c91e5db10fe87379a5cf129a": {
"balance": "0x0",
"nonce": "0x0",
"code": "0x",
"address": "0x2cb215323857bec1c91e5db10fe87379a5cf129a"
},
"0xc5091450b7548b0dce3a76b8d325929c39e648d1": {
"balance": "0x0",
"nonce": "0x0",
"code": "0x",
"address": "0xc5091450b7548b0dce3a76b8d325929c39e648d1"
}
},
"transactions": [], "transactions": [],
"selectedAddress": "0x843963b837841dad3b0f5969ff271108776616df", "selectedAddress": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"network": "2", "network": "1473186153102",
"seedWords": null,
"isConfirmed": true, "isConfirmed": true,
"unconfMsgs": {}, "unconfMsgs": {},
"messages": [], "messages": [],
"shapeShiftTxList": [],
"provider": { "provider": {
"type": "testnet" "type": "rpc",
"rpcTarget": "http://localhost:8545"
}, },
"selectedAccount": "0x843963b837841dad3b0f5969ff271108776616df" "selectedAccount": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
}, },
"appState": { "appState": {
"menuOpen": false, "menuOpen": false,
"currentView": { "currentView": {
"name": "accountDetail" "name": "accountDetail",
"detailView": null,
"context": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
}, },
"accountDetail": { "accountDetail": {
"subview": "transactions", "subview": "transactions"
"accountExport": "none",
"privateKey": ""
}, },
"currentDomain": "testfaucet.metamask.io", "currentDomain": "127.0.0.1:9966",
"transForward": false, "transForward": true,
"isLoading": false, "isLoading": false,
"warning": null, "warning": null
"scrollToBottom": false
}, },
"identities": {} "identities": {}
} }

@ -0,0 +1,39 @@
{
"metamask": {
"isInitialized": false,
"isUnlocked": false,
"isEthConfirmed": false,
"currentDomain": "example.com",
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {},
"unconfTxs": {},
"currentFiat": "USD",
"conversionRate": 0,
"conversionDate": "N/A",
"accounts": {},
"transactions": [],
"seedWords": null,
"isConfirmed": true,
"unconfMsgs": {},
"messages": [],
"shapeShiftTxList": [],
"provider": {
"type": "testnet"
},
"network": "2"
},
"appState": {
"menuOpen": false,
"currentView": {
"name": "restoreVault"
},
"accountDetail": {
"subview": "transactions"
},
"currentDomain": "extensions",
"transForward": true,
"isLoading": false,
"warning": null
},
"identities": {}
}

@ -0,0 +1,76 @@
{
"metamask": {
"isInitialized": true,
"isUnlocked": true,
"isEthConfirmed": false,
"currentDomain": "example.com",
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {
"name": "Wallet 1",
"address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"mayBeFauceting": false
},
"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": {
"name": "Wallet 2",
"address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb",
"mayBeFauceting": false
},
"0x2f8d4a878cfa04a6e60d46362f5644deab66572d": {
"name": "Wallet 3",
"address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d",
"mayBeFauceting": false
}
},
"unconfTxs": {},
"currentFiat": "USD",
"conversionRate": 11.21283484,
"conversionDate": 1472158984,
"accounts": {
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {
"code": "0x",
"balance": "0x34693f54a1e25900",
"nonce": "0x100013",
"address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
},
"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": {
"code": "0x",
"nonce": "0x100000",
"balance": "0x18af912cee770000",
"address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb"
},
"0x2f8d4a878cfa04a6e60d46362f5644deab66572d": {
"code": "0x",
"nonce": "0x100000",
"balance": "0x2386f26fc10000",
"address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d"
}
},
"transactions": [],
"network": "2",
"seedWords": null,
"isConfirmed": true,
"unconfMsgs": {},
"messages": [],
"shapeShiftTxList": [],
"provider": {
"type": "testnet"
},
"selectedAccount": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
},
"appState": {
"menuOpen": false,
"currentView": {
"name": "sendTransaction"
},
"accountDetail": {
"subview": "transactions"
},
"currentDomain": "127.0.0.1:9966",
"transForward": true,
"isLoading": false,
"warning": null,
"detailView": {}
},
"identities": {}
}

@ -0,0 +1,348 @@
{
"metamask": {
"isInitialized": true,
"isUnlocked": true,
"isEthConfirmed": true,
"currentDomain": "example.com",
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {
"name": "Wallet 1",
"address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"mayBeFauceting": false
},
"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": {
"name": "Wallet 2",
"address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb",
"mayBeFauceting": false
},
"0x2f8d4a878cfa04a6e60d46362f5644deab66572d": {
"name": "Wallet 3",
"address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d",
"mayBeFauceting": false
}
},
"unconfTxs": {},
"currentFiat": "USD",
"conversionRate": 11.21274318,
"conversionDate": 1472159644,
"accounts": {
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {
"code": "0x",
"nonce": "0x13",
"balance": "0x461d4a64e937d3d1",
"address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
},
"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": {
"code": "0x",
"nonce": "0x0",
"balance": "0x0",
"address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb"
},
"0x2f8d4a878cfa04a6e60d46362f5644deab66572d": {
"code": "0x",
"balance": "0x0",
"nonce": "0x0",
"address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d"
}
},
"transactions": [],
"selectedAddress": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"network": "1",
"seedWords": null,
"isConfirmed": true,
"unconfMsgs": {},
"messages": [],
"shapeShiftTxList": [],
"provider": {
"type": "mainnet"
},
"selectedAccount": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
},
"appState": {
"menuOpen": false,
"currentView": {
"name": "buyEth",
"context": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
},
"accountDetail": {
"subview": "transactions"
},
"currentDomain": "127.0.0.1:9966",
"transForward": true,
"isLoading": false,
"detailView": {},
"buyView": {
"subview": "buyForm",
"formView": {
"coinbase": false,
"shapeshift": true,
"marketinfo": {
"pair": "btc_eth",
"rate": 51.14252949,
"minerFee": 0.01,
"limit": 2.60306578,
"minimum": 0.00038935,
"maxLimit": 8.67688592
},
"coinOptions": {
"BTC": {
"name": "Bitcoin",
"symbol": "BTC",
"image": "https://shapeshift.io/images/coins/bitcoin.png",
"status": "available"
},
"BCY": {
"name": "BitCrystals",
"symbol": "BCY",
"image": "https://shapeshift.io/images/coins/bitcrystals.png",
"status": "available"
},
"BLK": {
"name": "Blackcoin",
"symbol": "BLK",
"image": "https://shapeshift.io/images/coins/blackcoin.png",
"status": "available"
},
"BTS": {
"name": "Bitshares",
"symbol": "BTS",
"specialReturn": false,
"specialOutgoing": true,
"specialIncoming": true,
"fieldName": "destTag",
"fieldKey": "destTag",
"image": "https://shapeshift.io/images/coins/bitshares.png",
"status": "available"
},
"CLAM": {
"name": "Clams",
"symbol": "CLAM",
"image": "https://shapeshift.io/images/coins/clams.png",
"status": "available"
},
"DASH": {
"name": "Dash",
"symbol": "DASH",
"image": "https://shapeshift.io/images/coins/dash.png",
"status": "available"
},
"DGB": {
"name": "Digibyte",
"symbol": "DGB",
"image": "https://shapeshift.io/images/coins/digibyte.png",
"status": "available"
},
"DAO": {
"name": "TheDao",
"symbol": "DAO",
"image": "https://shapeshift.io/images/coins/thedao.png",
"status": "available"
},
"DGD": {
"name": "DigixDao",
"symbol": "DGD",
"image": "https://shapeshift.io/images/coins/digixdao.png",
"status": "available"
},
"DOGE": {
"name": "Dogecoin",
"symbol": "DOGE",
"image": "https://shapeshift.io/images/coins/dogecoin.png",
"status": "available"
},
"EMC": {
"name": "Emercoin",
"symbol": "EMC",
"image": "https://shapeshift.io/images/coins/emercoin.png",
"status": "available"
},
"ETH": {
"name": "Ether",
"symbol": "ETH",
"image": "https://shapeshift.io/images/coins/ether.png",
"status": "available"
},
"ETC": {
"name": "Ether Classic",
"symbol": "ETC",
"image": "https://shapeshift.io/images/coins/etherclassic.png",
"status": "available"
},
"FCT": {
"name": "Factoids",
"symbol": "FCT",
"image": "https://shapeshift.io/images/coins/factoids.png",
"status": "available"
},
"LBC": {
"name": "LBRY Credits",
"symbol": "LBC",
"image": "https://shapeshift.io/images/coins/lbry.png",
"status": "available"
},
"LSK": {
"name": "Lisk",
"symbol": "LSK",
"image": "https://shapeshift.io/images/coins/lisk.png",
"status": "available"
},
"LTC": {
"name": "Litecoin",
"symbol": "LTC",
"image": "https://shapeshift.io/images/coins/litecoin.png",
"status": "available"
},
"MAID": {
"name": "Maidsafe",
"symbol": "MAID",
"image": "https://shapeshift.io/images/coins/maidsafe.png",
"status": "available"
},
"MINT": {
"name": "Mintcoin",
"symbol": "MINT",
"image": "https://shapeshift.io/images/coins/mintcoin.png",
"status": "available"
},
"MONA": {
"name": "Monacoin",
"symbol": "MONA",
"image": "https://shapeshift.io/images/coins/monacoin.png",
"status": "available"
},
"MSC": {
"name": "Omni",
"symbol": "MSC",
"image": "https://shapeshift.io/images/coins/mastercoin.png",
"status": "available"
},
"NBT": {
"name": "Nubits",
"symbol": "NBT",
"image": "https://shapeshift.io/images/coins/nubits.png",
"status": "available"
},
"NMC": {
"name": "Namecoin",
"symbol": "NMC",
"image": "https://shapeshift.io/images/coins/namecoin.png",
"status": "available"
},
"NVC": {
"name": "Novacoin",
"symbol": "NVC",
"image": "https://shapeshift.io/images/coins/novacoin.png",
"status": "available"
},
"NXT": {
"name": "Nxt",
"symbol": "NXT",
"specialReturn": false,
"specialOutgoing": true,
"specialIncoming": true,
"specialIncomingStatus": false,
"fieldName": "Public Key (only for unfunded accounts!)",
"fieldKey": "rsAddress",
"image": "https://shapeshift.io/images/coins/nxt.png",
"status": "available"
},
"PPC": {
"name": "Peercoin",
"symbol": "PPC",
"image": "https://shapeshift.io/images/coins/peercoin.png",
"status": "available"
},
"RDD": {
"name": "Reddcoin",
"symbol": "RDD",
"image": "https://shapeshift.io/images/coins/reddcoin.png",
"status": "available"
},
"SDC": {
"name": "Shadowcash",
"symbol": "SDC",
"image": "https://shapeshift.io/images/coins/shadowcash.png",
"status": "available"
},
"SC": {
"name": "Siacoin",
"symbol": "SC",
"image": "https://shapeshift.io/images/coins/siacoin.png",
"status": "available"
},
"SJCX": {
"name": "StorjX",
"symbol": "SJCX",
"image": "https://shapeshift.io/images/coins/storjcoinx.png",
"status": "available"
},
"START": {
"name": "Startcoin",
"symbol": "START",
"image": "https://shapeshift.io/images/coins/startcoin.png",
"status": "available"
},
"STEEM": {
"name": "Steem",
"symbol": "STEEM",
"specialReturn": false,
"specialOutgoing": true,
"specialIncoming": true,
"fieldName": "destTag",
"fieldKey": "destTag",
"image": "https://shapeshift.io/images/coins/steem.png",
"status": "available"
},
"USDT": {
"name": "Tether",
"symbol": "USDT",
"image": "https://shapeshift.io/images/coins/tether.png",
"status": "available"
},
"VOX": {
"name": "Voxels",
"symbol": "VOX",
"image": "https://shapeshift.io/images/coins/voxels.png",
"status": "available"
},
"VRC": {
"name": "Vericoin",
"symbol": "VRC",
"image": "https://shapeshift.io/images/coins/vericoin.png",
"status": "available"
},
"VTC": {
"name": "Vertcoin",
"symbol": "VTC",
"image": "https://shapeshift.io/images/coins/vertcoin.png",
"status": "available"
},
"XCP": {
"name": "Counterparty",
"symbol": "XCP",
"image": "https://shapeshift.io/images/coins/counterparty.png",
"status": "available"
},
"XMR": {
"name": "Monero",
"symbol": "XMR",
"specialReturn": false,
"specialOutgoing": true,
"specialIncoming": true,
"fieldName": "Payment Id",
"qrName": "tx_payment_id",
"fieldKey": "paymentId",
"image": "https://shapeshift.io/images/coins/monero.png",
"status": "available"
}
}
},
"buyAddress": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"amount": "5.00",
"warning": null
},
"isSubLoading": false
},
"identities": {}
}

@ -0,0 +1,28 @@
# Form Persisting Architecture
Since:
- The popup is torn down completely on every click outside of it.
- We have forms with multiple fields (like passwords & seed phrases) that might encourage a user to leave our panel to refer to a password manager.
We cause user friction when we lose the contents of certain forms.
This calls for an architecture of a form component that can completely persist its values to LocalStorage on every relevant change, and restore those values on reopening.
To achieve this, we have defined a class, a subclass of `React.Component`, called `PersistentForm`, and it's stored at `ui/lib/persistent-form.js`.
To use this class, simply take your form component (the component that renders `input`, `select`, or `textarea` elements), and make it subclass from `PersistentForm` instead of `React.Component`.
You can see an example of this in use in `ui/app/first-time/restore-vault.js`.
Additionally, any field whose value should be persisted, should have a `persistentFormId` attribute, which needs to be assigned under a `dataset` key on the main `attributes` hash. For example:
```javascript
return h('textarea.twelve-word-phrase.letter-spacey', {
dataset: {
persistentFormId: 'wallet-seed',
},
})
```
That's it! This field should be persisted to `localStorage` on each `keyUp`, those values should be restored on view load, and the cached values should be cleared when navigating deliberately away from the form.

@ -16,6 +16,10 @@ var eslint = require('gulp-eslint')
var fs = require('fs') var fs = require('fs')
var path = require('path') var path = require('path')
var manifest = require('./app/manifest.json') var manifest = require('./app/manifest.json')
var gulpif = require('gulp-if')
var replace = require('gulp-replace')
var disableLiveReload = gutil.env.disableLiveReload
// browser reload // browser reload
@ -34,6 +38,7 @@ gulp.task('copy:locales', copyTask({
destinations: [ destinations: [
'./dist/firefox/_locales', './dist/firefox/_locales',
'./dist/chrome/_locales', './dist/chrome/_locales',
'./dist/edge/_locales',
] ]
})) }))
gulp.task('copy:images', copyTask({ gulp.task('copy:images', copyTask({
@ -41,6 +46,7 @@ gulp.task('copy:images', copyTask({
destinations: [ destinations: [
'./dist/firefox/images', './dist/firefox/images',
'./dist/chrome/images', './dist/chrome/images',
'./dist/edge/images',
], ],
})) }))
gulp.task('copy:fonts', copyTask({ gulp.task('copy:fonts', copyTask({
@ -48,6 +54,7 @@ gulp.task('copy:fonts', copyTask({
destinations: [ destinations: [
'./dist/firefox/fonts', './dist/firefox/fonts',
'./dist/chrome/fonts', './dist/chrome/fonts',
'./dist/edge/fonts',
], ],
})) }))
gulp.task('copy:reload', copyTask({ gulp.task('copy:reload', copyTask({
@ -55,6 +62,7 @@ gulp.task('copy:reload', copyTask({
destinations: [ destinations: [
'./dist/firefox/scripts', './dist/firefox/scripts',
'./dist/chrome/scripts', './dist/chrome/scripts',
'./dist/edge/scripts',
], ],
pattern: '/chromereload.js', pattern: '/chromereload.js',
})) }))
@ -63,12 +71,13 @@ gulp.task('copy:root', copyTask({
destinations: [ destinations: [
'./dist/firefox', './dist/firefox',
'./dist/chrome', './dist/chrome',
'./dist/edge',
], ],
pattern: '/*', pattern: '/*',
})) }))
gulp.task('manifest:cleanup', function() { gulp.task('manifest:chrome', function() {
return gulp.src('./dist/firefox/manifest.json') return gulp.src('./dist/chrome/manifest.json')
.pipe(jsoneditor(function(json) { .pipe(jsoneditor(function(json) {
delete json.applications delete json.applications
return json return json
@ -76,7 +85,33 @@ gulp.task('manifest:cleanup', function() {
.pipe(gulp.dest('./dist/chrome', { overwrite: true })) .pipe(gulp.dest('./dist/chrome', { overwrite: true }))
}) })
gulp.task('copy', gulp.series(gulp.parallel('copy:locales','copy:images','copy:fonts','copy:reload','copy:root'), 'manifest:cleanup')) gulp.task('manifest:production', function() {
return gulp.src([
'./dist/firefox/manifest.json',
'./dist/chrome/manifest.json',
'./dist/edge/manifest.json',
],{base: './dist/'})
.pipe(gulpif(disableLiveReload,jsoneditor(function(json) {
json.background.scripts = ["scripts/background.js"]
return json
})))
.pipe(gulp.dest('./dist/', { overwrite: true }))
})
const staticFiles = [
'locales',
'images',
'fonts',
'root'
]
var copyStrings = staticFiles.map(staticFile => `copy:${staticFile}`)
if (!disableLiveReload) {
copyStrings.push('copy:reload')
}
gulp.task('copy', gulp.series(gulp.parallel(...copyStrings), 'manifest:production', 'manifest:chrome'))
gulp.task('copy:watch', function(){ gulp.task('copy:watch', function(){
gulp.watch(['./app/{_locales,images}/*', './app/scripts/chromereload.js', './app/*.{html,json}'], gulp.series('copy')) gulp.watch(['./app/{_locales,images}/*', './app/scripts/chromereload.js', './app/*.{html,json}'], gulp.series('copy'))
}) })
@ -110,14 +145,18 @@ const jsFiles = [
'popup', 'popup',
] ]
var jsDevStrings = jsFiles.map(jsFile => `dev:js:${jsFile}`)
var jsBuildStrings = jsFiles.map(jsFile => `build:js:${jsFile}`)
jsFiles.forEach((jsFile) => { jsFiles.forEach((jsFile) => {
gulp.task(`dev:js:${jsFile}`, bundleTask({ watch: true, filename: `${jsFile}.js` })) gulp.task(`dev:js:${jsFile}`, bundleTask({ watch: true, filename: `${jsFile}.js` }))
gulp.task(`build:js:${jsFile}`, bundleTask({ watch: false, filename: `${jsFile}.js` })) gulp.task(`build:js:${jsFile}`, bundleTask({ watch: false, filename: `${jsFile}.js` }))
}) })
gulp.task('dev:js', gulp.parallel('dev:js:inpage','dev:js:contentscript','dev:js:background','dev:js:popup')) gulp.task('dev:js', gulp.parallel(...jsDevStrings))
gulp.task('build:js', gulp.parallel(...jsBuildStrings))
gulp.task('build:js', gulp.parallel('build:js:inpage','build:js:contentscript','build:js:background','build:js:popup'))
// clean dist // clean dist
@ -131,17 +170,23 @@ gulp.task('zip:chrome', () => {
return gulp.src('dist/chrome/**') return gulp.src('dist/chrome/**')
.pipe(zip(`metamask-chrome-${manifest.version}.zip`)) .pipe(zip(`metamask-chrome-${manifest.version}.zip`))
.pipe(gulp.dest('builds')); .pipe(gulp.dest('builds'));
}); })
gulp.task('zip:firefox', () => { gulp.task('zip:firefox', () => {
return gulp.src('dist/firefox/**') return gulp.src('dist/firefox/**')
.pipe(zip(`metamask-firefox-${manifest.version}.zip`)) .pipe(zip(`metamask-firefox-${manifest.version}.zip`))
.pipe(gulp.dest('builds')); .pipe(gulp.dest('builds'));
}); })
gulp.task('zip', gulp.parallel('zip:chrome', 'zip:firefox')) gulp.task('zip:edge', () => {
return gulp.src('dist/edge/**')
.pipe(zip(`metamask-edge-${manifest.version}.zip`))
.pipe(gulp.dest('builds'));
})
gulp.task('zip', gulp.parallel('zip:chrome', 'zip:firefox', 'zip:edge'))
// high level tasks // high level tasks
gulp.task('dev', gulp.series('dev:js', 'copy', gulp.parallel('copy:watch', 'dev:reload'))) gulp.task('dev', gulp.series('dev:js', 'copy', gulp.parallel('copy:watch', 'dev:reload')))
gulp.task('build', gulp.series('clean', gulp.parallel('build:js', 'copy'))) gulp.task('build', gulp.series('clean', gulp.parallel('build:js', 'copy')))
gulp.task('dist', gulp.series('build', 'zip')) gulp.task('dist', gulp.series('build', 'zip'))
@ -160,7 +205,7 @@ function copyTask(opts){
destinations.forEach(function(destination) { destinations.forEach(function(destination) {
stream = stream.pipe(gulp.dest(destination)) stream = stream.pipe(gulp.dest(destination))
}) })
stream.pipe(livereload()) stream.pipe(gulpif(!disableLiveReload,livereload()))
return stream return stream
} }
@ -200,7 +245,8 @@ function bundleTask(opts) {
.pipe(sourcemaps.write('./')) // writes .map file .pipe(sourcemaps.write('./')) // writes .map file
.pipe(gulp.dest('./dist/firefox/scripts')) .pipe(gulp.dest('./dist/firefox/scripts'))
.pipe(gulp.dest('./dist/chrome/scripts')) .pipe(gulp.dest('./dist/chrome/scripts'))
.pipe(livereload()) .pipe(gulp.dest('./dist/edge/scripts'))
.pipe(gulpif(!disableLiveReload,livereload()))
) )
} }

@ -54,7 +54,7 @@
"inject-css": "^0.1.1", "inject-css": "^0.1.1",
"jazzicon": "^1.1.3", "jazzicon": "^1.1.3",
"menu-droppo": "^1.1.0", "menu-droppo": "^1.1.0",
"metamask-logo": "^1.3.1", "metamask-logo": "^2.1.2",
"mississippi": "^1.2.0", "mississippi": "^1.2.0",
"multiplex": "^6.7.0", "multiplex": "^6.7.0",
"once": "^1.3.3", "once": "^1.3.3",
@ -97,8 +97,10 @@
"del": "^2.2.0", "del": "^2.2.0",
"gulp": "github:gulpjs/gulp#4.0", "gulp": "github:gulpjs/gulp#4.0",
"gulp-brfs": "^0.1.0", "gulp-brfs": "^0.1.0",
"gulp-if": "^2.0.1",
"gulp-json-editor": "^2.2.1", "gulp-json-editor": "^2.2.1",
"gulp-livereload": "^3.8.1", "gulp-livereload": "^3.8.1",
"gulp-replace": "^0.5.4",
"gulp-sourcemaps": "^1.6.0", "gulp-sourcemaps": "^1.6.0",
"gulp-util": "^3.0.7", "gulp-util": "^3.0.7",
"gulp-watch": "^4.3.5", "gulp-watch": "^4.3.5",

@ -9,6 +9,34 @@ var Extension = require(path.join(__dirname, '..', '..', 'app', 'scripts', 'lib'
describe('extension', function() { describe('extension', function() {
describe('extension.getURL', function() {
const desiredResult = 'http://the-desired-result.io'
describe('in Chrome or Firefox', function() {
GLOBAL.chrome.extension = {
getURL: () => desiredResult
}
it('returns the desired result', function() {
const extension = new Extension()
const result = extension.extension.getURL()
assert.equal(result, desiredResult)
})
})
describe('in Microsoft Edge', function() {
GLOBAL.browser.extension = {
getURL: () => desiredResult
}
it('returns the desired result', function() {
const extension = new Extension()
const result = extension.extension.getURL()
assert.equal(result, desiredResult)
})
})
})
describe('with chrome global', function() { describe('with chrome global', function() {
let extension let extension
@ -45,4 +73,5 @@ describe('extension', function() {
}) })
}) })
}) })

@ -0,0 +1,97 @@
var assert = require('assert')
var MetaMaskController = require('../../app/scripts/metamask-controller')
var sinon = require('sinon')
var extend = require('xtend')
const STORAGE_KEY = 'metamask-config'
describe('MetaMaskController', function() {
const noop = () => {}
let controller = new MetaMaskController({
showUnconfirmedMessage: noop,
unlockAccountMessage: noop,
showUnconfirmedTx: noop,
setData,
loadData,
})
beforeEach(function() {
// sinon allows stubbing methods that are easily verified
this.sinon = sinon.sandbox.create()
window.localStorage = {} // Hacking localStorage support into JSDom
})
afterEach(function() {
// sinon requires cleanup otherwise it will overwrite context
this.sinon.restore()
})
describe('#enforceTxValidations', function () {
it('returns null for positive values', function() {
var sample = {
value: '0x01'
}
var res = controller.enforceTxValidations(sample)
assert.equal(res, null, 'no error')
})
it('returns error for negative values', function() {
var sample = {
value: '-0x01'
}
var res = controller.enforceTxValidations(sample)
assert.ok(res, 'error')
})
})
})
function loadData () {
var oldData = getOldStyleData()
var newData
try {
newData = JSON.parse(window.localStorage[STORAGE_KEY])
} catch (e) {}
var data = extend({
meta: {
version: 0,
},
data: {
config: {
provider: {
type: 'testnet',
},
},
},
}, oldData || null, newData || null)
return data
}
function getOldStyleData () {
var config, wallet, seedWords
var result = {
meta: { version: 0 },
data: {},
}
try {
config = JSON.parse(window.localStorage['config'])
result.data.config = config
} catch (e) {}
try {
wallet = JSON.parse(window.localStorage['lightwallet'])
result.data.wallet = wallet
} catch (e) {}
try {
seedWords = window.localStorage['seedWords']
result.data.seedWords = seedWords
} catch (e) {}
return result
}
function setData (data) {
window.localStorage[STORAGE_KEY] = JSON.stringify(data)
}

@ -10,7 +10,7 @@ const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
const valuesFor = require('./util').valuesFor const valuesFor = require('./util').valuesFor
const Identicon = require('./components/identicon') const Identicon = require('./components/identicon')
const AccountEtherBalance = require('./components/account-eth-balance') const EthBalance = require('./components/eth-balance')
const TransactionList = require('./components/transaction-list') const TransactionList = require('./components/transaction-list')
const ExportAccountView = require('./components/account-export') const ExportAccountView = require('./components/account-export')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
@ -168,7 +168,7 @@ AccountDetailScreen.prototype.render = function () {
}, },
}, [ }, [
h(AccountEtherBalance, { h(EthBalance, {
value: account && account.balance, value: account && account.balance,
style: { style: {
lineHeight: '7px', lineHeight: '7px',
@ -235,7 +235,7 @@ AccountDetailScreen.prototype.subview = function () {
AccountDetailScreen.prototype.transactionList = function () { AccountDetailScreen.prototype.transactionList = function () {
const { transactions, unconfTxs, unconfMsgs, address, network, shapeShiftTxList } = this.props const { transactions, unconfTxs, unconfMsgs, address, network, shapeShiftTxList } = this.props
var txsToRender = transactions var txsToRender = transactions.concat(unconfTxs)
// only transactions that are from the current address // only transactions that are from the current address
.filter(tx => tx.txParams.from === address) .filter(tx => tx.txParams.from === address)
// only transactions that are on the current network // only transactions that are on the current network

@ -3,7 +3,7 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const AccountEtherBalance = require('../components/account-eth-balance') const EthBalance = require('../components/eth-balance')
const CopyButton = require('../components/copyButton') const CopyButton = require('../components/copyButton')
const Identicon = require('../components/identicon') const Identicon = require('../components/identicon')
@ -50,7 +50,7 @@ NewComponent.prototype.render = function () {
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
}, },
}, ethUtil.toChecksumAddress(identity.address)), }, ethUtil.toChecksumAddress(identity.address)),
h(AccountEtherBalance, { h(EthBalance, {
value: account.balance, value: account.balance,
style: { style: {
lineHeight: '7px', lineHeight: '7px',

@ -11,6 +11,7 @@ module.exports = connect(mapStateToProps)(AccountsScreen)
function mapStateToProps (state) { function mapStateToProps (state) {
const pendingTxs = valuesFor(state.metamask.unconfTxs) const pendingTxs = valuesFor(state.metamask.unconfTxs)
.filter(tx => tx.txParams.metamaskNetworkId === state.metamask.network)
const pendingMsgs = valuesFor(state.metamask.unconfMsgs) const pendingMsgs = valuesFor(state.metamask.unconfMsgs)
const pending = pendingTxs.concat(pendingMsgs) const pending = pendingTxs.concat(pendingMsgs)

@ -137,6 +137,12 @@ var actions = {
getQr: getQr, getQr: getQr,
reshowQrCode: reshowQrCode, reshowQrCode: reshowQrCode,
SHOW_QR_VIEW: 'SHOW_QR_VIEW', SHOW_QR_VIEW: 'SHOW_QR_VIEW',
// FORGOT PASSWORD:
BACK_TO_INIT_MENU: 'BACK_TO_INIT_MENU',
goBackToInitView: goBackToInitView,
RECOVERY_IN_PROGRESS: 'RECOVERY_IN_PROGRESS',
BACK_TO_UNLOCK_VIEW: 'BACK_TO_UNLOCK_VIEW',
backToUnlockView: backToUnlockView,
} }
module.exports = actions module.exports = actions
@ -156,8 +162,10 @@ function goHome () {
function tryUnlockMetamask (password) { function tryUnlockMetamask (password) {
return (dispatch) => { return (dispatch) => {
dispatch(actions.showLoadingIndication())
dispatch(actions.unlockInProgress()) dispatch(actions.unlockInProgress())
_accountManager.submitPassword(password, (err, selectedAccount) => { _accountManager.submitPassword(password, (err, selectedAccount) => {
dispatch(actions.hideLoadingIndication())
if (err) { if (err) {
dispatch(actions.unlockFailed()) dispatch(actions.unlockFailed())
} else { } else {
@ -270,8 +278,6 @@ function signMsg (msgData) {
function signTx (txData) { function signTx (txData) {
return (dispatch) => { return (dispatch) => {
dispatch(actions.showLoadingIndication())
web3.eth.sendTransaction(txData, (err, data) => { web3.eth.sendTransaction(txData, (err, data) => {
dispatch(actions.hideLoadingIndication()) dispatch(actions.hideLoadingIndication())
@ -279,6 +285,7 @@ function signTx (txData) {
dispatch(actions.hideWarning()) dispatch(actions.hideWarning())
dispatch(actions.goHome()) dispatch(actions.goHome())
}) })
dispatch(this.showConfTxPage())
} }
} }
@ -370,6 +377,12 @@ function showNewVaultSeed (seed) {
} }
} }
function backToUnlockView () {
return {
type: actions.BACK_TO_UNLOCK_VIEW,
}
}
// //
// unlock screen // unlock screen
// //
@ -498,6 +511,12 @@ function showConfigPage (transitionForward = true) {
} }
} }
function goBackToInitView () {
return {
type: actions.BACK_TO_INIT_MENU,
}
}
// //
// config // config
// //

@ -51,6 +51,7 @@ function mapStateToProps (state) {
menuOpen: state.appState.menuOpen, menuOpen: state.appState.menuOpen,
network: state.metamask.network, network: state.metamask.network,
provider: state.metamask.provider, provider: state.metamask.provider,
forgottenPassword: state.appState.forgottenPassword,
} }
} }
@ -89,6 +90,7 @@ App.prototype.render = function () {
transitionLeaveTimeout: 300, transitionLeaveTimeout: 300,
}, [ }, [
this.renderPrimary(), this.renderPrimary(),
this.renderBackToInitButton(),
]), ]),
]), ]),
]) ])
@ -96,6 +98,11 @@ App.prototype.render = function () {
} }
App.prototype.renderAppBar = function () { App.prototype.renderAppBar = function () {
if (window.METAMASK_UI_TYPE === 'notification') {
return null
}
const props = this.props const props = this.props
const state = this.state || {} const state = this.state || {}
const isNetworkMenuOpen = state.isNetworkMenuOpen || false const isNetworkMenuOpen = state.isNetworkMenuOpen || false
@ -238,10 +245,17 @@ App.prototype.renderNetworkDropdown = function () {
label: 'Localhost 8545', label: 'Localhost 8545',
closeMenu: () => this.setState({ isNetworkMenuOpen: false }), closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
action: () => props.dispatch(actions.setRpcTarget('http://localhost:8545')), action: () => props.dispatch(actions.setRpcTarget('http://localhost:8545')),
icon: h('i.fa.fa-question-circle.fa-lg', { ariaHidden: true }), icon: h('i.fa.fa-question-circle.fa-lg'),
activeNetworkRender: props.provider.rpcTarget, activeNetworkRender: props.provider.rpcTarget,
}), }),
h(DropMenuItem, {
label: 'Custom RPC',
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
action: () => this.props.dispatch(actions.showConfigPage()),
icon: h('i.fa.fa-question-circle.fa-lg'),
}),
this.renderCustomOption(props.provider.rpcTarget), this.renderCustomOption(props.provider.rpcTarget),
]) ])
} }
@ -275,24 +289,109 @@ App.prototype.renderDropdown = function () {
label: 'Settings', label: 'Settings',
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
action: () => this.props.dispatch(actions.showConfigPage()), action: () => this.props.dispatch(actions.showConfigPage()),
icon: h('i.fa.fa-gear.fa-lg', { ariaHidden: true }), icon: h('i.fa.fa-gear.fa-lg'),
}), }),
h(DropMenuItem, { h(DropMenuItem, {
label: 'Lock', label: 'Lock',
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
action: () => this.props.dispatch(actions.lockMetamask()), action: () => this.props.dispatch(actions.lockMetamask()),
icon: h('i.fa.fa-lock.fa-lg', { ariaHidden: true }), icon: h('i.fa.fa-lock.fa-lg'),
}), }),
h(DropMenuItem, { h(DropMenuItem, {
label: 'Help', label: 'Help',
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
action: () => this.props.dispatch(actions.showInfoPage()), action: () => this.props.dispatch(actions.showInfoPage()),
icon: h('i.fa.fa-question.fa-lg', { ariaHidden: true }), icon: h('i.fa.fa-question.fa-lg'),
}), }),
]) ])
} }
App.prototype.renderBackButton = function (style, justArrow = false) {
var props = this.props
return (
h('.flex-row', {
key: 'leftArrow',
style: style,
onClick: () => props.dispatch(actions.goBackToInitView()),
}, [
h('i.fa.fa-arrow-left.cursor-pointer'),
justArrow ? null : h('div.cursor-pointer', {
style: {
marginLeft: '3px',
},
onClick: () => props.dispatch(actions.goBackToInitView()),
}, 'BACK'),
])
)
}
App.prototype.renderBackToInitButton = function () {
var props = this.props
var button = null
if (!props.isUnlocked) {
if (props.currentView.name === 'InitMenu') {
button = props.forgottenPassword ? h('.flex-row', {
key: 'rightArrow',
style: {
position: 'absolute',
bottom: '10px',
right: '15px',
fontSize: '21px',
fontFamily: 'Montserrat Light',
color: '#7F8082',
width: '77.578px',
alignItems: 'flex-end',
},
}, [
h('div.cursor-pointer', {
style: {
marginRight: '3px',
},
onClick: () => props.dispatch(actions.backToUnlockView()),
}, 'LOGIN'),
h('i.fa.fa-arrow-right.cursor-pointer'),
]) : null
} else if (props.isInitialized) {
var style
switch (props.currentView.name) {
case 'createVault':
style = {
position: 'absolute',
top: '41px',
left: '80px',
fontSize: '21px',
fontFamily: 'Montserrat Bold',
color: 'rgb(174, 174, 174)',
}
return this.renderBackButton(style, true)
case 'restoreVault':
style = {
position: 'absolute',
top: '41px',
left: '70px',
fontSize: '21px',
fontFamily: 'Montserrat Bold',
color: 'rgb(174, 174, 174)',
}
return this.renderBackButton(style, true)
default:
style = {
position: 'absolute',
bottom: '10px',
left: '15px',
fontSize: '21px',
fontFamily: 'Montserrat Light',
color: '#7F8082',
width: '71.969px',
alignItems: 'flex-end',
}
return this.renderBackButton(style)
}
}
}
return button
}
App.prototype.renderPrimary = function () { App.prototype.renderPrimary = function () {
var props = this.props var props = this.props
@ -306,7 +405,7 @@ App.prototype.renderPrimary = function () {
} }
// show initialize screen // show initialize screen
if (!props.isInitialized) { if (!props.isInitialized || props.forgottenPassword) {
// show current view // show current view
switch (props.currentView.name) { switch (props.currentView.name) {
@ -408,25 +507,21 @@ App.prototype.toggleMetamaskActive = function () {
App.prototype.renderCustomOption = function (rpcTarget) { App.prototype.renderCustomOption = function (rpcTarget) {
switch (rpcTarget) { switch (rpcTarget) {
case undefined: case undefined:
return h(DropMenuItem, { return null
label: 'Custom RPC',
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
action: () => this.props.dispatch(actions.showConfigPage()),
icon: h('i.fa.fa-question-circle.fa-lg', { ariaHidden: true }),
})
case 'http://localhost:8545': case 'http://localhost:8545':
return h(DropMenuItem, { return h(DropMenuItem, {
label: 'Custom RPC', label: 'Custom RPC',
closeMenu: () => this.setState({ isNetworkMenuOpen: false }), closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
action: () => this.props.dispatch(actions.showConfigPage()), action: () => this.props.dispatch(actions.showConfigPage()),
icon: h('i.fa.fa-question-circle.fa-lg', { ariaHidden: true }), icon: h('i.fa.fa-question-circle.fa-lg'),
}) })
default: default:
return h(DropMenuItem, { return h(DropMenuItem, {
label: `${rpcTarget}`, label: `${rpcTarget}`,
closeMenu: () => this.setState({ isNetworkMenuOpen: false }), closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
icon: h('i.fa.fa-question-circle.fa-lg', { ariaHidden: true }), icon: h('i.fa.fa-question-circle.fa-lg'),
activeNetworkRender: 'custom', activeNetworkRender: 'custom',
}) })
} }

@ -1,140 +0,0 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const formatBalance = require('../util').formatBalance
const generateBalanceObject = require('../util').generateBalanceObject
const Tooltip = require('./tooltip.js')
module.exports = connect(mapStateToProps)(EthBalanceComponent)
function mapStateToProps (state) {
return {
conversionRate: state.metamask.conversionRate,
conversionDate: state.metamask.conversionDate,
currentFiat: state.metamask.currentFiat,
}
}
inherits(EthBalanceComponent, Component)
function EthBalanceComponent () {
Component.call(this)
}
EthBalanceComponent.prototype.render = function () {
var state = this.props
var style = state.style
const value = formatBalance(state.value, 6)
var width = state.width
return (
h('.ether-balance', {
style: style,
}, [
h('.ether-balance-amount', {
style: {
display: 'inline',
width: width,
},
}, this.renderBalance(value, state)),
])
)
}
EthBalanceComponent.prototype.renderBalance = function (value, state) {
if (value === 'None') return value
var balanceObj = generateBalanceObject(value, state.shorten ? 1 : 3)
var balance, fiatDisplayNumber, fiatTooltipNumber
var splitBalance = value.split(' ')
var ethNumber = splitBalance[0]
var ethSuffix = splitBalance[1]
if (state.conversionRate !== 0) {
fiatTooltipNumber = Number(splitBalance[0]) * state.conversionRate
fiatDisplayNumber = fiatTooltipNumber.toFixed(2)
} else {
fiatDisplayNumber = 'N/A'
}
var fiatSuffix = state.currentFiat
if (state.shorten) {
balance = balanceObj.shortBalance
} else {
balance = balanceObj.balance
}
var label = balanceObj.label
return (
h('.flex-column', [
h(Tooltip, {
position: 'bottom',
title: `${ethNumber} ${ethSuffix}`,
}, [
h('.flex-row', {
style: {
alignItems: 'flex-end',
lineHeight: '13px',
fontFamily: 'Montserrat Light',
textRendering: 'geometricPrecision',
marginBottom: '5px',
},
}, [
h('div', {
style: {
width: '100%',
textAlign: 'right',
},
}, balance),
h('div', {
style: {
color: '#AEAEAE',
marginLeft: '5px',
},
}, label),
]),
]),
h(Tooltip, {
position: 'bottom',
title: `${fiatTooltipNumber} ${fiatSuffix}`,
}, [
fiatDisplay(fiatDisplayNumber, fiatSuffix),
]),
])
)
}
function fiatDisplay (fiatDisplayNumber, fiatSuffix) {
if (fiatDisplayNumber !== 'N/A') {
return h('.flex-row', {
style: {
alignItems: 'flex-end',
lineHeight: '13px',
fontFamily: 'Montserrat Light',
textRendering: 'geometricPrecision',
},
}, [
h('div', {
style: {
width: '100%',
textAlign: 'right',
fontSize: '12px',
color: '#333333',
},
}, fiatDisplayNumber),
h('div', {
style: {
color: '#AEAEAE',
marginLeft: '5px',
fontSize: '12px',
},
}, fiatSuffix),
])
} else {
return h('div')
}
}

@ -14,7 +14,7 @@ function AccountInfoLink () {
AccountInfoLink.prototype.render = function () { AccountInfoLink.prototype.render = function () {
const { selected, network } = this.props const { selected, network } = this.props
const title = 'View account on etherscan' const title = 'View account on Etherscan'
const url = genAccountLink(selected, network) const url = genAccountLink(selected, network)
if (!url) { if (!url) {

@ -106,7 +106,7 @@ BuyButtonSubview.prototype.formVersionSubview = function () {
style: { style: {
width: '225px', width: '225px',
}, },
}, 'In order to access this feature please switch too the Main Network'), }, 'In order to access this feature please switch to the Main Network'),
h('h3.text-transform-uppercase', 'or:'), h('h3.text-transform-uppercase', 'or:'),
this.props.network === '2' ? h('button.text-transform-uppercase', { this.props.network === '2' ? h('button.text-transform-uppercase', {
onClick: () => this.props.dispatch(actions.buyEth()), onClick: () => this.props.dispatch(actions.buyEth()),

@ -4,6 +4,7 @@ const inherits = require('util').inherits
const formatBalance = require('../util').formatBalance const formatBalance = require('../util').formatBalance
const generateBalanceObject = require('../util').generateBalanceObject const generateBalanceObject = require('../util').generateBalanceObject
const Tooltip = require('./tooltip.js') const Tooltip = require('./tooltip.js')
const FiatValue = require('./fiat-value.js')
module.exports = EthBalanceComponent module.exports = EthBalanceComponent
@ -13,11 +14,11 @@ function EthBalanceComponent () {
} }
EthBalanceComponent.prototype.render = function () { EthBalanceComponent.prototype.render = function () {
var state = this.props var props = this.props
var style = state.style var style = props.style
var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true
const value = formatBalance(state.value, 6, needsParse) const value = formatBalance(props.value, 6, needsParse)
var width = state.width var width = props.width
return ( return (
@ -35,15 +36,16 @@ EthBalanceComponent.prototype.render = function () {
) )
} }
EthBalanceComponent.prototype.renderBalance = function (value) { EthBalanceComponent.prototype.renderBalance = function (value) {
var state = this.props var props = this.props
if (value === 'None') return value if (value === 'None') return value
var balanceObj = generateBalanceObject(value, state.shorten ? 1 : 3) var balanceObj = generateBalanceObject(value, props.shorten ? 1 : 3)
var balance var balance
var splitBalance = value.split(' ') var splitBalance = value.split(' ')
var ethNumber = splitBalance[0] var ethNumber = splitBalance[0]
var ethSuffix = splitBalance[1] var ethSuffix = splitBalance[1]
const showFiat = 'showFiat' in props ? props.showFiat : true
if (state.shorten) { if (props.shorten) {
balance = balanceObj.shortBalance balance = balanceObj.shortBalance
} else { } else {
balance = balanceObj.balance balance = balanceObj.balance
@ -55,8 +57,8 @@ EthBalanceComponent.prototype.renderBalance = function (value) {
h(Tooltip, { h(Tooltip, {
position: 'bottom', position: 'bottom',
title: `${ethNumber} ${ethSuffix}`, title: `${ethNumber} ${ethSuffix}`,
}, [ }, h('div.flex-column', [
h('.flex-column', { h('.flex-row', {
style: { style: {
alignItems: 'flex-end', alignItems: 'flex-end',
lineHeight: '13px', lineHeight: '13px',
@ -74,9 +76,12 @@ EthBalanceComponent.prototype.renderBalance = function (value) {
style: { style: {
color: ' #AEAEAE', color: ' #AEAEAE',
fontSize: '12px', fontSize: '12px',
marginLeft: '5px',
}, },
}, label), }, label),
]), ]),
])
showFiat ? h(FiatValue, { value: props.value }) : null,
]))
) )
} }

@ -0,0 +1,71 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const formatBalance = require('../util').formatBalance
module.exports = connect(mapStateToProps)(FiatValue)
function mapStateToProps (state) {
return {
conversionRate: state.metamask.conversionRate,
currentFiat: state.metamask.currentFiat,
}
}
inherits(FiatValue, Component)
function FiatValue () {
Component.call(this)
}
FiatValue.prototype.render = function () {
const props = this.props
const value = formatBalance(props.value, 6)
if (value === 'None') return value
var fiatDisplayNumber, fiatTooltipNumber
var splitBalance = value.split(' ')
if (props.conversionRate !== 0) {
fiatTooltipNumber = Number(splitBalance[0]) * props.conversionRate
fiatDisplayNumber = fiatTooltipNumber.toFixed(2)
} else {
fiatDisplayNumber = 'N/A'
fiatTooltipNumber = 'Unknown'
}
var fiatSuffix = props.currentFiat
return fiatDisplay(fiatDisplayNumber, fiatSuffix)
}
function fiatDisplay (fiatDisplayNumber, fiatSuffix) {
if (fiatDisplayNumber !== 'N/A') {
return h('.flex-row', {
style: {
alignItems: 'flex-end',
lineHeight: '13px',
fontFamily: 'Montserrat Light',
textRendering: 'geometricPrecision',
},
}, [
h('div', {
style: {
width: '100%',
textAlign: 'right',
fontSize: '12px',
color: '#333333',
},
}, fiatDisplayNumber),
h('div', {
style: {
color: '#AEAEAE',
marginLeft: '5px',
fontSize: '12px',
},
}, fiatSuffix),
])
} else {
return h('div')
}
}

@ -14,9 +14,8 @@ function Mascot () {
pxNotRatio: true, pxNotRatio: true,
width: 200, width: 200,
height: 200, height: 200,
staticImage: './images/icon-512.png',
}) })
if (!this.logo.webGLSupport) return
this.refollowMouse = debounce(this.logo.setFollowMouse.bind(this.logo, true), 1000) this.refollowMouse = debounce(this.logo.setFollowMouse.bind(this.logo, true), 1000)
this.unfollowMouse = this.logo.setFollowMouse.bind(this.logo, false) this.unfollowMouse = this.logo.setFollowMouse.bind(this.logo, false)
} }
@ -27,32 +26,25 @@ Mascot.prototype.render = function () {
// and we dont get that until render // and we dont get that until render
this.handleAnimationEvents() this.handleAnimationEvents()
return ( return h('#metamask-mascot-container', {
style: { zIndex: 2 },
h('#metamask-mascot-container') })
)
} }
Mascot.prototype.componentDidMount = function () { Mascot.prototype.componentDidMount = function () {
var targetDivId = 'metamask-mascot-container' var targetDivId = 'metamask-mascot-container'
var container = document.getElementById(targetDivId) var container = document.getElementById(targetDivId)
if (!this.logo.webGLSupport) { container.appendChild(this.logo.container)
var staticLogo = this.logo.staticLogo
staticLogo.style.marginBottom = '40px'
container.appendChild(staticLogo)
} else {
container.appendChild(this.logo.canvas)
}
} }
Mascot.prototype.componentWillUnmount = function () { Mascot.prototype.componentWillUnmount = function () {
if (!this.logo.webGLSupport) return this.animations = this.props.animationEventEmitter
this.logo.canvas.remove() this.animations.removeAllListeners()
this.logo.container.remove()
this.logo.stopAnimation()
} }
Mascot.prototype.handleAnimationEvents = function () { Mascot.prototype.handleAnimationEvents = function () {
if (!this.logo.webGLSupport) return
// only setup listeners once // only setup listeners once
if (this.animations) return if (this.animations) return
this.animations = this.props.animationEventEmitter this.animations = this.props.animationEventEmitter
@ -61,7 +53,6 @@ Mascot.prototype.handleAnimationEvents = function () {
} }
Mascot.prototype.lookAt = function (target) { Mascot.prototype.lookAt = function (target) {
if (!this.logo.webGLSupport) return
this.unfollowMouse() this.unfollowMouse()
this.logo.lookAt(target) this.logo.lookAt(target)
this.refollowMouse() this.refollowMouse()

@ -61,7 +61,7 @@ Network.prototype.render = function () {
style: { style: {
color: '#039396', color: '#039396',
}}, }},
'Etherum Main Net'), 'Ethereum Main Net'),
]) ])
case 'morden-test-network': case 'morden-test-network':
return h('.network-indicator', [ return h('.network-indicator', [
@ -75,7 +75,6 @@ Network.prototype.render = function () {
default: default:
return h('.network-indicator', [ return h('.network-indicator', [
h('i.fa.fa-question-circle.fa-lg', { h('i.fa.fa-question-circle.fa-lg', {
ariaHidden: true,
style: { style: {
margin: '10px', margin: '10px',
color: 'rgb(125, 128, 130)', color: 'rgb(125, 128, 130)',

@ -4,9 +4,8 @@ const inherits = require('util').inherits
const carratInline = require('fs').readFileSync('./images/forward-carrat.svg', 'utf8') const carratInline = require('fs').readFileSync('./images/forward-carrat.svg', 'utf8')
const MiniAccountPanel = require('./mini-account-panel') const MiniAccountPanel = require('./mini-account-panel')
const EtherBalance = require('./eth-balance') const EthBalance = require('./eth-balance')
const addressSummary = require('../util').addressSummary const addressSummary = require('../util').addressSummary
const formatBalance = require('../util').formatBalance
const nameForAddress = require('../../lib/contract-namer') const nameForAddress = require('../../lib/contract-namer')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN const BN = ethUtil.BN
@ -70,7 +69,7 @@ PTXP.render = function () {
fontFamily: 'Montserrat Light, Montserrat, sans-serif', fontFamily: 'Montserrat Light, Montserrat, sans-serif',
}, },
}, [ }, [
h(EtherBalance, { h(EthBalance, {
value: balance, value: balance,
inline: true, inline: true,
labelColor: '#F7861C', labelColor: '#F7861C',
@ -107,12 +106,12 @@ PTXP.render = function () {
h('.row', [ h('.row', [
h('.cell.label', 'Amount'), h('.cell.label', 'Amount'),
h('.cell.value', formatBalance(txParams.value)), h(EthBalance, { value: txParams.value }),
]), ]),
h('.cell.row', [ h('.cell.row', [
h('.cell.label', 'Max Transaction Fee'), h('.cell.label', 'Max Transaction Fee'),
h('.cell.value', formatBalance(txFee.toString(16))), h(EthBalance, { value: txFee.toString(16) }),
]), ]),
h('.cell.row', { h('.cell.row', {
@ -129,7 +128,7 @@ PTXP.render = function () {
alignItems: 'center', alignItems: 'center',
}, },
}, [ }, [
h(EtherBalance, { h(EthBalance, {
value: maxCost.toString(16), value: maxCost.toString(16),
inline: true, inline: true,
labelColor: 'black', labelColor: 'black',

@ -1,4 +1,4 @@
const Component = require('react').Component const PersistentForm = require('../../lib/persistent-form')
const h = require('react-hyperscript') const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const connect = require('react-redux').connect const connect = require('react-redux').connect
@ -17,12 +17,15 @@ function mapStateToProps(state) {
} }
} }
inherits(ShapeshiftForm, Component) inherits(ShapeshiftForm, PersistentForm)
function ShapeshiftForm () { function ShapeshiftForm () {
Component.call(this) PersistentForm.call(this)
this.persistentFormParentId = 'shapeshift-buy-form'
} }
ShapeshiftForm.prototype.render = function () { ShapeshiftForm.prototype.render = function () {
return h(ReactCSSTransitionGroup, { return h(ReactCSSTransitionGroup, {
className: 'css-transition-group', className: 'css-transition-group',
transitionName: 'main', transitionName: 'main',
@ -66,6 +69,9 @@ ShapeshiftForm.prototype.renderMain = function () {
h('input#fromCoin.buy-inputs.ex-coins', { h('input#fromCoin.buy-inputs.ex-coins', {
type: 'text', type: 'text',
list: 'coinList', list: 'coinList',
dataset: {
persistentFormId: 'input-coin',
},
style: { style: {
boxSizing: 'border-box', boxSizing: 'border-box',
}, },
@ -159,6 +165,9 @@ ShapeshiftForm.prototype.renderMain = function () {
h('input#fromCoinAddress.buy-inputs', { h('input#fromCoinAddress.buy-inputs', {
type: 'text', type: 'text',
placeholder: `Your ${coin} Refund Address`, placeholder: `Your ${coin} Refund Address`,
dataset: {
persistentFormId: 'refund-address',
},
style: { style: {
boxSizing: 'border-box', boxSizing: 'border-box',
width: '278px', width: '278px',

@ -11,12 +11,14 @@ function Tooltip () {
} }
Tooltip.prototype.render = function () { Tooltip.prototype.render = function () {
const props = this.props const props = this.props
const { position, title, children } = props
return h(ReactTooltip, { return h(ReactTooltip, {
position: props.position ? props.position : 'left', position: position || 'left',
title: props.title, title,
fixed: false, fixed: false,
}, props.children) }, children)
} }

@ -77,6 +77,7 @@ TransactionListItem.prototype.render = function () {
value: txParams.value, value: txParams.value,
width: '55px', width: '55px',
shorten: true, shorten: true,
showFiat: false,
style: {fontSize: '15px'}, style: {fontSize: '15px'},
}) : h('.flex-column'), }) : h('.flex-column'),
]) ])

@ -5,6 +5,7 @@ const h = require('react-hyperscript')
const connect = require('react-redux').connect const connect = require('react-redux').connect
const actions = require('./actions') const actions = require('./actions')
const txHelper = require('../lib/tx-helper') const txHelper = require('../lib/tx-helper')
const isPopupOrNotification = require('../../app/scripts/lib/is-popup-or-notification')
const PendingTx = require('./components/pending-tx') const PendingTx = require('./components/pending-tx')
const PendingMsg = require('./components/pending-msg') const PendingMsg = require('./components/pending-msg')
@ -20,6 +21,7 @@ function mapStateToProps (state) {
unconfMsgs: state.metamask.unconfMsgs, unconfMsgs: state.metamask.unconfMsgs,
index: state.appState.currentView.context, index: state.appState.currentView.context,
warning: state.appState.warning, warning: state.appState.warning,
network: state.metamask.network,
} }
} }
@ -31,11 +33,13 @@ function ConfirmTxScreen () {
ConfirmTxScreen.prototype.render = function () { ConfirmTxScreen.prototype.render = function () {
var state = this.props var state = this.props
var network = state.network
var unconfTxs = state.unconfTxs var unconfTxs = state.unconfTxs
var unconfMsgs = state.unconfMsgs var unconfMsgs = state.unconfMsgs
var unconfTxList = txHelper(unconfTxs, unconfMsgs) var unconfTxList = txHelper(unconfTxs, unconfMsgs, network)
var index = state.index !== undefined ? state.index : 0 var index = state.index !== undefined ? state.index : 0
var txData = unconfTxList[index] || unconfTxList[0] || {} var txData = unconfTxList[index] || unconfTxList[0] || {}
var isNotification = isPopupOrNotification() === 'notification'
return ( return (
@ -43,9 +47,9 @@ ConfirmTxScreen.prototype.render = function () {
// subtitle and nav // subtitle and nav
h('.section-title.flex-row.flex-center', [ h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { !isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: this.goHome.bind(this), onClick: this.goHome.bind(this),
}), }) : null,
h('h2.page-subtitle', 'Confirm Transaction'), h('h2.page-subtitle', 'Confirm Transaction'),
]), ]),

@ -35,10 +35,9 @@ EthStoreWarning.prototype.render = function () {
margin: '10px 10px 10px 10px', margin: '10px 10px 10px 10px',
}, },
}, },
`The MetaMask team would like to `MetaMask is currently in beta; use
remind you that MetaMask is currently in beta - so caution in storing large
don't store large amounts of ether.
amounts of ether in MetaMask.
`), `),
h('i.fa.fa-exclamation-triangle.fa-4', { h('i.fa.fa-exclamation-triangle.fa-4', {

@ -73,9 +73,7 @@ InitializeMenuScreen.prototype.renderMenu = function () {
margin: 12, margin: 12,
}, },
}, 'Restore Existing Vault'), }, 'Restore Existing Vault'),
]) ])
) )
} }

@ -1,14 +1,14 @@
const inherits = require('util').inherits const inherits = require('util').inherits
const Component = require('react').Component const PersistentForm = require('../../lib/persistent-form')
const connect = require('react-redux').connect const connect = require('react-redux').connect
const h = require('react-hyperscript') const h = require('react-hyperscript')
const actions = require('../actions') const actions = require('../actions')
module.exports = connect(mapStateToProps)(RestoreVaultScreen) module.exports = connect(mapStateToProps)(RestoreVaultScreen)
inherits(RestoreVaultScreen, Component) inherits(RestoreVaultScreen, PersistentForm)
function RestoreVaultScreen () { function RestoreVaultScreen () {
Component.call(this) PersistentForm.call(this)
} }
function mapStateToProps (state) { function mapStateToProps (state) {
@ -19,6 +19,8 @@ function mapStateToProps (state) {
RestoreVaultScreen.prototype.render = function () { RestoreVaultScreen.prototype.render = function () {
var state = this.props var state = this.props
this.persistentFormParentId = 'restore-vault-form'
return ( return (
h('.initialize-screen.flex-column.flex-center.flex-grow', [ h('.initialize-screen.flex-column.flex-center.flex-grow', [
@ -39,6 +41,9 @@ RestoreVaultScreen.prototype.render = function () {
// wallet seed entry // wallet seed entry
h('h3', 'Wallet Seed'), h('h3', 'Wallet Seed'),
h('textarea.twelve-word-phrase.letter-spacey', { h('textarea.twelve-word-phrase.letter-spacey', {
dataset: {
persistentFormId: 'wallet-seed',
},
placeholder: 'Enter your secret twelve word phrase here to restore your vault.', placeholder: 'Enter your secret twelve word phrase here to restore your vault.',
}), }),
@ -47,6 +52,9 @@ RestoreVaultScreen.prototype.render = function () {
type: 'password', type: 'password',
id: 'password-box', id: 'password-box',
placeholder: 'New Password (min 8 chars)', placeholder: 'New Password (min 8 chars)',
dataset: {
persistentFormId: 'password',
},
style: { style: {
width: 260, width: 260,
marginTop: 12, marginTop: 12,
@ -59,6 +67,9 @@ RestoreVaultScreen.prototype.render = function () {
id: 'password-box-confirm', id: 'password-box-confirm',
placeholder: 'Confirm Password', placeholder: 'Confirm Password',
onKeyPress: this.onMaybeCreate.bind(this), onKeyPress: this.onMaybeCreate.bind(this),
dataset: {
persistentFormId: 'password-confirmation',
},
style: { style: {
width: 260, width: 260,
marginTop: 16, marginTop: 16,

@ -67,7 +67,7 @@ InfoScreen.prototype.render = function () {
`For more information on MetaMask `For more information on MetaMask
you can visit our web site. If you want to you can visit our web site. If you want to
contact us with questions or just contact us with questions or just
say 'Hi', you can find us on theise platforms:`), say 'Hi', you can find us on these platforms:`),
h('div', { h('div', {
style: { style: {

@ -1,6 +1,7 @@
const extend = require('xtend') const extend = require('xtend')
const actions = require('../actions') const actions = require('../actions')
const txHelper = require('../../lib/tx-helper') const txHelper = require('../../lib/tx-helper')
const notification = require('../../../app/scripts/lib/notifications')
module.exports = reduceApp module.exports = reduceApp
@ -123,6 +124,7 @@ function reduceApp (state, action) {
case actions.UNLOCK_METAMASK: case actions.UNLOCK_METAMASK:
return extend(appState, { return extend(appState, {
forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null,
detailView: {}, detailView: {},
transForward: true, transForward: true,
isLoading: false, isLoading: false,
@ -136,6 +138,25 @@ function reduceApp (state, action) {
warning: null, warning: null,
}) })
case actions.BACK_TO_INIT_MENU:
return extend(appState, {
warning: null,
transForward: false,
forgottenPassword: true,
currentView: {
name: 'InitMenu',
},
})
case actions.BACK_TO_UNLOCK_VIEW:
return extend(appState, {
warning: null,
transForward: true,
forgottenPassword: !appState.forgottenPassword,
currentView: {
name: 'UnlockScreen',
},
})
// reveal seed words // reveal seed words
case actions.REVEAL_SEED_CONFIRMATION: case actions.REVEAL_SEED_CONFIRMATION:
@ -170,6 +191,7 @@ function reduceApp (state, action) {
case actions.SHOW_ACCOUNT_DETAIL: case actions.SHOW_ACCOUNT_DETAIL:
return extend(appState, { return extend(appState, {
forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null,
currentView: { currentView: {
name: 'accountDetail', name: 'accountDetail',
context: action.value, context: action.value,
@ -236,8 +258,9 @@ function reduceApp (state, action) {
case actions.COMPLETED_TX: case actions.COMPLETED_TX:
var unconfTxs = state.metamask.unconfTxs var unconfTxs = state.metamask.unconfTxs
var unconfMsgs = state.metamask.unconfMsgs var unconfMsgs = state.metamask.unconfMsgs
var network = state.metamask.network
var unconfTxList = txHelper(unconfTxs, unconfMsgs) var unconfTxList = txHelper(unconfTxs, unconfMsgs, network)
.filter(tx => tx !== tx.id) .filter(tx => tx !== tx.id)
if (unconfTxList && unconfTxList.length > 0) { if (unconfTxList && unconfTxList.length > 0) {
@ -250,6 +273,9 @@ function reduceApp (state, action) {
warning: null, warning: null,
}) })
} else { } else {
notification.closePopup()
return extend(appState, { return extend(appState, {
transForward: false, transForward: false,
warning: null, warning: null,
@ -498,14 +524,16 @@ function reduceApp (state, action) {
function hasPendingTxs (state) { function hasPendingTxs (state) {
var unconfTxs = state.metamask.unconfTxs var unconfTxs = state.metamask.unconfTxs
var unconfMsgs = state.metamask.unconfMsgs var unconfMsgs = state.metamask.unconfMsgs
var unconfTxList = txHelper(unconfTxs, unconfMsgs) var network = state.metamask.network
var unconfTxList = txHelper(unconfTxs, unconfMsgs, network)
return unconfTxList.length > 0 return unconfTxList.length > 0
} }
function indexForPending (state, txId) { function indexForPending (state, txId) {
var unconfTxs = state.metamask.unconfTxs var unconfTxs = state.metamask.unconfTxs
var unconfMsgs = state.metamask.unconfMsgs var unconfMsgs = state.metamask.unconfMsgs
var unconfTxList = txHelper(unconfTxs, unconfMsgs) var network = state.metamask.network
var unconfTxList = txHelper(unconfTxs, unconfMsgs, network)
let idx let idx
unconfTxList.forEach((tx, i) => { unconfTxList.forEach((tx, i) => {
if (tx.id === txId) { if (tx.id === txId) {
@ -515,4 +543,3 @@ function indexForPending (state, txId) {
return idx return idx
} }

@ -1,5 +1,5 @@
const inherits = require('util').inherits const inherits = require('util').inherits
const Component = require('react').Component const PersistentForm = require('../lib/persistent-form')
const h = require('react-hyperscript') const h = require('react-hyperscript')
const connect = require('react-redux').connect const connect = require('react-redux').connect
const Identicon = require('./components/identicon') const Identicon = require('./components/identicon')
@ -7,7 +7,7 @@ const actions = require('./actions')
const util = require('./util') const util = require('./util')
const numericBalance = require('./util').numericBalance const numericBalance = require('./util').numericBalance
const addressSummary = require('./util').addressSummary const addressSummary = require('./util').addressSummary
const EtherBalance = require('./components/eth-balance') const EthBalance = require('./components/eth-balance')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
module.exports = connect(mapStateToProps)(SendTransactionScreen) module.exports = connect(mapStateToProps)(SendTransactionScreen)
@ -29,12 +29,14 @@ function mapStateToProps (state) {
return result return result
} }
inherits(SendTransactionScreen, Component) inherits(SendTransactionScreen, PersistentForm)
function SendTransactionScreen () { function SendTransactionScreen () {
Component.call(this) PersistentForm.call(this)
} }
SendTransactionScreen.prototype.render = function () { SendTransactionScreen.prototype.render = function () {
this.persistentFormParentId = 'send-tx-form'
var state = this.props var state = this.props
var address = state.address var address = state.address
var account = state.account var account = state.account
@ -105,8 +107,7 @@ SendTransactionScreen.prototype.render = function () {
// balance // balance
h('.flex-row.flex-center', [ h('.flex-row.flex-center', [
// h('div', formatBalance(account && account.balance)), h(EthBalance, {
h(EtherBalance, {
value: account && account.balance, value: account && account.balance,
}), }),
@ -137,6 +138,9 @@ SendTransactionScreen.prototype.render = function () {
h('input.large-input', { h('input.large-input', {
name: 'address', name: 'address',
placeholder: 'Recipient Address', placeholder: 'Recipient Address',
dataset: {
persistentFormId: 'recipient-address',
},
}), }),
]), ]),
@ -150,6 +154,9 @@ SendTransactionScreen.prototype.render = function () {
style: { style: {
marginRight: 6, marginRight: 6,
}, },
dataset: {
persistentFormId: 'tx-amount',
},
}), }),
h('button.primary', { h('button.primary', {
@ -185,11 +192,12 @@ SendTransactionScreen.prototype.render = function () {
width: '100%', width: '100%',
resize: 'none', resize: 'none',
}, },
dataset: {
persistentFormId: 'tx-data',
},
}), }),
]), ]),
]) ])
) )
} }
@ -227,7 +235,6 @@ SendTransactionScreen.prototype.onSubmit = function () {
} }
this.props.dispatch(actions.hideWarning()) this.props.dispatch(actions.hideWarning())
this.props.dispatch(actions.showLoadingIndication())
var txParams = { var txParams = {
from: this.props.address, from: this.props.address,

@ -26,47 +26,46 @@ UnlockScreen.prototype.render = function () {
const state = this.props const state = this.props
const warning = state.warning const warning = state.warning
return ( return (
h('.flex-column.hey-im-here', [
h('.unlock-screen.flex-column.flex-center.flex-grow', [ h('.unlock-screen.flex-column.flex-center.flex-grow', [
h(Mascot, { h(Mascot, {
animationEventEmitter: this.animationEventEmitter, animationEventEmitter: this.animationEventEmitter,
}), }),
h('h1', { h('h1', {
style: { style: {
fontSize: '1.4em', fontSize: '1.4em',
textTransform: 'uppercase', textTransform: 'uppercase',
color: '#7F8082', color: '#7F8082',
}, },
}, 'MetaMask'), }, 'MetaMask'),
h('input.large-input', { h('input.large-input', {
type: 'password', type: 'password',
id: 'password-box', id: 'password-box',
placeholder: 'enter password', placeholder: 'enter password',
style: { style: {
}, },
onKeyPress: this.onKeyPress.bind(this), onKeyPress: this.onKeyPress.bind(this),
onInput: this.inputChanged.bind(this), onInput: this.inputChanged.bind(this),
}), }),
h('.error', { h('.error', {
style: { style: {
display: warning ? 'block' : 'none', display: warning ? 'block' : 'none',
}, },
}, warning), }, warning),
h('button.primary.cursor-pointer', { h('button.primary.cursor-pointer', {
onClick: this.onSubmit.bind(this), onClick: this.onSubmit.bind(this),
style: { style: {
margin: 10, margin: 10,
}, },
}, 'Unlock'), }, 'Unlock'),
]),
]) ])
) )
} }

@ -3,7 +3,7 @@ const h = require('react-hyperscript')
const Root = require('./app/root') const Root = require('./app/root')
const actions = require('./app/actions') const actions = require('./app/actions')
const configureStore = require('./app/store') const configureStore = require('./app/store')
const txHelper = require('./lib/tx-helper')
module.exports = launchApp module.exports = launchApp
function launchApp (opts) { function launchApp (opts) {
@ -34,7 +34,8 @@ function startApp (metamaskState, accountManager, opts) {
}) })
// if unconfirmed txs, start on txConf page // if unconfirmed txs, start on txConf page
if (Object.keys(metamaskState.unconfTxs || {}).length) { var unconfirmedTxsAll = txHelper(metamaskState.unconfTxs, metamaskState.unconfMsgs, metamaskState.network)
if (unconfirmedTxsAll.length > 0) {
store.dispatch(actions.showConfTxPage()) store.dispatch(actions.showConfTxPage())
} }

@ -0,0 +1,61 @@
const inherits = require('util').inherits
const Component = require('react').Component
const defaultKey = 'persistent-form-default'
const eventName = 'keyup'
module.exports = PersistentForm
function PersistentForm () {
Component.call(this)
}
inherits(PersistentForm, Component)
PersistentForm.prototype.componentDidMount = function () {
const fields = document.querySelectorAll('[data-persistent-formid]')
const store = this.getPersistentStore()
for (var i = 0; i < fields.length; i++) {
const field = fields[i]
const key = field.getAttribute('data-persistent-formid')
const cached = store[key]
if (cached !== undefined) {
field.value = cached
}
field.addEventListener(eventName, this.persistentFieldDidUpdate.bind(this))
}
}
PersistentForm.prototype.getPersistentStore = function () {
let store = window.localStorage[this.persistentFormParentId || defaultKey]
if (store && store !== 'null') {
store = JSON.parse(store)
} else {
store = {}
}
return store
}
PersistentForm.prototype.setPersistentStore = function (newStore) {
window.localStorage[this.persistentFormParentId || defaultKey] = JSON.stringify(newStore)
}
PersistentForm.prototype.persistentFieldDidUpdate = function (event) {
const field = event.target
const store = this.getPersistentStore()
const key = field.getAttribute('data-persistent-formid')
const val = field.value
store[key] = val
this.setPersistentStore(store)
}
PersistentForm.prototype.componentWillUnmount = function () {
const fields = document.querySelectorAll('[data-persistent-formid]')
for (var i = 0; i < fields.length; i++) {
const field = fields[i]
field.removeEventListener(eventName, this.persistentFieldDidUpdate.bind(this))
}
this.setPersistentStore({})
}

@ -1,7 +1,7 @@
const valuesFor = require('../app/util').valuesFor const valuesFor = require('../app/util').valuesFor
module.exports = function (unconfTxs, unconfMsgs) { module.exports = function (unconfTxs, unconfMsgs, network) {
var txValues = valuesFor(unconfTxs) var txValues = network ? valuesFor(unconfTxs).filter(tx => tx.txParams.metamaskNetworkId === network) : valuesFor(unconfTxs)
var msgValues = valuesFor(unconfMsgs) var msgValues = valuesFor(unconfMsgs)
var allValues = txValues.concat(msgValues) var allValues = txValues.concat(msgValues)
return allValues.sort(tx => tx.time) return allValues.sort(tx => tx.time)

Loading…
Cancel
Save