Merge branch 'i328-MultiVault' of github.com:MetaMask/metamask-plugin into i328-MultiVault

feature/default_network_editable
Dan Finlay 8 years ago
commit 331bb735c7
  1. 11
      CHANGELOG.md
  2. 2
      app/manifest.json
  3. 36
      app/scripts/contentscript.js
  4. 21
      app/scripts/inpage.js
  5. 44
      app/scripts/keyring-controller.js
  6. 20
      app/scripts/lib/auto-reload.js
  7. 44
      app/scripts/lib/config-manager.js
  8. 7
      app/scripts/lib/id-management.js
  9. 14
      app/scripts/lib/idStore.js
  10. 17
      app/scripts/lib/inpage-provider.js
  11. 8
      app/scripts/lib/obj-multiplex.js
  12. 3
      app/scripts/lib/port-stream.js
  13. 13
      app/scripts/metamask-controller.js
  14. 2
      mock-dev.js
  15. 5
      package.json
  16. 2
      test/unit/actions/restore_vault_test.js
  17. 8
      test/unit/actions/tx_test.js
  18. 18
      test/unit/idStore-test.js
  19. 2
      ui-dev.js
  20. 62
      ui/app/actions.js
  21. 4
      ui/app/app.js
  22. 2
      ui/app/components/pending-tx-details.js
  23. 58
      ui/app/components/range-slider.js
  24. 33
      ui/app/new-keychain.js
  25. 10
      ui/app/reducers/app.js
  26. 113
      ui/app/send.js
  27. 2
      ui/index.js

@ -2,6 +2,17 @@
## Current Master ## Current Master
## 2.13.5 2016-10-18
- Increase default max gas to `100000` over the RPC's `estimateGas` response.
- Fix bug where slow-loading dapps would sometimes trigger infinite reload loops.
## 2.13.4 2016-10-17
- Add custom transaction fee field to send form.
- Fix bug where web3 was being injected into XML files.
- Fix bug where changing network would not reload current Dapps.
## 2.13.3 2016-10-4 ## 2.13.3 2016-10-4
- Fix bug where log queries were filtered out. - Fix bug where log queries were filtered out.

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

@ -1,4 +1,5 @@
const LocalMessageDuplexStream = require('post-message-stream') const LocalMessageDuplexStream = require('post-message-stream')
const PongStream = require('ping-pong-stream/pong')
const PortStream = require('./lib/port-stream.js') const PortStream = require('./lib/port-stream.js')
const ObjectMultiplex = require('./lib/obj-multiplex') const ObjectMultiplex = require('./lib/obj-multiplex')
const extension = require('./lib/extension') const extension = require('./lib/extension')
@ -51,20 +52,35 @@ function setupStreams(){
// forward communication plugin->inpage // forward communication plugin->inpage
pageStream.pipe(pluginStream).pipe(pageStream) pageStream.pipe(pluginStream).pipe(pageStream)
// connect contentscript->inpage reload stream // setup local multistream channels
var mx = ObjectMultiplex() var mx = ObjectMultiplex()
mx.on('error', console.error) mx.on('error', console.error)
mx.pipe(pageStream) mx.pipe(pageStream).pipe(mx)
var reloadStream = mx.createStream('reload')
reloadStream.on('error', console.error) // connect ping stream
var pongStream = new PongStream({ objectMode: true })
pongStream.pipe(mx.createStream('pingpong')).pipe(pongStream)
// ignore unused channels (handled by background)
mx.ignoreStream('provider')
mx.ignoreStream('publicConfig')
mx.ignoreStream('reload')
// if we lose connection with the plugin, trigger tab refresh
pluginStream.on('close', function () {
reloadStream.write({ method: 'reset' })
})
} }
function shouldInjectWeb3(){ function shouldInjectWeb3(){
var shouldInject = (window.location.href.indexOf('.pdf') === -1) return isAllowedSuffix(window.location.href)
return shouldInject }
function isAllowedSuffix(testCase) {
var prohibitedTypes = ['xml', 'pdf']
var currentUrl = window.location.href
var currentRegex
for (let i = 0; i < prohibitedTypes.length; i++) {
currentRegex = new RegExp(`\.${prohibitedTypes[i]}$`)
if (currentRegex.test(currentUrl)) {
return false
}
}
return true
} }

@ -2,6 +2,8 @@
cleanContextForImports() cleanContextForImports()
require('web3/dist/web3.min.js') require('web3/dist/web3.min.js')
const LocalMessageDuplexStream = require('post-message-stream') const LocalMessageDuplexStream = require('post-message-stream')
const PingStream = require('ping-pong-stream/ping')
const endOfStream = require('end-of-stream')
const setupDappAutoReload = require('./lib/auto-reload.js') const setupDappAutoReload = require('./lib/auto-reload.js')
const MetamaskInpageProvider = require('./lib/inpage-provider.js') const MetamaskInpageProvider = require('./lib/inpage-provider.js')
restoreContextAfterImports() restoreContextAfterImports()
@ -29,13 +31,22 @@ web3.setProvider = function () {
console.log('MetaMask - overrode web3.setProvider') console.log('MetaMask - overrode web3.setProvider')
} }
console.log('MetaMask - injected web3') console.log('MetaMask - injected web3')
// export global web3, with usage-detection reload fn
var triggerReload = setupDappAutoReload(web3)
// // listen for reset requests from metamask
// export global web3 with auto dapp reload
//
var reloadStream = inpageProvider.multiStream.createStream('reload') var reloadStream = inpageProvider.multiStream.createStream('reload')
setupDappAutoReload(web3, reloadStream) reloadStream.once('data', triggerReload)
// setup ping timeout autoreload
// LocalMessageDuplexStream does not self-close, so reload if pingStream fails
var pingChannel = inpageProvider.multiStream.createStream('pingpong')
var pingStream = new PingStream({ objectMode: true })
// wait for first successful reponse
metamaskStream.once('_data', function(){
pingStream.pipe(pingChannel).pipe(pingStream)
})
endOfStream(pingStream, triggerReload)
// set web3 defaultAcount // set web3 defaultAcount
inpageProvider.publicConfigStore.subscribe(function (state) { inpageProvider.publicConfigStore.subscribe(function (state) {

@ -1,7 +1,7 @@
const scrypt = require('scrypt-async')
const bitcore = require('bitcore-lib')
const configManager = require('./lib/config-manager')
const EventEmitter = require('events').EventEmitter const EventEmitter = require('events').EventEmitter
const encryptor = require('./lib/encryptor')
const messageManager = require('./lib/message-manager')
module.exports = class KeyringController extends EventEmitter { module.exports = class KeyringController extends EventEmitter {
@ -12,12 +12,17 @@ module.exports = class KeyringController extends EventEmitter {
this.keyChains = [] this.keyChains = []
} }
keyFromPassword(password, callback) {
deriveKeyFromPassword(password, callback);
}
// Takes a pw and callback, returns a password-dervied key
getKeyForPassword(password, callback) { getKeyForPassword(password, callback) {
let salt = this.configManager.getSalt() let salt = this.configManager.getSalt()
if (!salt) { if (!salt) {
salt = generateSalt(32) salt = generateSalt(32)
configManager.setSalt(salt) this.configManager.setSalt(salt)
} }
var logN = 14 var logN = 14
@ -39,7 +44,21 @@ module.exports = class KeyringController extends EventEmitter {
} }
getState() { getState() {
return {} return {
isInitialized: !!this.key,
isUnlocked: !!this.key,
isConfirmed: true, // this.configManager.getConfirmed(),
isEthConfirmed: this.configManager.getShouldntShowWarning(),
unconfTxs: this.configManager.unconfirmedTxs(),
transactions: this.configManager.getTxList(),
unconfMsgs: messageManager.unconfirmedMsgs(),
messages: messageManager.getMsgList(),
selectedAddress: this.configManager.getSelectedAccount(),
shapeShiftTxList: this.configManager.getShapeShiftTxList(),
currentFiat: this.configManager.getCurrentFiat(),
conversionRate: this.configManager.getConversionRate(),
conversionDate: this.configManager.getConversionDate(),
}
} }
setStore(ethStore) { setStore(ethStore) {
@ -47,9 +66,22 @@ module.exports = class KeyringController extends EventEmitter {
} }
createNewVault(password, entropy, cb) { createNewVault(password, entropy, cb) {
cb() encryptor.keyFromPassword(password)
.then((key) => {
this.key = key
return encryptor.encryptWithKey(key, {})
})
.then((encryptedString) => {
this.configManager.setVault(encryptedString)
cb(null, [])
})
.catch((err) => {
cb(err)
})
} }
submitPassword(password, cb) { submitPassword(password, cb) {
cb() cb()
} }

@ -3,7 +3,7 @@ const ensnare = require('ensnare')
module.exports = setupDappAutoReload module.exports = setupDappAutoReload
function setupDappAutoReload (web3, controlStream) { function setupDappAutoReload (web3) {
// export web3 as a global, checking for usage // export web3 as a global, checking for usage
var pageIsUsingWeb3 = false var pageIsUsingWeb3 = false
var resetWasRequested = false var resetWasRequested = false
@ -16,19 +16,19 @@ function setupDappAutoReload (web3, controlStream) {
global.web3 = web3 global.web3 = web3
})) }))
// listen for reset requests from metamask return handleResetRequest
controlStream.once('data', function () {
function handleResetRequest() {
resetWasRequested = true resetWasRequested = true
// ignore if web3 was not used // ignore if web3 was not used
if (!pageIsUsingWeb3) return if (!pageIsUsingWeb3) return
// reload after short timeout // reload after short timeout
triggerReset() setTimeout(triggerReset, 500)
}) }
// reload the page }
function triggerReset () {
setTimeout(function () { // reload the page
function triggerReset () {
global.location.reload() global.location.reload()
}, 500)
}
} }

@ -110,6 +110,27 @@ ConfigManager.prototype.setWallet = function (wallet) {
this.setData(data) this.setData(data)
} }
ConfigManager.prototype.setVault = function (encryptedString) {
var data = this.getData()
data.vault = encryptedString
this.setData(data)
}
ConfigManager.prototype.getVault = function () {
var data = this.getData()
return ('vault' in data) && data.vault
}
ConfigManager.prototype.getKeychains = function () {
return this.migrator.getData().keychains || []
}
ConfigManager.prototype.setKeychains = function (keychains) {
var data = this.migrator.getData()
data.keychains = keychains
this.setData(data)
}
ConfigManager.prototype.getSelectedAccount = function () { ConfigManager.prototype.getSelectedAccount = function () {
var config = this.getConfig() var config = this.getConfig()
return config.selectedAccount return config.selectedAccount
@ -249,6 +270,17 @@ ConfigManager.prototype.setNicknameForWallet = function (account, nickname) {
// observable // observable
ConfigManager.prototype.getSalt = function () {
var data = this.getData()
return ('salt' in data) && data.salt
}
ConfigManager.prototype.setSalt = function(salt) {
var data = this.getData()
data.salt = salt
this.setData(data)
}
ConfigManager.prototype.subscribe = function (fn) { ConfigManager.prototype.subscribe = function (fn) {
this._subs.push(fn) this._subs.push(fn)
var unsubscribe = this.unsubscribe.bind(this, fn) var unsubscribe = this.unsubscribe.bind(this, fn)
@ -384,3 +416,15 @@ ConfigManager.prototype.createShapeShiftTx = function (depositAddress, depositTy
} }
this.setData(data) this.setData(data)
} }
ConfigManager.prototype.getGasMultiplier = function () {
var data = this.getData()
return ('gasMultiplier' in data) && data.gasMultiplier
}
ConfigManager.prototype.setGasMultiplier = function (gasMultiplier) {
var data = this.getData()
data.gasMultiplier = gasMultiplier
this.setData(data)
}

@ -7,6 +7,7 @@
*/ */
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const Transaction = require('ethereumjs-tx') const Transaction = require('ethereumjs-tx')
module.exports = IdManagement module.exports = IdManagement
@ -24,7 +25,13 @@ function IdManagement (opts) {
} }
this.signTx = function (txParams) { this.signTx = function (txParams) {
// calculate gas with custom gas multiplier
var gasMultiplier = this.configManager.getGasMultiplier() || 1
var gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice), 16)
gasPrice = gasPrice.mul(new BN(gasMultiplier * 100, 10)).div(new BN(100, 10))
txParams.gasPrice = ethUtil.intToHex(gasPrice.toNumber())
// normalize values // normalize values
txParams.to = ethUtil.addHexPrefix(txParams.to) txParams.to = ethUtil.addHexPrefix(txParams.to)
txParams.from = ethUtil.addHexPrefix(txParams.from.toLowerCase()) txParams.from = ethUtil.addHexPrefix(txParams.from.toLowerCase())
txParams.value = ethUtil.addHexPrefix(txParams.value) txParams.value = ethUtil.addHexPrefix(txParams.value)

@ -2,6 +2,7 @@ const EventEmitter = require('events').EventEmitter
const inherits = require('util').inherits const inherits = require('util').inherits
const async = require('async') const async = require('async')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const EthQuery = require('eth-query') const EthQuery = require('eth-query')
const KeyStore = require('eth-lightwallet').keystore const KeyStore = require('eth-lightwallet').keystore
const clone = require('clone') const clone = require('clone')
@ -112,6 +113,8 @@ IdentityStore.prototype.getState = function () {
currentFiat: configManager.getCurrentFiat(), currentFiat: configManager.getCurrentFiat(),
conversionRate: configManager.getConversionRate(), conversionRate: configManager.getConversionRate(),
conversionDate: configManager.getConversionDate(), conversionDate: configManager.getConversionDate(),
gasMultiplier: configManager.getGasMultiplier(),
})) }))
} }
@ -211,6 +214,7 @@ IdentityStore.prototype.exportAccount = function (address, cb) {
// comes from dapp via zero-client hooked-wallet provider // comes from dapp via zero-client hooked-wallet provider
IdentityStore.prototype.addUnconfirmedTransaction = function (txParams, onTxDoneCb, cb) { IdentityStore.prototype.addUnconfirmedTransaction = function (txParams, onTxDoneCb, cb) {
const configManager = this.configManager const configManager = this.configManager
var self = this var self = this
// create txData obj with parameters and meta data // create txData obj with parameters and meta data
var time = (new Date()).getTime() var time = (new Date()).getTime()
@ -222,6 +226,7 @@ IdentityStore.prototype.addUnconfirmedTransaction = function (txParams, onTxDone
txParams: txParams, txParams: txParams,
time: time, time: time,
status: 'unconfirmed', status: 'unconfirmed',
gasMultiplier: configManager.getGasMultiplier() || 1,
} }
console.log('addUnconfirmedTransaction:', txData) console.log('addUnconfirmedTransaction:', txData)
@ -262,7 +267,7 @@ IdentityStore.prototype.addUnconfirmedTransaction = function (txParams, onTxDone
function estimateGas(cb){ function estimateGas(cb){
query.estimateGas(txParams, function(err, result){ query.estimateGas(txParams, function(err, result){
if (err) return cb(err) if (err) return cb(err)
txData.estimatedGas = result txData.estimatedGas = self.addGasBuffer(result)
cb() cb()
}) })
} }
@ -277,6 +282,13 @@ IdentityStore.prototype.addUnconfirmedTransaction = function (txParams, onTxDone
} }
} }
IdentityStore.prototype.addGasBuffer = function (gasHex) {
var gas = new BN(gasHex, 16)
var buffer = new BN('100000', 10)
var result = gas.add(buffer)
return ethUtil.addHexPrefix(result.toString(16))
}
// comes from metamask ui // comes from metamask ui
IdentityStore.prototype.approveTransaction = function (txId, cb) { IdentityStore.prototype.approveTransaction = function (txId, cb) {
const configManager = this.configManager const configManager = this.configManager

@ -1,6 +1,6 @@
const Streams = require('mississippi') const Streams = require('mississippi')
const ObjectMultiplex = require('./obj-multiplex')
const StreamProvider = require('web3-stream-provider') const StreamProvider = require('web3-stream-provider')
const ObjectMultiplex = require('./obj-multiplex')
const RemoteStore = require('./remote-store.js').RemoteStore const RemoteStore = require('./remote-store.js').RemoteStore
module.exports = MetamaskInpageProvider module.exports = MetamaskInpageProvider
@ -11,8 +11,9 @@ function MetamaskInpageProvider (connectionStream) {
// setup connectionStream multiplexing // setup connectionStream multiplexing
var multiStream = ObjectMultiplex() var multiStream = ObjectMultiplex()
Streams.pipe(connectionStream, multiStream, connectionStream, function (err) { Streams.pipe(connectionStream, multiStream, connectionStream, function (err) {
console.warn('MetamaskInpageProvider - lost connection to MetaMask') let warningMsg = 'MetamaskInpageProvider - lost connection to MetaMask'
if (err) throw err if (err) warningMsg += '\n' + err.stack
console.warn(warningMsg)
}) })
self.multiStream = multiStream self.multiStream = multiStream
@ -20,16 +21,18 @@ function MetamaskInpageProvider (connectionStream) {
var publicConfigStore = remoteStoreWithLocalStorageCache('MetaMask-Config') var publicConfigStore = remoteStoreWithLocalStorageCache('MetaMask-Config')
var storeStream = publicConfigStore.createStream() var storeStream = publicConfigStore.createStream()
Streams.pipe(storeStream, multiStream.createStream('publicConfig'), storeStream, function (err) { Streams.pipe(storeStream, multiStream.createStream('publicConfig'), storeStream, function (err) {
console.warn('MetamaskInpageProvider - lost connection to MetaMask publicConfig') let warningMsg = 'MetamaskInpageProvider - lost connection to MetaMask publicConfig'
if (err) throw err if (err) warningMsg += '\n' + err.stack
console.warn(warningMsg)
}) })
self.publicConfigStore = publicConfigStore self.publicConfigStore = publicConfigStore
// connect to async provider // connect to async provider
var asyncProvider = new StreamProvider() var asyncProvider = new StreamProvider()
Streams.pipe(asyncProvider, multiStream.createStream('provider'), asyncProvider, function (err) { Streams.pipe(asyncProvider, multiStream.createStream('provider'), asyncProvider, function (err) {
console.warn('MetamaskInpageProvider - lost connection to MetaMask provider') let warningMsg = 'MetamaskInpageProvider - lost connection to MetaMask provider'
if (err) throw err if (err) warningMsg += '\n' + err.stack
console.warn(warningMsg)
}) })
asyncProvider.on('error', console.error.bind(console)) asyncProvider.on('error', console.error.bind(console))
self.asyncProvider = asyncProvider self.asyncProvider = asyncProvider

@ -10,9 +10,9 @@ function ObjectMultiplex (opts) {
var data = chunk.data var data = chunk.data
var substream = mx.streams[name] var substream = mx.streams[name]
if (!substream) { if (!substream) {
console.warn('orphaned data for stream ' + name) console.warn(`orphaned data for stream "${name}"`)
} else { } else {
substream.push(data) if (substream.push) substream.push(data)
} }
return cb() return cb()
}) })
@ -36,5 +36,9 @@ function ObjectMultiplex (opts) {
} }
return substream return substream
} }
// ignore streams (dont display orphaned data warning)
mx.ignoreStream = function (name) {
mx.streams[name] = true
}
return mx return mx
} }

@ -53,8 +53,7 @@ PortDuplexStream.prototype._write = function (msg, encoding, cb) {
} }
cb() cb()
} catch (err) { } catch (err) {
console.error(err) // console.error(err)
// this.emit('error', err)
cb(new Error('PortDuplexStream - disconnected')) cb(new Error('PortDuplexStream - disconnected'))
} }
} }

@ -57,6 +57,7 @@ module.exports = class MetamaskController {
agreeToEthWarning: this.agreeToEthWarning.bind(this), agreeToEthWarning: this.agreeToEthWarning.bind(this),
setTOSHash: this.setTOSHash.bind(this), setTOSHash: this.setTOSHash.bind(this),
checkTOSChange: this.checkTOSChange.bind(this), checkTOSChange: this.checkTOSChange.bind(this),
setGasMultiplier: this.setGasMultiplier.bind(this),
// forward directly to idStore // forward directly to idStore
createNewVault: idStore.createNewVault.bind(idStore), createNewVault: idStore.createNewVault.bind(idStore),
@ -278,9 +279,9 @@ module.exports = class MetamaskController {
checkTOSChange () { checkTOSChange () {
try { try {
const storedHash = this.configManager.getTOSHash() || 0 const storedHash = this.configManager.getTOSHash() || 0
if (storedHash !== global.newTOSHash) { if (storedHash !== global.TOS_HASH) {
this.resetDisclaimer() this.resetDisclaimer()
this.setTOSHash(global.newTOSHash) this.setTOSHash(global.TOS_HASH)
} }
} catch (e) { } catch (e) {
console.error('Error in checking TOS change.') console.error('Error in checking TOS change.')
@ -395,4 +396,12 @@ module.exports = class MetamaskController {
}) })
} }
setGasMultiplier (gasMultiplier, cb) {
try {
this.configManager.setGasMultiplier(gasMultiplier)
cb()
} catch (e) {
cb(e)
}
}
} }

@ -107,7 +107,7 @@ function getOldStyleData () {
return result return result
} }
actions._setAccountManager(controller.getApi()) actions._setKeyringController(controller.getApi())
actions.update = function(stateName) { actions.update = function(stateName) {
selectedView = stateName selectedView = stateName
updateQueryParams(stateName) updateQueryParams(stateName)

@ -6,9 +6,9 @@
"scripts": { "scripts": {
"start": "npm run dev", "start": "npm run dev",
"lint": "gulp lint", "lint": "gulp lint",
"dev": "gulp dev --debug",
"dist": "gulp dist",
"buildCiUnits": "node test/integration/index.js", "buildCiUnits": "node test/integration/index.js",
"dev": "gulp dev --debug",
"dist": "gulp dist --disableLiveReload",
"test": "npm run fastTest && npm run ci && npm run lint", "test": "npm run fastTest && npm run ci && npm run lint",
"fastTest": "mocha --require test/helper.js --compilers js:babel-register --recursive \"test/unit/**/*.js\"", "fastTest": "mocha --require test/helper.js --compilers js:babel-register --recursive \"test/unit/**/*.js\"",
"watch": "mocha watch --compilers js:babel-register --recursive \"test/unit/**/*.js\"", "watch": "mocha watch --compilers js:babel-register --recursive \"test/unit/**/*.js\"",
@ -60,6 +60,7 @@
"mississippi": "^1.2.0", "mississippi": "^1.2.0",
"multiplex": "^6.7.0", "multiplex": "^6.7.0",
"once": "^1.3.3", "once": "^1.3.3",
"ping-pong-stream": "^1.0.0",
"pojo-migrator": "^2.1.0", "pojo-migrator": "^2.1.0",
"polyfill-crypto.getrandomvalues": "^1.0.0", "polyfill-crypto.getrandomvalues": "^1.0.0",
"post-message-stream": "^1.0.0", "post-message-stream": "^1.0.0",

@ -20,7 +20,7 @@ describe('#recoverFromSeed(password, seed)', function() {
}) })
// stub out account manager // stub out account manager
actions._setAccountManager({ actions._setKeyringController({
recoverFromSeed(pw, seed, cb) { recoverFromSeed(pw, seed, cb) {
cb(null, { cb(null, {
identities: { identities: {

@ -46,7 +46,7 @@ describe('tx confirmation screen', function() {
describe('cancelTx', function() { describe('cancelTx', function() {
before(function(done) { before(function(done) {
actions._setAccountManager({ actions._setKeyringController({
approveTransaction(txId, cb) { cb('An error!') }, approveTransaction(txId, cb) { cb('An error!') },
cancelTransaction(txId) { /* noop */ }, cancelTransaction(txId) { /* noop */ },
clearSeedWordCache(cb) { cb() }, clearSeedWordCache(cb) { cb() },
@ -75,7 +75,7 @@ describe('tx confirmation screen', function() {
before(function(done) { before(function(done) {
alert = () => {/* noop */} alert = () => {/* noop */}
actions._setAccountManager({ actions._setKeyringController({
approveTransaction(txId, cb) { cb({message: 'An error!'}) }, approveTransaction(txId, cb) { cb({message: 'An error!'}) },
}) })
@ -96,7 +96,7 @@ describe('tx confirmation screen', function() {
describe('when there is success', function() { describe('when there is success', function() {
it('should complete tx and go home', function() { it('should complete tx and go home', function() {
actions._setAccountManager({ actions._setKeyringController({
approveTransaction(txId, cb) { cb() }, approveTransaction(txId, cb) { cb() },
}) })
@ -135,7 +135,7 @@ describe('tx confirmation screen', function() {
} }
freeze(initialState) freeze(initialState)
actions._setAccountManager({ actions._setKeyringController({
approveTransaction(txId, cb) { cb() }, approveTransaction(txId, cb) { cb() },
}) })

@ -2,6 +2,7 @@ var assert = require('assert')
var IdentityStore = require('../../app/scripts/lib/idStore') var IdentityStore = require('../../app/scripts/lib/idStore')
var configManagerGen = require('../lib/mock-config-manager') var configManagerGen = require('../lib/mock-config-manager')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const async = require('async') const async = require('async')
describe('IdentityStore', function() { describe('IdentityStore', function() {
@ -138,4 +139,21 @@ describe('IdentityStore', function() {
}) })
}) })
}) })
describe('#addGasBuffer', function() {
const idStore = new IdentityStore({
configManager: configManagerGen(),
ethStore: {
addAccount(acct) { accounts.push(ethUtil.addHexPrefix(acct)) },
},
})
const gas = '0x01'
const bnGas = new BN(gas, 16)
const result = idStore.addGasBuffer(gas)
const bnResult = new BN(result, 16)
assert.ok(bnResult.gt(gas), 'added more gas as buffer.')
assert.equal(result.indexOf('0x'), 0, 'include hex prefix')
})
}) })

@ -41,7 +41,7 @@ function updateQueryParams(newView) {
} }
const actions = { const actions = {
_setAccountManager(){}, _setKeyringController(){},
update: function(stateName) { update: function(stateName) {
selectedView = stateName selectedView = stateName
updateQueryParams(stateName) updateQueryParams(stateName)

@ -95,7 +95,7 @@ var actions = {
setRpcTarget: setRpcTarget, setRpcTarget: setRpcTarget,
setProviderType: setProviderType, setProviderType: setProviderType,
// hacky - need a way to get a reference to account manager // hacky - need a way to get a reference to account manager
_setAccountManager: _setAccountManager, _setKeyringController: _setKeyringController,
// loading overlay // loading overlay
SHOW_LOADING: 'SHOW_LOADING_INDICATION', SHOW_LOADING: 'SHOW_LOADING_INDICATION',
HIDE_LOADING: 'HIDE_LOADING_INDICATION', HIDE_LOADING: 'HIDE_LOADING_INDICATION',
@ -132,13 +132,17 @@ var actions = {
RECOVERY_IN_PROGRESS: 'RECOVERY_IN_PROGRESS', RECOVERY_IN_PROGRESS: 'RECOVERY_IN_PROGRESS',
BACK_TO_UNLOCK_VIEW: 'BACK_TO_UNLOCK_VIEW', BACK_TO_UNLOCK_VIEW: 'BACK_TO_UNLOCK_VIEW',
backToUnlockView: backToUnlockView, backToUnlockView: backToUnlockView,
// SHOWING KEYCHAIN
SHOW_NEW_KEYCHAIN: 'SHOW_NEW_KEYCHAIN',
showNewKeychain: showNewKeychain,
} }
module.exports = actions module.exports = actions
var _accountManager = null var _keyringController = null
function _setAccountManager (accountManager) { function _setKeyringController (accountManager) {
_accountManager = accountManager _keyringController = accountManager
} }
function goHome () { function goHome () {
@ -153,7 +157,7 @@ function tryUnlockMetamask (password) {
return (dispatch) => { return (dispatch) => {
dispatch(actions.showLoadingIndication()) dispatch(actions.showLoadingIndication())
dispatch(actions.unlockInProgress()) dispatch(actions.unlockInProgress())
_accountManager.submitPassword(password, (err, selectedAccount) => { _keyringController.submitPassword(password, (err, selectedAccount) => {
dispatch(actions.hideLoadingIndication()) dispatch(actions.hideLoadingIndication())
if (err) { if (err) {
dispatch(actions.unlockFailed()) dispatch(actions.unlockFailed())
@ -167,11 +171,11 @@ function tryUnlockMetamask (password) {
function createNewVault (password, entropy) { function createNewVault (password, entropy) {
return (dispatch) => { return (dispatch) => {
dispatch(actions.createNewVaultInProgress()) dispatch(actions.createNewVaultInProgress())
_accountManager.createNewVault(password, entropy, (err, result) => { _keyringController.createNewVault(password, entropy, (err, result) => {
if (err) { if (err) {
return dispatch(actions.showWarning(err.message)) return dispatch(actions.showWarning(err.message))
} }
dispatch(this.goHome()) dispatch(this.showAccountsPage())
dispatch(this.hideLoadingIndication()) dispatch(this.hideLoadingIndication())
}) })
} }
@ -185,14 +189,14 @@ function showInfoPage () {
function setSelectedAddress (address) { function setSelectedAddress (address) {
return (dispatch) => { return (dispatch) => {
_accountManager.setSelectedAddress(address) _keyringController.setSelectedAddress(address)
} }
} }
function setCurrentFiat (fiat) { function setCurrentFiat (fiat) {
return (dispatch) => { return (dispatch) => {
dispatch(this.showLoadingIndication()) dispatch(this.showLoadingIndication())
_accountManager.setCurrentFiat(fiat, (data, err) => { _keyringController.setCurrentFiat(fiat, (data, err) => {
dispatch(this.hideLoadingIndication()) dispatch(this.hideLoadingIndication())
dispatch({ dispatch({
type: this.SET_CURRENT_FIAT, type: this.SET_CURRENT_FIAT,
@ -210,7 +214,7 @@ function signMsg (msgData) {
return (dispatch) => { return (dispatch) => {
dispatch(actions.showLoadingIndication()) dispatch(actions.showLoadingIndication())
_accountManager.signMessage(msgData, (err) => { _keyringController.signMessage(msgData, (err) => {
dispatch(actions.hideLoadingIndication()) dispatch(actions.hideLoadingIndication())
if (err) return dispatch(actions.displayWarning(err.message)) if (err) return dispatch(actions.displayWarning(err.message))
@ -221,20 +225,22 @@ function signMsg (msgData) {
function signTx (txData) { function signTx (txData) {
return (dispatch) => { return (dispatch) => {
_accountManager.setGasMultiplier(txData.gasMultiplier, (err) => {
if (err) return dispatch(actions.displayWarning(err.message))
web3.eth.sendTransaction(txData, (err, data) => { web3.eth.sendTransaction(txData, (err, data) => {
dispatch(actions.hideLoadingIndication()) dispatch(actions.hideLoadingIndication())
if (err) return dispatch(actions.displayWarning(err.message)) if (err) return dispatch(actions.displayWarning(err.message))
dispatch(actions.hideWarning()) dispatch(actions.hideWarning())
dispatch(actions.goHome()) dispatch(actions.goHome())
}) })
dispatch(this.showConfTxPage()) dispatch(this.showConfTxPage())
})
} }
} }
function sendTx (txData) { function sendTx (txData) {
return (dispatch) => { return (dispatch) => {
_accountManager.approveTransaction(txData.id, (err) => { _keyringController.approveTransaction(txData.id, (err) => {
if (err) { if (err) {
alert(err.message) alert(err.message)
dispatch(actions.txError(err)) dispatch(actions.txError(err))
@ -260,12 +266,12 @@ function txError (err) {
} }
function cancelMsg (msgData) { function cancelMsg (msgData) {
_accountManager.cancelMessage(msgData.id) _keyringController.cancelMessage(msgData.id)
return actions.completedTx(msgData.id) return actions.completedTx(msgData.id)
} }
function cancelTx (txData) { function cancelTx (txData) {
_accountManager.cancelTransaction(txData.id) _keyringController.cancelTransaction(txData.id)
return actions.completedTx(txData.id) return actions.completedTx(txData.id)
} }
@ -294,7 +300,7 @@ function showInitializeMenu () {
function agreeToDisclaimer () { function agreeToDisclaimer () {
return (dispatch) => { return (dispatch) => {
dispatch(this.showLoadingIndication()) dispatch(this.showLoadingIndication())
_accountManager.agreeToDisclaimer((err) => { _keyringController.agreeToDisclaimer((err) => {
if (err) { if (err) {
return dispatch(actions.showWarning(err.message)) return dispatch(actions.showWarning(err.message))
} }
@ -326,6 +332,12 @@ function backToUnlockView () {
} }
} }
function showNewKeychain () {
return {
type: actions.SHOW_NEW_KEYCHAIN
}
}
// //
// unlock screen // unlock screen
// //
@ -358,7 +370,7 @@ function updateMetamaskState (newState) {
function lockMetamask () { function lockMetamask () {
return (dispatch) => { return (dispatch) => {
_accountManager.setLocked((err) => { _keyringController.setLocked((err) => {
dispatch(actions.hideLoadingIndication()) dispatch(actions.hideLoadingIndication())
if (err) { if (err) {
return dispatch(actions.showWarning(err.message)) return dispatch(actions.showWarning(err.message))
@ -374,7 +386,7 @@ function lockMetamask () {
function showAccountDetail (address) { function showAccountDetail (address) {
return (dispatch) => { return (dispatch) => {
dispatch(actions.showLoadingIndication()) dispatch(actions.showLoadingIndication())
_accountManager.setSelectedAddress(address, (err, address) => { _keyringController.setSelectedAddress(address, (err, address) => {
dispatch(actions.hideLoadingIndication()) dispatch(actions.hideLoadingIndication())
if (err) { if (err) {
return dispatch(actions.showWarning(err.message)) return dispatch(actions.showWarning(err.message))
@ -445,7 +457,7 @@ function goBackToInitView () {
// //
function setRpcTarget (newRpc) { function setRpcTarget (newRpc) {
_accountManager.setRpcTarget(newRpc) _keyringController.setRpcTarget(newRpc)
return { return {
type: actions.SET_RPC_TARGET, type: actions.SET_RPC_TARGET,
value: newRpc, value: newRpc,
@ -453,7 +465,7 @@ function setRpcTarget (newRpc) {
} }
function setProviderType (type) { function setProviderType (type) {
_accountManager.setProviderType(type) _keyringController.setProviderType(type)
return { return {
type: actions.SET_PROVIDER_TYPE, type: actions.SET_PROVIDER_TYPE,
value: type, value: type,
@ -461,7 +473,7 @@ function setProviderType (type) {
} }
function useEtherscanProvider () { function useEtherscanProvider () {
_accountManager.useEtherscanProvider() _keyringController.useEtherscanProvider()
return { return {
type: actions.USE_ETHERSCAN_PROVIDER, type: actions.USE_ETHERSCAN_PROVIDER,
} }
@ -520,7 +532,7 @@ function exportAccount (address) {
return function (dispatch) { return function (dispatch) {
dispatch(self.showLoadingIndication()) dispatch(self.showLoadingIndication())
_accountManager.exportAccount(address, function (err, result) { _keyringController.exportAccount(address, function (err, result) {
dispatch(self.hideLoadingIndication()) dispatch(self.hideLoadingIndication())
if (err) { if (err) {
@ -543,7 +555,7 @@ function showPrivateKey (key) {
function saveAccountLabel (account, label) { function saveAccountLabel (account, label) {
return (dispatch) => { return (dispatch) => {
dispatch(actions.showLoadingIndication()) dispatch(actions.showLoadingIndication())
_accountManager.saveAccountLabel(account, label, (err) => { _keyringController.saveAccountLabel(account, label, (err) => {
dispatch(actions.hideLoadingIndication()) dispatch(actions.hideLoadingIndication())
if (err) { if (err) {
return dispatch(actions.showWarning(err.message)) return dispatch(actions.showWarning(err.message))
@ -564,7 +576,7 @@ function showSendPage () {
function agreeToEthWarning () { function agreeToEthWarning () {
return (dispatch) => { return (dispatch) => {
_accountManager.agreeToEthWarning((err) => { _keyringController.agreeToEthWarning((err) => {
if (err) { if (err) {
return dispatch(actions.showEthWarning(err.message)) return dispatch(actions.showEthWarning(err.message))
} }
@ -583,7 +595,7 @@ function showEthWarning () {
function buyEth (address, amount) { function buyEth (address, amount) {
return (dispatch) => { return (dispatch) => {
_accountManager.buyEth(address, amount) _keyringController.buyEth(address, amount)
dispatch({ dispatch({
type: actions.BUY_ETH, type: actions.BUY_ETH,
}) })
@ -661,7 +673,7 @@ function coinShiftRquest (data, marketData) {
if (response.error) return dispatch(actions.showWarning(response.error)) if (response.error) return dispatch(actions.showWarning(response.error))
var message = ` var message = `
Deposit your ${response.depositType} to the address bellow:` Deposit your ${response.depositType} to the address bellow:`
_accountManager.createShapeShiftTx(response.deposit, response.depositType) _keyringController.createShapeShiftTx(response.deposit, response.depositType)
dispatch(actions.showQrView(response.deposit, [message].concat(marketData))) dispatch(actions.showQrView(response.deposit, [message].concat(marketData)))
}) })
} }

@ -8,6 +8,7 @@ const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
const DisclaimerScreen = require('./first-time/disclaimer') const DisclaimerScreen = require('./first-time/disclaimer')
const InitializeMenuScreen = require('./first-time/init-menu') const InitializeMenuScreen = require('./first-time/init-menu')
const CreateVaultScreen = require('./first-time/create-vault') const CreateVaultScreen = require('./first-time/create-vault')
const NewKeychainScreen = require('./new-keychain')
// unlock // unlock
const UnlockScreen = require('./unlock') const UnlockScreen = require('./unlock')
// accounts // accounts
@ -432,6 +433,9 @@ App.prototype.renderPrimary = function () {
case 'sendTransaction': case 'sendTransaction':
return h(SendTransactionScreen, {key: 'send-transaction'}) return h(SendTransactionScreen, {key: 'send-transaction'})
case 'newKeychain':
return h(NewKeyChainScreen, {key: 'new-keychain'})
case 'confTx': case 'confTx':
return h(ConfirmTxScreen, {key: 'confirm-tx'}) return h(ConfirmTxScreen, {key: 'confirm-tx'})

@ -29,8 +29,10 @@ PTXP.render = function () {
var account = props.accounts[address] var account = props.accounts[address]
var balance = account ? account.balance : '0x0' var balance = account ? account.balance : '0x0'
var gasMultiplier = txData.gasMultiplier
var gasCost = new BN(ethUtil.stripHexPrefix(txParams.gas || txData.estimatedGas), 16) var gasCost = new BN(ethUtil.stripHexPrefix(txParams.gas || txData.estimatedGas), 16)
var gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice || '0x4a817c800'), 16) var gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice || '0x4a817c800'), 16)
gasPrice = gasPrice.mul(new BN(gasMultiplier * 100), 10).div(new BN(100, 10))
var txFee = gasCost.mul(gasPrice) var txFee = gasCost.mul(gasPrice)
var txValue = new BN(ethUtil.stripHexPrefix(txParams.value || '0x0'), 16) var txValue = new BN(ethUtil.stripHexPrefix(txParams.value || '0x0'), 16)
var maxCost = txValue.add(txFee) var maxCost = txValue.add(txFee)

@ -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,33 @@
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)(NewKeychain)
function mapStateToProps (state) {
return {}
}
inherits(NewKeychain, Component)
function NewKeychain () {
Component.call(this)
}
NewKeychain.prototype.render = function () {
const props = this.props
return (
h('div', {
style: {
background: 'blue',
},
}, [
h('h1',`Here's a list!!!!`),
h('button',
{
onClick: () => this.props.dispatch(actions.goHome())
})
])
)
}

@ -119,6 +119,15 @@ function reduceApp (state, action) {
warning: null, warning: null,
}) })
case actions.SHOW_NEW_KEYCHAIN:
return extend(appState, {
currentView: {
name: 'newKeychain',
context: appState.currentView.context
},
transForward: true,
})
// unlock // unlock
case actions.UNLOCK_METAMASK: case actions.UNLOCK_METAMASK:
@ -540,4 +549,3 @@ function indexForPending (state, txId) {
}) })
return idx return idx
} }

@ -9,7 +9,8 @@ const numericBalance = require('./util').numericBalance
const addressSummary = require('./util').addressSummary const addressSummary = require('./util').addressSummary
const EthBalance = require('./components/eth-balance') const EthBalance = require('./components/eth-balance')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const RangeSlider = require('./components/range-slider')
const Tooltip = require('./components/tooltip')
module.exports = connect(mapStateToProps)(SendTransactionScreen) module.exports = connect(mapStateToProps)(SendTransactionScreen)
function mapStateToProps (state) { function mapStateToProps (state) {
@ -50,7 +51,7 @@ SendTransactionScreen.prototype.render = function () {
// Sender Profile // Sender Profile
// //
h('.account-data-subsection.flex-column.flex-grow', { h('.account-data-subsection.flex-row.flex-grow', {
style: { style: {
margin: '0 20px', margin: '0 20px',
}, },
@ -59,10 +60,9 @@ SendTransactionScreen.prototype.render = function () {
// header - identicon + nav // header - identicon + nav
h('.flex-row.flex-space-between', { h('.flex-row.flex-space-between', {
style: { style: {
marginTop: 28, marginTop: '15px',
}, },
}, [ }, [
// back button // back button
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
onClick: this.back.bind(this), onClick: this.back.bind(this),
@ -77,22 +77,33 @@ SendTransactionScreen.prototype.render = function () {
]), ]),
// invisible place holder // invisible place holder
h('i.fa.fa-users.fa-lg.invisible'), h('i.fa.fa-users.fa-lg.invisible', {
style: {
marginTop: '28px',
},
}),
]), ]),
// account label // account label
h('.flex-column', {
style: {
marginTop: '10px',
alignItems: 'flex-start',
},
}, [
h('h2.font-medium.color-forest.flex-center', { h('h2.font-medium.color-forest.flex-center', {
style: { style: {
paddingTop: 8, paddingTop: '8px',
marginBottom: 8, marginBottom: '8px',
}, },
}, identity && identity.name), }, identity && identity.name),
// address and getter actions // address and getter actions
h('.flex-row.flex-center', { h('.flex-row.flex-center', {
style: { style: {
marginBottom: 8, marginBottom: '8px',
}, },
}, [ }, [
@ -112,7 +123,7 @@ SendTransactionScreen.prototype.render = function () {
}), }),
]), ]),
]),
]), ]),
// //
@ -123,8 +134,8 @@ SendTransactionScreen.prototype.render = function () {
style: { style: {
background: '#EBEBEB', background: '#EBEBEB',
color: '#AEAEAE', color: '#AEAEAE',
marginTop: 32, marginTop: '15px',
marginBottom: 16, marginBottom: '16px',
}, },
}, [ }, [
'Send Transaction', 'Send Transaction',
@ -152,7 +163,7 @@ SendTransactionScreen.prototype.render = function () {
placeholder: 'Amount', placeholder: 'Amount',
type: 'number', type: 'number',
style: { style: {
marginRight: 6, marginRight: '6px',
}, },
dataset: { dataset: {
persistentFormId: 'tx-amount', persistentFormId: 'tx-amount',
@ -171,20 +182,19 @@ SendTransactionScreen.prototype.render = function () {
// //
// Optional Fields // Optional Fields
// //
h('h3.flex-center.text-transform-uppercase', { h('h3.flex-center.text-transform-uppercase', {
style: { style: {
background: '#EBEBEB', background: '#EBEBEB',
color: '#AEAEAE', color: '#AEAEAE',
marginTop: 16, marginTop: '16px',
marginBottom: 16, marginBottom: '16px',
}, },
}, [ }, [
'Transactional Data (optional)', 'Transactional Data (optional)',
]), ]),
// 'data' field // 'data' field
h('section.flex-row.flex-center', [ h('section.flex-column.flex-center', [
h('input.large-input', { h('input.large-input', {
name: 'txData', name: 'txData',
placeholder: '0x01234', placeholder: '0x01234',
@ -197,6 +207,73 @@ SendTransactionScreen.prototype.render = function () {
}, },
}), }),
]), ]),
// custom gasPrice field
h('h3.flex-center.text-transform-uppercase', {
style: {
background: '#EBEBEB',
color: '#AEAEAE',
marginBottom: '5px',
},
}, [
'Transaction Fee (optional)',
h(Tooltip, {
title: `
This is used to set the transaction's gas price.
Setting it to 100% will use the full recommended value. `,
}, [
h('i.fa.fa-question-circle', {
style: {
marginLeft: '5px',
},
}),
]),
]),
h('section.flex-column.flex-center', [
h('.flex-row', [
h(RangeSlider, {
name: 'gasInput',
options: {
mirrorInput: true,
defaultValue: 100,
min: 80,
max: 220,
},
style: {
container: {
marginBottom: '16px',
},
range: {
width: '68vw',
},
input: {
width: '5em',
marginLeft: '5px',
},
},
}),
h('div', {
style: {
fontSize: '12px',
paddingTop: '8px',
paddingLeft: '5px',
},
}, '%'),
]),
h('.flex-row', {
style: {
justifyContent: 'space-between',
width: '243px',
position: 'relative',
fontSize: '12px',
right: '42px',
bottom: '30px',
},
}, [
h('span', 'Cheaper'), h('span', 'Faster'),
]),
]),
]) ])
) )
} }
@ -211,11 +288,12 @@ SendTransactionScreen.prototype.back = function () {
this.props.dispatch(actions.backToAccountDetail(address)) this.props.dispatch(actions.backToAccountDetail(address))
} }
SendTransactionScreen.prototype.onSubmit = function () { SendTransactionScreen.prototype.onSubmit = function (gasPrice) {
const recipient = document.querySelector('input[name="address"]').value const recipient = document.querySelector('input[name="address"]').value
const input = document.querySelector('input[name="amount"]').value const input = document.querySelector('input[name="amount"]').value
const value = util.normalizeEthStringToWei(input) const value = util.normalizeEthStringToWei(input)
const txData = document.querySelector('input[name="txData"]').value const txData = document.querySelector('input[name="txData"]').value
const gasMultiplier = document.querySelector('input[name="gasInput"]').value
const balance = this.props.balance const balance = this.props.balance
let message let message
@ -239,6 +317,7 @@ SendTransactionScreen.prototype.onSubmit = function () {
var txParams = { var txParams = {
from: this.props.address, from: this.props.address,
value: '0x' + value.toString(16), value: '0x' + value.toString(16),
gasMultiplier: gasMultiplier * 0.01,
} }
if (recipient) txParams.to = ethUtil.addHexPrefix(recipient) if (recipient) txParams.to = ethUtil.addHexPrefix(recipient)

@ -8,7 +8,7 @@ module.exports = launchApp
function launchApp (opts) { function launchApp (opts) {
var accountManager = opts.accountManager var accountManager = opts.accountManager
actions._setAccountManager(accountManager) actions._setKeyringController(accountManager)
// check if we are unlocked first // check if we are unlocked first
accountManager.getState(function (err, metamaskState) { accountManager.getState(function (err, metamaskState) {

Loading…
Cancel
Save