Merge pull request #2799 from MetaMask/NewUI-flat

Update UAT to version 4.0.5
feature/default_network_editable
Alexander Tseung 7 years ago committed by GitHub
commit 409d1d30e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .eslintignore
  2. 2
      .gitignore
  3. 26
      CHANGELOG.md
  4. 6
      ISSUE_TEMPLATE
  5. 2
      README.md
  6. 15
      app/images/open.svg
  7. 2
      app/manifest.json
  8. 9
      app/scripts/background.js
  9. 22
      app/scripts/config.js
  10. 12
      app/scripts/contentscript.js
  11. 103
      app/scripts/controllers/network.js
  12. 34
      app/scripts/controllers/preferences.js
  13. 44
      app/scripts/controllers/recent-blocks.js
  14. 40
      app/scripts/controllers/transactions.js
  15. 7
      app/scripts/inpage.js
  16. 2
      app/scripts/lib/account-tracker.js
  17. 4
      app/scripts/lib/inpage-provider.js
  18. 21
      app/scripts/lib/pending-tx-tracker.js
  19. 6
      app/scripts/lib/tx-gas-utils.js
  20. 6
      app/scripts/lib/tx-state-manager.js
  21. 108
      app/scripts/metamask-controller.js
  22. 41
      app/scripts/migrations/020.js
  23. 1
      app/scripts/migrations/index.js
  24. 44
      app/scripts/notice-controller.js
  25. 5
      app/scripts/platforms/extension.js
  26. 20
      app/scripts/popup.js
  27. 1
      development/states/first-time.json
  28. 739
      development/states/pending-tx.json
  29. 8
      gulpfile.js
  30. 2
      mascara/src/app/first-time/notice-screen.js
  31. 11
      notices/archive/notice_3.md
  32. 2
      notices/notice-nonce.json
  33. 2
      notices/notices.json
  34. 66
      old-ui/.gitignore
  35. 289
      old-ui/app/account-detail.js
  36. 101
      old-ui/app/accounts/import/index.js
  37. 100
      old-ui/app/accounts/import/json.js
  38. 67
      old-ui/app/accounts/import/private-key.js
  39. 30
      old-ui/app/accounts/import/seed.js
  40. 238
      old-ui/app/add-token.js
  41. 684
      old-ui/app/app.js
  42. 319
      old-ui/app/components/account-dropdowns.js
  43. 132
      old-ui/app/components/account-export.js
  44. 86
      old-ui/app/components/account-panel.js
  45. 89
      old-ui/app/components/balance.js
  46. 46
      old-ui/app/components/binary-renderer.js
  47. 181
      old-ui/app/components/bn-as-decimal-input.js
  48. 262
      old-ui/app/components/buy-button-subview.js
  49. 63
      old-ui/app/components/coinbase-form.js
  50. 59
      old-ui/app/components/copyButton.js
  51. 46
      old-ui/app/components/copyable.js
  52. 60
      old-ui/app/components/custom-radio-list.js
  53. 98
      old-ui/app/components/dropdown.js
  54. 57
      old-ui/app/components/editable-label.js
  55. 170
      old-ui/app/components/ens-input.js
  56. 89
      old-ui/app/components/eth-balance.js
  57. 64
      old-ui/app/components/fiat-value.js
  58. 154
      old-ui/app/components/hex-as-decimal-input.js
  59. 74
      old-ui/app/components/identicon.js
  60. 45
      old-ui/app/components/loading.js
  61. 59
      old-ui/app/components/mascot.js
  62. 132
      old-ui/app/components/menu-droppo.js
  63. 74
      old-ui/app/components/mini-account-panel.js
  64. 129
      old-ui/app/components/network.js
  65. 132
      old-ui/app/components/notice.js
  66. 50
      old-ui/app/components/pending-msg-details.js
  67. 70
      old-ui/app/components/pending-msg.js
  68. 60
      old-ui/app/components/pending-personal-msg-details.js
  69. 47
      old-ui/app/components/pending-personal-msg.js
  70. 510
      old-ui/app/components/pending-tx.js
  71. 59
      old-ui/app/components/pending-typed-msg-details.js
  72. 46
      old-ui/app/components/pending-typed-msg.js
  73. 80
      old-ui/app/components/qr-code.js
  74. 58
      old-ui/app/components/range-slider.js
  75. 308
      old-ui/app/components/shapeshift-form.js
  76. 204
      old-ui/app/components/shift-list-item.js
  77. 37
      old-ui/app/components/tab-bar.js
  78. 18
      old-ui/app/components/template.js
  79. 72
      old-ui/app/components/token-cell.js
  80. 207
      old-ui/app/components/token-list.js
  81. 22
      old-ui/app/components/tooltip.js
  82. 68
      old-ui/app/components/transaction-list-item-icon.js
  83. 175
      old-ui/app/components/transaction-list-item.js
  84. 87
      old-ui/app/components/transaction-list.js
  85. 42
      old-ui/app/components/typed-message-renderer.js
  86. 235
      old-ui/app/conf-tx.js
  87. 222
      old-ui/app/config.js
  88. 21
      old-ui/app/css/debug.css
  89. 36
      old-ui/app/css/fonts.css
  90. 761
      old-ui/app/css/index.css
  91. 306
      old-ui/app/css/lib.css
  92. 5385
      old-ui/app/css/output/index.css
  93. 48
      old-ui/app/css/reset.css
  94. 42
      old-ui/app/css/transitions.css
  95. 179
      old-ui/app/first-time/init-menu.js
  96. BIN
      old-ui/app/img/identicon-tardigrade.png
  97. BIN
      old-ui/app/img/identicon-walrus.png
  98. 155
      old-ui/app/info.js
  99. 653
      old-ui/app/infura-conversion.json
  100. 91
      old-ui/app/keychains/hd/create-vault-complete.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -2,4 +2,5 @@ app/scripts/lib/extension-instance.js
test/integration/bundle.js test/integration/bundle.js
test/integration/jquery-3.1.0.min.js test/integration/jquery-3.1.0.min.js
test/integration/helpers.js test/integration/helpers.js
test/integration/lib/first-time.js test/integration/lib/first-time.js
ui/lib/blockies.js

2
.gitignore vendored

@ -6,6 +6,8 @@ app/bower_components
test/bower_components test/bower_components
package package
.idea
temp temp
.tmp .tmp
.sass-cache .sass-cache

@ -2,6 +2,32 @@
## Current Master ## Current Master
- Fix bug that prevented updating custom token details.
- No longer mark long-pending transactions as failed, since we now have button to retry with higher gas.
## 3.13.3 2017-12-14
- Show tokens that are held that have no balance.
- Reduce load on Infura by using a new block polling endpoint.
## 3.13.2 2017-12-9
- Reduce new block polling interval to 8000 ms, to ease server load.
## 3.13.1 2017-12-7
- Allow Dapps to specify a transaction nonce, allowing dapps to propose resubmit and force-cancel transactions.
## 3.13.0 2017-12-7
- Allow resubmitting transactions that are taking long to complete.
## 3.12.1 2017-11-29
- Fix bug where a user could be shown two different seed phrases.
- Detect when multiple web3 extensions are active, and provide useful error.
- Adds notice about seed phrase backup.
## 3.12.0 2017-10-25 ## 3.12.0 2017-10-25
- Add support for alternative ENS TLDs (Ethereum Name Service Top-Level Domains). - Add support for alternative ENS TLDs (Ethereum Name Service Top-Level Domains).

@ -1,7 +1,7 @@
<!-- <!--
FAQ:
BEFORE SUBMITTING, please make sure your question hasn't been answered in our FAQ: https://github.com/MetaMask/faq BEFORE SUBMITTING, please make sure your question hasn't been answered in our support center: https://support.metamask.io
Common questions such as "Where is my ether?" or "Where did my tokens go?" are answered in the FAQ. Common questions such as "Where is my ether?" or "Where did my tokens go?" are answered there.
Bug Reports: Bug Reports:

@ -1,4 +1,4 @@
# MetaMask Plugin # MetaMask Browser Extension
[![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) [![Coverage Status](https://coveralls.io/repos/github/MetaMask/metamask-extension/badge.svg?branch=master)](https://coveralls.io/github/MetaMask/metamask-extension?branch=master) [![Greenkeeper badge](https://badges.greenkeeper.io/MetaMask/metamask-extension.svg)](https://greenkeeper.io/) [![Stories in Ready](https://badge.waffle.io/MetaMask/metamask-extension.png?label=in%20progress&title=waffle.io)](http://waffle.io/MetaMask/metamask-extension) [![Build Status](https://circleci.com/gh/MetaMask/metamask-extension.svg?style=shield&circle-token=a1ddcf3cd38e29267f254c9c59d556d513e3a1fd)](https://circleci.com/gh/MetaMask/metamask-extension) [![Coverage Status](https://coveralls.io/repos/github/MetaMask/metamask-extension/badge.svg?branch=master)](https://coveralls.io/github/MetaMask/metamask-extension?branch=master) [![Greenkeeper badge](https://badges.greenkeeper.io/MetaMask/metamask-extension.svg)](https://greenkeeper.io/) [![Stories in Ready](https://badge.waffle.io/MetaMask/metamask-extension.png?label=in%20progress&title=waffle.io)](http://waffle.io/MetaMask/metamask-extension)

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 47.1 (45422) - http://www.bohemiancoding.com/sketch -->
<title>open</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Mobile-screens" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="MetaMascara-Mobile---structured" transform="translate(-329.000000, -93.000000)">
<g id="open" transform="translate(330.000000, 94.000000)">
<path d="M26,13 C26,20.1799 20.1799,26 13,26 C5.8201,26 0,20.1799 0,13 C0,5.8201 5.8201,0 13,0 C20.1799,0 26,5.8201 26,13 Z" id="Stroke-3" stroke="#4A4A4A"></path>
<path d="M6,17 C6,17 7.78735344,10.8360387 13.7616996,10.8360387 L13.7616996,8 L19,12.3733433 L13.7616996,17 L13.7616996,14.1639613 C13.7616996,14.1639613 9.54083576,13.4629933 6,17" id="Fill-5" fill="#4A4A4A"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@ -1,7 +1,7 @@
{ {
"name": "MetaMask", "name": "MetaMask",
"short_name": "Metamask", "short_name": "Metamask",
"version": "4.0.4", "version": "4.0.5",
"manifest_version": 2, "manifest_version": 2,
"author": "https://metamask.io", "author": "https://metamask.io",
"description": "Ethereum Browser Extension", "description": "Ethereum Browser Extension",

@ -1,10 +1,11 @@
const urlUtil = require('url') const urlUtil = require('url')
const endOfStream = require('end-of-stream') const endOfStream = require('end-of-stream')
const pipe = require('pump') const pump = require('pump')
const log = require('loglevel') const log = require('loglevel')
const extension = require('extensionizer') const extension = require('extensionizer')
const LocalStorageStore = require('obs-store/lib/localStorage') const LocalStorageStore = require('obs-store/lib/localStorage')
const storeTransform = require('obs-store/lib/transform') const storeTransform = require('obs-store/lib/transform')
const asStream = require('obs-store/lib/asStream')
const ExtensionPlatform = require('./platforms/extension') const ExtensionPlatform = require('./platforms/extension')
const Migrator = require('./lib/migrator/') const Migrator = require('./lib/migrator/')
const migrations = require('./migrations/') const migrations = require('./migrations/')
@ -72,10 +73,10 @@ function setupController (initState) {
global.metamaskController = controller global.metamaskController = controller
// setup state persistence // setup state persistence
pipe( pump(
controller.store, asStream(controller.store),
storeTransform(versionifyData), storeTransform(versionifyData),
diskStore asStream(diskStore)
) )
function versionifyData (state) { function versionifyData (state) {

@ -4,6 +4,15 @@ const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask'
const RINKEBY_RPC_URL = 'https://rinkeby.infura.io/metamask' const RINKEBY_RPC_URL = 'https://rinkeby.infura.io/metamask'
const LOCALHOST_RPC_URL = 'http://localhost:8545' const LOCALHOST_RPC_URL = 'http://localhost:8545'
const MAINET_RPC_URL_BETA = 'https://mainnet.infura.io/metamask2'
const ROPSTEN_RPC_URL_BETA = 'https://ropsten.infura.io/metamask2'
const KOVAN_RPC_URL_BETA = 'https://kovan.infura.io/metamask2'
const RINKEBY_RPC_URL_BETA = 'https://rinkeby.infura.io/metamask2'
const DEFAULT_RPC = 'rinkeby'
const OLD_UI_NETWORK_TYPE = 'network'
const BETA_UI_NETWORK_TYPE = 'networkBeta'
global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
module.exports = { module.exports = {
@ -14,9 +23,22 @@ module.exports = {
kovan: KOVAN_RPC_URL, kovan: KOVAN_RPC_URL,
rinkeby: RINKEBY_RPC_URL, rinkeby: RINKEBY_RPC_URL,
}, },
// Used for beta UI
networkBeta: {
localhost: LOCALHOST_RPC_URL,
mainnet: MAINET_RPC_URL_BETA,
ropsten: ROPSTEN_RPC_URL_BETA,
kovan: KOVAN_RPC_URL_BETA,
rinkeby: RINKEBY_RPC_URL_BETA,
},
networkNames: { networkNames: {
3: 'Ropsten', 3: 'Ropsten',
4: 'Rinkeby', 4: 'Rinkeby',
42: 'Kovan', 42: 'Kovan',
}, },
enums: {
DEFAULT_RPC,
OLD_UI_NETWORK_TYPE,
BETA_UI_NETWORK_TYPE,
},
} }

@ -96,7 +96,7 @@ function logStreamDisconnectWarning (remoteLabel, err) {
} }
function shouldInjectWeb3 () { function shouldInjectWeb3 () {
return doctypeCheck() || suffixCheck() return doctypeCheck() && suffixCheck() && documentElementCheck()
} }
function doctypeCheck () { function doctypeCheck () {
@ -104,7 +104,7 @@ function doctypeCheck () {
if (doctype) { if (doctype) {
return doctype.name === 'html' return doctype.name === 'html'
} else { } else {
return false return true
} }
} }
@ -121,6 +121,14 @@ function suffixCheck () {
return true return true
} }
function documentElementCheck () {
var documentElement = document.documentElement.nodeName
if (documentElement) {
return documentElement.toLowerCase() === 'html'
}
return true
}
function redirectToPhishingWarning () { function redirectToPhishingWarning () {
console.log('MetaMask - redirecting to phishing warning') console.log('MetaMask - redirecting to phishing warning')
window.location.href = 'https://metamask.io/phishing.html' window.location.href = 'https://metamask.io/phishing.html'

@ -1,18 +1,25 @@
const assert = require('assert') const assert = require('assert')
const EventEmitter = require('events') const EventEmitter = require('events')
const createMetamaskProvider = require('web3-provider-engine/zero.js') const createMetamaskProvider = require('web3-provider-engine/zero.js')
const createInfuraProvider = require('eth-json-rpc-infura/src/createProvider')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const ComposedStore = require('obs-store/lib/composed') const ComposedStore = require('obs-store/lib/composed')
const extend = require('xtend') const extend = require('xtend')
const EthQuery = require('eth-query') const EthQuery = require('eth-query')
const createEventEmitterProxy = require('../lib/events-proxy.js') const createEventEmitterProxy = require('../lib/events-proxy.js')
const RPC_ADDRESS_LIST = require('../config.js').network const networkConfig = require('../config.js')
const DEFAULT_RPC = RPC_ADDRESS_LIST['rinkeby'] const { OLD_UI_NETWORK_TYPE, DEFAULT_RPC } = networkConfig.enums
const INFURA_PROVIDER_TYPES = ['ropsten', 'rinkeby', 'kovan', 'mainnet']
module.exports = class NetworkController extends EventEmitter { module.exports = class NetworkController extends EventEmitter {
constructor (config) { constructor (config) {
super() super()
this._networkEndpointVersion = OLD_UI_NETWORK_TYPE
this._networkEndpoints = this.getNetworkEndpoints(OLD_UI_NETWORK_TYPE)
this._defaultRpc = this._networkEndpoints[DEFAULT_RPC]
config.provider.rpcTarget = this.getRpcAddressForType(config.provider.type, config.provider) config.provider.rpcTarget = this.getRpcAddressForType(config.provider.type, config.provider)
this.networkStore = new ObservableStore('loading') this.networkStore = new ObservableStore('loading')
this.providerStore = new ObservableStore(config.provider) this.providerStore = new ObservableStore(config.provider)
@ -22,10 +29,32 @@ module.exports = class NetworkController extends EventEmitter {
this.on('networkDidChange', this.lookupNetwork) this.on('networkDidChange', this.lookupNetwork)
} }
async setNetworkEndpoints (version) {
if (version === this._networkEndpointVersion) {
return
}
this._networkEndpointVersion = version
this._networkEndpoints = this.getNetworkEndpoints(version)
this._defaultRpc = this._networkEndpoints[DEFAULT_RPC]
const { type } = this.getProviderConfig()
return this.setProviderType(type, true)
}
getNetworkEndpoints (version = OLD_UI_NETWORK_TYPE) {
return networkConfig[version]
}
initializeProvider (_providerParams) { initializeProvider (_providerParams) {
this._baseProviderParams = _providerParams this._baseProviderParams = _providerParams
const rpcUrl = this.getCurrentRpcAddress() const { type, rpcTarget } = this.providerStore.getState()
this._configureStandardProvider({ rpcUrl }) // map rpcTarget to rpcUrl
const opts = {
type,
rpcUrl: rpcTarget,
}
this._configureProvider(opts)
this._proxy.on('block', this._logBlock.bind(this)) this._proxy.on('block', this._logBlock.bind(this))
this._proxy.on('error', this.verifyNetwork.bind(this)) this._proxy.on('error', this.verifyNetwork.bind(this))
this.ethQuery = new EthQuery(this._proxy) this.ethQuery = new EthQuery(this._proxy)
@ -53,7 +82,7 @@ module.exports = class NetworkController extends EventEmitter {
lookupNetwork () { lookupNetwork () {
// Prevent firing when provider is not defined. // Prevent firing when provider is not defined.
if (!this.ethQuery || !this.ethQuery.sendAsync) { if (!this.ethQuery || !this.ethQuery.sendAsync) {
return return log.warn('NetworkController - lookupNetwork aborted due to missing ethQuery')
} }
this.ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { this.ethQuery.sendAsync({ method: 'net_version' }, (err, network) => {
if (err) return this.setNetworkState('loading') if (err) return this.setNetworkState('loading')
@ -76,14 +105,17 @@ module.exports = class NetworkController extends EventEmitter {
return this.getRpcAddressForType(provider.type) return this.getRpcAddressForType(provider.type)
} }
async setProviderType (type) { async setProviderType (type, forceUpdate = false) {
assert(type !== 'rpc', `NetworkController.setProviderType - cannot connect by type "rpc"`) assert(type !== 'rpc', `NetworkController.setProviderType - cannot connect by type "rpc"`)
// skip if type already matches // skip if type already matches
if (type === this.getProviderConfig().type) return if (type === this.getProviderConfig().type && !forceUpdate) {
return
}
const rpcTarget = this.getRpcAddressForType(type) const rpcTarget = this.getRpcAddressForType(type)
assert(rpcTarget, `NetworkController - unknown rpc address for type "${type}"`) assert(rpcTarget, `NetworkController - unknown rpc address for type "${type}"`)
this.providerStore.updateState({ type, rpcTarget }) this.providerStore.updateState({ type, rpcTarget })
this._switchNetwork({ rpcUrl: rpcTarget }) this._switchNetwork({ type })
} }
getProviderConfig () { getProviderConfig () {
@ -91,22 +123,65 @@ module.exports = class NetworkController extends EventEmitter {
} }
getRpcAddressForType (type, provider = this.getProviderConfig()) { getRpcAddressForType (type, provider = this.getProviderConfig()) {
if (RPC_ADDRESS_LIST[type]) return RPC_ADDRESS_LIST[type] if (this._networkEndpoints[type]) {
return provider && provider.rpcTarget ? provider.rpcTarget : DEFAULT_RPC return this._networkEndpoints[type]
}
return provider && provider.rpcTarget ? provider.rpcTarget : this._defaultRpc
} }
// //
// Private // Private
// //
_switchNetwork (providerParams) { _switchNetwork (opts) {
this.setNetworkState('loading') this.setNetworkState('loading')
this._configureStandardProvider(providerParams) this._configureProvider(opts)
this.emit('networkDidChange') this.emit('networkDidChange')
} }
_configureStandardProvider (_providerParams) { _configureProvider (opts) {
const providerParams = extend(this._baseProviderParams, _providerParams) // type-based rpc endpoints
const { type } = opts
if (type) {
// type-based infura rpc endpoints
const isInfura = INFURA_PROVIDER_TYPES.includes(type)
opts.rpcUrl = this.getRpcAddressForType(type)
if (isInfura) {
this._configureInfuraProvider(opts)
// other type-based rpc endpoints
} else {
this._configureStandardProvider(opts)
}
// url-based rpc endpoints
} else {
this._configureStandardProvider(opts)
}
}
_configureInfuraProvider (opts) {
log.info('_configureInfuraProvider', opts)
const blockTrackerProvider = createInfuraProvider({
network: opts.type,
})
const providerParams = extend(this._baseProviderParams, {
rpcUrl: opts.rpcUrl,
engineParams: {
pollingInterval: 8000,
blockTrackerProvider,
},
})
const provider = createMetamaskProvider(providerParams)
this._setProvider(provider)
}
_configureStandardProvider ({ rpcUrl }) {
const providerParams = extend(this._baseProviderParams, {
rpcUrl,
engineParams: {
pollingInterval: 8000,
},
})
const provider = createMetamaskProvider(providerParams) const provider = createMetamaskProvider(providerParams)
this._setProvider(provider) this._setProvider(provider)
} }

@ -9,11 +9,21 @@ class PreferencesController {
frequentRpcList: [], frequentRpcList: [],
currentAccountTab: 'history', currentAccountTab: 'history',
tokens: [], tokens: [],
useBlockie: false,
featureFlags: {},
}, opts.initState) }, opts.initState)
this.store = new ObservableStore(initState) this.store = new ObservableStore(initState)
} }
// PUBLIC METHODS // PUBLIC METHODS
setUseBlockie (val) {
this.store.updateState({ useBlockie: val })
}
getUseBlockie () {
return this.store.getState().useBlockie
}
setSelectedAddress (_address) { setSelectedAddress (_address) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const address = normalizeAddress(_address) const address = normalizeAddress(_address)
@ -26,22 +36,24 @@ class PreferencesController {
return this.store.getState().selectedAddress return this.store.getState().selectedAddress
} }
addToken (rawAddress, symbol, decimals) { async addToken (rawAddress, symbol, decimals) {
const address = normalizeAddress(rawAddress) const address = normalizeAddress(rawAddress)
const newEntry = { address, symbol, decimals } const newEntry = { address, symbol, decimals }
const tokens = this.store.getState().tokens const tokens = this.store.getState().tokens
const previousIndex = tokens.find((token, index) => { const previousEntry = tokens.find((token, index) => {
return token.address === address return token.address === address
}) })
const previousIndex = tokens.indexOf(previousEntry)
if (previousIndex) { if (previousEntry) {
tokens[previousIndex] = newEntry tokens[previousIndex] = newEntry
} else { } else {
tokens.push(newEntry) tokens.push(newEntry)
} }
this.store.updateState({ tokens }) this.store.updateState({ tokens })
return Promise.resolve(tokens) return Promise.resolve(tokens)
} }
@ -91,6 +103,22 @@ class PreferencesController {
getFrequentRpcList () { getFrequentRpcList () {
return this.store.getState().frequentRpcList return this.store.getState().frequentRpcList
} }
setFeatureFlag (feature, activated) {
const currentFeatureFlags = this.store.getState().featureFlags
const updatedFeatureFlags = {
...currentFeatureFlags,
[feature]: activated,
}
this.store.updateState({ featureFlags: updatedFeatureFlags })
return Promise.resolve(updatedFeatureFlags)
}
getFeatureFlags () {
return this.store.getState().featureFlags
}
// //
// PRIVATE METHODS // PRIVATE METHODS
// //

@ -0,0 +1,44 @@
const ObservableStore = require('obs-store')
const extend = require('xtend')
class RecentBlocksController {
constructor (opts = {}) {
const { blockTracker } = opts
this.blockTracker = blockTracker
this.historyLength = opts.historyLength || 40
const initState = extend({
recentBlocks: [],
}, opts.initState)
this.store = new ObservableStore(initState)
this.blockTracker.on('block', this.processBlock.bind(this))
}
resetState () {
this.store.updateState({
recentBlocks: [],
})
}
processBlock (newBlock) {
const block = extend(newBlock, {
gasPrices: newBlock.transactions.map((tx) => {
return tx.gasPrice
}),
})
delete block.transactions
const state = this.store.getState()
state.recentBlocks.push(block)
while (state.recentBlocks.length > this.historyLength) {
state.recentBlocks.shift()
}
this.store.updateState(state)
}
}
module.exports = RecentBlocksController

@ -59,7 +59,6 @@ module.exports = class TransactionController extends EventEmitter {
this.pendingTxTracker = new PendingTransactionTracker({ this.pendingTxTracker = new PendingTransactionTracker({
provider: this.provider, provider: this.provider,
nonceTracker: this.nonceTracker, nonceTracker: this.nonceTracker,
retryTimePeriod: 86400000, // Retry 3500 blocks, or about 1 day.
publishTransaction: (rawTx) => this.query.sendRawTransaction(rawTx), publishTransaction: (rawTx) => this.query.sendRawTransaction(rawTx),
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager), getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
getCompletedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager), getCompletedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager),
@ -72,6 +71,12 @@ module.exports = class TransactionController extends EventEmitter {
}) })
this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager)) this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager))
this.pendingTxTracker.on('tx:confirmed', this.txStateManager.setTxStatusConfirmed.bind(this.txStateManager)) this.pendingTxTracker.on('tx:confirmed', this.txStateManager.setTxStatusConfirmed.bind(this.txStateManager))
this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => {
if (!txMeta.firstRetryBlockNumber) {
txMeta.firstRetryBlockNumber = latestBlockNumber
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:block-update')
}
})
this.pendingTxTracker.on('tx:retry', (txMeta) => { this.pendingTxTracker.on('tx:retry', (txMeta) => {
if (!('retryCount' in txMeta)) txMeta.retryCount = 0 if (!('retryCount' in txMeta)) txMeta.retryCount = 0
txMeta.retryCount++ txMeta.retryCount++
@ -132,18 +137,20 @@ module.exports = class TransactionController extends EventEmitter {
async newUnapprovedTransaction (txParams) { async newUnapprovedTransaction (txParams) {
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`) log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`)
const txMeta = await this.addUnapprovedTransaction(txParams) const initialTxMeta = await this.addUnapprovedTransaction(txParams)
this.emit('newUnapprovedTx', txMeta) this.emit('newUnapprovedTx', initialTxMeta)
// listen for tx completion (success, fail) // listen for tx completion (success, fail)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.txStateManager.once(`${txMeta.id}:finished`, (completedTx) => { this.txStateManager.once(`${initialTxMeta.id}:finished`, (finishedTxMeta) => {
switch (completedTx.status) { switch (finishedTxMeta.status) {
case 'submitted': case 'submitted':
return resolve(completedTx.hash) return resolve(finishedTxMeta.hash)
case 'rejected': case 'rejected':
return reject(new Error('MetaMask Tx Signature: User denied transaction signature.')) return reject(new Error('MetaMask Tx Signature: User denied transaction signature.'))
case 'failed':
return reject(new Error(finishedTxMeta.err.message))
default: default:
return reject(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(completedTx.txParams)}`)) return reject(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(finishedTxMeta.txParams)}`))
} }
}) })
}) })
@ -171,6 +178,7 @@ module.exports = class TransactionController extends EventEmitter {
const txParams = txMeta.txParams const txParams = txMeta.txParams
// ensure value // ensure value
txMeta.gasPriceSpecified = Boolean(txParams.gasPrice) txMeta.gasPriceSpecified = Boolean(txParams.gasPrice)
txMeta.nonceSpecified = Boolean(txParams.nonce)
const gasPrice = txParams.gasPrice || await this.query.gasPrice() const gasPrice = txParams.gasPrice || await this.query.gasPrice()
txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16)) txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16))
txParams.value = txParams.value || '0x0' txParams.value = txParams.value || '0x0'
@ -178,6 +186,17 @@ module.exports = class TransactionController extends EventEmitter {
return await this.txGasUtil.analyzeGasUsage(txMeta) return await this.txGasUtil.analyzeGasUsage(txMeta)
} }
async retryTransaction (txId) {
this.txStateManager.setTxStatusUnapproved(txId)
const txMeta = this.txStateManager.getTx(txId)
txMeta.lastGasPrice = txMeta.txParams.gasPrice
this.txStateManager.updateTx(txMeta, 'retryTransaction: manual retry')
}
async updateTransaction (txMeta) {
this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction')
}
async updateAndApproveTransaction (txMeta) { async updateAndApproveTransaction (txMeta) {
this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction') this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction')
await this.approveTransaction(txMeta.id) await this.approveTransaction(txMeta.id)
@ -194,7 +213,12 @@ module.exports = class TransactionController extends EventEmitter {
// wait for a nonce // wait for a nonce
nonceLock = await this.nonceTracker.getNonceLock(fromAddress) nonceLock = await this.nonceTracker.getNonceLock(fromAddress)
// add nonce to txParams // add nonce to txParams
txMeta.txParams.nonce = ethUtil.addHexPrefix(nonceLock.nextNonce.toString(16)) const nonce = txMeta.nonceSpecified ? txMeta.txParams.nonce : nonceLock.nextNonce
if (nonce > nonceLock.nextNonce) {
const message = `Specified nonce may not be larger than account's next valid nonce.`
throw new Error(message)
}
txMeta.txParams.nonce = ethUtil.addHexPrefix(nonce.toString(16))
// add nonce debugging information to txMeta // add nonce debugging information to txMeta
txMeta.nonceDetails = nonceLock.nonceDetails txMeta.nonceDetails = nonceLock.nonceDetails
this.txStateManager.updateTx(txMeta, 'transactions#approveTransaction') this.txStateManager.updateTx(txMeta, 'transactions#approveTransaction')

@ -31,6 +31,13 @@ var inpageProvider = new MetamaskInpageProvider(metamaskStream)
// setup web3 // setup web3
// //
if (typeof window.web3 !== 'undefined') {
throw new Error(`MetaMask detected another web3.
MetaMask will not work reliably with another web3 extension.
This usually happens if you have two MetaMasks installed,
or MetaMask and another web3 extension. Please remove one
and try again.`)
}
var web3 = new Web3(inpageProvider) var web3 = new Web3(inpageProvider)
web3.setProvider = function () { web3.setProvider = function () {
log.debug('MetaMask - overrode web3.setProvider') log.debug('MetaMask - overrode web3.setProvider')

@ -117,8 +117,6 @@ class AccountTracker extends EventEmitter {
const query = this._query const query = this._query
async.parallel({ async.parallel({
balance: query.getBalance.bind(query, address), balance: query.getBalance.bind(query, address),
nonce: query.getTransactionCount.bind(query, address),
code: query.getCode.bind(query, address),
}, cb) }, cb)
} }

@ -3,6 +3,7 @@ const RpcEngine = require('json-rpc-engine')
const createIdRemapMiddleware = require('json-rpc-engine/src/idRemapMiddleware') const createIdRemapMiddleware = require('json-rpc-engine/src/idRemapMiddleware')
const createStreamMiddleware = require('json-rpc-middleware-stream') const createStreamMiddleware = require('json-rpc-middleware-stream')
const LocalStorageStore = require('obs-store') const LocalStorageStore = require('obs-store')
const asStream = require('obs-store/lib/asStream')
const ObjectMultiplex = require('obj-multiplex') const ObjectMultiplex = require('obj-multiplex')
module.exports = MetamaskInpageProvider module.exports = MetamaskInpageProvider
@ -21,9 +22,10 @@ function MetamaskInpageProvider (connectionStream) {
// subscribe to metamask public config (one-way) // subscribe to metamask public config (one-way)
self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' }) self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' })
pump( pump(
mux.createStream('publicConfig'), mux.createStream('publicConfig'),
self.publicConfigStore, asStream(self.publicConfigStore),
(err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err) (err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err)
) )

@ -23,7 +23,6 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
this.query = new EthQuery(config.provider) this.query = new EthQuery(config.provider)
this.nonceTracker = config.nonceTracker this.nonceTracker = config.nonceTracker
// default is one day // default is one day
this.retryTimePeriod = config.retryTimePeriod || 86400000
this.getPendingTransactions = config.getPendingTransactions this.getPendingTransactions = config.getPendingTransactions
this.getCompletedTransactions = config.getCompletedTransactions this.getCompletedTransactions = config.getCompletedTransactions
this.publishTransaction = config.publishTransaction this.publishTransaction = config.publishTransaction
@ -65,11 +64,11 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
} }
resubmitPendingTxs () { resubmitPendingTxs (block) {
const pending = this.getPendingTransactions() const pending = this.getPendingTransactions()
// only try resubmitting if their are transactions to resubmit // only try resubmitting if their are transactions to resubmit
if (!pending.length) return if (!pending.length) return
pending.forEach((txMeta) => this._resubmitTx(txMeta).catch((err) => { pending.forEach((txMeta) => this._resubmitTx(txMeta, block.number).catch((err) => {
/* /*
Dont marked as failed if the error is a "known" transaction warning Dont marked as failed if the error is a "known" transaction warning
"there is already a transaction with the same sender-nonce "there is already a transaction with the same sender-nonce
@ -101,13 +100,19 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
})) }))
} }
async _resubmitTx (txMeta) { async _resubmitTx (txMeta, latestBlockNumber) {
if (Date.now() > txMeta.time + this.retryTimePeriod) { if (!txMeta.firstRetryBlockNumber) {
const hours = (this.retryTimePeriod / 3.6e+6).toFixed(1) this.emit('tx:block-update', txMeta, latestBlockNumber)
const err = new Error(`Gave up submitting after ${hours} hours.`)
return this.emit('tx:failed', txMeta.id, err)
} }
const firstRetryBlockNumber = txMeta.firstRetryBlockNumber || latestBlockNumber
const txBlockDistance = Number.parseInt(latestBlockNumber, 16) - Number.parseInt(firstRetryBlockNumber, 16)
const retryCount = txMeta.retryCount || 0
// Exponential backoff to limit retries at publishing
if (txBlockDistance <= Math.pow(2, retryCount) - 1) return
// Only auto-submit already-signed txs: // Only auto-submit already-signed txs:
if (!('rawTx' in txMeta)) return if (!('rawTx' in txMeta)) return

@ -22,7 +22,11 @@ module.exports = class txProvideUtil {
try { try {
estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit) estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit)
} catch (err) { } catch (err) {
if (err.message.includes('Transaction execution error.')) { const simulationFailed = (
err.message.includes('Transaction execution error.') ||
err.message.includes('gas required exceeds allowance or always failing transaction')
)
if ( simulationFailed ) {
txMeta.simulationFails = true txMeta.simulationFails = true
return txMeta return txMeta
} }

@ -187,6 +187,10 @@ module.exports = class TransactionStateManger extends EventEmitter {
this._setTxStatus(txId, 'rejected') this._setTxStatus(txId, 'rejected')
} }
// should update the status of the tx to 'unapproved'.
setTxStatusUnapproved (txId) {
this._setTxStatus(txId, 'unapproved')
}
// should update the status of the tx to 'approved'. // should update the status of the tx to 'approved'.
setTxStatusApproved (txId) { setTxStatusApproved (txId) {
this._setTxStatus(txId, 'approved') this._setTxStatus(txId, 'approved')
@ -236,7 +240,7 @@ module.exports = class TransactionStateManger extends EventEmitter {
txMeta.status = status txMeta.status = status
this.emit(`${txMeta.id}:${status}`, txId) this.emit(`${txMeta.id}:${status}`, txId)
this.emit(`tx:status-update`, txId, status) this.emit(`tx:status-update`, txId, status)
if (status === 'submitted' || status === 'rejected') { if (['submitted', 'rejected', 'failed'].includes(status)) {
this.emit(`${txMeta.id}:finished`, txMeta) this.emit(`${txMeta.id}:finished`, txMeta)
} }
this.updateTx(txMeta, `txStateManager: setting status to ${status}`) this.updateTx(txMeta, `txStateManager: setting status to ${status}`)

@ -3,6 +3,7 @@ const extend = require('xtend')
const pump = require('pump') const pump = require('pump')
const Dnode = require('dnode') const Dnode = require('dnode')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const asStream = require('obs-store/lib/asStream')
const AccountTracker = require('./lib/account-tracker') const AccountTracker = require('./lib/account-tracker')
const EthQuery = require('eth-query') const EthQuery = require('eth-query')
const RpcEngine = require('json-rpc-engine') const RpcEngine = require('json-rpc-engine')
@ -22,6 +23,7 @@ const ShapeShiftController = require('./controllers/shapeshift')
const AddressBookController = require('./controllers/address-book') const AddressBookController = require('./controllers/address-book')
const InfuraController = require('./controllers/infura') const InfuraController = require('./controllers/infura')
const BlacklistController = require('./controllers/blacklist') const BlacklistController = require('./controllers/blacklist')
const RecentBlocksController = require('./controllers/recent-blocks')
const MessageManager = require('./lib/message-manager') const MessageManager = require('./lib/message-manager')
const PersonalMessageManager = require('./lib/personal-message-manager') const PersonalMessageManager = require('./lib/personal-message-manager')
const TypedMessageManager = require('./lib/typed-message-manager') const TypedMessageManager = require('./lib/typed-message-manager')
@ -31,6 +33,7 @@ const ConfigManager = require('./lib/config-manager')
const nodeify = require('./lib/nodeify') const nodeify = require('./lib/nodeify')
const accountImporter = require('./account-import-strategies') const accountImporter = require('./account-import-strategies')
const getBuyEthUrl = require('./lib/buy-eth-url') const getBuyEthUrl = require('./lib/buy-eth-url')
const Mutex = require('await-semaphore').Mutex
const version = require('../manifest.json').version const version = require('../manifest.json').version
module.exports = class MetamaskController extends EventEmitter { module.exports = class MetamaskController extends EventEmitter {
@ -38,10 +41,12 @@ module.exports = class MetamaskController extends EventEmitter {
constructor (opts) { constructor (opts) {
super() super()
this.sendUpdate = debounce(this.privateSendUpdate.bind(this), 200) this.sendUpdate = debounce(this.privateSendUpdate.bind(this), 200)
this.opts = opts this.opts = opts
const initState = opts.initState || {} const initState = opts.initState || {}
this.recordFirstTimeInfo(initState)
// platform-specific api // platform-specific api
this.platform = opts.platform this.platform = opts.platform
@ -49,6 +54,9 @@ module.exports = class MetamaskController extends EventEmitter {
// observable state store // observable state store
this.store = new ObservableStore(initState) this.store = new ObservableStore(initState)
// lock to ensure only one vault created at once
this.createVaultMutex = new Mutex()
// network store // network store
this.networkController = new NetworkController(initState.NetworkController) this.networkController = new NetworkController(initState.NetworkController)
@ -84,6 +92,10 @@ module.exports = class MetamaskController extends EventEmitter {
this.provider = this.initializeProvider() this.provider = this.initializeProvider()
this.blockTracker = this.provider._blockTracker this.blockTracker = this.provider._blockTracker
this.recentBlocksController = new RecentBlocksController({
blockTracker: this.blockTracker,
})
// eth data query tools // eth data query tools
this.ethQuery = new EthQuery(this.provider) this.ethQuery = new EthQuery(this.provider)
// account tracker watches balances, nonces, and any code at their address. // account tracker watches balances, nonces, and any code at their address.
@ -144,6 +156,8 @@ module.exports = class MetamaskController extends EventEmitter {
// notices // notices
this.noticeController = new NoticeController({ this.noticeController = new NoticeController({
initState: initState.NoticeController, initState: initState.NoticeController,
version,
firstVersion: initState.firstTimeInfo.version,
}) })
this.noticeController.updateNoticesList() this.noticeController.updateNoticesList()
// to be uncommented when retrieving notices from a remote server. // to be uncommented when retrieving notices from a remote server.
@ -187,25 +201,30 @@ module.exports = class MetamaskController extends EventEmitter {
this.blacklistController.store.subscribe((state) => { this.blacklistController.store.subscribe((state) => {
this.store.updateState({ BlacklistController: state }) this.store.updateState({ BlacklistController: state })
}) })
this.recentBlocksController.store.subscribe((state) => {
this.store.updateState({ RecentBlocks: state })
})
this.infuraController.store.subscribe((state) => { this.infuraController.store.subscribe((state) => {
this.store.updateState({ InfuraController: state }) this.store.updateState({ InfuraController: state })
}) })
// manual mem state subscriptions // manual mem state subscriptions
this.networkController.store.subscribe(this.sendUpdate.bind(this)) const sendUpdate = this.sendUpdate.bind(this)
this.accountTracker.store.subscribe(this.sendUpdate.bind(this)) this.networkController.store.subscribe(sendUpdate)
this.txController.memStore.subscribe(this.sendUpdate.bind(this)) this.accountTracker.store.subscribe(sendUpdate)
this.balancesController.store.subscribe(this.sendUpdate.bind(this)) this.txController.memStore.subscribe(sendUpdate)
this.messageManager.memStore.subscribe(this.sendUpdate.bind(this)) this.balancesController.store.subscribe(sendUpdate)
this.personalMessageManager.memStore.subscribe(this.sendUpdate.bind(this)) this.messageManager.memStore.subscribe(sendUpdate)
this.typedMessageManager.memStore.subscribe(this.sendUpdate.bind(this)) this.personalMessageManager.memStore.subscribe(sendUpdate)
this.keyringController.memStore.subscribe(this.sendUpdate.bind(this)) this.typedMessageManager.memStore.subscribe(sendUpdate)
this.preferencesController.store.subscribe(this.sendUpdate.bind(this)) this.keyringController.memStore.subscribe(sendUpdate)
this.addressBookController.store.subscribe(this.sendUpdate.bind(this)) this.preferencesController.store.subscribe(sendUpdate)
this.currencyController.store.subscribe(this.sendUpdate.bind(this)) this.recentBlocksController.store.subscribe(sendUpdate)
this.noticeController.memStore.subscribe(this.sendUpdate.bind(this)) this.addressBookController.store.subscribe(sendUpdate)
this.shapeshiftController.store.subscribe(this.sendUpdate.bind(this)) this.currencyController.store.subscribe(sendUpdate)
this.infuraController.store.subscribe(this.sendUpdate.bind(this)) this.noticeController.memStore.subscribe(sendUpdate)
this.shapeshiftController.store.subscribe(sendUpdate)
this.infuraController.store.subscribe(sendUpdate)
} }
// //
@ -289,6 +308,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.currencyController.store.getState(), this.currencyController.store.getState(),
this.noticeController.memStore.getState(), this.noticeController.memStore.getState(),
this.infuraController.store.getState(), this.infuraController.store.getState(),
this.recentBlocksController.store.getState(),
// config manager // config manager
this.configManager.getConfig(), this.configManager.getConfig(),
this.shapeshiftController.store.getState(), this.shapeshiftController.store.getState(),
@ -315,6 +335,7 @@ module.exports = class MetamaskController extends EventEmitter {
// etc // etc
getState: (cb) => cb(null, this.getState()), getState: (cb) => cb(null, this.getState()),
setCurrentCurrency: this.setCurrentCurrency.bind(this), setCurrentCurrency: this.setCurrentCurrency.bind(this),
setUseBlockie: this.setUseBlockie.bind(this),
markAccountsFound: this.markAccountsFound.bind(this), markAccountsFound: this.markAccountsFound.bind(this),
// coinbase // coinbase
@ -332,6 +353,7 @@ module.exports = class MetamaskController extends EventEmitter {
submitPassword: nodeify(keyringController.submitPassword, keyringController), submitPassword: nodeify(keyringController.submitPassword, keyringController),
// network management // network management
setNetworkEndpoints: nodeify(networkController.setNetworkEndpoints, networkController),
setProviderType: nodeify(networkController.setProviderType, networkController), setProviderType: nodeify(networkController.setProviderType, networkController),
setCustomRpc: nodeify(this.setCustomRpc, this), setCustomRpc: nodeify(this.setCustomRpc, this),
@ -340,6 +362,7 @@ module.exports = class MetamaskController extends EventEmitter {
addToken: nodeify(preferencesController.addToken, preferencesController), addToken: nodeify(preferencesController.addToken, preferencesController),
removeToken: nodeify(preferencesController.removeToken, preferencesController), removeToken: nodeify(preferencesController.removeToken, preferencesController),
setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController), setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController),
setFeatureFlag: nodeify(preferencesController.setFeatureFlag, preferencesController),
// AddressController // AddressController
setAddressBook: nodeify(addressBookController.setAddressBook, addressBookController), setAddressBook: nodeify(addressBookController.setAddressBook, addressBookController),
@ -354,7 +377,9 @@ module.exports = class MetamaskController extends EventEmitter {
// txController // txController
cancelTransaction: nodeify(txController.cancelTransaction, txController), cancelTransaction: nodeify(txController.cancelTransaction, txController),
updateTransaction: nodeify(txController.updateTransaction, txController),
updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController), updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController),
retryTransaction: nodeify(this.retryTransaction, this),
// messageManager // messageManager
signMessage: nodeify(this.signMessage, this), signMessage: nodeify(this.signMessage, this),
@ -452,7 +477,7 @@ module.exports = class MetamaskController extends EventEmitter {
setupPublicConfig (outStream) { setupPublicConfig (outStream) {
pump( pump(
this.publicConfigStore, asStream(this.publicConfigStore),
outStream, outStream,
(err) => { (err) => {
if (err) log.error(err) if (err) log.error(err)
@ -468,15 +493,34 @@ module.exports = class MetamaskController extends EventEmitter {
// Vault Management // Vault Management
// //
async createNewVaultAndKeychain (password, cb) { async createNewVaultAndKeychain (password) {
const vault = await this.keyringController.createNewVaultAndKeychain(password) const release = await this.createVaultMutex.acquire()
this.selectFirstIdentity(vault) let vault
try {
const accounts = await this.keyringController.getAccounts()
if (accounts.length > 0) {
vault = await this.keyringController.fullUpdate()
} else {
vault = await this.keyringController.createNewVaultAndKeychain(password)
this.selectFirstIdentity(vault)
}
release()
} catch (err) {
release()
throw err
}
return vault return vault
} }
async createNewVaultAndRestore (password, seed, cb) { async createNewVaultAndRestore (password, seed) {
const release = await this.createVaultMutex.acquire()
const vault = await this.keyringController.createNewVaultAndRestore(password, seed) const vault = await this.keyringController.createNewVaultAndRestore(password, seed)
this.selectFirstIdentity(vault) this.selectFirstIdentity(vault)
release()
return vault return vault
} }
@ -546,6 +590,14 @@ module.exports = class MetamaskController extends EventEmitter {
// //
// Identity Management // Identity Management
// //
//
async retryTransaction (txId, cb) {
await this.txController.retryTransaction(txId)
const state = await this.getState()
return state
}
newUnsignedMessage (msgParams, cb) { newUnsignedMessage (msgParams, cb) {
const msgId = this.messageManager.addUnapprovedMessage(msgParams) const msgId = this.messageManager.addUnapprovedMessage(msgParams)
@ -774,4 +826,22 @@ module.exports = class MetamaskController extends EventEmitter {
return rpcTarget return rpcTarget
} }
setUseBlockie (val, cb) {
try {
this.preferencesController.setUseBlockie(val)
cb(null)
} catch (err) {
cb(err)
}
}
recordFirstTimeInfo (initState) {
if (!('firstTimeInfo' in initState)) {
initState.firstTimeInfo = {
version,
date: Date.now(),
}
}
}
} }

@ -0,0 +1,41 @@
const version = 20
/*
This migration ensures previous installations
get a `firstTimeInfo` key on the metamask state,
so that we can version notices in the future.
*/
const clone = require('clone')
module.exports = {
version,
migrate: function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
try {
const state = versionedData.data
const newState = transformState(state)
versionedData.data = newState
} catch (err) {
console.warn(`MetaMask Migration #${version}` + err.stack)
}
return Promise.resolve(versionedData)
},
}
function transformState (state) {
const newState = state
if ('metamask' in newState &&
!('firstTimeInfo' in newState.metamask)) {
newState.metamask.firstTimeInfo = {
version: '3.12.0',
date: Date.now(),
}
}
return newState
}

@ -30,4 +30,5 @@ module.exports = [
require('./017'), require('./017'),
require('./018'), require('./018'),
require('./019'), require('./019'),
require('./020'),
] ]

@ -1,13 +1,17 @@
const EventEmitter = require('events').EventEmitter const EventEmitter = require('events').EventEmitter
const semver = require('semver')
const extend = require('xtend') const extend = require('xtend')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const hardCodedNotices = require('../../notices/notices.json') const hardCodedNotices = require('../../notices/notices.json')
const uniqBy = require('lodash.uniqby')
module.exports = class NoticeController extends EventEmitter { module.exports = class NoticeController extends EventEmitter {
constructor (opts) { constructor (opts) {
super() super()
this.noticePoller = null this.noticePoller = null
this.firstVersion = opts.firstVersion
this.version = opts.version
const initState = extend({ const initState = extend({
noticesList: [], noticesList: [],
}, opts.initState) }, opts.initState)
@ -30,9 +34,9 @@ module.exports = class NoticeController extends EventEmitter {
return unreadNotices[unreadNotices.length - 1] return unreadNotices[unreadNotices.length - 1]
} }
setNoticesList (noticesList) { async setNoticesList (noticesList) {
this.store.updateState({ noticesList }) this.store.updateState({ noticesList })
return Promise.resolve(true) return true
} }
markNoticeRead (noticeToMark, cb) { markNoticeRead (noticeToMark, cb) {
@ -50,12 +54,14 @@ module.exports = class NoticeController extends EventEmitter {
} }
} }
updateNoticesList () { async updateNoticesList () {
return this._retrieveNoticeData().then((newNotices) => { const newNotices = await this._retrieveNoticeData()
var oldNotices = this.getNoticesList() const oldNotices = this.getNoticesList()
var combinedNotices = this._mergeNotices(oldNotices, newNotices) const combinedNotices = this._mergeNotices(oldNotices, newNotices)
return Promise.resolve(this.setNoticesList(combinedNotices)) const filteredNotices = this._filterNotices(combinedNotices)
}) const result = this.setNoticesList(filteredNotices)
this._updateMemstore()
return result
} }
startPolling () { startPolling () {
@ -68,22 +74,30 @@ module.exports = class NoticeController extends EventEmitter {
} }
_mergeNotices (oldNotices, newNotices) { _mergeNotices (oldNotices, newNotices) {
var noticeMap = this._mapNoticeIds(oldNotices) return uniqBy(oldNotices.concat(newNotices), 'id')
newNotices.forEach((notice) => { }
if (noticeMap.indexOf(notice.id) === -1) {
oldNotices.push(notice) _filterNotices(notices) {
return notices.filter((newNotice) => {
if ('version' in newNotice) {
const satisfied = semver.satisfies(this.version, newNotice.version)
return satisfied
}
if ('firstVersion' in newNotice) {
const satisfied = semver.satisfies(this.firstVersion, newNotice.firstVersion)
return satisfied
} }
return true
}) })
return oldNotices
} }
_mapNoticeIds (notices) { _mapNoticeIds (notices) {
return notices.map((notice) => notice.id) return notices.map((notice) => notice.id)
} }
_retrieveNoticeData () { async _retrieveNoticeData () {
// Placeholder for the API. // Placeholder for the API.
return Promise.resolve(hardCodedNotices) return hardCodedNotices
} }
_updateMemstore () { _updateMemstore () {

@ -17,6 +17,11 @@ class ExtensionPlatform {
return extension.runtime.getManifest().version return extension.runtime.getManifest().version
} }
openExtensionInBrowser () {
const extensionURL = extension.runtime.getURL('home.html')
this.openWindow({ url: extensionURL })
}
getPlatformInfo (cb) { getPlatformInfo (cb) {
try { try {
extension.runtime.getPlatformInfo((platform) => { extension.runtime.getPlatformInfo((platform) => {

@ -1,5 +1,6 @@
const injectCss = require('inject-css') const injectCss = require('inject-css')
const MetaMaskUiCss = require('../../ui/css') const OldMetaMaskUiCss = require('../../old-ui/css')
const NewMetaMaskUiCss = require('../../ui/css')
const startPopup = require('./popup-core') const startPopup = require('./popup-core')
const PortStream = require('./lib/port-stream.js') const PortStream = require('./lib/port-stream.js')
const isPopupOrNotification = require('./lib/is-popup-or-notification') const isPopupOrNotification = require('./lib/is-popup-or-notification')
@ -11,10 +12,6 @@ const notificationManager = new NotificationManager()
// create platform global // create platform global
global.platform = new ExtensionPlatform() global.platform = new ExtensionPlatform()
// inject css
const css = MetaMaskUiCss()
injectCss(css)
// identify window type (popup, notification) // identify window type (popup, notification)
const windowType = isPopupOrNotification() const windowType = isPopupOrNotification()
global.METAMASK_UI_TYPE = windowType global.METAMASK_UI_TYPE = windowType
@ -28,8 +25,21 @@ const connectionStream = new PortStream(extensionPort)
const container = document.getElementById('app-content') const container = document.getElementById('app-content')
startPopup({ container, connectionStream }, (err, store) => { startPopup({ container, connectionStream }, (err, store) => {
if (err) return displayCriticalError(err) if (err) return displayCriticalError(err)
let betaUIState = store.getState().metamask.featureFlags.betaUI
let css = betaUIState ? NewMetaMaskUiCss() : OldMetaMaskUiCss()
let deleteInjectedCss = injectCss(css)
let newBetaUIState
store.subscribe(() => { store.subscribe(() => {
const state = store.getState() const state = store.getState()
newBetaUIState = state.metamask.featureFlags.betaUI
if (newBetaUIState !== betaUIState) {
deleteInjectedCss()
betaUIState = newBetaUIState
css = betaUIState ? NewMetaMaskUiCss() : OldMetaMaskUiCss()
deleteInjectedCss = injectCss(css)
}
if (state.appState.shouldClose) notificationManager.closePopup() if (state.appState.shouldClose) notificationManager.closePopup()
}) })
}) })

@ -8,6 +8,7 @@
"frequentRpcList": [], "frequentRpcList": [],
"unapprovedTxs": {}, "unapprovedTxs": {},
"currentCurrency": "USD", "currentCurrency": "USD",
"featureFlags": {"betaUI": true},
"conversionRate": 12.7527416, "conversionRate": 12.7527416,
"conversionDate": 1487624341, "conversionDate": 1487624341,
"noActiveNotices": false, "noActiveNotices": false,

@ -0,0 +1,739 @@
{
"metamask": {
"isInitialized": true,
"isUnlocked": true,
"isMascara": false,
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {
"address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"name": "Account 1"
}
},
"unapprovedTxs": {},
"noActiveNotices": true,
"frequentRpcList": [
"http://192.168.1.34:7545/"
],
"addressBook": [],
"tokenExchangeRates": {},
"coinOptions": {},
"provider": {
"type": "mainnet",
"rpcTarget": "https://mainnet.infura.io/metamask"
},
"network": "1",
"accounts": {
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {
"code": "0x",
"balance": "0x1b3f641ed0c2f62",
"nonce": "0x35",
"address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
}
},
"currentBlockGasLimit": "0x66df83",
"selectedAddressTxList": [
{
"id": 3516145537630216,
"time": 1512615655535,
"status": "submitted",
"metamaskNetworkId": "1",
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"value": "0x16345785d8a0000",
"gasPrice": "0xc1b710800",
"gas": "0x7b0c",
"nonce": "0x35",
"chainId": "0x1"
},
"gasPriceSpecified": false,
"gasLimitSpecified": false,
"estimatedGas": "5208",
"history": [
{
"id": 3516145537630216,
"time": 1512615655535,
"status": "unapproved",
"metamaskNetworkId": "1",
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"value": "0x16345785d8a0000",
"gasPrice": "0xe6f7cec00",
"gas": "0x7b0c"
},
"gasPriceSpecified": false,
"gasLimitSpecified": false,
"estimatedGas": "5208"
},
[
{
"op": "replace",
"path": "/txParams/gasPrice",
"value": "0xc1b710800",
"note": "confTx: user approved transaction"
}
],
[
{
"op": "replace",
"path": "/status",
"value": "approved",
"note": "txStateManager: setting status to approved"
}
],
[
{
"op": "add",
"path": "/txParams/nonce",
"value": "0x35",
"note": "transactions#approveTransaction"
},
{
"op": "add",
"path": "/nonceDetails",
"value": {
"params": {
"highestLocalNonce": 53,
"highestSuggested": 53,
"nextNetworkNonce": 53
},
"local": {
"name": "local",
"nonce": 53,
"details": {
"startPoint": 53,
"highest": 53
}
},
"network": {
"name": "network",
"nonce": 53,
"details": {
"baseCount": 53
}
}
}
}
],
[
{
"op": "add",
"path": "/txParams/chainId",
"value": "0x1",
"note": "txStateManager: setting status to signed"
},
{
"op": "replace",
"path": "/status",
"value": "signed"
}
],
[
{
"op": "add",
"path": "/rawTx",
"value": "0xf86c35850c1b710800827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008026a0f5142ba79a13ca7ec65548953017edafb217803244bbf9821d9ad077d89921e9a03afcb614169c90be9905d5b469d06984825c76675d3a535937cdb8f2ad1c0a95",
"note": "transactions#publishTransaction"
}
],
[
{
"op": "add",
"path": "/hash",
"value": "0x7ce19c0d128ca11293b44a4e6d3cc9063665c00ea8c8eb400f548e132c147353",
"note": "transactions#setTxHash"
}
],
[
{
"op": "replace",
"path": "/status",
"value": "submitted",
"note": "txStateManager: setting status to submitted"
}
],
[
{
"op": "add",
"path": "/firstRetryBlockNumber",
"value": "0x478ab3",
"note": "transactions/pending-tx-tracker#event: tx:block-update"
}
]
],
"nonceDetails": {
"params": {
"highestLocalNonce": 53,
"highestSuggested": 53,
"nextNetworkNonce": 53
},
"local": {
"name": "local",
"nonce": 53,
"details": {
"startPoint": 53,
"highest": 53
}
},
"network": {
"name": "network",
"nonce": 53,
"details": {
"baseCount": 53
}
}
},
"rawTx": "0xf86c35850c1b710800827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008026a0f5142ba79a13ca7ec65548953017edafb217803244bbf9821d9ad077d89921e9a03afcb614169c90be9905d5b469d06984825c76675d3a535937cdb8f2ad1c0a95",
"hash": "0x7ce19c0d128ca11293b44a4e6d3cc9063665c00ea8c8eb400f548e132c147353",
"firstRetryBlockNumber": "0x478ab3"
},
{
"id": 3516145537630211,
"time": 1512613432658,
"status": "confirmed",
"metamaskNetworkId": "1",
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"value": "0x16345785d8a0000",
"gasPrice": "0xba43b7400",
"gas": "0x7b0c",
"nonce": "0x34",
"chainId": "0x1"
},
"gasPriceSpecified": false,
"gasLimitSpecified": false,
"estimatedGas": "5208",
"history": [
{
"id": 3516145537630211,
"time": 1512613432658,
"status": "unapproved",
"metamaskNetworkId": "1",
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"value": "0x16345785d8a0000",
"gasPrice": "0xdf8475800",
"gas": "0x7b0c"
},
"gasPriceSpecified": false,
"gasLimitSpecified": false,
"estimatedGas": "5208"
},
[
{
"op": "replace",
"path": "/txParams/gasPrice",
"value": "0xba43b7400",
"note": "confTx: user approved transaction"
}
],
[
{
"op": "replace",
"path": "/status",
"value": "approved",
"note": "txStateManager: setting status to approved"
}
],
[
{
"op": "add",
"path": "/txParams/nonce",
"value": "0x34",
"note": "transactions#approveTransaction"
},
{
"op": "add",
"path": "/nonceDetails",
"value": {
"params": {
"highestLocalNonce": 52,
"highestSuggested": 52,
"nextNetworkNonce": 52
},
"local": {
"name": "local",
"nonce": 52,
"details": {
"startPoint": 52,
"highest": 52
}
},
"network": {
"name": "network",
"nonce": 52,
"details": {
"baseCount": 52
}
}
}
}
],
[
{
"op": "add",
"path": "/txParams/chainId",
"value": "0x1",
"note": "txStateManager: setting status to signed"
},
{
"op": "replace",
"path": "/status",
"value": "signed"
}
],
[
{
"op": "add",
"path": "/rawTx",
"value": "0xf86c34850ba43b7400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008026a073a4afdb8e8ad32b0cf9039af56c66baffd60d30e75cee5c1b783208824eafb8a0021ca6c1714a2c71281333ab77f776d3514348ab77967280fca8a5b4be44285e",
"note": "transactions#publishTransaction"
}
],
[
{
"op": "add",
"path": "/hash",
"value": "0x5c98409883fdfd3cd24058a83b91470da6c40ffae41a40eb90d7dee0b837d26d",
"note": "transactions#setTxHash"
}
],
[
{
"op": "replace",
"path": "/status",
"value": "submitted",
"note": "txStateManager: setting status to submitted"
}
],
[
{
"op": "add",
"path": "/firstRetryBlockNumber",
"value": "0x478a2c",
"note": "transactions/pending-tx-tracker#event: tx:block-update"
}
],
[
{
"op": "replace",
"path": "/status",
"value": "confirmed",
"note": "txStateManager: setting status to confirmed"
}
]
],
"nonceDetails": {
"params": {
"highestLocalNonce": 52,
"highestSuggested": 52,
"nextNetworkNonce": 52
},
"local": {
"name": "local",
"nonce": 52,
"details": {
"startPoint": 52,
"highest": 52
}
},
"network": {
"name": "network",
"nonce": 52,
"details": {
"baseCount": 52
}
}
},
"rawTx": "0xf86c34850ba43b7400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008026a073a4afdb8e8ad32b0cf9039af56c66baffd60d30e75cee5c1b783208824eafb8a0021ca6c1714a2c71281333ab77f776d3514348ab77967280fca8a5b4be44285e",
"hash": "0x5c98409883fdfd3cd24058a83b91470da6c40ffae41a40eb90d7dee0b837d26d",
"firstRetryBlockNumber": "0x478a2c"
},
{
"id": 3516145537630210,
"time": 1512612826136,
"status": "confirmed",
"metamaskNetworkId": "1",
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"value": "0x16345785d8a0000",
"gasPrice": "0xa7a358200",
"gas": "0x7b0c",
"nonce": "0x33",
"chainId": "0x1"
},
"gasPriceSpecified": false,
"gasLimitSpecified": false,
"estimatedGas": "5208",
"history": [
{
"id": 3516145537630210,
"time": 1512612826136,
"status": "unapproved",
"metamaskNetworkId": "1",
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"value": "0x16345785d8a0000",
"gasPrice": "0xba43b7400",
"gas": "0x7b0c"
},
"gasPriceSpecified": false,
"gasLimitSpecified": false,
"estimatedGas": "5208"
},
[
{
"op": "replace",
"path": "/txParams/gasPrice",
"value": "0xa7a358200",
"note": "confTx: user approved transaction"
}
],
[
{
"op": "replace",
"path": "/status",
"value": "approved",
"note": "txStateManager: setting status to approved"
}
],
[
{
"op": "add",
"path": "/txParams/nonce",
"value": "0x33",
"note": "transactions#approveTransaction"
},
{
"op": "add",
"path": "/nonceDetails",
"value": {
"params": {
"highestLocalNonce": 0,
"highestSuggested": 51,
"nextNetworkNonce": 51
},
"local": {
"name": "local",
"nonce": 51,
"details": {
"startPoint": 51,
"highest": 51
}
},
"network": {
"name": "network",
"nonce": 51,
"details": {
"baseCount": 51
}
}
}
}
],
[
{
"op": "add",
"path": "/txParams/chainId",
"value": "0x1",
"note": "txStateManager: setting status to signed"
},
{
"op": "replace",
"path": "/status",
"value": "signed"
}
],
[
{
"op": "add",
"path": "/rawTx",
"value": "0xf86c33850a7a358200827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008026a0021a8cd6c10208cc593e22af53637e5d127cee5cc6f9443a3e758a02afff1d7ca025f7420e974d3f2c668c165040987c72543a8e709bfea3528a62836a6ced9ce8",
"note": "transactions#publishTransaction"
}
],
[
{
"op": "add",
"path": "/hash",
"value": "0x289772800898bc9cd414530d8581c0da257a9055e4aaaa6d10d92d700bfbd044",
"note": "transactions#setTxHash"
}
],
[
{
"op": "replace",
"path": "/status",
"value": "submitted",
"note": "txStateManager: setting status to submitted"
}
],
[
{
"op": "add",
"path": "/firstRetryBlockNumber",
"value": "0x478a04",
"note": "transactions/pending-tx-tracker#event: tx:block-update"
}
],
[
{
"op": "replace",
"path": "/status",
"value": "confirmed",
"note": "txStateManager: setting status to confirmed"
}
]
],
"nonceDetails": {
"params": {
"highestLocalNonce": 0,
"highestSuggested": 51,
"nextNetworkNonce": 51
},
"local": {
"name": "local",
"nonce": 51,
"details": {
"startPoint": 51,
"highest": 51
}
},
"network": {
"name": "network",
"nonce": 51,
"details": {
"baseCount": 51
}
}
},
"rawTx": "0xf86c33850a7a358200827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008026a0021a8cd6c10208cc593e22af53637e5d127cee5cc6f9443a3e758a02afff1d7ca025f7420e974d3f2c668c165040987c72543a8e709bfea3528a62836a6ced9ce8",
"hash": "0x289772800898bc9cd414530d8581c0da257a9055e4aaaa6d10d92d700bfbd044",
"firstRetryBlockNumber": "0x478a04"
},
{
"id": 3516145537630209,
"time": 1512612809252,
"status": "failed",
"metamaskNetworkId": "1",
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"value": "0x16345785d8a0000",
"gasPrice": "0x77359400",
"gas": "0x7b0c",
"nonce": "0x33",
"chainId": "0x1"
},
"gasPriceSpecified": false,
"gasLimitSpecified": false,
"estimatedGas": "5208",
"history": [
{
"id": 3516145537630209,
"time": 1512612809252,
"status": "unapproved",
"metamaskNetworkId": "1",
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"value": "0x16345785d8a0000",
"gasPrice": "0xba43b7400",
"gas": "0x7b0c"
},
"gasPriceSpecified": false,
"gasLimitSpecified": false,
"estimatedGas": "5208"
},
[
{
"op": "replace",
"path": "/txParams/gasPrice",
"value": "0x77359400",
"note": "confTx: user approved transaction"
}
],
[
{
"op": "replace",
"path": "/status",
"value": "approved",
"note": "txStateManager: setting status to approved"
}
],
[
{
"op": "add",
"path": "/txParams/nonce",
"value": "0x33",
"note": "transactions#approveTransaction"
},
{
"op": "add",
"path": "/nonceDetails",
"value": {
"params": {
"highestLocalNonce": 0,
"highestSuggested": 51,
"nextNetworkNonce": 51
},
"local": {
"name": "local",
"nonce": 51,
"details": {
"startPoint": 51,
"highest": 51
}
},
"network": {
"name": "network",
"nonce": 51,
"details": {
"baseCount": 51
}
}
}
}
],
[
{
"op": "add",
"path": "/txParams/chainId",
"value": "0x1",
"note": "txStateManager: setting status to signed"
},
{
"op": "replace",
"path": "/status",
"value": "signed"
}
],
[
{
"op": "add",
"path": "/rawTx",
"value": "0xf86b338477359400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008025a098624a27ae79b2b1adc63b913850f266a920cb9d93e6588b8df9b8883eb1b323a00cc6fd855723a234f4f93b48caf7a7659366d09e5c5887f0a4c2e5fa68012cd7",
"note": "transactions#publishTransaction"
}
],
[
{
"op": "add",
"path": "/err",
"value": {
"message": "Error: [ethjs-rpc] rpc error with payload {\"id\":7801900228852,\"jsonrpc\":\"2.0\",\"params\":[\"0xf86b338477359400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008025a098624a27ae79b2b1adc63b913850f266a920cb9d93e6588b8df9b8883eb1b323a00cc6fd855723a234f4f93b48caf7a7659366d09e5c5887f0a4c2e5fa68012cd7\"],\"method\":\"eth_sendRawTransaction\"} Error: transaction underpriced",
"stack": "Error: [ethjs-rpc] rpc error with payload {\"id\":7801900228852,\"jsonrpc\":\"2.0\",\"params\":[\"0xf86b338477359400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008025a098624a27ae79b2b1adc63b913850f266a920cb9d93e6588b8df9b8883eb1b323a00cc6fd855723a234f4f93b48caf7a7659366d09e5c5887f0a4c2e5fa68012cd7\"],\"method\":\"eth_sendRawTransaction\"} Error: transaction underpriced\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:60327:26\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:88030:9\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16678:16\n at replenish (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16522:25)\n at iterateeCallback (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16512:17)\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16694:16\n at resultObj.id (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:88012:9)\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16813:16\n at replenish (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16527:17)\n at iterateeCallback (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16512:17)"
}
}
],
[
{
"op": "replace",
"path": "/status",
"value": "failed",
"note": "txStateManager: setting status to failed"
}
]
],
"nonceDetails": {
"params": {
"highestLocalNonce": 0,
"highestSuggested": 51,
"nextNetworkNonce": 51
},
"local": {
"name": "local",
"nonce": 51,
"details": {
"startPoint": 51,
"highest": 51
}
},
"network": {
"name": "network",
"nonce": 51,
"details": {
"baseCount": 51
}
}
},
"rawTx": "0xf86b338477359400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008025a098624a27ae79b2b1adc63b913850f266a920cb9d93e6588b8df9b8883eb1b323a00cc6fd855723a234f4f93b48caf7a7659366d09e5c5887f0a4c2e5fa68012cd7",
"err": {
"message": "Error: [ethjs-rpc] rpc error with payload {\"id\":7801900228852,\"jsonrpc\":\"2.0\",\"params\":[\"0xf86b338477359400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008025a098624a27ae79b2b1adc63b913850f266a920cb9d93e6588b8df9b8883eb1b323a00cc6fd855723a234f4f93b48caf7a7659366d09e5c5887f0a4c2e5fa68012cd7\"],\"method\":\"eth_sendRawTransaction\"} Error: transaction underpriced",
"stack": "Error: [ethjs-rpc] rpc error with payload {\"id\":7801900228852,\"jsonrpc\":\"2.0\",\"params\":[\"0xf86b338477359400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008025a098624a27ae79b2b1adc63b913850f266a920cb9d93e6588b8df9b8883eb1b323a00cc6fd855723a234f4f93b48caf7a7659366d09e5c5887f0a4c2e5fa68012cd7\"],\"method\":\"eth_sendRawTransaction\"} Error: transaction underpriced\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:60327:26\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:88030:9\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16678:16\n at replenish (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16522:25)\n at iterateeCallback (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16512:17)\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16694:16\n at resultObj.id (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:88012:9)\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16813:16\n at replenish (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16527:17)\n at iterateeCallback (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16512:17)"
}
}
],
"unapprovedMsgs": {},
"unapprovedMsgCount": 0,
"unapprovedPersonalMsgs": {},
"unapprovedPersonalMsgCount": 0,
"unapprovedTypedMessages": {},
"unapprovedTypedMessagesCount": 0,
"keyringTypes": [
"Simple Key Pair",
"HD Key Tree"
],
"keyrings": [
{
"type": "HD Key Tree",
"accounts": [
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
]
}
],
"computedBalances": {},
"currentAccountTab": "history",
"tokens": [
{
"address": "0x0d8775f648430679a709e98d2b0cb6250d2887ef",
"symbol": "BAT",
"decimals": "18"
}
],
"selectedAddress": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"currentCurrency": "usd",
"conversionRate": 418.62,
"conversionDate": 1512615622,
"infuraNetworkStatus": {
"mainnet": "ok",
"ropsten": "ok",
"kovan": "ok",
"rinkeby": "ok"
},
"shapeShiftTxList": [],
"lostAccounts": []
},
"appState": {
"shouldClose": true,
"menuOpen": false,
"currentView": {
"name": "accountDetail",
"context": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
},
"accountDetail": {
"subview": "transactions",
"accountExport": "none",
"privateKey": ""
},
"transForward": false,
"isLoading": false,
"warning": null,
"forgottenPassword": false,
"scrollToBottom": false
},
"identities": {},
"version": "3.12.1",
"platform": {
"arch": "x86-64",
"nacl_arch": "x86-64",
"os": "mac"
},
"browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36"
}

@ -126,11 +126,17 @@ gulp.task('manifest:production', function() {
'./dist/firefox/manifest.json', './dist/firefox/manifest.json',
'./dist/chrome/manifest.json', './dist/chrome/manifest.json',
'./dist/edge/manifest.json', './dist/edge/manifest.json',
'./dist/opera/manifest.json',
],{base: './dist/'}) ],{base: './dist/'})
// Exclude chromereload script in production:
.pipe(gulpif(!debug,jsoneditor(function(json) { .pipe(gulpif(!debug,jsoneditor(function(json) {
json.background.scripts = ["scripts/background.js"] json.background.scripts = json.background.scripts.filter((script) => {
return !script.includes('chromereload')
})
return json return json
}))) })))
.pipe(gulp.dest('./dist/', { overwrite: true })) .pipe(gulp.dest('./dist/', { overwrite: true }))
}) })

@ -64,7 +64,7 @@ class NoticeScreen extends Component {
<Identicon address={address} diameter={70} /> <Identicon address={address} diameter={70} />
<div className="tou__title">{title}</div> <div className="tou__title">{title}</div>
<Markdown <Markdown
className="tou__body" className="tou__body markdown"
source={body} source={body}
skipHtml skipHtml
/> />

@ -0,0 +1,11 @@
Please take a moment to [back up your seed phrase again](https://support.metamask.io/kb/article/28-abbu-always-be-backed-up-how-to-make-sure-your-12-word-metamask-seed-phrase-is-backed-up).
MetaMask has become aware of a previous issue where a very small number of users were shown the wrong seed phrase to back up. The only way to protect yourself from this issue, is to back up your seed phrase again now.
You can follow the guide at this link:
[https://support.metamask.io/kb/article/28-abbu-always-be-backed-up-how-to-make-sure-your-12-word-metamask-seed-phrase-is-backed-up](https://support.metamask.io/kb/article/28-abbu-always-be-backed-up-how-to-make-sure-your-12-word-metamask-seed-phrase-is-backed-up)
We have fixed the known issue, but will be issuing ongoing bug bounties to help prevent this kind of problem in the future.
For more information on this issue, [see this blog post](https://medium.com/metamask/seed-phrase-issue-bounty-awarded-e1986e811021)

File diff suppressed because one or more lines are too long

66
old-ui/.gitignore vendored

@ -0,0 +1,66 @@
# Created by https://www.gitignore.io/api/osx,node
### OSX ###
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Node ###
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
node_modules
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history

@ -0,0 +1,289 @@
const inherits = require('util').inherits
const extend = require('xtend')
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('../../ui/app/actions')
const valuesFor = require('./util').valuesFor
const Identicon = require('./components/identicon')
const EthBalance = require('./components/eth-balance')
const TransactionList = require('./components/transaction-list')
const ExportAccountView = require('./components/account-export')
const ethUtil = require('ethereumjs-util')
const EditableLabel = require('./components/editable-label')
const TabBar = require('./components/tab-bar')
const TokenList = require('./components/token-list')
const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns
module.exports = connect(mapStateToProps)(AccountDetailScreen)
function mapStateToProps (state) {
return {
metamask: state.metamask,
identities: state.metamask.identities,
accounts: state.metamask.accounts,
address: state.metamask.selectedAddress,
accountDetail: state.appState.accountDetail,
network: state.metamask.network,
unapprovedMsgs: valuesFor(state.metamask.unapprovedMsgs),
shapeShiftTxList: state.metamask.shapeShiftTxList,
transactions: state.metamask.selectedAddressTxList || [],
conversionRate: state.metamask.conversionRate,
currentCurrency: state.metamask.currentCurrency,
currentAccountTab: state.metamask.currentAccountTab,
tokens: state.metamask.tokens,
computedBalances: state.metamask.computedBalances,
}
}
inherits(AccountDetailScreen, Component)
function AccountDetailScreen () {
Component.call(this)
}
AccountDetailScreen.prototype.render = function () {
var props = this.props
var selected = props.address || Object.keys(props.accounts)[0]
var checksumAddress = selected && ethUtil.toChecksumAddress(selected)
var identity = props.identities[selected]
var account = props.accounts[selected]
const { network, conversionRate, currentCurrency } = props
return (
h('.account-detail-section.full-flex-height', [
// identicon, label, balance, etc
h('.account-data-subsection', {
style: {
margin: '0 20px',
flex: '1 0 auto',
},
}, [
// header - identicon + nav
h('div', {
style: {
paddingTop: '20px',
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'flex-start',
},
}, [
// large identicon and addresses
h('.identicon-wrapper.select-none', [
h(Identicon, {
diameter: 62,
address: selected,
}),
]),
h('div.flex-column', {
style: {
lineHeight: '10px',
width: '100%',
},
}, [
h(EditableLabel, {
textValue: identity ? identity.name : '',
state: {
isEditingLabel: false,
},
saveText: (text) => {
props.dispatch(actions.saveAccountLabel(selected, text))
},
}, [
// What is shown when not editing + edit text:
h('label.editing-label', [h('.edit-text', 'edit')]),
h(
'div',
{
style: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
},
[
h(
'div.font-medium.color-forest',
{
name: 'edit',
style: {
},
},
[
h('h2', {
style: {
maxWidth: '180px',
overflow: 'hidden',
textOverflow: 'ellipsis',
padding: '5px 0px',
lineHeight: '25px',
},
}, [
identity && identity.name,
]),
]
),
h(
AccountDropdowns,
{
style: {
cursor: 'pointer',
},
selected,
network,
identities: props.identities,
enableAccountOptions: true,
},
),
]
),
]),
h('.flex-row', {
style: {
justifyContent: 'space-between',
alignItems: 'baseline',
},
}, [
// address
h('div', {
style: {
overflow: 'hidden',
textOverflow: 'ellipsis',
paddingTop: '3px',
width: '5em',
fontSize: '13px',
fontFamily: 'Montserrat Light',
textRendering: 'geometricPrecision',
marginTop: '15px',
marginBottom: '15px',
color: '#AEAEAE',
},
}, checksumAddress),
]),
// account ballence
]),
]),
h('.flex-row', {
style: {
justifyContent: 'space-between',
alignItems: 'flex-start',
},
}, [
h(EthBalance, {
value: account && account.balance,
conversionRate,
currentCurrency,
style: {
lineHeight: '7px',
marginTop: '10px',
},
}),
h('div', {}, [
h('button', {
onClick: () => props.dispatch(actions.buyEthView(selected)),
style: { marginRight: '10px' },
}, 'BUY'),
h('button', {
onClick: () => props.dispatch(actions.showSendPage()),
style: {
marginBottom: '20px',
},
}, 'SEND'),
]),
]),
]),
// subview (tx history, pk export confirm, buy eth warning)
this.subview(),
])
)
}
AccountDetailScreen.prototype.subview = function () {
var subview
try {
subview = this.props.accountDetail.subview
} catch (e) {
subview = null
}
switch (subview) {
case 'transactions':
return this.tabSections()
case 'export':
var state = extend({key: 'export'}, this.props)
return h(ExportAccountView, state)
default:
return this.tabSections()
}
}
AccountDetailScreen.prototype.tabSections = function () {
const { currentAccountTab } = this.props
return h('section.tabSection.full-flex-height.grow-tenx', [
h(TabBar, {
tabs: [
{ content: 'Sent', key: 'history' },
{ content: 'Tokens', key: 'tokens' },
],
defaultTab: currentAccountTab || 'history',
tabSelected: (key) => {
this.props.dispatch(actions.setCurrentAccountTab(key))
},
}),
this.tabSwitchView(),
])
}
AccountDetailScreen.prototype.tabSwitchView = function () {
const props = this.props
const { address, network } = props
const { currentAccountTab, tokens } = this.props
switch (currentAccountTab) {
case 'tokens':
return h(TokenList, {
userAddress: address,
network,
tokens,
addToken: () => this.props.dispatch(actions.showAddTokenPage()),
})
default:
return this.transactionList()
}
}
AccountDetailScreen.prototype.transactionList = function () {
const {transactions, unapprovedMsgs, address,
network, shapeShiftTxList, conversionRate } = this.props
return h(TransactionList, {
transactions: transactions.sort((a, b) => b.time - a.time),
network,
unapprovedMsgs,
conversionRate,
address,
shapeShiftTxList,
viewPendingTx: (txId) => {
this.props.dispatch(actions.viewPendingTx(txId))
},
})
}

@ -0,0 +1,101 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('../../../../ui/app/actions')
import Select from 'react-select'
// Subviews
const JsonImportView = require('./json.js')
const PrivateKeyImportView = require('./private-key.js')
const menuItems = [
'Private Key',
'JSON File',
]
module.exports = connect(mapStateToProps)(AccountImportSubview)
function mapStateToProps (state) {
return {
menuItems,
}
}
inherits(AccountImportSubview, Component)
function AccountImportSubview () {
Component.call(this)
}
AccountImportSubview.prototype.render = function () {
const props = this.props
const state = this.state || {}
const { menuItems } = props
const { type } = state
return (
h('div', {
style: {
},
}, [
h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: (event) => {
props.dispatch(actions.goHome())
},
}),
h('h2.page-subtitle', 'Import Accounts'),
]),
h('div', {
style: {
padding: '10px',
color: 'rgb(174, 174, 174)',
},
}, [
h('h3', { style: { padding: '3px' } }, 'SELECT TYPE'),
h('style', `
.has-value.Select--single > .Select-control .Select-value .Select-value-label, .Select-value-label {
color: rgb(174,174,174);
}
`),
h(Select, {
name: 'import-type-select',
clearable: false,
value: type || menuItems[0],
options: menuItems.map((type) => {
return {
value: type,
label: type,
}
}),
onChange: (opt) => {
props.dispatch(actions.showImportPage())
this.setState({ type: opt.value })
},
}),
]),
this.renderImportView(),
])
)
}
AccountImportSubview.prototype.renderImportView = function () {
const props = this.props
const state = this.state || {}
const { type } = state
const { menuItems } = props
const current = type || menuItems[0]
switch (current) {
case 'Private Key':
return h(PrivateKeyImportView)
case 'JSON File':
return h(JsonImportView)
default:
return h(JsonImportView)
}
}

@ -0,0 +1,100 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('../../../../ui/app/actions')
const FileInput = require('react-simple-file-input').default
const HELP_LINK = 'https://github.com/MetaMask/faq/blob/master/README.md#q-i-cant-use-the-import-feature-for-uploading-a-json-file-the-window-keeps-closing-when-i-try-to-select-a-file'
module.exports = connect(mapStateToProps)(JsonImportSubview)
function mapStateToProps (state) {
return {
error: state.appState.warning,
}
}
inherits(JsonImportSubview, Component)
function JsonImportSubview () {
Component.call(this)
}
JsonImportSubview.prototype.render = function () {
const { error } = this.props
return (
h('div', {
style: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: '5px 15px 0px 15px',
},
}, [
h('p', 'Used by a variety of different clients'),
h('a.warning', { href: HELP_LINK, target: '_blank' }, 'File import not working? Click here!'),
h(FileInput, {
readAs: 'text',
onLoad: this.onLoad.bind(this),
style: {
margin: '20px 0px 12px 20px',
fontSize: '15px',
},
}),
h('input.large-input.letter-spacey', {
type: 'password',
placeholder: 'Enter password',
id: 'json-password-box',
onKeyPress: this.createKeyringOnEnter.bind(this),
style: {
width: 260,
marginTop: 12,
},
}),
h('button.primary', {
onClick: this.createNewKeychain.bind(this),
style: {
margin: 12,
},
}, 'Import'),
error ? h('span.error', error) : null,
])
)
}
JsonImportSubview.prototype.onLoad = function (event, file) {
this.setState({file: file, fileContents: event.target.result})
}
JsonImportSubview.prototype.createKeyringOnEnter = function (event) {
if (event.key === 'Enter') {
event.preventDefault()
this.createNewKeychain()
}
}
JsonImportSubview.prototype.createNewKeychain = function () {
const state = this.state
const { fileContents } = state
if (!fileContents) {
const message = 'You must select a file to import.'
return this.props.dispatch(actions.displayWarning(message))
}
const passwordInput = document.getElementById('json-password-box')
const password = passwordInput.value
if (!password) {
const message = 'You must enter a password for the selected file.'
return this.props.dispatch(actions.displayWarning(message))
}
this.props.dispatch(actions.importNewAccount('JSON File', [ fileContents, password ]))
}

@ -0,0 +1,67 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('../../../../ui/app/actions')
module.exports = connect(mapStateToProps)(PrivateKeyImportView)
function mapStateToProps (state) {
return {
error: state.appState.warning,
}
}
inherits(PrivateKeyImportView, Component)
function PrivateKeyImportView () {
Component.call(this)
}
PrivateKeyImportView.prototype.render = function () {
const { error } = this.props
return (
h('div', {
style: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: '5px 15px 0px 15px',
},
}, [
h('span', 'Paste your private key string here'),
h('input.large-input.letter-spacey', {
type: 'password',
id: 'private-key-box',
onKeyPress: this.createKeyringOnEnter.bind(this),
style: {
width: 260,
marginTop: 12,
},
}),
h('button.primary', {
onClick: this.createNewKeychain.bind(this),
style: {
margin: 12,
},
}, 'Import'),
error ? h('span.error', error) : null,
])
)
}
PrivateKeyImportView.prototype.createKeyringOnEnter = function (event) {
if (event.key === 'Enter') {
event.preventDefault()
this.createNewKeychain()
}
}
PrivateKeyImportView.prototype.createNewKeychain = function () {
const input = document.getElementById('private-key-box')
const privateKey = input.value
this.props.dispatch(actions.importNewAccount('Private Key', [ privateKey ]))
}

@ -0,0 +1,30 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
module.exports = connect(mapStateToProps)(SeedImportSubview)
function mapStateToProps (state) {
return {}
}
inherits(SeedImportSubview, Component)
function SeedImportSubview () {
Component.call(this)
}
SeedImportSubview.prototype.render = function () {
return (
h('div', {
style: {
},
}, [
`Paste your seed phrase here!`,
h('textarea'),
h('br'),
h('button', 'Submit'),
])
)
}

@ -0,0 +1,238 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('../../ui/app/actions')
const Tooltip = require('./components/tooltip.js')
const ethUtil = require('ethereumjs-util')
const abi = require('human-standard-token-abi')
const Eth = require('ethjs-query')
const EthContract = require('ethjs-contract')
const emptyAddr = '0x0000000000000000000000000000000000000000'
module.exports = connect(mapStateToProps)(AddTokenScreen)
function mapStateToProps (state) {
return {
identities: state.metamask.identities,
}
}
inherits(AddTokenScreen, Component)
function AddTokenScreen () {
this.state = {
warning: null,
address: null,
symbol: 'TOKEN',
decimals: 18,
}
Component.call(this)
}
AddTokenScreen.prototype.render = function () {
const state = this.state
const props = this.props
const { warning, symbol, decimals } = state
return (
h('.flex-column.flex-grow', [
// subtitle and nav
h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: (event) => {
props.dispatch(actions.goHome())
},
}),
h('h2.page-subtitle', 'Add Token'),
]),
h('.error', {
style: {
display: warning ? 'block' : 'none',
padding: '0 20px',
textAlign: 'center',
},
}, warning),
// conf view
h('.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-space-around', {
style: {
padding: '20px',
},
}, [
h('div', [
h(Tooltip, {
position: 'top',
title: 'The contract of the actual token contract. Click for more info.',
}, [
h('a', {
style: { fontWeight: 'bold', paddingRight: '10px'},
href: 'https://support.metamask.io/kb/article/24-what-is-a-token-contract-address',
target: '_blank',
}, [
h('span', 'Token Contract Address '),
h('i.fa.fa-question-circle'),
]),
]),
]),
h('section.flex-row.flex-center', [
h('input#token-address', {
name: 'address',
placeholder: 'Token Contract Address',
onChange: this.tokenAddressDidChange.bind(this),
style: {
width: 'inherit',
flex: '1 0 auto',
height: '30px',
margin: '8px',
},
}),
]),
h('div', [
h('span', {
style: { fontWeight: 'bold', paddingRight: '10px'},
}, 'Token Symbol'),
]),
h('div', { style: {display: 'flex'} }, [
h('input#token_symbol', {
placeholder: `Like "ETH"`,
value: symbol,
style: {
width: 'inherit',
flex: '1 0 auto',
height: '30px',
margin: '8px',
},
onChange: (event) => {
var element = event.target
var symbol = element.value
this.setState({ symbol })
},
}),
]),
h('div', [
h('span', {
style: { fontWeight: 'bold', paddingRight: '10px'},
}, 'Decimals of Precision'),
]),
h('div', { style: {display: 'flex'} }, [
h('input#token_decimals', {
value: decimals,
type: 'number',
min: 0,
max: 36,
style: {
width: 'inherit',
flex: '1 0 auto',
height: '30px',
margin: '8px',
},
onChange: (event) => {
var element = event.target
var decimals = element.value.trim()
this.setState({ decimals })
},
}),
]),
h('button', {
style: {
alignSelf: 'center',
},
onClick: (event) => {
const valid = this.validateInputs()
if (!valid) return
const { address, symbol, decimals } = this.state
this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals))
},
}, 'Add'),
]),
]),
])
)
}
AddTokenScreen.prototype.componentWillMount = function () {
if (typeof global.ethereumProvider === 'undefined') return
this.eth = new Eth(global.ethereumProvider)
this.contract = new EthContract(this.eth)
this.TokenContract = this.contract(abi)
}
AddTokenScreen.prototype.tokenAddressDidChange = function (event) {
const el = event.target
const address = el.value.trim()
if (ethUtil.isValidAddress(address) && address !== emptyAddr) {
this.setState({ address })
this.attemptToAutoFillTokenParams(address)
}
}
AddTokenScreen.prototype.validateInputs = function () {
let msg = ''
const state = this.state
const identitiesList = Object.keys(this.props.identities)
const { address, symbol, decimals } = state
const standardAddress = ethUtil.addHexPrefix(address).toLowerCase()
const validAddress = ethUtil.isValidAddress(address)
if (!validAddress) {
msg += 'Address is invalid. '
}
const validDecimals = decimals >= 0 && decimals < 36
if (!validDecimals) {
msg += 'Decimals must be at least 0, and not over 36. '
}
const symbolLen = symbol.trim().length
const validSymbol = symbolLen > 0 && symbolLen < 10
if (!validSymbol) {
msg += 'Symbol must be between 0 and 10 characters.'
}
const ownAddress = identitiesList.includes(standardAddress)
if (ownAddress) {
msg = 'Personal address detected. Input the token contract address.'
}
const isValid = validAddress && validDecimals && !ownAddress
if (!isValid) {
this.setState({
warning: msg,
})
} else {
this.setState({ warning: null })
}
return isValid
}
AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) {
const contract = this.TokenContract.at(address)
const results = await Promise.all([
contract.symbol(),
contract.decimals(),
])
const [ symbol, decimals ] = results
if (symbol && decimals) {
console.log('SETTING SYMBOL AND DECIMALS', { symbol, decimals })
this.setState({ symbol: symbol[0], decimals: decimals[0].toString() })
}
}

@ -0,0 +1,684 @@
const inherits = require('util').inherits
const Component = require('react').Component
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const actions = require('../../ui/app/actions')
// mascara
const MascaraFirstTime = require('../../mascara/src/app/first-time').default
const MascaraBuyEtherScreen = require('../../mascara/src/app/first-time/buy-ether-screen').default
// init
const InitializeMenuScreen = require('./first-time/init-menu')
const NewKeyChainScreen = require('./new-keychain')
// unlock
const UnlockScreen = require('./unlock')
// accounts
const AccountDetailScreen = require('./account-detail')
const SendTransactionScreen = require('./send')
const ConfirmTxScreen = require('./conf-tx')
// notice
const NoticeScreen = require('./components/notice')
const generateLostAccountsNotice = require('../lib/lost-accounts-notice')
// other views
const ConfigScreen = require('./config')
const AddTokenScreen = require('./add-token')
const Import = require('./accounts/import')
const InfoScreen = require('./info')
const Loading = require('./components/loading')
const SandwichExpando = require('sandwich-expando')
const Dropdown = require('./components/dropdown').Dropdown
const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem
const NetworkIndicator = require('./components/network')
const BuyView = require('./components/buy-button-subview')
const QrView = require('./components/qr-code')
const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete')
const HDRestoreVaultScreen = require('./keychains/hd/restore-vault')
const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation')
const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns
const { BETA_UI_NETWORK_TYPE } = require('../../app/scripts/config').enums
module.exports = connect(mapStateToProps)(App)
inherits(App, Component)
function App () { Component.call(this) }
function mapStateToProps (state) {
const {
identities,
accounts,
address,
keyrings,
isInitialized,
noActiveNotices,
seedWords,
featureFlags,
} = state.metamask
const selected = address || Object.keys(accounts)[0]
return {
// state from plugin
isLoading: state.appState.isLoading,
loadingMessage: state.appState.loadingMessage,
noActiveNotices: state.metamask.noActiveNotices,
isInitialized: state.metamask.isInitialized,
isUnlocked: state.metamask.isUnlocked,
currentView: state.appState.currentView,
activeAddress: state.appState.activeAddress,
transForward: state.appState.transForward,
isMascara: state.metamask.isMascara,
isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized),
seedWords: state.metamask.seedWords,
unapprovedTxs: state.metamask.unapprovedTxs,
unapprovedMsgs: state.metamask.unapprovedMsgs,
menuOpen: state.appState.menuOpen,
network: state.metamask.network,
provider: state.metamask.provider,
forgottenPassword: state.appState.forgottenPassword,
lastUnreadNotice: state.metamask.lastUnreadNotice,
lostAccounts: state.metamask.lostAccounts,
frequentRpcList: state.metamask.frequentRpcList || [],
featureFlags,
// state needed to get account dropdown temporarily rendering from app bar
identities,
selected,
keyrings,
}
}
App.prototype.render = function () {
var props = this.props
const { isLoading, loadingMessage, transForward, network } = props
const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config'
const loadMessage = loadingMessage || isLoadingNetwork ?
`Connecting to ${this.getNetworkName()}` : null
log.debug('Main ui render function')
return (
h('.flex-column.full-height', {
style: {
// Windows was showing a vertical scroll bar:
overflow: 'hidden',
position: 'relative',
alignItems: 'center',
},
}, [
// app bar
this.renderAppBar(),
this.renderNetworkDropdown(),
this.renderDropdown(),
this.renderLoadingIndicator({ isLoading, isLoadingNetwork, loadMessage }),
// panel content
h('.app-primary' + (transForward ? '.from-right' : '.from-left'), {
style: {
width: '100%',
},
}, [
this.renderPrimary(),
]),
])
)
}
App.prototype.renderAppBar = function () {
if (window.METAMASK_UI_TYPE === 'notification') {
return null
}
const props = this.props
const state = this.state || {}
const isNetworkMenuOpen = state.isNetworkMenuOpen || false
const {isMascara, isOnboarding} = props
// Do not render header if user is in mascara onboarding
if (isMascara && isOnboarding) {
return null
}
// Do not render header if user is in mascara buy ether
if (isMascara && props.currentView.name === 'buyEth') {
return null
}
return (
h('.full-width', {
height: '38px',
}, [
h('.app-header.flex-row.flex-space-between', {
style: {
alignItems: 'center',
visibility: props.isUnlocked ? 'visible' : 'none',
background: props.isUnlocked ? 'white' : 'none',
height: '38px',
position: 'relative',
zIndex: 12,
},
}, [
h('div.left-menu-section', {
style: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
},
}, [
// mini logo
h('img', {
height: 24,
width: 24,
src: '/images/icon-128.png',
}),
h(NetworkIndicator, {
network: this.props.network,
provider: this.props.provider,
onClick: (event) => {
event.preventDefault()
event.stopPropagation()
this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen })
},
}),
]),
props.isUnlocked && h('div', {
style: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
},
}, [
props.isUnlocked && h(AccountDropdowns, {
style: {},
enableAccountsSelector: true,
identities: this.props.identities,
selected: this.props.currentView.context,
network: this.props.network,
keyrings: this.props.keyrings,
}, []),
// hamburger
props.isUnlocked && h(SandwichExpando, {
className: 'sandwich-expando',
width: 16,
barHeight: 2,
padding: 0,
isOpen: state.isMainMenuOpen,
color: 'rgb(247,146,30)',
onClick: () => {
this.setState({
isMainMenuOpen: !state.isMainMenuOpen,
})
},
}),
]),
]),
])
)
}
App.prototype.renderNetworkDropdown = function () {
const props = this.props
const { provider: { type: providerType, rpcTarget: activeNetwork } } = props
const rpcList = props.frequentRpcList
const state = this.state || {}
const isOpen = state.isNetworkMenuOpen
return h(Dropdown, {
useCssTransition: true,
isOpen,
onClickOutside: (event) => {
const { classList } = event.target
const isNotToggleElement = [
classList.contains('menu-icon'),
classList.contains('network-name'),
classList.contains('network-indicator'),
].filter(bool => bool).length === 0
// classes from three constituent nodes of the toggle element
if (isNotToggleElement) {
this.setState({ isNetworkMenuOpen: false })
}
},
zIndex: 11,
style: {
position: 'absolute',
left: '2px',
top: '36px',
},
innerStyle: {
padding: '2px 16px 2px 0px',
},
}, [
h(
DropdownMenuItem,
{
key: 'main',
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => props.dispatch(actions.setProviderType('mainnet')),
style: {
fontSize: '18px',
},
},
[
h('.menu-icon.diamond'),
'Main Ethereum Network',
providerType === 'mainnet' ? h('.check', '✓') : null,
]
),
h(
DropdownMenuItem,
{
key: 'ropsten',
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => props.dispatch(actions.setProviderType('ropsten')),
style: {
fontSize: '18px',
},
},
[
h('.menu-icon.red-dot'),
'Ropsten Test Network',
providerType === 'ropsten' ? h('.check', '✓') : null,
]
),
h(
DropdownMenuItem,
{
key: 'kovan',
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => props.dispatch(actions.setProviderType('kovan')),
style: {
fontSize: '18px',
},
},
[
h('.menu-icon.hollow-diamond'),
'Kovan Test Network',
providerType === 'kovan' ? h('.check', '✓') : null,
]
),
h(
DropdownMenuItem,
{
key: 'rinkeby',
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => props.dispatch(actions.setProviderType('rinkeby')),
style: {
fontSize: '18px',
},
},
[
h('.menu-icon.golden-square'),
'Rinkeby Test Network',
providerType === 'rinkeby' ? h('.check', '✓') : null,
]
),
h(
DropdownMenuItem,
{
key: 'default',
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => props.dispatch(actions.setProviderType('localhost')),
style: {
fontSize: '18px',
},
},
[
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
'Localhost 8545',
activeNetwork === 'http://localhost:8545' ? h('.check', '✓') : null,
]
),
this.renderCustomOption(props.provider),
this.renderCommonRpc(rpcList, props.provider),
h(
DropdownMenuItem,
{
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
onClick: () => this.props.dispatch(actions.showConfigPage()),
style: {
fontSize: '18px',
},
},
[
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
'Custom RPC',
activeNetwork === 'custom' ? h('.check', '✓') : null,
]
),
])
}
App.prototype.renderDropdown = function () {
const state = this.state || {}
const isOpen = state.isMainMenuOpen
return h(Dropdown, {
useCssTransition: true,
isOpen: isOpen,
zIndex: 11,
onClickOutside: (event) => {
const classList = event.target.classList
const parentClassList = event.target.parentElement.classList
const isToggleElement = classList.contains('sandwich-expando') ||
parentClassList.contains('sandwich-expando')
if (isOpen && !isToggleElement) {
this.setState({ isMainMenuOpen: false })
}
},
style: {
position: 'absolute',
right: '2px',
top: '38px',
},
innerStyle: {},
}, [
h(DropdownMenuItem, {
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
onClick: () => { this.props.dispatch(actions.showConfigPage()) },
}, 'Settings'),
h(DropdownMenuItem, {
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
onClick: () => { this.props.dispatch(actions.lockMetamask()) },
}, 'Lock'),
h(DropdownMenuItem, {
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
onClick: () => { this.props.dispatch(actions.showInfoPage()) },
}, 'Info/Help'),
h(DropdownMenuItem, {
closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }),
onClick: () => {
this.props.dispatch(actions.setFeatureFlag('betaUI', true, 'BETA_UI_NOTIFICATION_MODAL'))
.then(() => this.props.dispatch(actions.setNetworkEndpoints(BETA_UI_NETWORK_TYPE)))
},
}, 'Try Beta!'),
])
}
App.prototype.renderLoadingIndicator = function ({ isLoading, isLoadingNetwork, loadMessage }) {
const { isMascara } = this.props
return isMascara
? null
: h(Loading, {
isLoading: isLoading || isLoadingNetwork,
loadingMessage: loadMessage,
})
}
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.renderPrimary = function () {
log.debug('rendering primary')
var props = this.props
const {isMascara, isOnboarding} = props
if (isMascara && isOnboarding) {
return h(MascaraFirstTime)
}
// notices
if (!props.noActiveNotices) {
log.debug('rendering notice screen for unread notices.')
return h(NoticeScreen, {
notice: props.lastUnreadNotice,
key: 'NoticeScreen',
onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)),
})
} else if (props.lostAccounts && props.lostAccounts.length > 0) {
log.debug('rendering notice screen for lost accounts view.')
return h(NoticeScreen, {
notice: generateLostAccountsNotice(props.lostAccounts),
key: 'LostAccountsNotice',
onConfirm: () => props.dispatch(actions.markAccountsFound()),
})
}
if (props.seedWords) {
log.debug('rendering seed words')
return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'})
}
// show initialize screen
if (!props.isInitialized || props.forgottenPassword) {
// show current view
log.debug('rendering an initialize screen')
switch (props.currentView.name) {
case 'restoreVault':
log.debug('rendering restore vault screen')
return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'})
default:
log.debug('rendering menu screen')
return h(InitializeMenuScreen, {key: 'menuScreenInit'})
}
}
// show unlock screen
if (!props.isUnlocked) {
switch (props.currentView.name) {
case 'restoreVault':
log.debug('rendering restore vault screen')
return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'})
case 'config':
log.debug('rendering config screen from unlock screen.')
return h(ConfigScreen, {key: 'config'})
default:
log.debug('rendering locked screen')
return h(UnlockScreen, {key: 'locked'})
}
}
// show current view
switch (props.currentView.name) {
case 'accountDetail':
log.debug('rendering account detail screen')
return h(AccountDetailScreen, {key: 'account-detail'})
case 'sendTransaction':
log.debug('rendering send tx screen')
return h(SendTransactionScreen, {key: 'send-transaction'})
case 'newKeychain':
log.debug('rendering new keychain screen')
return h(NewKeyChainScreen, {key: 'new-keychain'})
case 'confTx':
log.debug('rendering confirm tx screen')
return h(ConfirmTxScreen, {key: 'confirm-tx'})
case 'add-token':
log.debug('rendering add-token screen from unlock screen.')
return h(AddTokenScreen, {key: 'add-token'})
case 'config':
log.debug('rendering config screen')
return h(ConfigScreen, {key: 'config'})
case 'import-menu':
log.debug('rendering import screen')
return h(Import, {key: 'import-menu'})
case 'reveal-seed-conf':
log.debug('rendering reveal seed confirmation screen')
return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'})
case 'info':
log.debug('rendering info screen')
return h(InfoScreen, {key: 'info'})
case 'buyEth':
log.debug('rendering buy ether screen')
return h(BuyView, {key: 'buyEthView'})
case 'onboardingBuyEth':
log.debug('rendering onboarding buy ether screen')
return h(MascaraBuyEtherScreen, {key: 'buyEthView'})
case 'qr':
log.debug('rendering show qr screen')
console.log(`QrView`, QrView);
return h('div', {
style: {
position: 'absolute',
height: '100%',
top: '0px',
left: '0px',
},
}, [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
onClick: () => props.dispatch(actions.backToAccountDetail(props.activeAddress)),
style: {
marginLeft: '10px',
marginTop: '50px',
},
}),
h('div', {
style: {
position: 'absolute',
left: '44px',
width: '285px',
},
}, [
h(QrView, {key: 'qr'}),
]),
])
default:
log.debug('rendering default, account detail screen')
return h(AccountDetailScreen, {key: 'account-detail'})
}
}
App.prototype.toggleMetamaskActive = function () {
if (!this.props.isUnlocked) {
// currently inactive: redirect to password box
var passwordBox = document.querySelector('input[type=password]')
if (!passwordBox) return
passwordBox.focus()
} else {
// currently active: deactivate
this.props.dispatch(actions.lockMetamask(false))
}
}
App.prototype.renderCustomOption = function (provider) {
const { rpcTarget, type } = provider
const props = this.props
if (type !== 'rpc') return null
// Concatenate long URLs
let label = rpcTarget
if (rpcTarget.length > 31) {
label = label.substr(0, 34) + '...'
}
switch (rpcTarget) {
case 'http://localhost:8545':
return null
default:
return h(
DropdownMenuItem,
{
key: rpcTarget,
onClick: () => props.dispatch(actions.setRpcTarget(rpcTarget)),
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
},
[
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
label,
h('.check', '✓'),
]
)
}
}
App.prototype.getNetworkName = function () {
const { provider } = this.props
const providerName = provider.type
let name
if (providerName === 'mainnet') {
name = 'Main Ethereum Network'
} else if (providerName === 'ropsten') {
name = 'Ropsten Test Network'
} else if (providerName === 'kovan') {
name = 'Kovan Test Network'
} else if (providerName === 'rinkeby') {
name = 'Rinkeby Test Network'
} else {
name = 'Unknown Private Network'
}
return name
}
App.prototype.renderCommonRpc = function (rpcList, provider) {
const props = this.props
const rpcTarget = provider.rpcTarget
return rpcList.map((rpc) => {
if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) {
return null
} else {
return h(
DropdownMenuItem,
{
key: `common${rpc}`,
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
onClick: () => props.dispatch(actions.setRpcTarget(rpc)),
},
[
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
rpc,
rpcTarget === rpc ? h('.check', '✓') : null,
]
)
}
})
}

@ -0,0 +1,319 @@
const Component = require('react').Component
const PropTypes = require('react').PropTypes
const h = require('react-hyperscript')
const actions = require('../../../ui/app/actions')
const genAccountLink = require('etherscan-link').createAccountLink
const connect = require('react-redux').connect
const Dropdown = require('./dropdown').Dropdown
const DropdownMenuItem = require('./dropdown').DropdownMenuItem
const Identicon = require('./identicon')
const ethUtil = require('ethereumjs-util')
const copyToClipboard = require('copy-to-clipboard')
class AccountDropdowns extends Component {
constructor (props) {
super(props)
this.state = {
accountSelectorActive: false,
optionsMenuActive: false,
}
this.accountSelectorToggleClassName = 'accounts-selector'
this.optionsMenuToggleClassName = 'fa-ellipsis-h'
}
renderAccounts () {
const { identities, selected, keyrings } = this.props
return Object.keys(identities).map((key, index) => {
const identity = identities[key]
const isSelected = identity.address === selected
const simpleAddress = identity.address.substring(2).toLowerCase()
const keyring = keyrings.find((kr) => {
return kr.accounts.includes(simpleAddress) ||
kr.accounts.includes(identity.address)
})
return h(
DropdownMenuItem,
{
closeMenu: () => {},
onClick: () => {
this.props.actions.showAccountDetail(identity.address)
},
style: {
marginTop: index === 0 ? '5px' : '',
fontSize: '24px',
},
},
[
h(
Identicon,
{
address: identity.address,
diameter: 32,
style: {
marginLeft: '10px',
},
},
),
this.indicateIfLoose(keyring),
h('span', {
style: {
marginLeft: '20px',
fontSize: '24px',
maxWidth: '145px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
}, identity.name || ''),
h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null),
]
)
})
}
indicateIfLoose (keyring) {
try { // Sometimes keyrings aren't loaded yet:
const type = keyring.type
const isLoose = type !== 'HD Key Tree'
return isLoose ? h('.keyring-label', 'LOOSE') : null
} catch (e) { return }
}
renderAccountSelector () {
const { actions } = this.props
const { accountSelectorActive } = this.state
return h(
Dropdown,
{
useCssTransition: true, // Hardcoded because account selector is temporarily in app-header
style: {
marginLeft: '-238px',
marginTop: '38px',
minWidth: '180px',
overflowY: 'auto',
maxHeight: '300px',
width: '300px',
},
innerStyle: {
padding: '8px 25px',
},
isOpen: accountSelectorActive,
onClickOutside: (event) => {
const { classList } = event.target
const isNotToggleElement = !classList.contains(this.accountSelectorToggleClassName)
if (accountSelectorActive && isNotToggleElement) {
this.setState({ accountSelectorActive: false })
}
},
},
[
...this.renderAccounts(),
h(
DropdownMenuItem,
{
closeMenu: () => {},
onClick: () => actions.addNewAccount(),
},
[
h(
Identicon,
{
style: {
marginLeft: '10px',
},
diameter: 32,
},
),
h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, 'Create Account'),
],
),
h(
DropdownMenuItem,
{
closeMenu: () => {},
onClick: () => actions.showImportPage(),
},
[
h(
Identicon,
{
style: {
marginLeft: '10px',
},
diameter: 32,
},
),
h('span', {
style: {
marginLeft: '20px',
fontSize: '24px',
marginBottom: '5px',
},
}, 'Import Account'),
]
),
]
)
}
renderAccountOptions () {
const { actions } = this.props
const { optionsMenuActive } = this.state
return h(
Dropdown,
{
style: {
marginLeft: '-215px',
minWidth: '180px',
},
isOpen: optionsMenuActive,
onClickOutside: () => {
const { classList } = event.target
const isNotToggleElement = !classList.contains(this.optionsMenuToggleClassName)
if (optionsMenuActive && isNotToggleElement) {
this.setState({ optionsMenuActive: false })
}
},
},
[
h(
DropdownMenuItem,
{
closeMenu: () => {},
onClick: () => {
const { selected, network } = this.props
const url = genAccountLink(selected, network)
global.platform.openWindow({ url })
},
},
'View account on Etherscan',
),
h(
DropdownMenuItem,
{
closeMenu: () => {},
onClick: () => {
const { selected, identities } = this.props
var identity = identities[selected]
actions.showQrView(selected, identity ? identity.name : '')
},
},
'Show QR Code',
),
h(
DropdownMenuItem,
{
closeMenu: () => {},
onClick: () => {
const { selected } = this.props
const checkSumAddress = selected && ethUtil.toChecksumAddress(selected)
copyToClipboard(checkSumAddress)
},
},
'Copy Address to clipboard',
),
h(
DropdownMenuItem,
{
closeMenu: () => {},
onClick: () => {
actions.requestAccountExport()
},
},
'Export Private Key',
),
]
)
}
render () {
const { style, enableAccountsSelector, enableAccountOptions } = this.props
const { optionsMenuActive, accountSelectorActive } = this.state
return h(
'span',
{
style: style,
},
[
enableAccountsSelector && h(
// 'i.fa.fa-angle-down',
'div.cursor-pointer.color-orange.accounts-selector',
{
style: {
// fontSize: '1.8em',
background: 'url(images/switch_acc.svg) white center center no-repeat',
height: '25px',
width: '25px',
transform: 'scale(0.75)',
marginRight: '3px',
},
onClick: (event) => {
event.stopPropagation()
this.setState({
accountSelectorActive: !accountSelectorActive,
optionsMenuActive: false,
})
},
},
this.renderAccountSelector(),
),
enableAccountOptions && h(
'i.fa.fa-ellipsis-h',
{
style: {
fontSize: '1.8em',
},
onClick: (event) => {
event.stopPropagation()
this.setState({
accountSelectorActive: false,
optionsMenuActive: !optionsMenuActive,
})
},
},
this.renderAccountOptions()
),
]
)
}
}
AccountDropdowns.defaultProps = {
enableAccountsSelector: false,
enableAccountOptions: false,
}
AccountDropdowns.propTypes = {
identities: PropTypes.objectOf(PropTypes.object),
selected: PropTypes.string,
keyrings: PropTypes.array,
actions: PropTypes.objectOf(PropTypes.func),
network: PropTypes.string,
style: PropTypes.object,
enableAccountOptions: PropTypes.bool,
enableAccountsSelector: PropTypes.bool,
}
const mapDispatchToProps = (dispatch) => {
return {
actions: {
showConfigPage: () => dispatch(actions.showConfigPage()),
requestAccountExport: () => dispatch(actions.requestExportAccount()),
showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)),
addNewAccount: () => dispatch(actions.addNewAccount()),
showImportPage: () => dispatch(actions.showImportPage()),
showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)),
},
}
}
module.exports = {
AccountDropdowns: connect(null, mapDispatchToProps)(AccountDropdowns),
}

@ -0,0 +1,132 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const exportAsFile = require('../util').exportAsFile
const copyToClipboard = require('copy-to-clipboard')
const actions = require('../../../ui/app/actions')
const ethUtil = require('ethereumjs-util')
const connect = require('react-redux').connect
module.exports = connect(mapStateToProps)(ExportAccountView)
inherits(ExportAccountView, Component)
function ExportAccountView () {
Component.call(this)
}
function mapStateToProps (state) {
return {
warning: state.appState.warning,
}
}
ExportAccountView.prototype.render = function () {
const state = this.props
const accountDetail = state.accountDetail
const nickname = state.identities[state.address].name
if (!accountDetail) return h('div')
const accountExport = accountDetail.accountExport
const notExporting = accountExport === 'none'
const exportRequested = accountExport === 'requested'
const accountExported = accountExport === 'completed'
if (notExporting) return h('div')
if (exportRequested) {
const warning = `Export private keys at your own risk.`
return (
h('div', {
style: {
display: 'inline-block',
textAlign: 'center',
},
},
[
h('div', {
key: 'exporting',
style: {
margin: '0 20px',
},
}, [
h('p.error', warning),
h('input#exportAccount.sizing-input', {
type: 'password',
placeholder: 'confirm password',
onKeyPress: this.onExportKeyPress.bind(this),
style: {
position: 'relative',
top: '1.5px',
marginBottom: '7px',
},
}),
]),
h('div', {
key: 'buttons',
style: {
margin: '0 20px',
},
},
[
h('button', {
onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }),
style: {
marginRight: '10px',
},
}, 'Submit'),
h('button', {
onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)),
}, 'Cancel'),
]),
(this.props.warning) && (
h('span.error', {
style: {
margin: '20px',
},
}, this.props.warning.split('-'))
),
])
)
}
if (accountExported) {
const plainKey = ethUtil.stripHexPrefix(accountDetail.privateKey)
return h('div.privateKey', {
style: {
margin: '0 20px',
},
}, [
h('label', 'Your private key (click to copy):'),
h('p.error.cursor-pointer', {
style: {
textOverflow: 'ellipsis',
overflow: 'hidden',
webkitUserSelect: 'text',
maxWidth: '275px',
},
onClick: function (event) {
copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey))
},
}, plainKey),
h('button', {
onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)),
}, 'Done'),
h('button', {
style: {
marginLeft: '10px',
},
onClick: () => exportAsFile(`MetaMask ${nickname} Private Key`, plainKey),
}, 'Save as File'),
])
}
}
ExportAccountView.prototype.onExportKeyPress = function (event) {
if (event.key !== 'Enter') return
event.preventDefault()
const input = document.getElementById('exportAccount').value
this.props.dispatch(actions.exportAccount(input, this.props.address))
}

@ -0,0 +1,86 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const Identicon = require('./identicon')
const formatBalance = require('../util').formatBalance
const addressSummary = require('../util').addressSummary
module.exports = AccountPanel
inherits(AccountPanel, Component)
function AccountPanel () {
Component.call(this)
}
AccountPanel.prototype.render = function () {
var state = this.props
var identity = state.identity || {}
var account = state.account || {}
var isFauceting = state.isFauceting
var panelState = {
key: `accountPanel${identity.address}`,
identiconKey: identity.address,
identiconLabel: identity.name || '',
attributes: [
{
key: 'ADDRESS',
value: addressSummary(identity.address),
},
balanceOrFaucetingIndication(account, isFauceting),
],
}
return (
h('.identity-panel.flex-row.flex-space-between', {
style: {
flex: '1 0 auto',
cursor: panelState.onClick ? 'pointer' : undefined,
},
onClick: panelState.onClick,
}, [
// account identicon
h('.identicon-wrapper.flex-column.select-none', [
h(Identicon, {
address: panelState.identiconKey,
imageify: state.imageifyIdenticons,
}),
h('span.font-small', panelState.identiconLabel.substring(0, 7) + '...'),
]),
// account address, balance
h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [
panelState.attributes.map((attr) => {
return h('.flex-row.flex-space-between', {
key: '' + Math.round(Math.random() * 1000000),
}, [
h('label.font-small.no-select', attr.key),
h('span.font-small', attr.value),
])
}),
]),
])
)
}
function balanceOrFaucetingIndication (account, isFauceting) {
// Temporarily deactivating isFauceting indication
// because it shows fauceting for empty restored accounts.
if (/* isFauceting*/ false) {
return {
key: 'Account is auto-funding.',
value: 'Please wait.',
}
} else {
return {
key: 'BALANCE',
value: formatBalance(account.balance),
}
}
}

@ -0,0 +1,89 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const formatBalance = require('../util').formatBalance
const generateBalanceObject = require('../util').generateBalanceObject
const Tooltip = require('./tooltip.js')
const FiatValue = require('./fiat-value.js')
module.exports = EthBalanceComponent
inherits(EthBalanceComponent, Component)
function EthBalanceComponent () {
Component.call(this)
}
EthBalanceComponent.prototype.render = function () {
var props = this.props
let { value } = props
var style = props.style
var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true
value = value ? formatBalance(value, 6, needsParse) : '...'
var width = props.width
return (
h('.ether-balance.ether-balance-amount', {
style: style,
}, [
h('div', {
style: {
display: 'inline',
width: width,
},
}, this.renderBalance(value)),
])
)
}
EthBalanceComponent.prototype.renderBalance = function (value) {
var props = this.props
if (value === 'None') return value
if (value === '...') return value
var balanceObj = generateBalanceObject(value, props.shorten ? 1 : 3)
var balance
var splitBalance = value.split(' ')
var ethNumber = splitBalance[0]
var ethSuffix = splitBalance[1]
const showFiat = 'showFiat' in props ? props.showFiat : true
if (props.shorten) {
balance = balanceObj.shortBalance
} else {
balance = balanceObj.balance
}
var label = balanceObj.label
return (
h(Tooltip, {
position: 'bottom',
title: `${ethNumber} ${ethSuffix}`,
}, h('div.flex-column', [
h('.flex-row', {
style: {
alignItems: 'flex-end',
lineHeight: '13px',
fontFamily: 'Montserrat Light',
textRendering: 'geometricPrecision',
},
}, [
h('div', {
style: {
width: '100%',
textAlign: 'right',
},
}, this.props.incoming ? `+${balance}` : balance),
h('div', {
style: {
color: ' #AEAEAE',
fontSize: '12px',
marginLeft: '5px',
},
}, label),
]),
showFiat ? h(FiatValue, { value: props.value }) : null,
]))
)
}

@ -0,0 +1,46 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const ethUtil = require('ethereumjs-util')
const extend = require('xtend')
module.exports = BinaryRenderer
inherits(BinaryRenderer, Component)
function BinaryRenderer () {
Component.call(this)
}
BinaryRenderer.prototype.render = function () {
const props = this.props
const { value, style } = props
const text = this.hexToText(value)
const defaultStyle = extend({
width: '315px',
maxHeight: '210px',
resize: 'none',
border: 'none',
background: 'white',
padding: '3px',
}, style)
return (
h('textarea.font-small', {
readOnly: true,
style: defaultStyle,
defaultValue: text,
})
)
}
BinaryRenderer.prototype.hexToText = function (hex) {
try {
const stripped = ethUtil.stripHexPrefix(hex)
const buff = Buffer.from(stripped, 'hex')
return buff.toString('utf8')
} catch (e) {
return hex
}
}

@ -0,0 +1,181 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const extend = require('xtend')
module.exports = BnAsDecimalInput
inherits(BnAsDecimalInput, Component)
function BnAsDecimalInput () {
this.state = { invalid: null }
Component.call(this)
}
/* Bn as Decimal Input
*
* A component for allowing easy, decimal editing
* of a passed in bn string value.
*
* On change, calls back its `onChange` function parameter
* and passes it an updated bn string.
*/
BnAsDecimalInput.prototype.render = function () {
const props = this.props
const state = this.state
const { value, scale, precision, onChange, min, max } = props
const suffix = props.suffix
const style = props.style
const valueString = value.toString(10)
const newMin = min && this.downsize(min.toString(10), scale)
const newMax = max && this.downsize(max.toString(10), scale)
const newValue = this.downsize(valueString, scale)
return (
h('.flex-column', [
h('.flex-row', {
style: {
alignItems: 'flex-end',
lineHeight: '13px',
fontFamily: 'Montserrat Light',
textRendering: 'geometricPrecision',
},
}, [
h('input.hex-input', {
type: 'number',
step: 'any',
required: true,
min: newMin,
max: newMax,
style: extend({
display: 'block',
textAlign: 'right',
backgroundColor: 'transparent',
border: '1px solid #bdbdbd',
}, style),
value: newValue,
onBlur: (event) => {
this.updateValidity(event)
},
onChange: (event) => {
this.updateValidity(event)
const value = (event.target.value === '') ? '' : event.target.value
const scaledNumber = this.upsize(value, scale, precision)
const precisionBN = new BN(scaledNumber, 10)
onChange(precisionBN, event.target.checkValidity())
},
onInvalid: (event) => {
const msg = this.constructWarning()
if (msg === state.invalid) {
return
}
this.setState({ invalid: msg })
event.preventDefault()
return false
},
}),
h('div', {
style: {
color: ' #AEAEAE',
fontSize: '12px',
marginLeft: '5px',
marginRight: '6px',
width: '20px',
},
}, suffix),
]),
state.invalid ? h('span.error', {
style: {
position: 'absolute',
right: '0px',
textAlign: 'right',
transform: 'translateY(26px)',
padding: '3px',
background: 'rgba(255,255,255,0.85)',
zIndex: '1',
textTransform: 'capitalize',
border: '2px solid #E20202',
},
}, state.invalid) : null,
])
)
}
BnAsDecimalInput.prototype.setValid = function (message) {
this.setState({ invalid: null })
}
BnAsDecimalInput.prototype.updateValidity = function (event) {
const target = event.target
const value = this.props.value
const newValue = target.value
if (value === newValue) {
return
}
const valid = target.checkValidity()
if (valid) {
this.setState({ invalid: null })
}
}
BnAsDecimalInput.prototype.constructWarning = function () {
const { name, min, max, scale, suffix } = this.props
const newMin = min && this.downsize(min.toString(10), scale)
const newMax = max && this.downsize(max.toString(10), scale)
let message = name ? name + ' ' : ''
if (min && max) {
message += `must be greater than or equal to ${newMin} ${suffix} and less than or equal to ${newMax} ${suffix}.`
} else if (min) {
message += `must be greater than or equal to ${newMin} ${suffix}.`
} else if (max) {
message += `must be less than or equal to ${newMax} ${suffix}.`
} else {
message += 'Invalid input.'
}
return message
}
BnAsDecimalInput.prototype.downsize = function (number, scale) {
// if there is no scaling, simply return the number
if (scale === 0) {
return Number(number)
} else {
// if the scale is the same as the precision, account for this edge case.
var adjustedNumber = number
while (adjustedNumber.length < scale) {
adjustedNumber = '0' + adjustedNumber
}
return Number(adjustedNumber.slice(0, -scale) + '.' + adjustedNumber.slice(-scale))
}
}
BnAsDecimalInput.prototype.upsize = function (number, scale, precision) {
var stringArray = number.toString().split('.')
var decimalLength = stringArray[1] ? stringArray[1].length : 0
var newString = stringArray[0]
// If there is scaling and decimal parts exist, integrate them in.
if ((scale !== 0) && (decimalLength !== 0)) {
newString += stringArray[1].slice(0, precision)
}
// Add 0s to account for the upscaling.
for (var i = decimalLength; i < scale; i++) {
newString += '0'
}
return newString
}

@ -0,0 +1,262 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../../../ui/app/actions')
const CoinbaseForm = require('./coinbase-form')
const ShapeshiftForm = require('./shapeshift-form')
const Loading = require('./loading')
const AccountPanel = require('./account-panel')
const RadioList = require('./custom-radio-list')
const networkNames = require('../../../app/scripts/config.js').networkNames
module.exports = connect(mapStateToProps)(BuyButtonSubview)
function mapStateToProps (state) {
return {
identity: state.appState.identity,
account: state.metamask.accounts[state.appState.buyView.buyAddress],
warning: state.appState.warning,
buyView: state.appState.buyView,
network: state.metamask.network,
provider: state.metamask.provider,
context: state.appState.currentView.context,
isSubLoading: state.appState.isSubLoading,
}
}
inherits(BuyButtonSubview, Component)
function BuyButtonSubview () {
Component.call(this)
}
BuyButtonSubview.prototype.render = function () {
return (
h('div', {
style: {
width: '100%',
},
}, [
this.headerSubview(),
this.primarySubview(),
])
)
}
BuyButtonSubview.prototype.headerSubview = function () {
const props = this.props
const isLoading = props.isSubLoading
return (
h('.flex-column', {
style: {
alignItems: 'center',
},
}, [
// header bar (back button, label)
h('.flex-row', {
style: {
alignItems: 'center',
justifyContent: 'center',
},
}, [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
onClick: this.backButtonContext.bind(this),
style: {
position: 'absolute',
left: '10px',
},
}),
h('h2.text-transform-uppercase.flex-center', {
style: {
width: '100vw',
background: 'rgb(235, 235, 235)',
color: 'rgb(174, 174, 174)',
paddingTop: '4px',
paddingBottom: '4px',
},
}, 'Buy Eth'),
]),
// loading indication
h('div', {
style: {
position: 'absolute',
top: '57vh',
left: '49vw',
},
}, [
h(Loading, { isLoading }),
]),
// account panel
h('div', {
style: {
width: '80%',
},
}, [
h(AccountPanel, {
showFullAddress: true,
identity: props.identity,
account: props.account,
}),
]),
h('.flex-row', {
style: {
alignItems: 'center',
justifyContent: 'center',
},
}, [
h('h3.text-transform-uppercase.flex-center', {
style: {
paddingLeft: '15px',
width: '100vw',
background: 'rgb(235, 235, 235)',
color: 'rgb(174, 174, 174)',
paddingTop: '4px',
paddingBottom: '4px',
},
}, 'Select Service'),
]),
])
)
}
BuyButtonSubview.prototype.primarySubview = function () {
const props = this.props
const network = props.network
switch (network) {
case 'loading':
return
case '1':
return this.mainnetSubview()
// Ropsten, Rinkeby, Kovan
case '3':
case '4':
case '42':
const networkName = networkNames[network]
const label = `${networkName} Test Faucet`
return (
h('div.flex-column', {
style: {
alignItems: 'center',
margin: '20px 50px',
},
}, [
h('button.text-transform-uppercase', {
onClick: () => this.props.dispatch(actions.buyEth({ network })),
style: {
marginTop: '15px',
},
}, label),
// Kovan only: Dharma loans beta
network === '42' ? (
h('button.text-transform-uppercase', {
onClick: () => this.navigateTo('https://borrow.dharma.io/'),
style: {
marginTop: '15px',
},
}, 'Borrow With Dharma (Beta)')
) : null,
])
)
default:
return (
h('h2.error', 'Unknown network ID')
)
}
}
BuyButtonSubview.prototype.mainnetSubview = function () {
const props = this.props
return (
h('.flex-column', {
style: {
alignItems: 'center',
},
}, [
h('.flex-row.selected-exchange', {
style: {
position: 'relative',
right: '35px',
marginTop: '20px',
marginBottom: '20px',
},
}, [
h(RadioList, {
defaultFocus: props.buyView.subview,
labels: [
'Coinbase',
'ShapeShift',
],
subtext: {
'Coinbase': 'Crypto/FIAT (USA only)',
'ShapeShift': 'Crypto',
},
onClick: this.radioHandler.bind(this),
}),
]),
h('h3.text-transform-uppercase', {
style: {
paddingLeft: '15px',
fontFamily: 'Montserrat Light',
width: '100vw',
background: 'rgb(235, 235, 235)',
color: 'rgb(174, 174, 174)',
paddingTop: '4px',
paddingBottom: '4px',
},
}, props.buyView.subview),
this.formVersionSubview(),
])
)
}
BuyButtonSubview.prototype.formVersionSubview = function () {
const network = this.props.network
if (network === '1') {
if (this.props.buyView.formView.coinbase) {
return h(CoinbaseForm, this.props)
} else if (this.props.buyView.formView.shapeshift) {
return h(ShapeshiftForm, this.props)
}
}
}
BuyButtonSubview.prototype.navigateTo = function (url) {
global.platform.openWindow({ url })
}
BuyButtonSubview.prototype.backButtonContext = function () {
if (this.props.context === 'confTx') {
this.props.dispatch(actions.showConfTxPage(false))
} else {
console.log(`actions.goHome`, actions.goHome);
this.props.dispatch(actions.goHome())
}
}
BuyButtonSubview.prototype.radioHandler = function (event) {
switch (event.target.title) {
case 'Coinbase':
return this.props.dispatch(actions.coinBaseSubview())
case 'ShapeShift':
return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type))
}
}

@ -0,0 +1,63 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../../../ui/app/actions')
module.exports = connect(mapStateToProps)(CoinbaseForm)
function mapStateToProps (state) {
return {
warning: state.appState.warning,
}
}
inherits(CoinbaseForm, Component)
function CoinbaseForm () {
Component.call(this)
}
CoinbaseForm.prototype.render = function () {
var props = this.props
return h('.flex-column', {
style: {
marginTop: '35px',
padding: '25px',
width: '100%',
},
}, [
h('.flex-row', {
style: {
justifyContent: 'space-around',
margin: '33px',
marginTop: '0px',
},
}, [
h('button.btn-green', {
onClick: this.toCoinbase.bind(this),
}, 'Continue to Coinbase'),
h('button.btn-red', {
onClick: () => props.dispatch(actions.backTobuyView(props.accounts.address)),
}, 'Cancel'),
]),
])
}
CoinbaseForm.prototype.toCoinbase = function () {
const props = this.props
const address = props.buyView.buyAddress
props.dispatch(actions.buyEth({ network: '1', address, amount: 0 }))
}
CoinbaseForm.prototype.renderLoading = function () {
return h('img', {
style: {
width: '27px',
marginRight: '-27px',
},
src: 'images/loading.svg',
})
}

@ -0,0 +1,59 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const copyToClipboard = require('copy-to-clipboard')
const Tooltip = require('./tooltip')
module.exports = CopyButton
inherits(CopyButton, Component)
function CopyButton () {
Component.call(this)
}
// As parameters, accepts:
// "value", which is the value to copy (mandatory)
// "title", which is the text to show on hover (optional, defaults to 'Copy')
CopyButton.prototype.render = function () {
const props = this.props
const state = this.state || {}
const value = props.value
const copied = state.copied
const message = copied ? 'Copied' : props.title || ' Copy '
return h('.copy-button', {
style: {
display: 'flex',
alignItems: 'center',
},
}, [
h(Tooltip, {
title: message,
}, [
h('i.fa.fa-clipboard.cursor-pointer.color-orange', {
style: {
margin: '5px',
},
onClick: (event) => {
event.preventDefault()
event.stopPropagation()
copyToClipboard(value)
this.debounceRestore()
},
}),
]),
])
}
CopyButton.prototype.debounceRestore = function () {
this.setState({ copied: true })
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.setState({ copied: false })
}, 850)
}

@ -0,0 +1,46 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const Tooltip = require('./tooltip')
const copyToClipboard = require('copy-to-clipboard')
module.exports = Copyable
inherits(Copyable, Component)
function Copyable () {
Component.call(this)
this.state = {
copied: false,
}
}
Copyable.prototype.render = function () {
const props = this.props
const state = this.state
const { value, children } = props
const { copied } = state
return h(Tooltip, {
title: copied ? 'Copied!' : 'Copy',
position: 'bottom',
}, h('span', {
style: {
cursor: 'pointer',
},
onClick: (event) => {
event.preventDefault()
event.stopPropagation()
copyToClipboard(value)
this.debounceRestore()
},
}, children))
}
Copyable.prototype.debounceRestore = function () {
this.setState({ copied: true })
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.setState({ copied: false })
}, 850)
}

@ -0,0 +1,60 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
module.exports = RadioList
inherits(RadioList, Component)
function RadioList () {
Component.call(this)
}
RadioList.prototype.render = function () {
const props = this.props
const activeClass = '.custom-radio-selected'
const inactiveClass = '.custom-radio-inactive'
const {
labels,
defaultFocus,
} = props
return (
h('.flex-row', {
style: {
fontSize: '12px',
},
}, [
h('.flex-column.custom-radios', {
style: {
marginRight: '5px',
},
},
labels.map((lable, i) => {
let isSelcted = (this.state !== null)
isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable)
return h(isSelcted ? activeClass : inactiveClass, {
title: lable,
onClick: (event) => {
this.setState({selected: event.target.title})
props.onClick(event)
},
})
})
),
h('.text', {},
labels.map((lable) => {
if (props.subtext) {
return h('.flex-row', {}, [
h('.radio-titles', lable),
h('.radio-titles-subtext', `- ${props.subtext[lable]}`),
])
} else {
return h('.radio-titles', lable)
}
})
),
])
)
}

@ -0,0 +1,98 @@
const Component = require('react').Component
const PropTypes = require('react').PropTypes
const h = require('react-hyperscript')
const MenuDroppo = require('./menu-droppo')
const extend = require('xtend')
const noop = () => {}
class Dropdown extends Component {
render () {
const { isOpen, onClickOutside, style, innerStyle, children, useCssTransition } = this.props
const innerStyleDefaults = extend({
borderRadius: '4px',
padding: '8px 16px',
background: 'rgba(0, 0, 0, 0.8)',
boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px',
}, innerStyle)
return h(
MenuDroppo,
{
useCssTransition,
isOpen,
zIndex: 11,
onClickOutside,
style,
innerStyle: innerStyleDefaults,
},
[
h(
'style',
`
li.dropdown-menu-item:hover { color:rgb(225, 225, 225); }
li.dropdown-menu-item { color: rgb(185, 185, 185); position: relative }
`
),
...children,
]
)
}
}
Dropdown.defaultProps = {
isOpen: false,
onClick: noop,
useCssTransition: false,
}
Dropdown.propTypes = {
isOpen: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
children: PropTypes.node,
style: PropTypes.object.isRequired,
onClickOutside: PropTypes.func,
innerStyle: PropTypes.object,
useCssTransition: PropTypes.bool,
}
class DropdownMenuItem extends Component {
render () {
const { onClick, closeMenu, children, style } = this.props
return h(
'li.dropdown-menu-item',
{
onClick: () => {
onClick()
closeMenu()
},
style: Object.assign({
listStyle: 'none',
padding: '8px 0px 8px 0px',
fontSize: '18px',
fontStyle: 'normal',
fontFamily: 'Montserrat Regular',
cursor: 'pointer',
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
}, style),
},
children
)
}
}
DropdownMenuItem.propTypes = {
closeMenu: PropTypes.func.isRequired,
onClick: PropTypes.func.isRequired,
children: PropTypes.node,
style: PropTypes.object,
}
module.exports = {
Dropdown,
DropdownMenuItem,
}

@ -0,0 +1,57 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const findDOMNode = require('react-dom').findDOMNode
module.exports = EditableLabel
inherits(EditableLabel, Component)
function EditableLabel () {
Component.call(this)
}
EditableLabel.prototype.render = function () {
const props = this.props
const state = this.state
if (state && state.isEditingLabel) {
return h('div.editable-label', [
h('input.sizing-input', {
defaultValue: props.textValue,
maxLength: '20',
onKeyPress: (event) => {
this.saveIfEnter(event)
},
}),
h('button.editable-button', {
onClick: () => this.saveText(),
}, 'Save'),
])
} else {
return h('div.name-label', {
onClick: (event) => {
const nameAttribute = event.target.getAttribute('name')
// checks for class to handle smaller CTA above the account name
const classAttribute = event.target.getAttribute('class')
if (nameAttribute === 'edit' || classAttribute === 'edit-text') {
this.setState({ isEditingLabel: true })
}
},
}, this.props.children)
}
}
EditableLabel.prototype.saveIfEnter = function (event) {
if (event.key === 'Enter') {
this.saveText()
}
}
EditableLabel.prototype.saveText = function () {
// eslint-disable-next-line react/no-find-dom-node
var container = findDOMNode(this)
var text = container.querySelector('.editable-label input').value
var truncatedText = text.substring(0, 20)
this.props.saveText(truncatedText)
this.setState({ isEditingLabel: false, textLabel: truncatedText })
}

@ -0,0 +1,170 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const extend = require('xtend')
const debounce = require('debounce')
const copyToClipboard = require('copy-to-clipboard')
const ENS = require('ethjs-ens')
const networkMap = require('ethjs-ens/lib/network-map.json')
const ensRE = /.+\..+$/
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
module.exports = EnsInput
inherits(EnsInput, Component)
function EnsInput () {
Component.call(this)
}
EnsInput.prototype.render = function () {
const props = this.props
const opts = extend(props, {
list: 'addresses',
onChange: () => {
const network = this.props.network
const networkHasEnsSupport = getNetworkEnsSupport(network)
if (!networkHasEnsSupport) return
const recipient = document.querySelector('input[name="address"]').value
if (recipient.match(ensRE) === null) {
return this.setState({
loadingEns: false,
ensResolution: null,
ensFailure: null,
})
}
this.setState({
loadingEns: true,
})
this.checkName()
},
})
return h('div', {
style: { width: '100%' },
}, [
h('input.large-input', opts),
// The address book functionality.
h('datalist#addresses',
[
// Corresponds to the addresses owned.
Object.keys(props.identities).map((key) => {
const identity = props.identities[key]
return h('option', {
value: identity.address,
label: identity.name,
key: identity.address,
})
}),
// Corresponds to previously sent-to addresses.
props.addressBook.map((identity) => {
return h('option', {
value: identity.address,
label: identity.name,
key: identity.address,
})
}),
]),
this.ensIcon(),
])
}
EnsInput.prototype.componentDidMount = function () {
const network = this.props.network
const networkHasEnsSupport = getNetworkEnsSupport(network)
this.setState({ ensResolution: ZERO_ADDRESS })
if (networkHasEnsSupport) {
const provider = global.ethereumProvider
this.ens = new ENS({ provider, network })
this.checkName = debounce(this.lookupEnsName.bind(this), 200)
}
}
EnsInput.prototype.lookupEnsName = function () {
const recipient = document.querySelector('input[name="address"]').value
const { ensResolution } = this.state
log.info(`ENS attempting to resolve name: ${recipient}`)
this.ens.lookup(recipient.trim())
.then((address) => {
if (address === ZERO_ADDRESS) throw new Error('No address has been set for this name.')
if (address !== ensResolution) {
this.setState({
loadingEns: false,
ensResolution: address,
nickname: recipient.trim(),
hoverText: address + '\nClick to Copy',
ensFailure: false,
})
}
})
.catch((reason) => {
log.error(reason)
return this.setState({
loadingEns: false,
ensResolution: ZERO_ADDRESS,
ensFailure: true,
hoverText: reason.message,
})
})
}
EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) {
const state = this.state || {}
const ensResolution = state.ensResolution
// If an address is sent without a nickname, meaning not from ENS or from
// the user's own accounts, a default of a one-space string is used.
const nickname = state.nickname || ' '
if (prevState && ensResolution && this.props.onChange &&
ensResolution !== prevState.ensResolution) {
this.props.onChange(ensResolution, nickname)
}
}
EnsInput.prototype.ensIcon = function (recipient) {
const { hoverText } = this.state || {}
return h('span', {
title: hoverText,
style: {
position: 'absolute',
padding: '9px',
transform: 'translatex(-40px)',
},
}, this.ensIconContents(recipient))
}
EnsInput.prototype.ensIconContents = function (recipient) {
const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: ZERO_ADDRESS}
if (loadingEns) {
return h('img', {
src: 'images/loading.svg',
style: {
width: '30px',
height: '30px',
transform: 'translateY(-6px)',
},
})
}
if (ensFailure) {
return h('i.fa.fa-warning.fa-lg.warning')
}
if (ensResolution && (ensResolution !== ZERO_ADDRESS)) {
return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', {
style: { color: 'green' },
onClick: (event) => {
event.preventDefault()
event.stopPropagation()
copyToClipboard(ensResolution)
},
})
}
}
function getNetworkEnsSupport (network) {
return Boolean(networkMap[network])
}

@ -0,0 +1,89 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const formatBalance = require('../util').formatBalance
const generateBalanceObject = require('../util').generateBalanceObject
const Tooltip = require('./tooltip.js')
const FiatValue = require('./fiat-value.js')
module.exports = EthBalanceComponent
inherits(EthBalanceComponent, Component)
function EthBalanceComponent () {
Component.call(this)
}
EthBalanceComponent.prototype.render = function () {
var props = this.props
let { value } = props
const { style, width } = props
var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true
value = value ? formatBalance(value, 6, needsParse) : '...'
return (
h('.ether-balance.ether-balance-amount', {
style,
}, [
h('div', {
style: {
display: 'inline',
width,
},
}, this.renderBalance(value)),
])
)
}
EthBalanceComponent.prototype.renderBalance = function (value) {
var props = this.props
const { conversionRate, shorten, incoming, currentCurrency } = props
if (value === 'None') return value
if (value === '...') return value
var balanceObj = generateBalanceObject(value, shorten ? 1 : 3)
var balance
var splitBalance = value.split(' ')
var ethNumber = splitBalance[0]
var ethSuffix = splitBalance[1]
const showFiat = 'showFiat' in props ? props.showFiat : true
if (shorten) {
balance = balanceObj.shortBalance
} else {
balance = balanceObj.balance
}
var label = balanceObj.label
return (
h(Tooltip, {
position: 'bottom',
title: `${ethNumber} ${ethSuffix}`,
}, h('div.flex-column', [
h('.flex-row', {
style: {
alignItems: 'flex-end',
lineHeight: '13px',
fontFamily: 'Montserrat Light',
textRendering: 'geometricPrecision',
},
}, [
h('div', {
style: {
width: '100%',
textAlign: 'right',
},
}, incoming ? `+${balance}` : balance),
h('div', {
style: {
color: ' #AEAEAE',
fontSize: '12px',
marginLeft: '5px',
},
}, label),
]),
showFiat ? h(FiatValue, { value: props.value, conversionRate, currentCurrency }) : null,
]))
)
}

@ -0,0 +1,64 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const formatBalance = require('../util').formatBalance
module.exports = FiatValue
inherits(FiatValue, Component)
function FiatValue () {
Component.call(this)
}
FiatValue.prototype.render = function () {
const props = this.props
const { conversionRate, currentCurrency } = props
const renderedCurrency = currentCurrency || ''
const value = formatBalance(props.value, 6)
if (value === 'None') return value
var fiatDisplayNumber, fiatTooltipNumber
var splitBalance = value.split(' ')
if (conversionRate !== 0) {
fiatTooltipNumber = Number(splitBalance[0]) * conversionRate
fiatDisplayNumber = fiatTooltipNumber.toFixed(2)
} else {
fiatDisplayNumber = 'N/A'
fiatTooltipNumber = 'Unknown'
}
return fiatDisplay(fiatDisplayNumber, renderedCurrency.toUpperCase())
}
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')
}
}

@ -0,0 +1,154 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const extend = require('xtend')
module.exports = HexAsDecimalInput
inherits(HexAsDecimalInput, Component)
function HexAsDecimalInput () {
this.state = { invalid: null }
Component.call(this)
}
/* Hex as Decimal Input
*
* A component for allowing easy, decimal editing
* of a passed in hex string value.
*
* On change, calls back its `onChange` function parameter
* and passes it an updated hex string.
*/
HexAsDecimalInput.prototype.render = function () {
const props = this.props
const state = this.state
const { value, onChange, min, max } = props
const toEth = props.toEth
const suffix = props.suffix
const decimalValue = decimalize(value, toEth)
const style = props.style
return (
h('.flex-column', [
h('.flex-row', {
style: {
alignItems: 'flex-end',
lineHeight: '13px',
fontFamily: 'Montserrat Light',
textRendering: 'geometricPrecision',
},
}, [
h('input.hex-input', {
type: 'number',
required: true,
min: min,
max: max,
style: extend({
display: 'block',
textAlign: 'right',
backgroundColor: 'transparent',
border: '1px solid #bdbdbd',
}, style),
value: parseInt(decimalValue),
onBlur: (event) => {
this.updateValidity(event)
},
onChange: (event) => {
this.updateValidity(event)
const hexString = (event.target.value === '') ? '' : hexify(event.target.value)
onChange(hexString)
},
onInvalid: (event) => {
const msg = this.constructWarning()
if (msg === state.invalid) {
return
}
this.setState({ invalid: msg })
event.preventDefault()
return false
},
}),
h('div', {
style: {
color: ' #AEAEAE',
fontSize: '12px',
marginLeft: '5px',
marginRight: '6px',
width: '20px',
},
}, suffix),
]),
state.invalid ? h('span.error', {
style: {
position: 'absolute',
right: '0px',
textAlign: 'right',
transform: 'translateY(26px)',
padding: '3px',
background: 'rgba(255,255,255,0.85)',
zIndex: '1',
textTransform: 'capitalize',
border: '2px solid #E20202',
},
}, state.invalid) : null,
])
)
}
HexAsDecimalInput.prototype.setValid = function (message) {
this.setState({ invalid: null })
}
HexAsDecimalInput.prototype.updateValidity = function (event) {
const target = event.target
const value = this.props.value
const newValue = target.value
if (value === newValue) {
return
}
const valid = target.checkValidity()
if (valid) {
this.setState({ invalid: null })
}
}
HexAsDecimalInput.prototype.constructWarning = function () {
const { name, min, max } = this.props
let message = name ? name + ' ' : ''
if (min && max) {
message += `must be greater than or equal to ${min} and less than or equal to ${max}.`
} else if (min) {
message += `must be greater than or equal to ${min}.`
} else if (max) {
message += `must be less than or equal to ${max}.`
} else {
message += 'Invalid input.'
}
return message
}
function hexify (decimalString) {
const hexBN = new BN(parseInt(decimalString), 10)
return '0x' + hexBN.toString('hex')
}
function decimalize (input, toEth) {
if (input === '') {
return ''
} else {
const strippedInput = ethUtil.stripHexPrefix(input)
const inputBN = new BN(strippedInput, 'hex')
return inputBN.toString(10)
}
}

@ -0,0 +1,74 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const isNode = require('detect-node')
const findDOMNode = require('react-dom').findDOMNode
const jazzicon = require('jazzicon')
const iconFactoryGen = require('../../lib/icon-factory')
const iconFactory = iconFactoryGen(jazzicon)
module.exports = IdenticonComponent
inherits(IdenticonComponent, Component)
function IdenticonComponent () {
Component.call(this)
this.defaultDiameter = 46
}
IdenticonComponent.prototype.render = function () {
var props = this.props
var diameter = props.diameter || this.defaultDiameter
return (
h('div', {
key: 'identicon-' + this.props.address,
style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: diameter,
width: diameter,
borderRadius: diameter / 2,
overflow: 'hidden',
},
})
)
}
IdenticonComponent.prototype.componentDidMount = function () {
var props = this.props
const { address } = props
if (!address) return
// eslint-disable-next-line react/no-find-dom-node
var container = findDOMNode(this)
var diameter = props.diameter || this.defaultDiameter
if (!isNode) {
var img = iconFactory.iconForAddress(address, diameter)
container.appendChild(img)
}
}
IdenticonComponent.prototype.componentDidUpdate = function () {
var props = this.props
const { address } = props
if (!address) return
// eslint-disable-next-line react/no-find-dom-node
var container = findDOMNode(this)
var children = container.children
for (var i = 0; i < children.length; i++) {
container.removeChild(children[i])
}
var diameter = props.diameter || this.defaultDiameter
if (!isNode) {
var img = iconFactory.iconForAddress(address, diameter)
container.appendChild(img)
}
}

@ -0,0 +1,45 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
inherits(LoadingIndicator, Component)
module.exports = LoadingIndicator
function LoadingIndicator () {
Component.call(this)
}
LoadingIndicator.prototype.render = function () {
const { isLoading, loadingMessage } = this.props
return (
isLoading ? h('.full-flex-height', {
style: {
left: '0px',
zIndex: 10,
position: 'absolute',
flexDirection: 'column',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
width: '100%',
background: 'rgba(255, 255, 255, 0.8)',
},
}, [
h('img', {
src: 'images/loading.svg',
}),
h('br'),
showMessageIfAny(loadingMessage),
]) : null
)
}
function showMessageIfAny (loadingMessage) {
if (!loadingMessage) return null
return h('span', loadingMessage)
}

@ -0,0 +1,59 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const metamaskLogo = require('metamask-logo')
const debounce = require('debounce')
module.exports = Mascot
inherits(Mascot, Component)
function Mascot () {
Component.call(this)
this.logo = metamaskLogo({
followMouse: true,
pxNotRatio: true,
width: 200,
height: 200,
})
this.refollowMouse = debounce(this.logo.setFollowMouse.bind(this.logo, true), 1000)
this.unfollowMouse = this.logo.setFollowMouse.bind(this.logo, false)
}
Mascot.prototype.render = function () {
// this is a bit hacky
// the event emitter is on `this.props`
// and we dont get that until render
this.handleAnimationEvents()
return h('#metamask-mascot-container', {
style: { zIndex: 0 },
})
}
Mascot.prototype.componentDidMount = function () {
var targetDivId = 'metamask-mascot-container'
var container = document.getElementById(targetDivId)
container.appendChild(this.logo.container)
}
Mascot.prototype.componentWillUnmount = function () {
this.animations = this.props.animationEventEmitter
this.animations.removeAllListeners()
this.logo.container.remove()
this.logo.stopAnimation()
}
Mascot.prototype.handleAnimationEvents = function () {
// only setup listeners once
if (this.animations) return
this.animations = this.props.animationEventEmitter
this.animations.on('point', this.lookAt.bind(this))
this.animations.on('setFollowMouse', this.logo.setFollowMouse.bind(this.logo))
}
Mascot.prototype.lookAt = function (target) {
this.unfollowMouse()
this.logo.lookAt(target)
this.refollowMouse()
}

@ -0,0 +1,132 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const findDOMNode = require('react-dom').findDOMNode
const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
module.exports = MenuDroppoComponent
inherits(MenuDroppoComponent, Component)
function MenuDroppoComponent () {
Component.call(this)
}
MenuDroppoComponent.prototype.render = function () {
const speed = this.props.speed || '300ms'
const useCssTransition = this.props.useCssTransition
const zIndex = ('zIndex' in this.props) ? this.props.zIndex : 0
this.manageListeners()
const style = this.props.style || {}
if (!('position' in style)) {
style.position = 'fixed'
}
style.zIndex = zIndex
return (
h('.menu-droppo-container', {
style,
}, [
h('style', `
.menu-droppo-enter {
transition: transform ${speed} ease-in-out;
transform: translateY(-200%);
}
.menu-droppo-enter.menu-droppo-enter-active {
transition: transform ${speed} ease-in-out;
transform: translateY(0%);
}
.menu-droppo-leave {
transition: transform ${speed} ease-in-out;
transform: translateY(0%);
}
.menu-droppo-leave.menu-droppo-leave-active {
transition: transform ${speed} ease-in-out;
transform: translateY(-200%);
}
`),
useCssTransition
? h(ReactCSSTransitionGroup, {
className: 'css-transition-group',
transitionName: 'menu-droppo',
transitionEnterTimeout: parseInt(speed),
transitionLeaveTimeout: parseInt(speed),
}, this.renderPrimary())
: this.renderPrimary(),
])
)
}
MenuDroppoComponent.prototype.renderPrimary = function () {
const isOpen = this.props.isOpen
if (!isOpen) {
return null
}
const innerStyle = this.props.innerStyle || {}
return (
h('.menu-droppo', {
key: 'menu-droppo-drawer',
style: innerStyle,
},
[ this.props.children ])
)
}
MenuDroppoComponent.prototype.manageListeners = function () {
const isOpen = this.props.isOpen
const onClickOutside = this.props.onClickOutside
if (isOpen) {
this.outsideClickHandler = onClickOutside
} else if (isOpen) {
this.outsideClickHandler = null
}
}
MenuDroppoComponent.prototype.componentDidMount = function () {
if (this && document.body) {
this.globalClickHandler = this.globalClickOccurred.bind(this)
document.body.addEventListener('click', this.globalClickHandler)
// eslint-disable-next-line react/no-find-dom-node
var container = findDOMNode(this)
this.container = container
}
}
MenuDroppoComponent.prototype.componentWillUnmount = function () {
if (this && document.body) {
document.body.removeEventListener('click', this.globalClickHandler)
}
}
MenuDroppoComponent.prototype.globalClickOccurred = function (event) {
const target = event.target
// eslint-disable-next-line react/no-find-dom-node
const container = findDOMNode(this)
if (target !== container &&
!isDescendant(this.container, event.target) &&
this.outsideClickHandler) {
this.outsideClickHandler(event)
}
}
function isDescendant (parent, child) {
var node = child.parentNode
while (node !== null) {
if (node === parent) {
return true
}
node = node.parentNode
}
return false
}

@ -0,0 +1,74 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const Identicon = require('./identicon')
module.exports = AccountPanel
inherits(AccountPanel, Component)
function AccountPanel () {
Component.call(this)
}
AccountPanel.prototype.render = function () {
var props = this.props
var picOrder = props.picOrder || 'left'
const { imageSeed } = props
return (
h('.identity-panel.flex-row.flex-left', {
style: {
cursor: props.onClick ? 'pointer' : undefined,
},
onClick: props.onClick,
}, [
this.genIcon(imageSeed, picOrder),
h('div.flex-column.flex-justify-center', {
style: {
lineHeight: '15px',
order: 2,
display: 'flex',
alignItems: picOrder === 'left' ? 'flex-begin' : 'flex-end',
},
}, this.props.children),
])
)
}
AccountPanel.prototype.genIcon = function (seed, picOrder) {
const props = this.props
// When there is no seed value, this is a contract creation.
// We then show the contract icon.
if (!seed) {
return h('.identicon-wrapper.flex-column.select-none', {
style: {
order: picOrder === 'left' ? 1 : 3,
},
}, [
h('i.fa.fa-file-text-o.fa-lg', {
style: {
fontSize: '42px',
transform: 'translate(0px, -16px)',
},
}),
])
}
// If there was a seed, we return an identicon for that address.
return h('.identicon-wrapper.flex-column.select-none', {
style: {
order: picOrder === 'left' ? 1 : 3,
},
}, [
h(Identicon, {
address: seed,
imageify: props.imageifyIdenticons,
}),
])
}

@ -0,0 +1,129 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
module.exports = Network
inherits(Network, Component)
function Network () {
Component.call(this)
}
Network.prototype.render = function () {
const props = this.props
const networkNumber = props.network
let providerName
try {
providerName = props.provider.type
} catch (e) {
providerName = null
}
let iconName, hoverText
if (networkNumber === 'loading') {
return h('span.pointer', {
style: {
display: 'flex',
alignItems: 'center',
flexDirection: 'row',
},
onClick: (event) => this.props.onClick(event),
}, [
h('img', {
title: 'Attempting to connect to blockchain.',
style: {
width: '27px',
},
src: 'images/loading.svg',
}),
h('i.fa.fa-caret-down'),
])
} else if (providerName === 'mainnet') {
hoverText = 'Main Ethereum Network'
iconName = 'ethereum-network'
} else if (providerName === 'ropsten') {
hoverText = 'Ropsten Test Network'
iconName = 'ropsten-test-network'
} else if (parseInt(networkNumber) === 3) {
hoverText = 'Ropsten Test Network'
iconName = 'ropsten-test-network'
} else if (providerName === 'kovan') {
hoverText = 'Kovan Test Network'
iconName = 'kovan-test-network'
} else if (providerName === 'rinkeby') {
hoverText = 'Rinkeby Test Network'
iconName = 'rinkeby-test-network'
} else {
hoverText = 'Unknown Private Network'
iconName = 'unknown-private-network'
}
return (
h('#network_component.pointer', {
title: hoverText,
onClick: (event) => this.props.onClick(event),
}, [
(function () {
switch (iconName) {
case 'ethereum-network':
return h('.network-indicator', [
h('.menu-icon.diamond'),
h('.network-name', {
style: {
color: '#039396',
}},
'Main Network'),
h('i.fa.fa-caret-down.fa-lg'),
])
case 'ropsten-test-network':
return h('.network-indicator', [
h('.menu-icon.red-dot'),
h('.network-name', {
style: {
color: '#ff6666',
}},
'Ropsten Test Net'),
h('i.fa.fa-caret-down.fa-lg'),
])
case 'kovan-test-network':
return h('.network-indicator', [
h('.menu-icon.hollow-diamond'),
h('.network-name', {
style: {
color: '#690496',
}},
'Kovan Test Net'),
h('i.fa.fa-caret-down.fa-lg'),
])
case 'rinkeby-test-network':
return h('.network-indicator', [
h('.menu-icon.golden-square'),
h('.network-name', {
style: {
color: '#e7a218',
}},
'Rinkeby Test Net'),
h('i.fa.fa-caret-down.fa-lg'),
])
default:
return h('.network-indicator', [
h('i.fa.fa-question-circle.fa-lg', {
style: {
margin: '10px',
color: 'rgb(125, 128, 130)',
},
}),
h('.network-name', {
style: {
color: '#AEAEAE',
}},
'Private Network'),
h('i.fa.fa-caret-down.fa-lg'),
])
}
})(),
])
)
}

@ -0,0 +1,132 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const ReactMarkdown = require('react-markdown')
const linker = require('extension-link-enabler')
const findDOMNode = require('react-dom').findDOMNode
module.exports = Notice
inherits(Notice, Component)
function Notice () {
Component.call(this)
}
Notice.prototype.render = function () {
const { notice, onConfirm } = this.props
const { title, date, body } = notice
const state = this.state || { disclaimerDisabled: true }
const disabled = state.disclaimerDisabled
return (
h('.flex-column.flex-center.flex-grow', {
style: {
width: '100%',
},
}, [
h('h3.flex-center.text-transform-uppercase.terms-header', {
style: {
background: '#EBEBEB',
color: '#AEAEAE',
width: '100%',
fontSize: '20px',
textAlign: 'center',
padding: 6,
},
}, [
title,
]),
h('h5.flex-center.text-transform-uppercase.terms-header', {
style: {
background: '#EBEBEB',
color: '#AEAEAE',
marginBottom: 24,
width: '100%',
fontSize: '20px',
textAlign: 'center',
padding: 6,
},
}, [
date,
]),
h('style', `
.markdown {
overflow-x: hidden;
}
.markdown h1, .markdown h2, .markdown h3 {
margin: 10px 0;
font-weight: bold;
}
.markdown strong {
font-weight: bold;
}
.markdown em {
font-style: italic;
}
.markdown p {
margin: 10px 0;
}
.markdown a {
color: #df6b0e;
}
`),
h('div.markdown', {
onScroll: (e) => {
var object = e.currentTarget
if (object.offsetHeight + object.scrollTop + 100 >= object.scrollHeight) {
this.setState({disclaimerDisabled: false})
}
},
style: {
background: 'rgb(235, 235, 235)',
height: '310px',
padding: '6px',
width: '90%',
overflowY: 'scroll',
scroll: 'auto',
},
}, [
h(ReactMarkdown, {
className: 'notice-box',
source: body,
skipHtml: true,
}),
]),
h('button', {
disabled,
onClick: () => {
this.setState({disclaimerDisabled: true})
onConfirm()
},
style: {
marginTop: '18px',
},
}, 'Accept'),
])
)
}
Notice.prototype.componentDidMount = function () {
// eslint-disable-next-line react/no-find-dom-node
var node = findDOMNode(this)
linker.setupListener(node)
if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) {
this.setState({disclaimerDisabled: false})
}
}
Notice.prototype.componentWillUnmount = function () {
// eslint-disable-next-line react/no-find-dom-node
var node = findDOMNode(this)
linker.teardownListener(node)
}

@ -0,0 +1,50 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const AccountPanel = require('./account-panel')
module.exports = PendingMsgDetails
inherits(PendingMsgDetails, Component)
function PendingMsgDetails () {
Component.call(this)
}
PendingMsgDetails.prototype.render = function () {
var state = this.props
var msgData = state.txData
var msgParams = msgData.msgParams || {}
var address = msgParams.from || state.selectedAddress
var identity = state.identities[address] || { address: address }
var account = state.accounts[address] || { address: address }
return (
h('div', {
key: msgData.id,
style: {
margin: '10px 20px',
},
}, [
// account that will sign
h(AccountPanel, {
showFullAddress: true,
identity: identity,
account: account,
imageifyIdenticons: state.imageifyIdenticons,
}),
// message data
h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-column.flex-space-between', [
h('label.font-small', 'MESSAGE'),
h('span.font-small', msgParams.data),
]),
]),
])
)
}

@ -0,0 +1,70 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const PendingTxDetails = require('./pending-msg-details')
module.exports = PendingMsg
inherits(PendingMsg, Component)
function PendingMsg () {
Component.call(this)
}
PendingMsg.prototype.render = function () {
var state = this.props
var msgData = state.txData
return (
h('div', {
key: msgData.id,
style: {
maxWidth: '350px',
},
}, [
// header
h('h3', {
style: {
fontWeight: 'bold',
textAlign: 'center',
},
}, 'Sign Message'),
h('.error', {
style: {
margin: '10px',
},
}, [
`Signing this message can have
dangerous side effects. Only sign messages from
sites you fully trust with your entire account.
This dangerous method will be removed in a future version. `,
h('a', {
href: 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527',
style: { color: 'rgb(247, 134, 28)' },
onClick: (event) => {
event.preventDefault()
const url = 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527'
global.platform.openWindow({ url })
},
}, 'Read more here.'),
]),
// message details
h(PendingTxDetails, state),
// sign + cancel
h('.flex-row.flex-space-around', [
h('button', {
onClick: state.cancelMessage,
}, 'Cancel'),
h('button', {
onClick: state.signMessage,
}, 'Sign'),
]),
])
)
}

@ -0,0 +1,60 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const AccountPanel = require('./account-panel')
const BinaryRenderer = require('./binary-renderer')
module.exports = PendingMsgDetails
inherits(PendingMsgDetails, Component)
function PendingMsgDetails () {
Component.call(this)
}
PendingMsgDetails.prototype.render = function () {
var state = this.props
var msgData = state.txData
var msgParams = msgData.msgParams || {}
var address = msgParams.from || state.selectedAddress
var identity = state.identities[address] || { address: address }
var account = state.accounts[address] || { address: address }
var { data } = msgParams
return (
h('div', {
key: msgData.id,
style: {
margin: '10px 20px',
},
}, [
// account that will sign
h(AccountPanel, {
showFullAddress: true,
identity: identity,
account: account,
imageifyIdenticons: state.imageifyIdenticons,
}),
// message data
h('div', {
style: {
height: '260px',
},
}, [
h('label.font-small', { style: { display: 'block' } }, 'MESSAGE'),
h(BinaryRenderer, {
value: data,
style: {
height: '215px',
},
}),
]),
])
)
}

@ -0,0 +1,47 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const PendingTxDetails = require('./pending-personal-msg-details')
module.exports = PendingMsg
inherits(PendingMsg, Component)
function PendingMsg () {
Component.call(this)
}
PendingMsg.prototype.render = function () {
var state = this.props
var msgData = state.txData
return (
h('div', {
key: msgData.id,
}, [
// header
h('h3', {
style: {
fontWeight: 'bold',
textAlign: 'center',
},
}, 'Sign Message'),
// message details
h(PendingTxDetails, state),
// sign + cancel
h('.flex-row.flex-space-around', [
h('button', {
onClick: state.cancelPersonalMessage,
}, 'Cancel'),
h('button', {
onClick: state.signPersonalMessage,
}, 'Sign'),
]),
])
)
}

@ -0,0 +1,510 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const actions = require('../../../ui/app/actions')
const clone = require('clone')
const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const hexToBn = require('../../../app/scripts/lib/hex-to-bn')
const util = require('../util')
const MiniAccountPanel = require('./mini-account-panel')
const Copyable = require('./copyable')
const EthBalance = require('./eth-balance')
const addressSummary = util.addressSummary
const nameForAddress = require('../../lib/contract-namer')
const BNInput = require('./bn-as-decimal-input')
// corresponds with 0.1 GWEI
const MIN_GAS_PRICE_BN = new BN('100000000')
const MIN_GAS_LIMIT_BN = new BN('21000')
module.exports = PendingTx
inherits(PendingTx, Component)
function PendingTx () {
Component.call(this)
this.state = {
valid: true,
txData: null,
submitting: false,
}
}
PendingTx.prototype.render = function () {
const props = this.props
const { currentCurrency, blockGasLimit } = props
const conversionRate = props.conversionRate
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
// Allow retry txs
const { lastGasPrice } = txMeta
let forceGasMin
if (lastGasPrice) {
const stripped = ethUtil.stripHexPrefix(lastGasPrice)
const lastGas = new BN(stripped, 16)
const priceBump = lastGas.divn('10')
forceGasMin = lastGas.add(priceBump)
}
// Account Details
const address = txParams.from || props.selectedAddress
const identity = props.identities[address] || { address: address }
const account = props.accounts[address]
const balance = account ? account.balance : '0x0'
// recipient check
const isValidAddress = !txParams.to || util.isValidAddress(txParams.to)
// Gas
const gas = txParams.gas
const gasBn = hexToBn(gas)
const gasLimit = new BN(parseInt(blockGasLimit))
const safeGasLimitBN = this.bnMultiplyByFraction(gasLimit, 19, 20)
const saferGasLimitBN = this.bnMultiplyByFraction(gasLimit, 18, 20)
const safeGasLimit = safeGasLimitBN.toString(10)
// Gas Price
const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16)
const gasPriceBn = hexToBn(gasPrice)
const txFeeBn = gasBn.mul(gasPriceBn)
const valueBn = hexToBn(txParams.value)
const maxCost = txFeeBn.add(valueBn)
const dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0
const balanceBn = hexToBn(balance)
const insufficientBalance = balanceBn.lt(maxCost)
const dangerousGasLimit = gasBn.gte(saferGasLimitBN)
const gasLimitSpecified = txMeta.gasLimitSpecified
const buyDisabled = insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting
const showRejectAll = props.unconfTxListLength > 1
this.inputs = []
return (
h('div', {
key: txMeta.id,
}, [
h('form#pending-tx-form', {
onSubmit: this.onSubmit.bind(this),
}, [
// tx info
h('div', [
h('.flex-row.flex-center', {
style: {
maxWidth: '100%',
},
}, [
h(MiniAccountPanel, {
imageSeed: address,
picOrder: 'right',
}, [
h('span.font-small', {
style: {
fontFamily: 'Montserrat Bold, Montserrat, sans-serif',
},
}, identity.name),
h(Copyable, {
value: ethUtil.toChecksumAddress(address),
}, [
h('span.font-small', {
style: {
fontFamily: 'Montserrat Light, Montserrat, sans-serif',
},
}, addressSummary(address, 6, 4, false)),
]),
h('span.font-small', {
style: {
fontFamily: 'Montserrat Light, Montserrat, sans-serif',
},
}, [
h(EthBalance, {
value: balance,
conversionRate,
currentCurrency,
inline: true,
labelColor: '#F7861C',
}),
]),
]),
forwardCarrat(),
this.miniAccountPanelForRecipient(),
]),
h('style', `
.table-box {
margin: 7px 0px 0px 0px;
width: 100%;
}
.table-box .row {
margin: 0px;
background: rgb(236,236,236);
display: flex;
justify-content: space-between;
font-family: Montserrat Light, sans-serif;
font-size: 13px;
padding: 5px 25px;
}
.table-box .row .value {
font-family: Montserrat Regular;
}
`),
h('.table-box', [
// Ether Value
// Currently not customizable, but easily modified
// in the way that gas and gasLimit currently are.
h('.row', [
h('.cell.label', 'Amount'),
h(EthBalance, { value: txParams.value, currentCurrency, conversionRate }),
]),
// Gas Limit (customizable)
h('.cell.row', [
h('.cell.label', 'Gas Limit'),
h('.cell.value', {
}, [
h(BNInput, {
name: 'Gas Limit',
value: gasBn,
precision: 0,
scale: 0,
// The hard lower limit for gas.
min: MIN_GAS_LIMIT_BN,
max: safeGasLimit,
suffix: 'UNITS',
style: {
position: 'relative',
top: '5px',
},
onChange: this.gasLimitChanged.bind(this),
ref: (hexInput) => { this.inputs.push(hexInput) },
}),
]),
]),
// Gas Price (customizable)
h('.cell.row', [
h('.cell.label', 'Gas Price'),
h('.cell.value', {
}, [
h(BNInput, {
name: 'Gas Price',
value: gasPriceBn,
precision: 9,
scale: 9,
suffix: 'GWEI',
min: forceGasMin || MIN_GAS_PRICE_BN,
style: {
position: 'relative',
top: '5px',
},
onChange: this.gasPriceChanged.bind(this),
ref: (hexInput) => { this.inputs.push(hexInput) },
}),
]),
]),
// Max Transaction Fee (calculated)
h('.cell.row', [
h('.cell.label', 'Max Transaction Fee'),
h(EthBalance, { value: txFeeBn.toString(16), currentCurrency, conversionRate }),
]),
h('.cell.row', {
style: {
fontFamily: 'Montserrat Regular',
background: 'white',
padding: '10px 25px',
},
}, [
h('.cell.label', 'Max Total'),
h('.cell.value', {
style: {
display: 'flex',
alignItems: 'center',
},
}, [
h(EthBalance, {
value: maxCost.toString(16),
currentCurrency,
conversionRate,
inline: true,
labelColor: 'black',
fontSize: '16px',
}),
]),
]),
// Data size row:
h('.cell.row', {
style: {
background: '#f7f7f7',
paddingBottom: '0px',
},
}, [
h('.cell.label'),
h('.cell.value', {
style: {
fontFamily: 'Montserrat Light',
fontSize: '11px',
},
}, `Data included: ${dataLength} bytes`),
]),
]), // End of Table
]),
h('style', `
.conf-buttons button {
margin-left: 10px;
text-transform: uppercase;
}
`),
h('.cell.row', {
style: {
textAlign: 'center',
},
}, [
txMeta.simulationFails ?
h('.error', {
style: {
fontSize: '0.9em',
},
}, 'Transaction Error. Exception thrown in contract code.')
: null,
!isValidAddress ?
h('.error', {
style: {
fontSize: '0.9em',
},
}, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.')
: null,
insufficientBalance ?
h('span.error', {
style: {
fontSize: '0.9em',
},
}, 'Insufficient balance for transaction')
: null,
(dangerousGasLimit && !gasLimitSpecified) ?
h('span.error', {
style: {
fontSize: '0.9em',
},
}, 'Gas limit set dangerously high. Approving this transaction is likely to fail.')
: null,
]),
// send + cancel
h('.flex-row.flex-space-around.conf-buttons', {
style: {
display: 'flex',
justifyContent: 'flex-end',
margin: '14px 25px',
},
}, [
h('button', {
onClick: (event) => {
this.resetGasFields()
event.preventDefault()
},
}, 'Reset'),
// Accept Button or Buy Button
insufficientBalance ? h('button.btn-green', { onClick: props.buyEth }, 'Buy Ether') :
h('input.confirm.btn-green', {
type: 'submit',
value: 'SUBMIT',
style: { marginLeft: '10px' },
disabled: buyDisabled,
}),
h('button.cancel.btn-red', {
onClick: props.cancelTransaction,
}, 'Reject'),
]),
showRejectAll ? h('.flex-row.flex-space-around.conf-buttons', {
style: {
display: 'flex',
justifyContent: 'flex-end',
margin: '14px 25px',
},
}, [
h('button.cancel.btn-red', {
onClick: props.cancelAllTransactions,
}, 'Reject All'),
]) : null,
]),
])
)
}
PendingTx.prototype.miniAccountPanelForRecipient = function () {
const props = this.props
const txData = props.txData
const txParams = txData.txParams || {}
const isContractDeploy = !('to' in txParams)
// If it's not a contract deploy, send to the account
if (!isContractDeploy) {
return h(MiniAccountPanel, {
imageSeed: txParams.to,
picOrder: 'left',
}, [
h('span.font-small', {
style: {
fontFamily: 'Montserrat Bold, Montserrat, sans-serif',
},
}, nameForAddress(txParams.to, props.identities)),
h(Copyable, {
value: ethUtil.toChecksumAddress(txParams.to),
}, [
h('span.font-small', {
style: {
fontFamily: 'Montserrat Light, Montserrat, sans-serif',
},
}, addressSummary(txParams.to, 6, 4, false)),
]),
])
} else {
return h(MiniAccountPanel, {
picOrder: 'left',
}, [
h('span.font-small', {
style: {
fontFamily: 'Montserrat Bold, Montserrat, sans-serif',
},
}, 'New Contract'),
])
}
}
PendingTx.prototype.gasPriceChanged = function (newBN, valid) {
log.info(`Gas price changed to: ${newBN.toString(10)}`)
const txMeta = this.gatherTxMeta()
txMeta.txParams.gasPrice = '0x' + newBN.toString('hex')
this.setState({
txData: clone(txMeta),
valid,
})
}
PendingTx.prototype.gasLimitChanged = function (newBN, valid) {
log.info(`Gas limit changed to ${newBN.toString(10)}`)
const txMeta = this.gatherTxMeta()
txMeta.txParams.gas = '0x' + newBN.toString('hex')
this.setState({
txData: clone(txMeta),
valid,
})
}
PendingTx.prototype.resetGasFields = function () {
log.debug(`pending-tx resetGasFields`)
this.inputs.forEach((hexInput) => {
if (hexInput) {
hexInput.setValid()
}
})
this.setState({
txData: null,
valid: true,
})
}
PendingTx.prototype.onSubmit = function (event) {
event.preventDefault()
const txMeta = this.gatherTxMeta()
const valid = this.checkValidity()
this.setState({ valid, submitting: true })
if (valid && this.verifyGasParams()) {
this.props.sendTransaction(txMeta, event)
} else {
this.props.dispatch(actions.displayWarning('Invalid Gas Parameters'))
this.setState({ submitting: false })
}
}
PendingTx.prototype.checkValidity = function () {
const form = this.getFormEl()
const valid = form.checkValidity()
return valid
}
PendingTx.prototype.getFormEl = function () {
const form = document.querySelector('form#pending-tx-form')
// Stub out form for unit tests:
if (!form) {
return { checkValidity () { return true } }
}
return form
}
// After a customizable state value has been updated,
PendingTx.prototype.gatherTxMeta = function () {
log.debug(`pending-tx gatherTxMeta`)
const props = this.props
const state = this.state
const txData = clone(state.txData) || clone(props.txData)
log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`)
return txData
}
PendingTx.prototype.verifyGasParams = function () {
// We call this in case the gas has not been modified at all
if (!this.state) { return true }
return (
this._notZeroOrEmptyString(this.state.gas) &&
this._notZeroOrEmptyString(this.state.gasPrice)
)
}
PendingTx.prototype._notZeroOrEmptyString = function (obj) {
return obj !== '' && obj !== '0x0'
}
PendingTx.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) {
const numBN = new BN(numerator)
const denomBN = new BN(denominator)
return targetBN.mul(numBN).div(denomBN)
}
function forwardCarrat () {
return (
h('img', {
src: 'images/forward-carrat.svg',
style: {
padding: '5px 6px 0px 10px',
height: '37px',
},
})
)
}

@ -0,0 +1,59 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const AccountPanel = require('./account-panel')
const TypedMessageRenderer = require('./typed-message-renderer')
module.exports = PendingMsgDetails
inherits(PendingMsgDetails, Component)
function PendingMsgDetails () {
Component.call(this)
}
PendingMsgDetails.prototype.render = function () {
var state = this.props
var msgData = state.txData
var msgParams = msgData.msgParams || {}
var address = msgParams.from || state.selectedAddress
var identity = state.identities[address] || { address: address }
var account = state.accounts[address] || { address: address }
var { data } = msgParams
return (
h('div', {
key: msgData.id,
style: {
margin: '10px 20px',
},
}, [
// account that will sign
h(AccountPanel, {
showFullAddress: true,
identity: identity,
account: account,
imageifyIdenticons: state.imageifyIdenticons,
}),
// message data
h('div', {
style: {
height: '260px',
},
}, [
h('label.font-small', { style: { display: 'block' } }, 'YOU ARE SIGNING'),
h(TypedMessageRenderer, {
value: data,
style: {
height: '215px',
},
}),
]),
])
)
}

@ -0,0 +1,46 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const PendingTxDetails = require('./pending-typed-msg-details')
module.exports = PendingMsg
inherits(PendingMsg, Component)
function PendingMsg () {
Component.call(this)
}
PendingMsg.prototype.render = function () {
var state = this.props
var msgData = state.txData
return (
h('div', {
key: msgData.id,
}, [
// header
h('h3', {
style: {
fontWeight: 'bold',
textAlign: 'center',
},
}, 'Sign Message'),
// message details
h(PendingTxDetails, state),
// sign + cancel
h('.flex-row.flex-space-around', [
h('button', {
onClick: state.cancelTypedMessage,
}, 'Cancel'),
h('button', {
onClick: state.signTypedMessage,
}, 'Sign'),
]),
])
)
}

@ -0,0 +1,80 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const qrCode = require('qrcode-npm').qrcode
const inherits = require('util').inherits
const connect = require('react-redux').connect
const isHexPrefixed = require('ethereumjs-util').isHexPrefixed
const CopyButton = require('./copyButton')
module.exports = connect(mapStateToProps)(QrCodeView)
function mapStateToProps (state) {
return {
Qr: state.appState.Qr,
buyView: state.appState.buyView,
warning: state.appState.warning,
}
}
inherits(QrCodeView, Component)
function QrCodeView () {
Component.call(this)
}
QrCodeView.prototype.render = function () {
const props = this.props
const Qr = props.Qr
console.log(`QrCodeView Qr`, Qr);
const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}`
const qrImage = qrCode(4, 'M')
qrImage.addData(address)
qrImage.make()
return h('.main-container.flex-column', {
key: 'qr',
style: {
justifyContent: 'center',
paddingBottom: '45px',
paddingLeft: '45px',
paddingRight: '45px',
alignItems: 'center',
},
}, [
Array.isArray(Qr.message) ? h('.message-container', this.renderMultiMessage()) : h('.qr-header', Qr.message),
this.props.warning ? this.props.warning && h('span.error.flex-center', {
style: {
textAlign: 'center',
width: '229px',
height: '82px',
},
},
this.props.warning) : null,
h('#qr-container.flex-column', {
style: {
marginTop: '25px',
marginBottom: '15px',
},
dangerouslySetInnerHTML: {
__html: qrImage.createTableTag(4),
},
}),
h('.flex-row', [
h('h3.ellip-address', {
style: {
width: '247px',
},
}, Qr.data),
h(CopyButton, {
value: Qr.data,
}),
]),
])
}
QrCodeView.prototype.renderMultiMessage = function () {
var Qr = this.props.Qr
var multiMessage = Qr.message.map((message) => h('.qr-message', message))
return multiMessage
}

@ -0,0 +1,58 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
module.exports = RangeSlider
inherits(RangeSlider, Component)
function RangeSlider () {
Component.call(this)
}
RangeSlider.prototype.render = function () {
const state = this.state || {}
const props = this.props
const onInput = props.onInput || function () {}
const name = props.name
const {
min = 0,
max = 100,
increment = 1,
defaultValue = 50,
mirrorInput = false,
} = this.props.options
const {container, input, range} = props.style
return (
h('.flex-row', {
style: container,
}, [
h('input', {
type: 'range',
name: name,
min: min,
max: max,
step: increment,
style: range,
value: state.value || defaultValue,
onChange: mirrorInput ? this.mirrorInputs.bind(this, event) : onInput,
}),
// Mirrored input for range
mirrorInput ? h('input.large-input', {
type: 'number',
name: `${name}Mirror`,
min: min,
max: max,
value: state.value || defaultValue,
step: increment,
style: input,
onChange: this.mirrorInputs.bind(this, event),
}) : null,
])
)
}
RangeSlider.prototype.mirrorInputs = function (event) {
this.setState({value: event.target.value})
}

@ -0,0 +1,308 @@
const PersistentForm = require('../../lib/persistent-form')
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../../../ui/app/actions')
const Qr = require('./qr-code')
const isValidAddress = require('../util').isValidAddress
module.exports = connect(mapStateToProps)(ShapeshiftForm)
function mapStateToProps (state) {
return {
warning: state.appState.warning,
isSubLoading: state.appState.isSubLoading,
qrRequested: state.appState.qrRequested,
}
}
inherits(ShapeshiftForm, PersistentForm)
function ShapeshiftForm () {
PersistentForm.call(this)
this.persistentFormParentId = 'shapeshift-buy-form'
}
ShapeshiftForm.prototype.render = function () {
return this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain()
}
ShapeshiftForm.prototype.renderMain = function () {
const marketinfo = this.props.buyView.formView.marketinfo
const coinOptions = this.props.buyView.formView.coinOptions
var coin = marketinfo.pair.split('_')[0].toUpperCase()
return h('.flex-column', {
style: {
position: 'relative',
padding: '25px',
paddingTop: '5px',
width: '90%',
minHeight: '215px',
alignItems: 'center',
overflowY: 'auto',
},
}, [
h('.flex-row', {
style: {
justifyContent: 'center',
alignItems: 'baseline',
height: '42px',
},
}, [
h('img', {
src: coinOptions[coin].image,
width: '25px',
height: '25px',
style: {
marginRight: '5px',
},
}),
h('.input-container', {
position: 'relative',
}, [
h('input#fromCoin.buy-inputs.ex-coins', {
type: 'text',
list: 'coinList',
autoFocus: true,
dataset: {
persistentFormId: 'input-coin',
},
style: {
boxSizing: 'border-box',
},
onChange: this.handleLiveInput.bind(this),
defaultValue: 'BTC',
}),
this.renderCoinList(),
h('i.fa.fa-pencil-square-o.edit-text', {
style: {
fontSize: '12px',
color: '#F7861C',
position: 'absolute',
},
}),
]),
h('.icon-control', {
style: {
position: 'relative',
},
}, [
// Not visible on the screen, can't see it on master.
// h('i.fa.fa-refresh.fa-4.orange', {
// style: {
// bottom: '5px',
// left: '5px',
// color: '#F7861C',
// },
// onClick: this.updateCoin.bind(this),
// }),
h('i.fa.fa-chevron-right.fa-4.orange', {
style: {
position: 'absolute',
bottom: '35%',
left: '0%',
color: '#F7861C',
},
onClick: this.updateCoin.bind(this),
}),
]),
h('#toCoin.ex-coins', marketinfo.pair.split('_')[1].toUpperCase()),
h('img', {
src: coinOptions[marketinfo.pair.split('_')[1].toUpperCase()].image,
width: '25px',
height: '25px',
style: {
marginLeft: '5px',
},
}),
]),
h('.flex-column', {
style: {
marginTop: '1%',
alignItems: 'flex-start',
},
}, [
this.props.warning ?
this.props.warning &&
h('span.error.flex-center', {
style: {
textAlign: 'center',
width: '229px',
height: '82px',
},
}, this.props.warning)
: this.renderInfo(),
this.renderRefundAddressForCoin(coin),
]),
])
}
ShapeshiftForm.prototype.renderRefundAddressForCoin = function (coin) {
return h(this.activeToggle('.input-container'), {
style: {
marginTop: '1%',
},
}, [
h('div', `${coin} Address:`),
h('input#fromCoinAddress.buy-inputs', {
type: 'text',
placeholder: `Your ${coin} Refund Address`,
dataset: {
persistentFormId: 'refund-address',
},
style: {
boxSizing: 'border-box',
width: '227px',
height: '30px',
padding: ' 5px ',
},
}),
h('i.fa.fa-pencil-square-o.edit-text', {
style: {
fontSize: '12px',
color: '#F7861C',
position: 'absolute',
},
}),
h('div.flex-row', {
style: {
justifyContent: 'flex-start',
},
}, [
h('button', {
onClick: this.shift.bind(this),
style: {
marginTop: '1%',
},
},
'Submit'),
]),
])
}
ShapeshiftForm.prototype.shift = function () {
var props = this.props
var withdrawal = this.props.buyView.buyAddress
var returnAddress = document.getElementById('fromCoinAddress').value
var pair = this.props.buyView.formView.marketinfo.pair
var data = {
'withdrawal': withdrawal,
'pair': pair,
'returnAddress': returnAddress,
// Public api key
'apiKey': '803d1f5df2ed1b1476e4b9e6bcd089e34d8874595dda6a23b67d93c56ea9cc2445e98a6748b219b2b6ad654d9f075f1f1db139abfa93158c04e825db122c14b6',
}
var message = [
`Deposit Limit: ${props.buyView.formView.marketinfo.limit}`,
`Deposit Minimum:${props.buyView.formView.marketinfo.minimum}`,
]
if (isValidAddress(withdrawal)) {
this.props.dispatch(actions.coinShiftRquest(data, message))
}
}
ShapeshiftForm.prototype.renderCoinList = function () {
var list = Object.keys(this.props.buyView.formView.coinOptions).map((item) => {
return h('option', {
value: item,
}, item)
})
return h('datalist#coinList', {
onClick: (event) => {
event.preventDefault()
},
}, list)
}
ShapeshiftForm.prototype.updateCoin = function (event) {
event.preventDefault()
const props = this.props
var coinOptions = this.props.buyView.formView.coinOptions
var coin = document.getElementById('fromCoin').value
if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') {
var message = 'Not a valid coin'
return props.dispatch(actions.displayWarning(message))
} else {
return props.dispatch(actions.pairUpdate(coin))
}
}
ShapeshiftForm.prototype.handleLiveInput = function () {
const props = this.props
var coinOptions = this.props.buyView.formView.coinOptions
var coin = document.getElementById('fromCoin').value
if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') {
return null
} else {
return props.dispatch(actions.pairUpdate(coin))
}
}
ShapeshiftForm.prototype.renderInfo = function () {
const marketinfo = this.props.buyView.formView.marketinfo
const coinOptions = this.props.buyView.formView.coinOptions
var coin = marketinfo.pair.split('_')[0].toUpperCase()
return h('span', {
style: {
},
}, [
h('h3.flex-row.text-transform-uppercase', {
style: {
color: '#868686',
paddingTop: '4px',
justifyContent: 'space-around',
textAlign: 'center',
fontSize: '17px',
},
}, `Market Info for ${marketinfo.pair.replace('_', ' to ').toUpperCase()}:`),
h('.marketinfo', ['Status : ', `${coinOptions[coin].status}`]),
h('.marketinfo', ['Exchange Rate: ', `${marketinfo.rate}`]),
h('.marketinfo', ['Limit: ', `${marketinfo.limit}`]),
h('.marketinfo', ['Minimum : ', `${marketinfo.minimum}`]),
])
}
ShapeshiftForm.prototype.activeToggle = function (elementType) {
if (!this.props.buyView.formView.response || this.props.warning) return elementType
return `${elementType}.inactive`
}
ShapeshiftForm.prototype.renderLoading = function () {
return h('span', {
style: {
position: 'absolute',
left: '70px',
bottom: '194px',
background: 'transparent',
width: '229px',
height: '82px',
display: 'flex',
justifyContent: 'center',
},
}, [
h('img', {
style: {
width: '60px',
},
src: 'images/loading.svg',
}),
])
}

@ -0,0 +1,204 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const vreme = new (require('vreme'))()
const explorerLink = require('etherscan-link').createExplorerLink
const actions = require('../../../ui/app/actions')
const addressSummary = require('../util').addressSummary
const CopyButton = require('./copyButton')
const EthBalance = require('./eth-balance')
const Tooltip = require('./tooltip')
module.exports = connect(mapStateToProps)(ShiftListItem)
function mapStateToProps (state) {
return {
conversionRate: state.metamask.conversionRate,
currentCurrency: state.metamask.currentCurrency,
}
}
inherits(ShiftListItem, Component)
function ShiftListItem () {
Component.call(this)
}
ShiftListItem.prototype.render = function () {
return (
h('.transaction-list-item.flex-row', {
style: {
paddingTop: '20px',
paddingBottom: '20px',
justifyContent: 'space-around',
alignItems: 'center',
},
}, [
h('div', {
style: {
width: '0px',
position: 'relative',
bottom: '19px',
},
}, [
h('img', {
src: 'https://info.shapeshift.io/sites/default/files/logo.png',
style: {
height: '35px',
width: '132px',
position: 'absolute',
clip: 'rect(0px,23px,34px,0px)',
},
}),
]),
this.renderInfo(),
this.renderUtilComponents(),
])
)
}
function formatDate (date) {
return vreme.format(new Date(date), 'March 16 2014 14:30')
}
ShiftListItem.prototype.renderUtilComponents = function () {
var props = this.props
const { conversionRate, currentCurrency } = props
switch (props.response.status) {
case 'no_deposits':
return h('.flex-row', [
h(CopyButton, {
value: this.props.depositAddress,
}),
h(Tooltip, {
title: 'QR Code',
}, [
h('i.fa.fa-qrcode.pointer.pop-hover', {
onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType)),
style: {
margin: '5px',
marginLeft: '23px',
marginRight: '12px',
fontSize: '20px',
color: '#F7861C',
},
}),
]),
])
case 'received':
return h('.flex-row')
case 'complete':
return h('.flex-row', [
h(CopyButton, {
value: this.props.response.transaction,
}),
h(EthBalance, {
value: `${props.response.outgoingCoin}`,
conversionRate,
currentCurrency,
width: '55px',
shorten: true,
needsParse: false,
incoming: true,
style: {
fontSize: '15px',
color: '#01888C',
},
}),
])
case 'failed':
return ''
default:
return ''
}
}
ShiftListItem.prototype.renderInfo = function () {
var props = this.props
switch (props.response.status) {
case 'no_deposits':
return h('.flex-column', {
style: {
width: '200px',
overflow: 'hidden',
},
}, [
h('div', {
style: {
fontSize: 'x-small',
color: '#ABA9AA',
width: '100%',
},
}, `${props.depositType} to ETH via ShapeShift`),
h('div', 'No deposits received'),
h('div', {
style: {
fontSize: 'x-small',
color: '#ABA9AA',
width: '100%',
},
}, formatDate(props.time)),
])
case 'received':
return h('.flex-column', {
style: {
width: '200px',
overflow: 'hidden',
},
}, [
h('div', {
style: {
fontSize: 'x-small',
color: '#ABA9AA',
width: '100%',
},
}, `${props.depositType} to ETH via ShapeShift`),
h('div', 'Conversion in progress'),
h('div', {
style: {
fontSize: 'x-small',
color: '#ABA9AA',
width: '100%',
},
}, formatDate(props.time)),
])
case 'complete':
var url = explorerLink(props.response.transaction, parseInt('1'))
return h('.flex-column.pointer', {
style: {
width: '200px',
overflow: 'hidden',
},
onClick: () => global.platform.openWindow({ url }),
}, [
h('div', {
style: {
fontSize: 'x-small',
color: '#ABA9AA',
width: '100%',
},
}, 'From ShapeShift'),
h('div', formatDate(props.time)),
h('div', {
style: {
fontSize: 'x-small',
color: '#ABA9AA',
width: '100%',
},
}, addressSummary(props.response.transaction)),
])
case 'failed':
return h('span.error', '(Failed)')
default:
return ''
}
}

@ -0,0 +1,37 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
module.exports = TabBar
inherits(TabBar, Component)
function TabBar () {
Component.call(this)
}
TabBar.prototype.render = function () {
const props = this.props
const state = this.state || {}
const { tabs = [], defaultTab, tabSelected } = props
const { subview = defaultTab } = state
return (
h('.flex-row.space-around.text-transform-uppercase', {
style: {
background: '#EBEBEB',
color: '#AEAEAE',
paddingTop: '4px',
minHeight: '30px',
},
}, tabs.map((tab) => {
const { key, content } = tab
return h(subview === key ? '.activeForm' : '.inactiveForm.pointer', {
onClick: () => {
this.setState({ subview: key })
tabSelected(key)
},
}, content)
}))
)
}

@ -0,0 +1,18 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
module.exports = NewComponent
inherits(NewComponent, Component)
function NewComponent () {
Component.call(this)
}
NewComponent.prototype.render = function () {
const props = this.props
return (
h('span', props.message)
)
}

@ -0,0 +1,72 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const Identicon = require('./identicon')
const prefixForNetwork = require('../../lib/etherscan-prefix-for-network')
module.exports = TokenCell
inherits(TokenCell, Component)
function TokenCell () {
Component.call(this)
}
TokenCell.prototype.render = function () {
const props = this.props
const { address, symbol, string, network, userAddress } = props
return (
h('li.token-cell', {
style: { cursor: network === '1' ? 'pointer' : 'default' },
onClick: this.view.bind(this, address, userAddress, network),
}, [
h(Identicon, {
diameter: 50,
address,
network,
}),
h('h3', `${string || 0} ${symbol}`),
h('span', { style: { flex: '1 0 auto' } }),
/*
h('button', {
onClick: this.send.bind(this, address),
}, 'SEND'),
*/
])
)
}
TokenCell.prototype.send = function (address, event) {
event.preventDefault()
event.stopPropagation()
const url = tokenFactoryFor(address)
if (url) {
navigateTo(url)
}
}
TokenCell.prototype.view = function (address, userAddress, network, event) {
const url = etherscanLinkFor(address, userAddress, network)
if (url) {
navigateTo(url)
}
}
function navigateTo (url) {
global.platform.openWindow({ url })
}
function etherscanLinkFor (tokenAddress, address, network) {
const prefix = prefixForNetwork(network)
return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}`
}
function tokenFactoryFor (tokenAddress) {
return `https://tokenfactory.surge.sh/#/token/${tokenAddress}`
}

@ -0,0 +1,207 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const TokenTracker = require('eth-token-tracker')
const TokenCell = require('./token-cell.js')
module.exports = TokenList
inherits(TokenList, Component)
function TokenList () {
this.state = {
tokens: [],
isLoading: true,
network: null,
}
Component.call(this)
}
TokenList.prototype.render = function () {
const state = this.state
const { tokens, isLoading, error } = state
const { userAddress, network } = this.props
if (isLoading) {
return this.message('Loading')
}
if (error) {
log.error(error)
return h('.hotFix', {
style: {
padding: '80px',
},
}, [
'We had trouble loading your token balances. You can view them ',
h('span.hotFix', {
style: {
color: 'rgba(247, 134, 28, 1)',
cursor: 'pointer',
},
onClick: () => {
global.platform.openWindow({
url: `https://ethplorer.io/address/${userAddress}`,
})
},
}, 'here'),
])
}
const tokenViews = tokens.map((tokenData) => {
tokenData.network = network
tokenData.userAddress = userAddress
return h(TokenCell, tokenData)
})
return h('.full-flex-height', [
this.renderTokenStatusBar(),
h('ol.full-flex-height.flex-column', {
style: {
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
},
}, [
h('style', `
li.token-cell {
display: flex;
flex-direction: row;
align-items: center;
padding: 10px;
min-height: 50px;
}
li.token-cell > h3 {
margin-left: 12px;
}
li.token-cell:hover {
background: white;
cursor: pointer;
}
`),
...tokenViews,
h('.flex-grow'),
]),
])
}
TokenList.prototype.renderTokenStatusBar = function () {
const { tokens } = this.state
let msg
if (tokens.length === 1) {
msg = `You own 1 token`
} else if (tokens.length > 1) {
msg = `You own ${tokens.length} tokens`
} else {
msg = `No tokens found`
}
return h('div', {
style: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
minHeight: '70px',
padding: '10px',
},
}, [
h('span', msg),
h('button', {
key: 'reveal-account-bar',
onClick: (event) => {
event.preventDefault()
this.props.addToken()
},
style: {
display: 'flex',
height: '40px',
padding: '10px',
justifyContent: 'center',
alignItems: 'center',
},
}, [
'ADD TOKEN',
]),
])
}
TokenList.prototype.message = function (body) {
return h('div', {
style: {
display: 'flex',
height: '250px',
alignItems: 'center',
justifyContent: 'center',
padding: '30px',
},
}, body)
}
TokenList.prototype.componentDidMount = function () {
this.createFreshTokenTracker()
}
TokenList.prototype.createFreshTokenTracker = function () {
if (this.tracker) {
// Clean up old trackers when refreshing:
this.tracker.stop()
this.tracker.removeListener('update', this.balanceUpdater)
this.tracker.removeListener('error', this.showError)
}
if (!global.ethereumProvider) return
const { userAddress } = this.props
this.tracker = new TokenTracker({
userAddress,
provider: global.ethereumProvider,
tokens: this.props.tokens,
pollingInterval: 8000,
})
// Set up listener instances for cleaning up
this.balanceUpdater = this.updateBalances.bind(this)
this.showError = (error) => {
this.setState({ error, isLoading: false })
}
this.tracker.on('update', this.balanceUpdater)
this.tracker.on('error', this.showError)
this.tracker.updateBalances()
.then(() => {
this.updateBalances(this.tracker.serialize())
})
.catch((reason) => {
log.error(`Problem updating balances`, reason)
this.setState({ isLoading: false })
})
}
TokenList.prototype.componentWillUpdate = function (nextProps) {
if (nextProps.network === 'loading') return
const oldNet = this.props.network
const newNet = nextProps.network
if (oldNet && newNet && newNet !== oldNet) {
this.setState({ isLoading: true })
this.createFreshTokenTracker()
}
}
TokenList.prototype.updateBalances = function (tokens) {
const heldTokens = tokens.filter(token => {
return token.balance !== '0' && token.string !== '0.000'
})
this.setState({ tokens: heldTokens, isLoading: false })
}
TokenList.prototype.componentWillUnmount = function () {
if (!this.tracker) return
this.tracker.stop()
}

@ -0,0 +1,22 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const ReactTooltip = require('react-tooltip-component')
module.exports = Tooltip
inherits(Tooltip, Component)
function Tooltip () {
Component.call(this)
}
Tooltip.prototype.render = function () {
const props = this.props
const { position, title, children } = props
return h(ReactTooltip, {
position: position || 'left',
title,
fixed: true,
}, children)
}

@ -0,0 +1,68 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const Tooltip = require('./tooltip')
const Identicon = require('./identicon')
module.exports = TransactionIcon
inherits(TransactionIcon, Component)
function TransactionIcon () {
Component.call(this)
}
TransactionIcon.prototype.render = function () {
const { transaction, txParams, isMsg } = this.props
switch (transaction.status) {
case 'unapproved':
return h(!isMsg ? '.unapproved-tx-icon' : 'i.fa.fa-certificate.fa-lg')
case 'rejected':
return h('i.fa.fa-exclamation-triangle.fa-lg.warning', {
style: {
width: '24px',
},
})
case 'failed':
return h('i.fa.fa-exclamation-triangle.fa-lg.error', {
style: {
width: '24px',
},
})
case 'submitted':
return h(Tooltip, {
title: 'Pending',
position: 'right',
}, [
h('i.fa.fa-ellipsis-h', {
style: {
fontSize: '27px',
},
}),
])
}
if (isMsg) {
return h('i.fa.fa-certificate.fa-lg', {
style: {
width: '24px',
},
})
}
if (txParams.to) {
return h(Identicon, {
diameter: 24,
address: txParams.to || transaction.hash,
})
} else {
return h('i.fa.fa-file-text-o.fa-lg', {
style: {
width: '24px',
},
})
}
}

@ -0,0 +1,175 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const EthBalance = require('./eth-balance')
const addressSummary = require('../util').addressSummary
const explorerLink = require('etherscan-link').createExplorerLink
const CopyButton = require('./copyButton')
const vreme = new (require('vreme'))()
const Tooltip = require('./tooltip')
const numberToBN = require('number-to-bn')
const TransactionIcon = require('./transaction-list-item-icon')
const ShiftListItem = require('./shift-list-item')
module.exports = TransactionListItem
inherits(TransactionListItem, Component)
function TransactionListItem () {
Component.call(this)
}
TransactionListItem.prototype.render = function () {
const { transaction, network, conversionRate, currentCurrency } = this.props
if (transaction.key === 'shapeshift') {
if (network === '1') return h(ShiftListItem, transaction)
}
var date = formatDate(transaction.time)
let isLinkable = false
const numericNet = parseInt(network)
isLinkable = numericNet === 1 || numericNet === 3 || numericNet === 4 || numericNet === 42
var isMsg = ('msgParams' in transaction)
var isTx = ('txParams' in transaction)
var isPending = transaction.status === 'unapproved'
let txParams
if (isTx) {
txParams = transaction.txParams
} else if (isMsg) {
txParams = transaction.msgParams
}
const nonce = txParams.nonce ? numberToBN(txParams.nonce).toString(10) : ''
const isClickable = ('hash' in transaction && isLinkable) || isPending
return (
h(`.transaction-list-item.flex-row.flex-space-between${isClickable ? '.pointer' : ''}`, {
onClick: (event) => {
if (isPending) {
this.props.showTx(transaction.id)
}
event.stopPropagation()
if (!transaction.hash || !isLinkable) return
var url = explorerLink(transaction.hash, parseInt(network))
global.platform.openWindow({ url })
},
style: {
padding: '20px 0',
display: 'flex',
justifyContent: 'space-between',
},
}, [
h('.identicon-wrapper.flex-column.flex-center.select-none', [
h(TransactionIcon, { txParams, transaction, isTx, isMsg }),
]),
h(Tooltip, {
title: 'Transaction Number',
position: 'right',
}, [
h('span', {
style: {
display: 'flex',
cursor: 'normal',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
},
}, nonce),
]),
h('.flex-column', {style: {width: '150px', overflow: 'hidden'}}, [
domainField(txParams),
h('div', date),
recipientField(txParams, transaction, isTx, isMsg),
]),
// Places a copy button if tx is successful, else places a placeholder empty div.
transaction.hash ? h(CopyButton, { value: transaction.hash }) : h('div', {style: { display: 'flex', alignItems: 'center', width: '26px' }}),
isTx ? h(EthBalance, {
value: txParams.value,
conversionRate,
currentCurrency,
shorten: true,
showFiat: false,
style: {fontSize: '15px'},
}) : h('.flex-column'),
])
)
}
function domainField (txParams) {
return h('div', {
style: {
fontSize: 'x-small',
color: '#ABA9AA',
overflow: 'hidden',
textOverflow: 'ellipsis',
width: '100%',
},
}, [
txParams.origin,
])
}
function recipientField (txParams, transaction, isTx, isMsg) {
let message
if (isMsg) {
message = 'Signature Requested'
} else if (txParams.to) {
message = addressSummary(txParams.to)
} else {
message = 'Contract Published'
}
return h('div', {
style: {
fontSize: 'x-small',
color: '#ABA9AA',
},
}, [
message,
renderErrorOrWarning(transaction),
])
}
function formatDate (date) {
return vreme.format(new Date(date), 'March 16 2014 14:30')
}
function renderErrorOrWarning (transaction) {
const { status, err, warning } = transaction
// show rejected
if (status === 'rejected') {
return h('span.error', ' (Rejected)')
}
// show error
if (err) {
const message = err.message || ''
return (
h(Tooltip, {
title: message,
position: 'bottom',
}, [
h(`span.error`, ` (Failed)`),
])
)
}
// show warning
if (warning) {
const message = warning.message
return h(Tooltip, {
title: message,
position: 'bottom',
}, [
h(`span.warning`, ` (Warning)`),
])
}
}

@ -0,0 +1,87 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const TransactionListItem = require('./transaction-list-item')
module.exports = TransactionList
inherits(TransactionList, Component)
function TransactionList () {
Component.call(this)
}
TransactionList.prototype.render = function () {
const { transactions, network, unapprovedMsgs, conversionRate } = this.props
var shapeShiftTxList
if (network === '1') {
shapeShiftTxList = this.props.shapeShiftTxList
}
const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList)
.sort((a, b) => b.time - a.time)
return (
h('section.transaction-list.full-flex-height', {
style: {
justifyContent: 'center',
},
}, [
h('style', `
.transaction-list .transaction-list-item:not(:last-of-type) {
border-bottom: 1px solid #D4D4D4;
}
.transaction-list .transaction-list-item .ether-balance-label {
display: block !important;
font-size: small;
}
`),
h('.tx-list', {
style: {
overflowY: 'auto',
height: '100%',
padding: '0 25px 0 15px',
textAlign: 'center',
},
}, [
txsToRender.length
? txsToRender.map((transaction, i) => {
let key
switch (transaction.key) {
case 'shapeshift':
const { depositAddress, time } = transaction
key = `shift-tx-${depositAddress}-${time}-${i}`
break
default:
key = `tx-${transaction.id}-${i}`
}
return h(TransactionListItem, {
transaction, i, network, key,
conversionRate,
showTx: (txId) => {
this.props.viewPendingTx(txId)
},
})
})
: h('.flex-center.full-flex-height', {
style: {
flexDirection: 'column',
justifyContent: 'center',
},
}, [
h('p', {
style: {
marginTop: '50px',
},
}, 'No transaction history.'),
]),
]),
])
)
}

@ -0,0 +1,42 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const extend = require('xtend')
module.exports = TypedMessageRenderer
inherits(TypedMessageRenderer, Component)
function TypedMessageRenderer () {
Component.call(this)
}
TypedMessageRenderer.prototype.render = function () {
const props = this.props
const { value, style } = props
const text = renderTypedData(value)
const defaultStyle = extend({
width: '315px',
maxHeight: '210px',
resize: 'none',
border: 'none',
background: 'white',
padding: '3px',
overflow: 'scroll',
}, style)
return (
h('div.font-small', {
style: defaultStyle,
}, text)
)
}
function renderTypedData (values) {
return values.map(function (value) {
return h('div', {}, [
h('strong', {style: {display: 'block', fontWeight: 'bold'}}, String(value.name) + ':'),
h('div', {}, value.value),
])
})
}

@ -0,0 +1,235 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('../../ui/app/actions')
const NetworkIndicator = require('./components/network')
const txHelper = require('../lib/tx-helper')
const isPopupOrNotification = require('../../app/scripts/lib/is-popup-or-notification')
const PendingTx = require('./components/pending-tx')
const PendingMsg = require('./components/pending-msg')
const PendingPersonalMsg = require('./components/pending-personal-msg')
const PendingTypedMsg = require('./components/pending-typed-msg')
const Loading = require('./components/loading')
module.exports = connect(mapStateToProps)(ConfirmTxScreen)
function mapStateToProps (state) {
return {
identities: state.metamask.identities,
accounts: state.metamask.accounts,
selectedAddress: state.metamask.selectedAddress,
unapprovedTxs: state.metamask.unapprovedTxs,
unapprovedMsgs: state.metamask.unapprovedMsgs,
unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs,
unapprovedTypedMessages: state.metamask.unapprovedTypedMessages,
index: state.appState.currentView.context,
warning: state.appState.warning,
network: state.metamask.network,
provider: state.metamask.provider,
conversionRate: state.metamask.conversionRate,
currentCurrency: state.metamask.currentCurrency,
blockGasLimit: state.metamask.currentBlockGasLimit,
computedBalances: state.metamask.computedBalances,
}
}
inherits(ConfirmTxScreen, Component)
function ConfirmTxScreen () {
Component.call(this)
}
ConfirmTxScreen.prototype.render = function () {
const props = this.props
const { network, provider, unapprovedTxs, currentCurrency, computedBalances,
unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, conversionRate, blockGasLimit } = props
var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, network)
var txData = unconfTxList[props.index] || {}
var txParams = txData.params || {}
var isNotification = isPopupOrNotification() === 'notification'
log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`)
if (unconfTxList.length === 0) return h(Loading, { isLoading: true })
const unconfTxListLength = unconfTxList.length
return (
h('.flex-column.flex-grow', [
// subtitle and nav
h('.section-title.flex-row.flex-center', [
!isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: this.goHome.bind(this),
}) : null,
h('h2.page-subtitle', 'Confirm Transaction'),
isNotification ? h(NetworkIndicator, {
network: network,
provider: provider,
}) : null,
]),
h('h3', {
style: {
alignSelf: 'center',
display: unconfTxList.length > 1 ? 'block' : 'none',
},
}, [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
style: {
display: props.index === 0 ? 'none' : 'inline-block',
},
onClick: () => props.dispatch(actions.previousTx()),
}),
` ${props.index + 1} of ${unconfTxList.length} `,
h('i.fa.fa-arrow-right.fa-lg.cursor-pointer', {
style: {
display: props.index + 1 === unconfTxList.length ? 'none' : 'inline-block',
},
onClick: () => props.dispatch(actions.nextTx()),
}),
]),
warningIfExists(props.warning),
currentTxView({
// Properties
txData: txData,
key: txData.id,
selectedAddress: props.selectedAddress,
accounts: props.accounts,
identities: props.identities,
conversionRate,
currentCurrency,
blockGasLimit,
unconfTxListLength,
computedBalances,
// Actions
buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress),
sendTransaction: this.sendTransaction.bind(this),
cancelTransaction: this.cancelTransaction.bind(this, txData),
cancelAllTransactions: this.cancelAllTransactions.bind(this, unconfTxList),
signMessage: this.signMessage.bind(this, txData),
signPersonalMessage: this.signPersonalMessage.bind(this, txData),
signTypedMessage: this.signTypedMessage.bind(this, txData),
cancelMessage: this.cancelMessage.bind(this, txData),
cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData),
cancelTypedMessage: this.cancelTypedMessage.bind(this, txData),
}),
])
)
}
function currentTxView (opts) {
log.info('rendering current tx view')
const { txData } = opts
const { txParams, msgParams, type } = txData
if (txParams) {
log.debug('txParams detected, rendering pending tx')
return h(PendingTx, opts)
} else if (msgParams) {
log.debug('msgParams detected, rendering pending msg')
if (type === 'eth_sign') {
log.debug('rendering eth_sign message')
return h(PendingMsg, opts)
} else if (type === 'personal_sign') {
log.debug('rendering personal_sign message')
return h(PendingPersonalMsg, opts)
} else if (type === 'eth_signTypedData') {
log.debug('rendering eth_signTypedData message')
return h(PendingTypedMsg, opts)
}
}
}
ConfirmTxScreen.prototype.buyEth = function (address, event) {
event.preventDefault()
this.props.dispatch(actions.buyEthView(address))
}
ConfirmTxScreen.prototype.sendTransaction = function (txData, event) {
this.stopPropagation(event)
this.props.dispatch(actions.updateAndApproveTx(txData))
}
ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) {
this.stopPropagation(event)
event.preventDefault()
this.props.dispatch(actions.cancelTx(txData))
}
ConfirmTxScreen.prototype.cancelAllTransactions = function (unconfTxList, event) {
this.stopPropagation(event)
event.preventDefault()
this.props.dispatch(actions.cancelAllTx(unconfTxList))
}
ConfirmTxScreen.prototype.signMessage = function (msgData, event) {
log.info('conf-tx.js: signing message')
var params = msgData.msgParams
params.metamaskId = msgData.id
this.stopPropagation(event)
this.props.dispatch(actions.signMsg(params))
}
ConfirmTxScreen.prototype.stopPropagation = function (event) {
if (event.stopPropagation) {
event.stopPropagation()
}
}
ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) {
log.info('conf-tx.js: signing personal message')
var params = msgData.msgParams
params.metamaskId = msgData.id
this.stopPropagation(event)
this.props.dispatch(actions.signPersonalMsg(params))
}
ConfirmTxScreen.prototype.signTypedMessage = function (msgData, event) {
log.info('conf-tx.js: signing typed message')
var params = msgData.msgParams
params.metamaskId = msgData.id
this.stopPropagation(event)
this.props.dispatch(actions.signTypedMsg(params))
}
ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) {
log.info('canceling message')
this.stopPropagation(event)
this.props.dispatch(actions.cancelMsg(msgData))
}
ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) {
log.info('canceling personal message')
this.stopPropagation(event)
this.props.dispatch(actions.cancelPersonalMsg(msgData))
}
ConfirmTxScreen.prototype.cancelTypedMessage = function (msgData, event) {
log.info('canceling typed message')
this.stopPropagation(event)
this.props.dispatch(actions.cancelTypedMsg(msgData))
}
ConfirmTxScreen.prototype.goHome = function (event) {
this.stopPropagation(event)
this.props.dispatch(actions.goHome())
}
function warningIfExists (warning) {
if (warning &&
// Do not display user rejections on this screen:
warning.indexOf('User denied transaction signature') === -1) {
return h('.error', {
style: {
margin: 'auto',
},
}, warning)
}
}

@ -0,0 +1,222 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('../../ui/app/actions')
const infuraCurrencies = require('./infura-conversion.json').objects.sort((a, b) => {
return a.quote.name.toLocaleLowerCase().localeCompare(b.quote.name.toLocaleLowerCase())
})
const validUrl = require('valid-url')
const exportAsFile = require('./util').exportAsFile
const Modal = require('../../ui/app/components/modals/index').Modal
module.exports = connect(mapStateToProps)(ConfigScreen)
function mapStateToProps (state) {
return {
metamask: state.metamask,
warning: state.appState.warning,
}
}
inherits(ConfigScreen, Component)
function ConfigScreen () {
Component.call(this)
}
ConfigScreen.prototype.render = function () {
var state = this.props
var metamaskState = state.metamask
var warning = state.warning
return (
h('.flex-column.flex-grow', [
h(Modal, {}, []),
// subtitle and nav
h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: (event) => {
state.dispatch(actions.goHome())
},
}),
h('h2.page-subtitle', 'Settings'),
]),
h('.error', {
style: {
display: warning ? 'block' : 'none',
padding: '0 20px',
textAlign: 'center',
},
}, warning),
// conf view
h('.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-space-around', {
style: {
padding: '20px',
},
}, [
currentProviderDisplay(metamaskState),
h('div', { style: {display: 'flex'} }, [
h('input#new_rpc', {
placeholder: 'New RPC URL',
style: {
width: 'inherit',
flex: '1 0 auto',
height: '30px',
margin: '8px',
},
onKeyPress (event) {
if (event.key === 'Enter') {
var element = event.target
var newRpc = element.value
rpcValidation(newRpc, state)
}
},
}),
h('button', {
style: {
alignSelf: 'center',
},
onClick (event) {
event.preventDefault()
var element = document.querySelector('input#new_rpc')
var newRpc = element.value
rpcValidation(newRpc, state)
},
}, 'Save'),
]),
h('hr.horizontal-line'),
currentConversionInformation(metamaskState, state),
h('hr.horizontal-line'),
h('div', {
style: {
marginTop: '20px',
},
}, [
h('p', {
style: {
fontFamily: 'Montserrat Light',
fontSize: '13px',
},
}, `State logs contain your public account addresses and sent transactions.`),
h('br'),
h('button', {
style: {
alignSelf: 'center',
},
onClick (event) {
window.logStateString((err, result) => {
if (err) {
state.dispatch(actions.displayWarning('Error in retrieving state logs.'))
} else {
exportAsFile('MetaMask State Logs.json', result)
}
})
},
}, 'Download State Logs'),
]),
h('hr.horizontal-line'),
h('div', {
style: {
marginTop: '20px',
},
}, [
h('button', {
style: {
alignSelf: 'center',
},
onClick (event) {
event.preventDefault()
state.dispatch(actions.revealSeedConfirmation())
},
}, 'Reveal Seed Words'),
]),
]),
]),
])
)
}
function rpcValidation (newRpc, state) {
if (validUrl.isWebUri(newRpc)) {
state.dispatch(actions.setRpcTarget(newRpc))
} else {
var appendedRpc = `http://${newRpc}`
if (validUrl.isWebUri(appendedRpc)) {
state.dispatch(actions.displayWarning('URIs require the appropriate HTTP/HTTPS prefix.'))
} else {
state.dispatch(actions.displayWarning('Invalid RPC URI'))
}
}
}
function currentConversionInformation (metamaskState, state) {
var currentCurrency = metamaskState.currentCurrency
var conversionDate = metamaskState.conversionDate
return h('div', [
h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, 'Current Conversion'),
h('span', {style: { fontWeight: 'bold', paddingRight: '10px', fontSize: '13px'}}, `Updated ${Date(conversionDate)}`),
h('select#currentCurrency', {
onChange (event) {
event.preventDefault()
var element = document.getElementById('currentCurrency')
var newCurrency = element.value
state.dispatch(actions.setCurrentCurrency(newCurrency))
},
defaultValue: currentCurrency,
}, infuraCurrencies.map((currency) => {
return h('option', {key: currency.quote.code, value: currency.quote.code}, `${currency.quote.code.toUpperCase()} - ${currency.quote.name}`)
})
),
])
}
function currentProviderDisplay (metamaskState) {
var provider = metamaskState.provider
var title, value
switch (provider.type) {
case 'mainnet':
title = 'Current Network'
value = 'Main Ethereum Network'
break
case 'ropsten':
title = 'Current Network'
value = 'Ropsten Test Network'
break
case 'kovan':
title = 'Current Network'
value = 'Kovan Test Network'
break
case 'rinkeby':
title = 'Current Network'
value = 'Rinkeby Test Network'
break
default:
title = 'Current RPC'
value = metamaskState.provider.rpcTarget
}
return h('div', [
h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, title),
h('span', value),
])
}

@ -0,0 +1,21 @@
/*
debug / dev
*/
#app-content {
border: 2px solid green;
}
#design-container {
position: absolute;
left: 360px;
top: -42px;
width: calc(100vw - 360px);
height: 100vh;
overflow: scroll;
}
#design-container img {
width: 2000px;
margin-right: 600px;
}

@ -0,0 +1,36 @@
@import url(https://fonts.googleapis.com/css?family=Roboto:300,500);
@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css);
@font-face {
font-family: 'Montserrat Regular';
src: url('/fonts/Montserrat/Montserrat-Regular.woff') format('woff');
src: url('/fonts/Montserrat/Montserrat-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-size: 'small';
}
@font-face {
font-family: 'Montserrat Bold';
src: url('/fonts/Montserrat/Montserrat-Bold.woff') format('woff');
src: url('/fonts/Montserrat/Montserrat-Bold.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Montserrat Light';
src: url('/fonts/Montserrat/Montserrat-Light.woff') format('woff');
src: url('/fonts/Montserrat/Montserrat-Light.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Montserrat UltraLight';
src: url('/fonts/Montserrat/Montserrat-UltraLight.woff') format('woff');
src: url('/fonts/Montserrat/Montserrat-UltraLight.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}

@ -0,0 +1,761 @@
/*
faint orange (textfield shades) #FAF6F0
light orange (button shades): #F5C26D
dark orange (text): #F5A623
borders/font/any gray: #4A4A4A
*/
/*
application specific styles
*/
* {
box-sizing: border-box;
}
html, body {
font-family: 'Montserrat Regular', Arial;
color: #4D4D4D;
font-weight: 300;
line-height: 1.4em;
background: #F7F7F7;
margin: 0;
padding: 0;
}
html {
min-height: 500px;
}
.app-root {
overflow: hidden;
position: relative
}
.app-primary {
display: flex;
}
input:focus, textarea:focus {
outline: none;
}
.full-size {
height: 100%;
width: 100%;
}
.full-width {
width: 100%;
}
.full-height {
height: 100%;
}
.full-flex-height {
display: flex;
flex: 1 1 auto;
flex-direction: column;
}
#app-content {
overflow-x: hidden;
height: 100%;
display: flex;
flex-direction: column;
}
button, input[type="submit"] {
font-family: 'Montserrat Bold';
outline: none;
cursor: pointer;
padding: 8px 12px;
border: none;
color: white;
transform-origin: center center;
transition: transform 50ms ease-in;
/* default orange */
background: rgba(247, 134, 28, 1);
box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36);
}
.btn-green, input[type="submit"].btn-green {
background: rgba(106, 195, 96, 1);
box-shadow: 0px 3px 6px rgba(106, 195, 96, 0.36);
}
.btn-red {
background: rgba(254, 35, 17, 1);
box-shadow: 0px 3px 6px rgba(254, 35, 17, 0.36);
}
button[disabled], input[type="submit"][disabled] {
cursor: not-allowed;
background: rgba(197, 197, 197, 1);
box-shadow: 0px 3px 6px rgba(197, 197, 197, 0.36);
}
button.spaced {
margin: 2px;
}
button:not([disabled]):hover, input[type="submit"]:not([disabled]):hover {
transform: scale(1.1);
}
button:not([disabled]):active, input[type="submit"]:not([disabled]):active {
transform: scale(0.95);
}
.grow-on-hover:hover {
transform: scale(1.05);
}
a {
text-decoration: none;
color: inherit;
}
a:hover{
color: #df6b0e;
}
/*
app
*/
.active {
color: #909090;
}
button.primary {
padding: 8px 12px;
background: #F7861C;
box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36);
color: white;
font-size: 1.1em;
font-family: 'Montserrat Regular';
text-transform: uppercase;
}
button.btn-thin {
border: 1px solid;
border-color: #4D4D4D;
color: #4D4D4D;
background: rgb(255, 174, 41);
border-radius: 4px;
min-width: 200px;
margin: 12px 0;
padding: 6px;
font-size: 13px;
}
.app-header {
padding: 6px 8px;
}
.app-header h1 {
font-family: 'Montserrat Regular';
text-transform: uppercase;
color: #AEAEAE;
}
h2.page-subtitle {
font-family: 'Montserrat Regular';
text-transform: uppercase;
color: #AEAEAE;
font-size: 1em;
margin: 12px;
}
.app-footer {
padding-bottom: 10px;
align-items: center;
}
.identicon {
height: 46px;
width: 46px;
background-size: cover;
border-radius: 100%;
border: 3px solid gray;
}
textarea.twelve-word-phrase {
padding: 12px;
width: 300px;
height: 140px;
font-size: 16px;
background: white;
resize: none;
}
.network-indicator {
display: flex;
align-items: center;
font-size: 0.6em;
}
.network-name {
width: 5.2em;
line-height: 9px;
text-rendering: geometricPrecision;
}
.check {
margin-left: 12px;
color: #F7861C;
flex: 1 0 auto;
display: flex;
justify-content: flex-end;
}
/*
app sections
*/
/* initialize */
.initialize-screen hr {
width: 60px;
margin: 12px;
border-color: #F7861C;
border-style: solid;
}
.initialize-screen label {
margin-top: 20px;
}
.initialize-screen button.create-vault {
margin-top: 40px;
}
.initialize-screen .warning {
font-size: 14px;
margin: 0 16px;
}
/* unlock */
.error {
color: #f7861c;
margin-bottom: 9px;
}
.warning {
color: #FFAE00;
}
.lock {
width: 50px;
height: 50px;
}
.lock.locked {
transform: scale(1.5);
opacity: 0.0;
transition: opacity 400ms ease-in, transform 400ms ease-in;
}
.lock.unlocked {
transform: scale(1);
opacity: 1;
transition: opacity 500ms ease-out, transform 500ms ease-out, background 200ms ease-in;
}
.lock.locked .lock-top {
transform: scaleX(1) translateX(0);
transition: transform 250ms ease-in;
}
.lock.unlocked .lock-top {
transform: scaleX(-1) translateX(-12px);
transition: transform 250ms ease-in;
}
.lock.unlocked:hover {
border-radius: 4px;
background: #e5e5e5;
border: 1px solid #b1b1b1;
}
.lock.unlocked:active {
background: #c3c3c3;
}
.section-title .fa-arrow-left {
margin: -2px 8px 0px -8px;
}
.unlock-screen #metamask-mascot-container {
margin-top: 24px;
}
.unlock-screen h1 {
margin-top: -28px;
margin-bottom: 42px;
}
.unlock-screen input[type=password] {
width: 260px;
/*height: 36px;
margin-bottom: 24px;
padding: 8px;*/
}
.sizing-input{
font-size: 14px;
height: 30px;
padding-left: 5px;
}
.editable-label{
display: flex;
}
/* Webkit */
.unlock-screen input::-webkit-input-placeholder {
text-align: center;
font-size: 1.2em;
}
/* Firefox 18- */
.unlock-screen input:-moz-placeholder {
text-align: center;
font-size: 1.2em;
}
/* Firefox 19+ */
.unlock-screen input::-moz-placeholder {
text-align: center;
font-size: 1.2em;
}
/* IE */
.unlock-screen input:-ms-input-placeholder {
text-align: center;
font-size: 1.2em;
}
input.large-input, textarea.large-input {
/*margin-bottom: 24px;*/
padding: 8px;
}
input.large-input {
height: 36px;
}
.letter-spacey {
letter-spacing: 0.1em;
}
/* accounts */
.accounts-section {
margin: 0 0px;
}
.accounts-section .horizontal-line {
margin: 0px 18px;
}
.accounts-list-option {
height: 120px;
}
.accounts-list-option .identicon-wrapper {
width: 100px;
}
.unconftx-link {
margin-top: 24px;
cursor: pointer;
}
.unconftx-link .fa-arrow-right {
margin: 0px -8px 0px 8px;
}
/* identity panel */
.identity-panel {
font-weight: 500;
}
.identity-panel .identicon-wrapper {
margin: 4px;
margin-top: 8px;
display: flex;
align-items: center;
}
.identity-panel .identicon-wrapper span {
margin: 0 auto;
}
.identity-panel .identity-data {
margin: 8px 8px 8px 18px;
}
.identity-panel i {
margin-top: 32px;
margin-right: 6px;
color: #B9B9B9;
}
.identity-panel .arrow-right {
padding-left: 18px;
width: 42px;
min-width: 18px;
height: 100%;
}
.identity-copy.flex-column {
flex: 0.25 0 auto;
justify-content: center;
}
/* accounts screen */
.identity-section {
}
.identity-section .identity-panel {
background: #E9E9E9;
border-bottom: 1px solid #B1B1B1;
cursor: pointer;
}
.identity-section .identity-panel.selected {
background: white;
color: #F3C83E;
}
.identity-section .identity-panel.selected .identicon {
border-color: orange;
}
.identity-section .accounts-list-option:hover,
.identity-section .accounts-list-option.selected {
background:white;
}
/* account detail screen */
.account-detail-section {
display: flex;
flex-wrap: wrap;
overflow-y: auto;
flex-direction: inherit;
.name-label {
margin-left: 15px;
}
}
.grow-tenx {
flex-grow: 10;
}
.name-label{
}
.unapproved-tx-icon {
height: 16px;
width: 16px;
background: rgb(47, 174, 244);
border-color: #AEAEAE;
border-radius: 13px;
}
.edit-text {
height: 100%;
visibility: hidden;
}
.editing-label {
display: flex;
justify-content: flex-start;
margin-left: 50px;
margin-bottom: 2px;
font-size: 11px;
text-rendering: geometricPrecision;
color: #F7861C;
}
.name-label:hover .edit-text {
visibility: visible;
}
/* tx confirm */
.unconftx-section input[type=password] {
height: 22px;
padding: 2px;
margin: 12px;
margin-bottom: 24px;
border-radius: 4px;
border: 2px solid #F3C83E;
background: #FAF6F0;
}
/* Send Screen */
.send-screen {
}
.send-screen section {
margin: 8px 16px;
}
.send-screen input {
width: 100%;
font-size: 12px;
}
/* Ether Balance Widget */
.ether-balance-amount {
color: #F7861C;
}
.ether-balance-label {
color: #ABA9AA;
}
/* Info screen */
.info-gray{
font-family: 'Montserrat Regular';
text-transform: uppercase;
color: #AEAEAE;
}
.icon-size{
width: 20px;
}
.info{
font-family: 'Montserrat Regular', Arial;
padding-bottom: 10px;
display: inline-block;
padding-left: 5px;
}
/* buy eth warning screen */
.custom-radios {
justify-content: space-around;
align-items: center;
}
.custom-radio-selected {
width: 17px;
height: 17px;
border: solid;
border-style: double;
border-radius: 15px;
border-width: 5px;
background: rgba(247, 134, 28, 1);
border-color: #F7F7F7;
}
.custom-radio-inactive {
width: 14px;
height: 14px;
border: solid;
border-width: 1px;
border-radius: 24px;
border-color: #AEAEAE;
}
.radio-titles {
color: rgba(247, 134, 28, 1);
}
.radio-titles-subtext {
}
.selected-exchange {
}
.buy-radio {
}
.eth-warning{
transition: opacity 400ms ease-in, transform 400ms ease-in;
}
.buy-subview{
transition: opacity 400ms ease-in, transform 400ms ease-in;
}
.input-container:hover .edit-text{
visibility: visible;
}
.buy-inputs{
font-family: 'Montserrat Light';
font-size: 13px;
height: 20px;
background: transparent;
box-sizing: border-box;
border: solid;
border-color: transparent;
border-width: 0.5px;
border-radius: 2px;
}
.input-container:hover .buy-inputs{
box-sizing: inherit;
border: solid;
border-color: #F7861C;
border-width: 0.5px;
border-radius: 2px;
}
.buy-inputs:focus{
border: solid;
border-color: #F7861C;
border-width: 0.5px;
border-radius: 2px;
}
.activeForm {
background: #F7F7F7;
border: none;
border-radius: 8px 8px 0px 0px;
width: 50%;
text-align: center;
padding-bottom: 4px;
}
.inactiveForm {
border: none;
border-radius: 8px 8px 0px 0px;
width: 50%;
text-align: center;
padding-bottom: 4px;
}
.ex-coins {
font-family: 'Montserrat Regular';
text-transform: uppercase;
text-align: center;
font-size: 33px;
width: 118px;
height: 42px;
padding: 1px;
color: #4D4D4D;
}
.marketinfo{
font-family: 'Montserrat light';
color: #AEAEAE;
font-size: 15px;
line-height: 17px;
}
#fromCoin::-webkit-calendar-picker-indicator {
display: none;
}
#coinList {
width: 400px;
height: 500px;
overflow: scroll;
}
.icon-control .fa-refresh{
visibility: hidden;
}
.icon-control:hover .fa-refresh{
visibility: visible;
}
.icon-control:hover .fa-chevron-right{
visibility: hidden;
}
.inactive {
color: #AEAEAE;
}
.inactive button{
background: #AEAEAE;
color: white;
}
.ellip-address {
overflow: hidden;
text-overflow: ellipsis;
width: 5em;
font-size: 14px;
font-family: "Montserrat Light";
margin-left: 5px;
}
.qr-header {
font-size: 25px;
margin-top: 40px;
}
.qr-message {
font-size: 12px;
color: #F7861C;
}
div.message-container > div:first-child {
margin-top: 18px;
font-size: 15px;
color: #4D4D4D;
}
.pop-hover:hover {
transform: scale(1.1);
}
//Notification Modal
.notification-modal-wrapper {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
position: relative;
border: 1px solid #dedede;
box-shadow: 0 0 2px 2px #dedede;
font-family: Roboto;
}
.notification-modal-header {
background: #f6f6f6;
width: 100%;
display: flex;
justify-content: center;
padding: 30px;
font-size: 22px;
color: #1b344d;
height: 79px;
}
.notification-modal-message {
padding: 20px;
}
.notification-modal-message {
width: 100%;
display: flex;
justify-content: center;
font-size: 17px;
color: #1b344d;
}
.modal-close-x::after {
content: '\00D7';
font-size: 2em;
color: #9b9b9b;
position: absolute;
top: 25px;
right: 17.5px;
font-family: sans-serif;
cursor: pointer;
}

@ -0,0 +1,306 @@
/* color */
.color-orange {
color: #F7861C;
}
.color-forest {
color: #0A5448;
}
/* lib */
.full-width {
width: 100%;
}
.full-height {
height: 100%;
}
.flex-column {
display: flex;
flex-direction: column;
}
.space-between {
justify-content: space-between;
}
.space-around {
justify-content: space-around;
}
.flex-column-bottom {
display: flex;
flex-direction: column-reverse;
}
.flex-row {
display: flex;
flex-direction: row;
}
.flex-space-between {
justify-content: space-between;
}
.flex-space-around {
justify-content: space-around;
}
.flex-right {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.flex-left {
display: flex;
flex-direction: row;
justify-content: flex-start;
}
.flex-fixed {
flex: none;
}
.flex-basis-auto {
flex-basis: auto;
}
.flex-grow {
flex: 1 1 auto;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.flex-justify-center {
justify-content: center;
}
.flex-align-center {
align-items: center;
}
.flex-self-end {
align-self: flex-end;
}
.flex-self-stretch {
align-self: stretch;
}
.flex-vertical {
flex-direction: column;
}
.z-bump {
z-index: 1;
}
.select-none {
cursor: inherit;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.pointer {
cursor: pointer;
}
.cursor-pointer {
cursor: pointer;
transform-origin: center center;
transition: transform 50ms ease-in-out;
}
.cursor-pointer:hover {
transform: scale(1.1);
}
.cursor-pointer:active {
transform: scale(0.95);
}
.cursor-disabled {
cursor: not-allowed;
}
.margin-bottom-sml {
margin-bottom: 20px;
}
.margin-bottom-med {
margin-bottom: 40px;
}
.margin-right-left {
margin: 0 20px;
}
.bold {
font-weight: bold;
}
.text-transform-uppercase {
text-transform: uppercase;
}
.font-small {
font-size: 12px;
}
.font-medium {
font-size: 1.2em;
}
hr.horizontal-line {
display: block;
height: 1px;
border: 0;
border-top: 1px solid #ccc;
margin: 1em 0;
padding: 0;
}
.hover-white:hover {
background: white;
}
.red-dot {
background: #E91550;
color: white;
border-radius: 10px;
}
.diamond {
transform: rotate(45deg);
background: #038789;
}
.hollow-diamond {
transform: rotate(45deg);
border: 3px solid #690496;
}
.golden-square {
background: #EBB33F;
}
.pending-dot {
background: red;
left: 14px;
top: 14px;
color: white;
border-radius: 10px;
height: 20px;
min-width: 20px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
z-index: 1;
}
.keyring-label {
z-index: 1;
font-size: 11px;
background: rgba(255,0,0,0.8);
color: white;
bottom: 0px;
left: -8px;
border-radius: 10px;
height: 20px;
min-width: 20px;
position: absolute;
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
}
.ether-balance {
display: flex;
align-items: center;
}
.tabSection {
min-width: 350px;
}
.menu-icon {
display: inline-block;
height: 12px;
min-width: 12px;
margin: 13px;
}
i.fa.fa-question-circle.fa-lg.menu-icon {
font-size: 18px;
}
.ether-icon {
background: rgb(0, 163, 68);
border-radius: 20px;
}
.testnet-icon {
background: #2465E1;
}
.drop-menu-item {
display: flex;
align-items: center;
}
.invisible {
visibility: hidden;
}
.one-line-concat {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.critical-error {
text-align: center;
margin-top: 20px;
color: red;
}
/*
Hacky breakpoint fix for account + tab sections
Resolves issue from @frankiebee in
https://github.com/MetaMask/metamask-extension/pull/1835
Please remove this when integrating new designs
*/
@media screen and (min-width: 575px) and (max-width: 800px) {
.account-data-subsection {
flex: 0 0 auto !important; // reset flex
margin-left: 10px !important; // create additional horizontal space
margin-right: 10px !important;
width: 40%;
}
.tabSection {
flex: 0 0 auto !important;
margin-left: 10px !important;
margin-right: 10px !important;
min-width: 285px;
width: 49%;
}
.name-label {
width: 80%;
}
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,48 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}

@ -0,0 +1,42 @@
/* universal */
.app-primary .main-enter {
position: absolute;
width: 100%;
}
/* center position */
.app-primary.from-right .main-enter-active,
.app-primary.from-left .main-enter-active {
overflow-x: hidden;
transform: translateX(0px);
transition: transform 300ms ease-in;
}
/* exited positions */
.app-primary.from-left .main-leave-active {
transform: translateX(360px);
transition: transform 300ms ease-in;
}
.app-primary.from-right .main-leave-active {
transform: translateX(-360px);
transition: transform 300ms ease-in;
}
/* loader transitions */
.loader-enter, .loader-leave-active {
opacity: 0.0;
transition: opacity 150 ease-in;
}
.loader-enter-active, .loader-leave {
opacity: 1.0;
transition: opacity 150 ease-in;
}
/* entering positions */
.app-primary.from-right .main-enter:not(.main-enter-active) {
transform: translateX(360px);
}
.app-primary.from-left .main-enter:not(.main-enter-active) {
transform: translateX(-360px);
}

@ -0,0 +1,179 @@
const inherits = require('util').inherits
const EventEmitter = require('events').EventEmitter
const Component = require('react').Component
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const Mascot = require('../components/mascot')
const actions = require('../../../ui/app/actions')
const Tooltip = require('../components/tooltip')
const getCaretCoordinates = require('textarea-caret')
module.exports = connect(mapStateToProps)(InitializeMenuScreen)
inherits(InitializeMenuScreen, Component)
function InitializeMenuScreen () {
Component.call(this)
this.animationEventEmitter = new EventEmitter()
}
function mapStateToProps (state) {
return {
// state from plugin
currentView: state.appState.currentView,
warning: state.appState.warning,
}
}
InitializeMenuScreen.prototype.render = function () {
var state = this.props
switch (state.currentView.name) {
default:
return this.renderMenu(state)
}
}
// InitializeMenuScreen.prototype.componentDidMount = function(){
// document.getElementById('password-box').focus()
// }
InitializeMenuScreen.prototype.renderMenu = function (state) {
return (
h('.initialize-screen.flex-column.flex-center.flex-grow', [
h(Mascot, {
animationEventEmitter: this.animationEventEmitter,
}),
h('h1', {
style: {
fontSize: '1.3em',
textTransform: 'uppercase',
color: '#7F8082',
marginBottom: 10,
},
}, 'MetaMask'),
h('div', [
h('h3', {
style: {
fontSize: '0.8em',
color: '#7F8082',
display: 'inline',
},
}, 'Encrypt your new DEN'),
h(Tooltip, {
title: 'Your DEN is your password-encrypted storage within MetaMask.',
}, [
h('i.fa.fa-question-circle.pointer', {
style: {
fontSize: '18px',
position: 'relative',
color: 'rgb(247, 134, 28)',
top: '2px',
marginLeft: '4px',
},
}),
]),
]),
h('span.in-progress-notification', state.warning),
// password
h('input.large-input.letter-spacey', {
type: 'password',
id: 'password-box',
placeholder: 'New Password (min 8 chars)',
onInput: this.inputChanged.bind(this),
style: {
width: 260,
marginTop: 12,
},
}),
// confirm password
h('input.large-input.letter-spacey', {
type: 'password',
id: 'password-box-confirm',
placeholder: 'Confirm Password',
onKeyPress: this.createVaultOnEnter.bind(this),
onInput: this.inputChanged.bind(this),
style: {
width: 260,
marginTop: 16,
},
}),
h('button.primary', {
onClick: this.createNewVaultAndKeychain.bind(this),
style: {
margin: 12,
},
}, 'Create'),
h('.flex-row.flex-center.flex-grow', [
h('p.pointer', {
onClick: this.showRestoreVault.bind(this),
style: {
fontSize: '0.8em',
color: 'rgb(247, 134, 28)',
textDecoration: 'underline',
},
}, 'Import Existing DEN'),
]),
])
)
}
InitializeMenuScreen.prototype.createVaultOnEnter = function (event) {
if (event.key === 'Enter') {
event.preventDefault()
this.createNewVaultAndKeychain()
}
}
InitializeMenuScreen.prototype.componentDidMount = function () {
document.getElementById('password-box').focus()
}
InitializeMenuScreen.prototype.showRestoreVault = function () {
this.props.dispatch(actions.showRestoreVault())
}
InitializeMenuScreen.prototype.createNewVaultAndKeychain = function () {
var passwordBox = document.getElementById('password-box')
var password = passwordBox.value
var passwordConfirmBox = document.getElementById('password-box-confirm')
var passwordConfirm = passwordConfirmBox.value
if (password.length < 8) {
this.warning = 'password not long enough'
this.props.dispatch(actions.displayWarning(this.warning))
return
}
if (password !== passwordConfirm) {
this.warning = 'passwords don\'t match'
this.props.dispatch(actions.displayWarning(this.warning))
return
}
this.props.dispatch(actions.createNewVaultAndKeychain(password))
}
InitializeMenuScreen.prototype.inputChanged = function (event) {
// tell mascot to look at page action
var element = event.target
var boundingRect = element.getBoundingClientRect()
var coordinates = getCaretCoordinates(element, element.selectionEnd)
this.animationEventEmitter.emit('point', {
x: boundingRect.left + coordinates.left - element.scrollLeft,
y: boundingRect.top + coordinates.top - element.scrollTop,
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

@ -0,0 +1,155 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('../../ui/app/actions')
module.exports = connect(mapStateToProps)(InfoScreen)
function mapStateToProps (state) {
return {}
}
inherits(InfoScreen, Component)
function InfoScreen () {
Component.call(this)
}
InfoScreen.prototype.render = function () {
const state = this.props
const version = global.platform.getVersion()
return (
h('.flex-column.flex-grow', {
style: {
maxWidth: '400px',
},
}, [
// subtitle and nav
h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: (event) => {
state.dispatch(actions.goHome())
},
}),
h('h2.page-subtitle', 'Info'),
]),
// main view
h('.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-space-around', {
style: {
padding: '20px',
},
}, [
// current version number
h('.info.info-gray', [
h('div', 'Metamask'),
h('div', {
style: {
marginBottom: '10px',
},
}, `Version: ${version}`),
]),
h('div', {
style: {
marginBottom: '5px',
}},
[
h('div', [
h('a', {
href: 'https://metamask.io/privacy.html',
target: '_blank',
onClick (event) { this.navigateTo(event.target.href) },
}, [
h('div.info', 'Privacy Policy'),
]),
]),
h('div', [
h('a', {
href: 'https://metamask.io/terms.html',
target: '_blank',
onClick (event) { this.navigateTo(event.target.href) },
}, [
h('div.info', 'Terms of Use'),
]),
]),
h('div', [
h('a', {
href: 'https://metamask.io/attributions.html',
target: '_blank',
onClick (event) { this.navigateTo(event.target.href) },
}, [
h('div.info', 'Attributions'),
]),
]),
]
),
h('hr', {
style: {
margin: '10px 0 ',
width: '7em',
},
}),
h('div', {
style: {
paddingLeft: '30px',
}},
[
h('div.fa.fa-support', [
h('a.info', {
href: 'https://support.metamask.io',
target: '_blank',
}, 'Visit our Support Center'),
]),
h('div', [
h('a', {
href: 'https://metamask.io/',
target: '_blank',
}, [
h('img.icon-size', {
src: 'images/icon-128.png',
style: {
// IE6-9
filter: 'grayscale(100%)',
// Microsoft Edge and Firefox 35+
WebkitFilter: 'grayscale(100%)',
},
}),
h('div.info', 'Visit our web site'),
]),
]),
h('div', [
h('.fa.fa-twitter', [
h('a.info', {
href: 'https://twitter.com/metamask_io',
target: '_blank',
}, 'Follow us on Twitter'),
]),
]),
h('div.fa.fa-envelope', [
h('a.info', {
target: '_blank',
style: { width: '85vw' },
href: 'mailto:help@metamask.io?subject=Feedback',
}, 'Email us!'),
]),
]),
]),
]),
])
)
}
InfoScreen.prototype.navigateTo = function (url) {
global.platform.openWindow({ url })
}

@ -0,0 +1,653 @@
{
"objects": [
{
"symbol": "ethaud",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "aud",
"name": "Australian Dollar"
}
},
{
"symbol": "ethhkd",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "hkd",
"name": "Hong Kong Dollar"
}
},
{
"symbol": "ethsgd",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "sgd",
"name": "Singapore Dollar"
}
},
{
"symbol": "ethidr",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "idr",
"name": "Indonesian Rupiah"
}
},
{
"symbol": "ethphp",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "php",
"name": "Philippine Peso"
}
},
{
"symbol": "eth1st",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "1st",
"name": "FirstBlood"
}
},
{
"symbol": "ethadt",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "adt",
"name": "adToken"
}
},
{
"symbol": "ethadx",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "adx",
"name": "AdEx"
}
},
{
"symbol": "ethant",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "ant",
"name": "Aragon"
}
},
{
"symbol": "ethbat",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "bat",
"name": "Basic Attention Token"
}
},
{
"symbol": "ethbnt",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "bnt",
"name": "Bancor"
}
},
{
"symbol": "ethbtc",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "btc",
"name": "Bitcoin"
}
},
{
"symbol": "ethcad",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "cad",
"name": "Canadian Dollar"
}
},
{
"symbol": "ethcfi",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "cfi",
"name": "Cofound.it"
}
},
{
"symbol": "ethcrb",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "crb",
"name": "CreditBit"
}
},
{
"symbol": "ethcvc",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "cvc",
"name": "Civic"
}
},
{
"symbol": "ethdash",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "dash",
"name": "Dash"
}
},
{
"symbol": "ethdgd",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "dgd",
"name": "DigixDAO"
}
},
{
"symbol": "ethetc",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "etc",
"name": "Ethereum Classic"
}
},
{
"symbol": "etheur",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "eur",
"name": "Euro"
}
},
{
"symbol": "ethfun",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "fun",
"name": "FunFair"
}
},
{
"symbol": "ethgbp",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "gbp",
"name": "Pound Sterling"
}
},
{
"symbol": "ethgno",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "gno",
"name": "Gnosis"
}
},
{
"symbol": "ethgnt",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "gnt",
"name": "Golem"
}
},
{
"symbol": "ethgup",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "gup",
"name": "Matchpool"
}
},
{
"symbol": "ethhmq",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "hmq",
"name": "Humaniq"
}
},
{
"symbol": "ethjpy",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "jpy",
"name": "Japanese Yen"
}
},
{
"symbol": "ethlgd",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "lgd",
"name": "Legends Room"
}
},
{
"symbol": "ethlsk",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "lsk",
"name": "Lisk"
}
},
{
"symbol": "ethltc",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "ltc",
"name": "Litecoin"
}
},
{
"symbol": "ethlun",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "lun",
"name": "Lunyr"
}
},
{
"symbol": "ethmco",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "mco",
"name": "Monaco"
}
},
{
"symbol": "ethmtl",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "mtl",
"name": "Metal"
}
},
{
"symbol": "ethmyst",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "myst",
"name": "Mysterium"
}
},
{
"symbol": "ethnmr",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "nmr",
"name": "Numeraire"
}
},
{
"symbol": "ethomg",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "omg",
"name": "OmiseGO"
}
},
{
"symbol": "ethpay",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "pay",
"name": "TenX"
}
},
{
"symbol": "ethptoy",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "ptoy",
"name": "Patientory"
}
},
{
"symbol": "ethqrl",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "qrl",
"name": "Quantum-Resistant Ledger"
}
},
{
"symbol": "ethqtum",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "qtum",
"name": "Qtum"
}
},
{
"symbol": "ethrep",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "rep",
"name": "Augur"
}
},
{
"symbol": "ethrlc",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "rlc",
"name": "iEx.ec"
}
},
{
"symbol": "ethrub",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "rub",
"name": "Russian Ruble"
}
},
{
"symbol": "ethsc",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "sc",
"name": "Siacoin"
}
},
{
"symbol": "ethsngls",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "sngls",
"name": "SingularDTV"
}
},
{
"symbol": "ethsnt",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "snt",
"name": "Status"
}
},
{
"symbol": "ethsteem",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "steem",
"name": "Steem"
}
},
{
"symbol": "ethstorj",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "storj",
"name": "Storj"
}
},
{
"symbol": "ethtime",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "time",
"name": "ChronoBank"
}
},
{
"symbol": "ethtkn",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "tkn",
"name": "TokenCard"
}
},
{
"symbol": "ethtrst",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "trst",
"name": "WeTrust"
}
},
{
"symbol": "ethuah",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "uah",
"name": "Ukrainian Hryvnia"
}
},
{
"symbol": "ethusd",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "usd",
"name": "United States Dollar"
}
},
{
"symbol": "ethwings",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "wings",
"name": "Wings"
}
},
{
"symbol": "ethxem",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "xem",
"name": "NEM"
}
},
{
"symbol": "ethxlm",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "xlm",
"name": "Stellar Lumen"
}
},
{
"symbol": "ethxmr",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "xmr",
"name": "Monero"
}
},
{
"symbol": "ethxrp",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "xrp",
"name": "Ripple"
}
},
{
"symbol": "ethzec",
"base": {
"code": "eth",
"name": "Ethereum"
},
"quote": {
"code": "zec",
"name": "Zcash"
}
}
]
}

@ -0,0 +1,91 @@
const inherits = require('util').inherits
const Component = require('react').Component
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const actions = require('../../../../ui/app/actions')
const exportAsFile = require('../../util').exportAsFile
module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen)
inherits(CreateVaultCompleteScreen, Component)
function CreateVaultCompleteScreen () {
Component.call(this)
}
function mapStateToProps (state) {
return {
seed: state.appState.currentView.seedWords,
cachedSeed: state.metamask.seedWords,
}
}
CreateVaultCompleteScreen.prototype.render = function () {
var state = this.props
var seed = state.seed || state.cachedSeed || ''
return (
h('.initialize-screen.flex-column.flex-center.flex-grow', [
// // subtitle and nav
// h('.section-title.flex-row.flex-center', [
// h('h2.page-subtitle', 'Vault Created'),
// ]),
h('h3.flex-center.text-transform-uppercase', {
style: {
background: '#EBEBEB',
color: '#AEAEAE',
marginTop: 36,
marginBottom: 8,
width: '100%',
fontSize: '20px',
padding: 6,
},
}, [
'Vault Created',
]),
h('div', {
style: {
fontSize: '1em',
marginTop: '10px',
textAlign: 'center',
},
}, [
h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'),
]),
h('textarea.twelve-word-phrase', {
readOnly: true,
value: seed,
}),
h('button.primary', {
onClick: () => this.confirmSeedWords()
.then(account => this.showAccountDetail(account)),
style: {
margin: '24px',
fontSize: '0.9em',
marginBottom: '10px',
},
}, 'I\'ve copied it somewhere safe'),
h('button.primary', {
onClick: () => exportAsFile(`MetaMask Seed Words`, seed),
style: {
margin: '10px',
fontSize: '0.9em',
},
}, 'Save Seed Words As File'),
])
)
}
CreateVaultCompleteScreen.prototype.confirmSeedWords = function () {
return this.props.dispatch(actions.confirmSeedWords())
}
CreateVaultCompleteScreen.prototype.showAccountDetail = function (account) {
return this.props.dispatch(actions.showAccountDetail(account))
}

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

Loading…
Cancel
Save