aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/manifest.json4
-rw-r--r--app/scripts/background.js22
-rw-r--r--app/scripts/config.js3
-rw-r--r--app/scripts/contentscript.js14
-rw-r--r--app/scripts/inpage.js20
-rw-r--r--app/scripts/keyring-controller.js946
-rw-r--r--app/scripts/keyrings/hd.js104
-rw-r--r--app/scripts/keyrings/simple.js61
-rw-r--r--app/scripts/lib/auto-reload.js5
-rw-r--r--app/scripts/lib/config-manager.js96
-rw-r--r--app/scripts/lib/encryptor.js149
-rw-r--r--app/scripts/lib/eth-store.js146
-rw-r--r--app/scripts/lib/idStore-migrator.js58
-rw-r--r--app/scripts/lib/idStore.js225
-rw-r--r--app/scripts/lib/inpage-provider.js36
-rw-r--r--app/scripts/lib/is-popup-or-notification.js2
-rw-r--r--app/scripts/lib/nodeify.js24
-rw-r--r--app/scripts/lib/notifications.js12
-rw-r--r--app/scripts/lib/port-stream.js4
-rw-r--r--app/scripts/lib/random-id.js9
-rw-r--r--app/scripts/lib/sig-util.js5
-rw-r--r--app/scripts/lib/tx-utils.js132
-rw-r--r--app/scripts/metamask-controller.js289
-rw-r--r--app/scripts/notice-controller.js96
-rw-r--r--app/scripts/popup-core.js4
-rw-r--r--app/scripts/popup.js2
-rw-r--r--app/scripts/transaction-manager.js362
27 files changed, 1723 insertions, 1107 deletions
diff --git a/app/manifest.json b/app/manifest.json
index e35f2918d..a13b43ca7 100644
--- a/app/manifest.json
+++ b/app/manifest.json
@@ -1,7 +1,7 @@
{
"name": "MetaMask",
"short_name": "Metamask",
- "version": "2.13.6",
+ "version": "3.0.1",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "Ethereum Browser Extension",
@@ -56,9 +56,7 @@
],
"permissions": [
"storage",
- "tabs",
"clipboardWrite",
- "clipboardRead",
"http://localhost:8545/"
],
"web_accessible_resources": [
diff --git a/app/scripts/background.js b/app/scripts/background.js
index 7cb25d8bf..f3837a028 100644
--- a/app/scripts/background.js
+++ b/app/scripts/background.js
@@ -17,18 +17,16 @@ const controller = new MetamaskController({
// User confirmation callbacks:
showUnconfirmedMessage: triggerUi,
unlockAccountMessage: triggerUi,
- showUnconfirmedTx: triggerUi,
+ showUnapprovedTx: triggerUi,
// Persistence Methods:
setData,
loadData,
})
-const keyringController = controller.keyringController
function triggerUi () {
if (!popupIsOpen) notification.show()
}
// On first install, open a window to MetaMask website to how-it-works.
-
extension.runtime.onInstalled.addListener(function (details) {
if ((details.reason === 'install') && (!METAMASK_DEBUG)) {
extension.tabs.create({url: 'https://metamask.io/#how-it-works'})
@@ -81,13 +79,11 @@ function setupControllerConnection (stream) {
stream.pipe(dnode).pipe(stream)
dnode.on('remote', (remote) => {
// push updates to popup
- controller.ethStore.on('update', controller.sendUpdate.bind(controller))
- controller.listeners.push(remote)
- keyringController.on('update', controller.sendUpdate.bind(controller))
-
+ var sendUpdate = remote.sendUpdate.bind(remote)
+ controller.on('update', sendUpdate)
// teardown on disconnect
eos(stream, () => {
- controller.ethStore.removeListener('update', controller.sendUpdate.bind(controller))
+ controller.removeListener('update', sendUpdate)
popupIsOpen = false
})
})
@@ -97,15 +93,15 @@ function setupControllerConnection (stream) {
// plugin badge text
//
-keyringController.on('update', updateBadge)
+controller.txManager.on('updateBadge', updateBadge)
+updateBadge()
function updateBadge () {
var label = ''
- var unconfTxs = controller.configManager.unconfirmedTxs()
- var unconfTxLen = Object.keys(unconfTxs).length
+ var unapprovedTxCount = controller.txManager.unapprovedTxCount
var unconfMsgs = messageManager.unconfirmedMsgs()
var unconfMsgLen = Object.keys(unconfMsgs).length
- var count = unconfTxLen + unconfMsgLen
+ var count = unapprovedTxCount + unconfMsgLen
if (count) {
label = String(count)
}
@@ -113,6 +109,8 @@ function updateBadge () {
extension.browserAction.setBadgeBackgroundColor({ color: '#506F8B' })
}
+// data :: setters/getters
+
function loadData () {
var oldData = getOldStyleData()
var newData
diff --git a/app/scripts/config.js b/app/scripts/config.js
index e40b5e104..e09206c5f 100644
--- a/app/scripts/config.js
+++ b/app/scripts/config.js
@@ -1,5 +1,5 @@
const MAINET_RPC_URL = 'https://mainnet.infura.io/metamask'
-const TESTNET_RPC_URL = 'https://morden.infura.io/metamask'
+const TESTNET_RPC_URL = 'https://ropsten.infura.io/metamask'
const DEFAULT_RPC_URL = TESTNET_RPC_URL
global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
@@ -10,5 +10,6 @@ module.exports = {
default: DEFAULT_RPC_URL,
mainnet: MAINET_RPC_URL,
testnet: TESTNET_RPC_URL,
+ morden: TESTNET_RPC_URL,
},
}
diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js
index e2a968ac9..ab64dc9fa 100644
--- a/app/scripts/contentscript.js
+++ b/app/scripts/contentscript.js
@@ -6,7 +6,7 @@ const extension = require('./lib/extension')
const fs = require('fs')
const path = require('path')
-const inpageText = fs.readFileSync(path.join(__dirname + '/inpage.js')).toString()
+const inpageText = fs.readFileSync(path.join(__dirname, 'inpage.js')).toString()
// Eventually this streaming injection could be replaced with:
// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction
@@ -20,9 +20,8 @@ if (shouldInjectWeb3()) {
setupStreams()
}
-function setupInjection(){
+function setupInjection () {
try {
-
// inject in-page script
var scriptTag = document.createElement('script')
scriptTag.src = extension.extension.getURL('scripts/inpage.js')
@@ -31,14 +30,12 @@ function setupInjection(){
var container = document.head || document.documentElement
// append as first child
container.insertBefore(scriptTag, container.children[0])
-
} catch (e) {
console.error('Metamask injection failed.', e)
}
}
-function setupStreams(){
-
+function setupStreams () {
// setup communication to page and plugin
var pageStream = new LocalMessageDuplexStream({
name: 'contentscript',
@@ -65,14 +62,13 @@ function setupStreams(){
mx.ignoreStream('provider')
mx.ignoreStream('publicConfig')
mx.ignoreStream('reload')
-
}
-function shouldInjectWeb3(){
+function shouldInjectWeb3 () {
return isAllowedSuffix(window.location.href)
}
-function isAllowedSuffix(testCase) {
+function isAllowedSuffix (testCase) {
var prohibitedTypes = ['xml', 'pdf']
var currentUrl = window.location.href
var currentRegex
diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js
index 85dd70b4d..42332d92e 100644
--- a/app/scripts/inpage.js
+++ b/app/scripts/inpage.js
@@ -2,8 +2,8 @@
cleanContextForImports()
require('web3/dist/web3.min.js')
const LocalMessageDuplexStream = require('post-message-stream')
-const PingStream = require('ping-pong-stream/ping')
-const endOfStream = require('end-of-stream')
+// const PingStream = require('ping-pong-stream/ping')
+// const endOfStream = require('end-of-stream')
const setupDappAutoReload = require('./lib/auto-reload.js')
const MetamaskInpageProvider = require('./lib/inpage-provider.js')
restoreContextAfterImports()
@@ -40,17 +40,19 @@ 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 })
+// 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)
+
+// disable pingStream until https://github.com/MetaMask/metamask-plugin/issues/746 is resolved more gracefully
+// metamaskStream.once('data', function(){
+// pingStream.pipe(pingChannel).pipe(pingStream)
+// })
+// endOfStream(pingStream, triggerReload)
// set web3 defaultAcount
inpageProvider.publicConfigStore.subscribe(function (state) {
- web3.eth.defaultAccount = state.selectedAddress
+ web3.eth.defaultAccount = state.selectedAccount
})
//
diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js
index fb3091143..4be00a5a5 100644
--- a/app/scripts/keyring-controller.js
+++ b/app/scripts/keyring-controller.js
@@ -1,18 +1,12 @@
-const async = require('async')
-const EventEmitter = require('events').EventEmitter
-const encryptor = require('./lib/encryptor')
-const messageManager = require('./lib/message-manager')
const ethUtil = require('ethereumjs-util')
-const ethBinToOps = require('eth-bin-to-ops')
-const EthQuery = require('eth-query')
-const BN = ethUtil.BN
-const Transaction = require('ethereumjs-tx')
-const createId = require('web3-provider-engine/util/random-id')
-const autoFaucet = require('./lib/auto-faucet')
const bip39 = require('bip39')
+const EventEmitter = require('events').EventEmitter
+const filter = require('promise-filter')
+const encryptor = require('browser-passworder')
-// TEMPORARY UNTIL FULL DEPRECATION:
-const IdStoreMigrator = require('./lib/idStore-migrator')
+const normalize = require('./lib/sig-util').normalize
+const messageManager = require('./lib/message-manager')
+const BN = ethUtil.BN
// Keyrings:
const SimpleKeyring = require('./keyrings/simple')
@@ -22,390 +16,322 @@ const keyringTypes = [
HdKeyring,
]
+const createId = require('./lib/random-id')
+
module.exports = class KeyringController extends EventEmitter {
+ // PUBLIC METHODS
+ //
+ // THE FIRST SECTION OF METHODS ARE PUBLIC-FACING,
+ // MEANING THEY ARE USED BY CONSUMERS OF THIS CLASS.
+ //
+ // THEIR SURFACE AREA SHOULD BE CHANGED WITH GREAT CARE.
+
constructor (opts) {
super()
- this.web3 = opts.web3
this.configManager = opts.configManager
this.ethStore = opts.ethStore
this.encryptor = encryptor
this.keyringTypes = keyringTypes
-
this.keyrings = []
this.identities = {} // Essentially a name hash
- this._unconfTxCbs = {}
this._unconfMsgCbs = {}
- this.network = opts.network
+ this.getNetwork = opts.getNetwork
+ }
- // TEMPORARY UNTIL FULL DEPRECATION:
- this.idStoreMigrator = new IdStoreMigrator({
- configManager: this.configManager,
- })
+ // Set Store
+ //
+ // Allows setting the ethStore after the constructor.
+ // This is currently required because of the initialization order
+ // of the ethStore and this class.
+ //
+ // Eventually would be nice to be able to add this in the constructor.
+ setStore (ethStore) {
+ this.ethStore = ethStore
}
- getState() {
+ // Full Update
+ // returns Promise( @object state )
+ //
+ // Emits the `update` event and
+ // returns a Promise that resolves to the current state.
+ //
+ // Frequently used to end asynchronous chains in this class,
+ // indicating consumers can often either listen for updates,
+ // or accept a state-resolving promise to consume their results.
+ //
+ // Not all methods end with this, that might be a nice refactor.
+ fullUpdate () {
+ this.emit('update')
+ return Promise.resolve(this.getState())
+ }
+
+ // Get State
+ // returns @object state
+ //
+ // This method returns a hash representing the current state
+ // that the keyringController manages.
+ //
+ // It is extended in the MetamaskController along with the EthStore
+ // state, and its own state, to create the metamask state branch
+ // that is passed to the UI.
+ //
+ // This is currently a rare example of a synchronously resolving method
+ // in this class, but will need to be Promisified when we move our
+ // persistence to an async model.
+ getState () {
const configManager = this.configManager
const address = configManager.getSelectedAccount()
const wallet = configManager.getWallet() // old style vault
const vault = configManager.getVault() // new style vault
+ const keyrings = this.keyrings
- return {
- seedWords: this.configManager.getSeedWords(),
- isInitialized: (!!wallet || !!vault),
- isUnlocked: !!this.key,
- isConfirmed: true, // AUDIT this.configManager.getConfirmed(),
- unconfTxs: this.configManager.unconfirmedTxs(),
- transactions: this.configManager.getTxList(),
- unconfMsgs: messageManager.unconfirmedMsgs(),
- messages: messageManager.getMsgList(),
- selectedAddress: address,
- selectedAccount: address,
- shapeShiftTxList: this.configManager.getShapeShiftTxList(),
- currentFiat: this.configManager.getCurrentFiat(),
- conversionRate: this.configManager.getConversionRate(),
- conversionDate: this.configManager.getConversionDate(),
- keyringTypes: this.keyringTypes.map((krt) => krt.type()),
- identities: this.identities,
- }
- }
-
- setStore(ethStore) {
- this.ethStore = ethStore
- }
-
- createNewVaultAndKeychain(password, entropy, cb) {
- this.createNewVault(password, entropy, (err) => {
- if (err) return cb(err)
- this.createFirstKeyTree(password, cb)
+ return Promise.all(keyrings.map(this.displayForKeyring))
+ .then((displayKeyrings) => {
+ return {
+ seedWords: this.configManager.getSeedWords(),
+ isInitialized: (!!wallet || !!vault),
+ isUnlocked: Boolean(this.password),
+ isDisclaimerConfirmed: this.configManager.getConfirmedDisclaimer(),
+ unconfMsgs: messageManager.unconfirmedMsgs(),
+ messages: messageManager.getMsgList(),
+ selectedAccount: address,
+ shapeShiftTxList: this.configManager.getShapeShiftTxList(),
+ currentFiat: this.configManager.getCurrentFiat(),
+ conversionRate: this.configManager.getConversionRate(),
+ conversionDate: this.configManager.getConversionDate(),
+ keyringTypes: this.keyringTypes.map(krt => krt.type),
+ identities: this.identities,
+ keyrings: displayKeyrings,
+ }
})
}
- createNewVaultAndRestore(password, seed, cb) {
+ // Create New Vault And Keychain
+ // @string password - The password to encrypt the vault with
+ //
+ // returns Promise( @object state )
+ //
+ // Destroys any old encrypted storage,
+ // creates a new encrypted store with the given password,
+ // randomly creates a new HD wallet with 1 account,
+ // faucets that account on the testnet.
+ createNewVaultAndKeychain (password) {
+ return this.persistAllKeyrings(password)
+ .then(this.createFirstKeyTree.bind(this))
+ .then(this.fullUpdate.bind(this))
+ }
+
+ // CreateNewVaultAndRestore
+ // @string password - The password to encrypt the vault with
+ // @string seed - The BIP44-compliant seed phrase.
+ //
+ // returns Promise( @object state )
+ //
+ // Destroys any old encrypted storage,
+ // creates a new encrypted store with the given password,
+ // creates a new HD wallet from the given seed with 1 account.
+ createNewVaultAndRestore (password, seed) {
if (typeof password !== 'string') {
- return cb('Password must be text.')
+ return Promise.reject('Password must be text.')
}
if (!bip39.validateMnemonic(seed)) {
- return cb('Seed phrase is invalid.')
+ return Promise.reject('Seed phrase is invalid.')
}
this.clearKeyrings()
- this.createNewVault(password, '', (err) => {
- if (err) return cb(err)
- this.addNewKeyring('HD Key Tree', {
+ return this.persistAllKeyrings(password)
+ .then(() => {
+ return this.addNewKeyring('HD Key Tree', {
mnemonic: seed,
- n: 1,
- }, (err) => {
- if (err) return cb(err)
- const firstKeyring = this.keyrings[0]
- const accounts = firstKeyring.getAccounts()
- const firstAccount = accounts[0]
- const hexAccount = normalize(firstAccount)
- this.configManager.setSelectedAccount(hexAccount)
- this.setupAccounts(accounts)
-
- this.emit('update')
- cb(null, this.getState())
+ numberOfAccounts: 1,
})
- })
- }
-
- migrateAndGetKey(password) {
- let key
- const shouldMigrate = !!this.configManager.getWallet() && !this.configManager.getVault()
-
- return this.loadKey(password)
- .then((derivedKey) => {
- key = derivedKey
- this.key = key
- return this.idStoreMigrator.oldSeedForPassword(password)
- })
- .then((serialized) => {
- if (serialized && shouldMigrate) {
- const keyring = this.restoreKeyring(serialized)
- this.keyrings.push(keyring)
- this.configManager.setSelectedAccount(keyring.getAccounts()[0])
- }
- return key
- })
- }
-
- createNewVault(password, entropy, cb) {
- const configManager = this.configManager
- const salt = this.encryptor.generateSalt()
- configManager.setSalt(salt)
-
- return this.migrateAndGetKey(password)
- .then(() => {
- return this.persistAllKeyrings()
- })
- .then(() => {
- cb(null)
- })
- .catch((err) => {
- cb(err)
- })
- }
-
- createFirstKeyTree(password, cb) {
- this.clearKeyrings()
- this.addNewKeyring('HD Key Tree', {n: 1}, (err) => {
+ }).then(() => {
const firstKeyring = this.keyrings[0]
- const accounts = firstKeyring.getAccounts()
+ return firstKeyring.getAccounts()
+ })
+ .then((accounts) => {
const firstAccount = accounts[0]
const hexAccount = normalize(firstAccount)
- const seedWords = firstKeyring.serialize().mnemonic
- this.configManager.setSelectedAccount(firstAccount)
+ this.configManager.setSelectedAccount(hexAccount)
+ return this.setupAccounts(accounts)
+ })
+ .then(this.persistAllKeyrings.bind(this, password))
+ .then(this.fullUpdate.bind(this))
+ }
+
+ // PlaceSeedWords
+ // returns Promise( @object state )
+ //
+ // Adds the current vault's seed words to the UI's state tree.
+ //
+ // Used when creating a first vault, to allow confirmation.
+ // Also used when revealing the seed words in the confirmation view.
+ placeSeedWords () {
+ const firstKeyring = this.keyrings[0]
+ return firstKeyring.serialize()
+ .then((serialized) => {
+ const seedWords = serialized.mnemonic
this.configManager.setSeedWords(seedWords)
- autoFaucet(hexAccount)
- this.setupAccounts(accounts)
- this.persistAllKeyrings()
- .then(() => {
- cb(err, this.getState())
- })
- .catch((reason) => {
- cb(reason)
- })
+ return this.fullUpdate()
})
}
- placeSeedWords (cb) {
- const firstKeyring = this.keyrings[0]
- const seedWords = firstKeyring.serialize().mnemonic
- this.configManager.setSeedWords(seedWords)
+ // ClearSeedWordCache
+ //
+ // returns Promise( @string currentSelectedAccount )
+ //
+ // Removes the current vault's seed words from the UI's state tree,
+ // ensuring they are only ever available in the background process.
+ clearSeedWordCache () {
+ this.configManager.setSeedWords(null)
+ return Promise.resolve(this.configManager.getSelectedAccount())
}
- submitPassword(password, cb) {
- this.migrateAndGetKey(password)
- .then((key) => {
- return this.unlockKeyrings(key)
- })
+ // Set Locked
+ // returns Promise( @object state )
+ //
+ // This method deallocates all secrets, and effectively locks metamask.
+ setLocked () {
+ this.password = null
+ this.keyrings = []
+ return this.fullUpdate()
+ }
+
+ // Submit Password
+ // @string password
+ //
+ // returns Promise( @object state )
+ //
+ // Attempts to decrypt the current vault and load its keyrings
+ // into memory.
+ //
+ // Temporarily also migrates any old-style vaults first, as well.
+ // (Pre MetaMask 3.0.0)
+ submitPassword (password) {
+ return this.unlockKeyrings(password)
.then((keyrings) => {
this.keyrings = keyrings
- this.setupAccounts()
- this.emit('update')
- cb(null, this.getState())
- })
- .catch((err) => {
- console.error(err)
- cb(err)
+ return this.fullUpdate()
})
}
- loadKey(password) {
- const salt = this.configManager.getSalt() || this.encryptor.generateSalt()
- return this.encryptor.keyFromPassword(password + salt)
- .then((key) => {
- this.key = key
- this.configManager.setSalt(salt)
- return key
- })
- }
-
- addNewKeyring(type, opts, cb) {
+ // Add New Keyring
+ // @string type
+ // @object opts
+ //
+ // returns Promise( @Keyring keyring )
+ //
+ // Adds a new Keyring of the given `type` to the vault
+ // and the current decrypted Keyrings array.
+ //
+ // All Keyring classes implement a unique `type` string,
+ // and this is used to retrieve them from the keyringTypes array.
+ addNewKeyring (type, opts) {
const Keyring = this.getKeyringClassForType(type)
const keyring = new Keyring(opts)
- const accounts = keyring.getAccounts()
-
- this.keyrings.push(keyring)
- this.setupAccounts(accounts)
- this.persistAllKeyrings()
- .then(() => {
- cb(null, this.getState())
- })
- .catch((reason) => {
- cb(reason)
+ return keyring.getAccounts()
+ .then((accounts) => {
+ this.keyrings.push(keyring)
+ return this.setupAccounts(accounts)
})
- }
-
- addNewAccount(keyRingNum = 0, cb) {
- const ring = this.keyrings[keyRingNum]
- const accounts = ring.addAccounts(1)
- this.setupAccounts(accounts)
- this.persistAllKeyrings()
+ .then(() => { return this.password })
+ .then(this.persistAllKeyrings.bind(this))
.then(() => {
- cb(null, this.getState())
- })
- .catch((reason) => {
- cb(reason)
- })
- }
-
- setupAccounts(accounts) {
- var arr = accounts || this.getAccounts()
- arr.forEach((account) => {
- this.loadBalanceAndNickname(account)
+ return keyring
})
}
- // Takes an account address and an iterator representing
- // the current number of named accounts.
- loadBalanceAndNickname(account) {
- const address = normalize(account)
- this.ethStore.addAccount(address)
- this.createNickname(address)
- }
-
- createNickname(address) {
- const hexAddress = normalize(address)
- var i = Object.keys(this.identities).length
- const oldNickname = this.configManager.nicknameForWallet(address)
- const name = oldNickname || `Account ${++i}`
- this.identities[hexAddress] = {
- address: hexAddress,
- name,
- }
- return this.saveAccountLabel(hexAddress, name)
+ // Add New Account
+ // @number keyRingNum
+ //
+ // returns Promise( @object state )
+ //
+ // Calls the `addAccounts` method on the Keyring
+ // in the kryings array at index `keyringNum`,
+ // and then saves those changes.
+ addNewAccount (keyRingNum = 0) {
+ const ring = this.keyrings[keyRingNum]
+ return ring.addAccounts(1)
+ .then(this.setupAccounts.bind(this))
+ .then(this.persistAllKeyrings.bind(this))
+ .then(this.fullUpdate.bind(this))
+ }
+
+ // Set Selected Account
+ // @string address
+ //
+ // returns Promise( @string address )
+ //
+ // Sets the state's `selectedAccount` value
+ // to the specified address.
+ setSelectedAccount (address) {
+ var addr = normalize(address)
+ this.configManager.setSelectedAccount(addr)
+ return this.fullUpdate()
}
- saveAccountLabel (account, label, cb) {
+ // Save Account Label
+ // @string account
+ // @string label
+ //
+ // returns Promise( @string label )
+ //
+ // Persists a nickname equal to `label` for the specified account.
+ saveAccountLabel (account, label) {
const address = normalize(account)
const configManager = this.configManager
configManager.setNicknameForWallet(address, label)
this.identities[address].name = label
- if (cb) {
- cb(null, label)
- } else {
- return label
+ return Promise.resolve(label)
+ }
+
+ // Export Account
+ // @string address
+ //
+ // returns Promise( @string privateKey )
+ //
+ // Requests the private key from the keyring controlling
+ // the specified address.
+ //
+ // Returns a Promise that may resolve with the private key string.
+ exportAccount (address) {
+ try {
+ return this.getKeyringForAccount(address)
+ .then((keyring) => {
+ return keyring.exportAccount(normalize(address))
+ })
+ } catch (e) {
+ return Promise.reject(e)
}
}
- persistAllKeyrings() {
- const serialized = this.keyrings.map((k) => {
- return {
- type: k.type,
- data: k.serialize(),
- }
- })
- return this.encryptor.encryptWithKey(this.key, serialized)
- .then((encryptedString) => {
- this.configManager.setVault(encryptedString)
- return true
- })
- }
+ // SIGNING METHODS
+ //
+ // This method signs tx and returns a promise for
+ // TX Manager to update the state after signing
- unlockKeyrings(key) {
- const encryptedVault = this.configManager.getVault()
- return this.encryptor.decryptWithKey(key, encryptedVault)
- .then((vault) => {
- vault.forEach(this.restoreKeyring.bind(this))
- return this.keyrings
+ signTransaction (ethTx, _fromAddress) {
+ const fromAddress = normalize(_fromAddress)
+ return this.getKeyringForAccount(fromAddress)
+ .then((keyring) => {
+ return keyring.signTransaction(fromAddress, ethTx)
})
}
-
- restoreKeyring(serialized) {
- const { type, data } = serialized
- const Keyring = this.getKeyringClassForType(type)
- const keyring = new Keyring()
- keyring.deserialize(data)
-
- const accounts = keyring.getAccounts()
- this.setupAccounts(accounts)
-
- this.keyrings.push(keyring)
- return keyring
- }
-
- getKeyringClassForType(type) {
- const Keyring = this.keyringTypes.reduce((res, kr) => {
- if (kr.type() === type) {
- return kr
- } else {
- return res
- }
- })
- return Keyring
- }
-
- getAccounts() {
- const keyrings = this.keyrings || []
- return keyrings.map(kr => kr.getAccounts())
- .reduce((res, arr) => {
- return res.concat(arr)
- }, [])
- }
-
- setSelectedAddress(address, cb) {
- var addr = normalize(address)
- this.configManager.setSelectedAccount(addr)
- cb(null, addr)
- }
-
- addUnconfirmedTransaction(txParams, onTxDoneCb, cb) {
- var self = this
- const configManager = this.configManager
-
- // create txData obj with parameters and meta data
- var time = (new Date()).getTime()
- var txId = createId()
- txParams.metamaskId = txId
- txParams.metamaskNetworkId = this.network
- var txData = {
- id: txId,
- txParams: txParams,
- time: time,
- status: 'unconfirmed',
- gasMultiplier: configManager.getGasMultiplier() || 1,
- }
-
-
- // keep the onTxDoneCb around for after approval/denial (requires user interaction)
- // This onTxDoneCb fires completion to the Dapp's write operation.
- this._unconfTxCbs[txId] = onTxDoneCb
-
- var provider = this.ethStore._query.currentProvider
- var query = new EthQuery(provider)
-
- // calculate metadata for tx
- async.parallel([
- analyzeForDelegateCall,
- estimateGas,
- ], didComplete)
-
- // perform static analyis on the target contract code
- function analyzeForDelegateCall(cb){
- if (txParams.to) {
- query.getCode(txParams.to, function (err, result) {
- if (err) return cb(err)
- var code = ethUtil.toBuffer(result)
- if (code !== '0x') {
- var ops = ethBinToOps(code)
- var containsDelegateCall = ops.some((op) => op.name === 'DELEGATECALL')
- txData.containsDelegateCall = containsDelegateCall
- cb()
- } else {
- cb()
- }
- })
- } else {
- cb()
- }
- }
-
- function estimateGas(cb){
- query.estimateGas(txParams, function(err, result){
- if (err) return cb(err)
- txData.estimatedGas = self.addGasBuffer(result)
- cb()
- })
- }
-
- function didComplete (err) {
- if (err) return cb(err)
- configManager.addTx(txData)
- // signal update
- self.emit('update')
- // signal completion of add tx
- cb(null, txData)
- }
- }
-
- addUnconfirmedMessage(msgParams, cb) {
+ // Add Unconfirmed Message
+ // @object msgParams
+ // @function cb
+ //
+ // Does not call back, only emits an `update` event.
+ //
+ // Adds the given `msgParams` and `cb` to a local cache,
+ // for displaying to a user for approval before signing or canceling.
+ addUnconfirmedMessage (msgParams, cb) {
// create txData obj with parameters and meta data
var time = (new Date()).getTime()
var msgId = createId()
@@ -427,125 +353,313 @@ module.exports = class KeyringController extends EventEmitter {
return msgId
}
- approveTransaction(txId, cb) {
- const configManager = this.configManager
- var approvalCb = this._unconfTxCbs[txId] || noop
-
- // accept tx
- cb()
- approvalCb(null, true)
- // clean up
- configManager.confirmTx(txId)
- delete this._unconfTxCbs[txId]
- this.emit('update')
- }
-
- cancelTransaction(txId, cb) {
- const configManager = this.configManager
- var approvalCb = this._unconfTxCbs[txId] || noop
+ // Cancel Message
+ // @string msgId
+ // @function cb (optional)
+ //
+ // Calls back to cached `unconfMsgCb`.
+ // Calls back to `cb` if provided.
+ //
+ // Forgets any messages matching `msgId`.
+ cancelMessage (msgId, cb) {
+ var approvalCb = this._unconfMsgCbs[msgId] || noop
// reject tx
approvalCb(null, false)
// clean up
- configManager.rejectTx(txId)
- delete this._unconfTxCbs[txId]
+ messageManager.rejectMsg(msgId)
+ delete this._unconfTxCbs[msgId]
if (cb && typeof cb === 'function') {
cb()
}
}
- signTransaction(txParams, cb) {
+ // Sign Message
+ // @object msgParams
+ // @function cb
+ //
+ // returns Promise(@buffer rawSig)
+ // calls back @function cb with @buffer rawSig
+ // calls back cached Dapp's @function unconfMsgCb.
+ //
+ // Attempts to sign the provided @object msgParams.
+ signMessage (msgParams, cb) {
try {
- const address = normalize(txParams.from)
- const keyring = this.getKeyringForAccount(address)
-
- // Handle gas pricing
- 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
- txParams.to = normalize(txParams.to)
- txParams.from = normalize(txParams.from)
- txParams.value = normalize(txParams.value)
- txParams.data = normalize(txParams.data)
- txParams.gasLimit = normalize(txParams.gasLimit || txParams.gas)
- txParams.nonce = normalize(txParams.nonce)
-
- let tx = new Transaction(txParams)
- tx = keyring.signTransaction(address, tx)
-
- // Add the tx hash to the persisted meta-tx object
- var txHash = ethUtil.bufferToHex(tx.hash())
- var metaTx = this.configManager.getTx(txParams.metamaskId)
- metaTx.hash = txHash
- this.configManager.updateTx(metaTx)
-
- // return raw serialized tx
- var rawTx = ethUtil.bufferToHex(tx.serialize())
- cb(null, rawTx)
- } catch (e) {
- cb(e)
- }
- }
+ const msgId = msgParams.metamaskId
+ delete msgParams.metamaskId
+ const approvalCb = this._unconfMsgCbs[msgId] || noop
- signMessage(msgParams, cb) {
- try {
- const keyring = this.getKeyringForAccount(msgParams.from)
const address = normalize(msgParams.from)
- const rawSig = keyring.signMessage(address, msgParams.data)
- cb(null, rawSig)
+ return this.getKeyringForAccount(address)
+ .then((keyring) => {
+ return keyring.signMessage(address, msgParams.data)
+ }).then((rawSig) => {
+ cb(null, rawSig)
+ approvalCb(null, true)
+ messageManager.confirmMsg(msgId)
+ return rawSig
+ })
} catch (e) {
cb(e)
}
}
- getKeyringForAccount(address) {
- const hexed = normalize(address)
- return this.keyrings.find((ring) => {
- return ring.getAccounts()
- .map(normalize)
- .includes(hexed)
+ // PRIVATE METHODS
+ //
+ // THESE METHODS ARE ONLY USED INTERNALLY TO THE KEYRING-CONTROLLER
+ // AND SO MAY BE CHANGED MORE LIBERALLY THAN THE ABOVE METHODS.
+
+ // Create First Key Tree
+ // returns @Promise
+ //
+ // Clears the vault,
+ // creates a new one,
+ // creates a random new HD Keyring with 1 account,
+ // makes that account the selected account,
+ // faucets that account on testnet,
+ // puts the current seed words into the state tree.
+ createFirstKeyTree () {
+ this.clearKeyrings()
+ return this.addNewKeyring('HD Key Tree', {numberOfAccounts: 1})
+ .then(() => {
+ return this.keyrings[0].getAccounts()
+ })
+ .then((accounts) => {
+ const firstAccount = accounts[0]
+ const hexAccount = normalize(firstAccount)
+ this.configManager.setSelectedAccount(hexAccount)
+ this.emit('newAccount', hexAccount)
+ return this.setupAccounts(accounts)
+ }).then(() => {
+ return this.placeSeedWords()
+ })
+ .then(this.persistAllKeyrings.bind(this))
+ }
+
+ // Setup Accounts
+ // @array accounts
+ //
+ // returns @Promise(@object account)
+ //
+ // Initializes the provided account array
+ // Gives them numerically incremented nicknames,
+ // and adds them to the ethStore for regular balance checking.
+ setupAccounts (accounts) {
+ return this.getAccounts()
+ .then((loadedAccounts) => {
+ const arr = accounts || loadedAccounts
+ return Promise.all(arr.map((account) => {
+ return this.getBalanceAndNickname(account)
+ }))
})
}
- cancelMessage(msgId, cb) {
- if (cb && typeof cb === 'function') {
- cb()
+ // Get Balance And Nickname
+ // @string account
+ //
+ // returns Promise( @string label )
+ //
+ // Takes an account address and an iterator representing
+ // the current number of named accounts.
+ getBalanceAndNickname (account) {
+ if (!account) {
+ throw new Error('Problem loading account.')
+ }
+ const address = normalize(account)
+ this.ethStore.addAccount(address)
+ return this.createNickname(address)
+ }
+
+ // Create Nickname
+ // @string address
+ //
+ // returns Promise( @string label )
+ //
+ // Takes an address, and assigns it an incremented nickname, persisting it.
+ createNickname (address) {
+ const hexAddress = normalize(address)
+ var i = Object.keys(this.identities).length
+ const oldNickname = this.configManager.nicknameForWallet(address)
+ const name = oldNickname || `Account ${++i}`
+ this.identities[hexAddress] = {
+ address: hexAddress,
+ name,
}
+ return this.saveAccountLabel(hexAddress, name)
}
- setLocked(cb) {
- this.key = null
- this.keyrings = []
- cb()
+ // Persist All Keyrings
+ // @password string
+ //
+ // returns Promise
+ //
+ // Iterates the current `keyrings` array,
+ // serializes each one into a serialized array,
+ // encrypts that array with the provided `password`,
+ // and persists that encrypted string to storage.
+ persistAllKeyrings (password = this.password) {
+ if (typeof password === 'string') {
+ this.password = password
+ }
+ return Promise.all(this.keyrings.map((keyring) => {
+ return Promise.all([keyring.type, keyring.serialize()])
+ .then((serializedKeyringArray) => {
+ // Label the output values on each serialized Keyring:
+ return {
+ type: serializedKeyringArray[0],
+ data: serializedKeyringArray[1],
+ }
+ })
+ }))
+ .then((serializedKeyrings) => {
+ return this.encryptor.encrypt(this.password, serializedKeyrings)
+ })
+ .then((encryptedString) => {
+ this.configManager.setVault(encryptedString)
+ return true
+ })
}
- exportAccount(address, cb) {
- try {
- const keyring = this.getKeyringForAccount(address)
- const privateKey = keyring.exportAccount(normalize(address))
- cb(null, privateKey)
- } catch (e) {
- cb(e)
+ // Unlock Keyrings
+ // @string password
+ //
+ // returns Promise( @array keyrings )
+ //
+ // Attempts to unlock the persisted encrypted storage,
+ // initializing the persisted keyrings to RAM.
+ unlockKeyrings (password) {
+ const encryptedVault = this.configManager.getVault()
+ if (!encryptedVault) {
+ throw new Error('Cannot unlock without a previous vault.')
}
+
+ return this.encryptor.decrypt(password, encryptedVault)
+ .then((vault) => {
+ this.password = password
+ vault.forEach(this.restoreKeyring.bind(this))
+ return this.keyrings
+ })
}
- addGasBuffer(gasHex) {
- var gas = new BN(gasHex, 16)
- var buffer = new BN('100000', 10)
- var result = gas.add(buffer)
- return normalize(result.toString(16))
+ // Restore Keyring
+ // @object serialized
+ //
+ // returns Promise( @Keyring deserialized )
+ //
+ // Attempts to initialize a new keyring from the provided
+ // serialized payload.
+ //
+ // On success, returns the resulting @Keyring instance.
+ restoreKeyring (serialized) {
+ const { type, data } = serialized
+
+ const Keyring = this.getKeyringClassForType(type)
+ const keyring = new Keyring()
+ return keyring.deserialize(data)
+ .then(() => {
+ return keyring.getAccounts()
+ })
+ .then((accounts) => {
+ return this.setupAccounts(accounts)
+ })
+ .then(() => {
+ this.keyrings.push(keyring)
+ return keyring
+ })
}
- clearSeedWordCache(cb) {
- this.configManager.setSeedWords(null)
- cb(null, this.configManager.getSelectedAccount())
+ // Get Keyring Class For Type
+ // @string type
+ //
+ // Returns @class Keyring
+ //
+ // Searches the current `keyringTypes` array
+ // for a Keyring class whose unique `type` property
+ // matches the provided `type`,
+ // returning it if it exists.
+ getKeyringClassForType (type) {
+ return this.keyringTypes.find(kr => kr.type === type)
+ }
+
+ // Get Accounts
+ // returns Promise( @Array[ @string accounts ] )
+ //
+ // Returns the public addresses of all current accounts
+ // managed by all currently unlocked keyrings.
+ getAccounts () {
+ const keyrings = this.keyrings || []
+ return Promise.all(keyrings.map(kr => kr.getAccounts()))
+ .then((keyringArrays) => {
+ return keyringArrays.reduce((res, arr) => {
+ return res.concat(arr)
+ }, [])
+ })
}
- clearKeyrings() {
+ // Get Keyring For Account
+ // @string address
+ //
+ // returns Promise(@Keyring keyring)
+ //
+ // Returns the currently initialized keyring that manages
+ // the specified `address` if one exists.
+ getKeyringForAccount (address) {
+ const hexed = normalize(address)
+
+ return Promise.all(this.keyrings.map((keyring) => {
+ return Promise.all([
+ keyring,
+ keyring.getAccounts(),
+ ])
+ }))
+ .then(filter((candidate) => {
+ const accounts = candidate[1].map(normalize)
+ return accounts.includes(hexed)
+ }))
+ .then((winners) => {
+ if (winners && winners.length > 0) {
+ return winners[0][0]
+ } else {
+ throw new Error('No keyring found for the requested account.')
+ }
+ })
+ }
+
+ // Display For Keyring
+ // @Keyring keyring
+ //
+ // returns Promise( @Object { type:String, accounts:Array } )
+ //
+ // Is used for adding the current keyrings to the state object.
+ displayForKeyring (keyring) {
+ return keyring.getAccounts()
+ .then((accounts) => {
+ return {
+ type: keyring.type,
+ accounts: accounts,
+ }
+ })
+ }
+
+ // Add Gas Buffer
+ // @string gas (as hexadecimal value)
+ //
+ // returns @string bufferedGas (as hexadecimal value)
+ //
+ // Adds a healthy buffer of gas to an initial gas estimate.
+ addGasBuffer (gas) {
+ const gasBuffer = new BN('100000', 10)
+ const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16)
+ const correct = bnGas.add(gasBuffer)
+ return ethUtil.addHexPrefix(correct.toString(16))
+ }
+
+ // Clear Keyrings
+ //
+ // Deallocates all currently managed keyrings and accounts.
+ // Used before initializing a new vault.
+ clearKeyrings () {
let accounts
try {
accounts = Object.keys(this.ethStore._currentState.accounts)
@@ -563,9 +677,5 @@ module.exports = class KeyringController extends EventEmitter {
}
-function normalize(address) {
- if (!address) return
- return ethUtil.addHexPrefix(address.toLowerCase())
-}
function noop () {}
diff --git a/app/scripts/keyrings/hd.js b/app/scripts/keyrings/hd.js
index 4bfc56c15..1b9796e07 100644
--- a/app/scripts/keyrings/hd.js
+++ b/app/scripts/keyrings/hd.js
@@ -2,106 +2,110 @@ const EventEmitter = require('events').EventEmitter
const hdkey = require('ethereumjs-wallet/hdkey')
const bip39 = require('bip39')
const ethUtil = require('ethereumjs-util')
-const type = 'HD Key Tree'
+
+// *Internal Deps
const sigUtil = require('../lib/sig-util')
+// Options:
const hdPathString = `m/44'/60'/0'/0`
+const type = 'HD Key Tree'
-module.exports = class HdKeyring extends EventEmitter {
+class HdKeyring extends EventEmitter {
- static type() {
- return type
- }
+ /* PUBLIC METHODS */
- constructor(opts = {}) {
+ constructor (opts = {}) {
super()
this.type = type
this.deserialize(opts)
}
- deserialize(opts = {}) {
+ serialize () {
+ return Promise.resolve({
+ mnemonic: this.mnemonic,
+ numberOfAccounts: this.wallets.length,
+ })
+ }
+
+ deserialize (opts = {}) {
this.opts = opts || {}
this.wallets = []
this.mnemonic = null
this.root = null
- if ('mnemonic' in opts) {
- this.initFromMnemonic(opts.mnemonic)
+ if (opts.mnemonic) {
+ this._initFromMnemonic(opts.mnemonic)
}
- if ('n' in opts) {
- this.addAccounts(opts.n)
- }
- }
-
- initFromMnemonic(mnemonic) {
- this.mnemonic = mnemonic
- const seed = bip39.mnemonicToSeed(mnemonic)
- this.hdWallet = hdkey.fromMasterSeed(seed)
- this.root = this.hdWallet.derivePath(hdPathString)
- }
-
- serialize() {
- return {
- mnemonic: this.mnemonic,
- n: this.wallets.length,
+ if (opts.numberOfAccounts) {
+ return this.addAccounts(opts.numberOfAccounts)
}
- }
- exportAccount(address) {
- const wallet = this.getWalletForAccount(address)
- return wallet.getPrivateKey().toString('hex')
+ return Promise.resolve([])
}
- addAccounts(n = 1) {
+ addAccounts (numberOfAccounts = 1) {
if (!this.root) {
- this.initFromMnemonic(bip39.generateMnemonic())
+ this._initFromMnemonic(bip39.generateMnemonic())
}
const oldLen = this.wallets.length
const newWallets = []
- for (let i = oldLen; i < n + oldLen; i++) {
+ for (let i = oldLen; i < numberOfAccounts + oldLen; i++) {
const child = this.root.deriveChild(i)
const wallet = child.getWallet()
newWallets.push(wallet)
this.wallets.push(wallet)
}
- return newWallets.map(w => w.getAddress().toString('hex'))
+ const hexWallets = newWallets.map(w => w.getAddress().toString('hex'))
+ return Promise.resolve(hexWallets)
}
- getAccounts() {
- return this.wallets.map(w => w.getAddress().toString('hex'))
+ getAccounts () {
+ return Promise.resolve(this.wallets.map(w => w.getAddress().toString('hex')))
}
// tx is an instance of the ethereumjs-transaction class.
- signTransaction(address, tx) {
- const wallet = this.getWalletForAccount(address)
+ signTransaction (address, tx) {
+ const wallet = this._getWalletForAccount(address)
var privKey = wallet.getPrivateKey()
tx.sign(privKey)
- return tx
+ return Promise.resolve(tx)
}
// For eth_sign, we need to sign transactions:
- signMessage(withAccount, data) {
- const wallet = this.getWalletForAccount(withAccount)
- const message = ethUtil.removeHexPrefix(data)
+ signMessage (withAccount, data) {
+ const wallet = this._getWalletForAccount(withAccount)
+ const message = ethUtil.stripHexPrefix(data)
var privKey = wallet.getPrivateKey()
var msgSig = ethUtil.ecsign(new Buffer(message, 'hex'), privKey)
var rawMsgSig = ethUtil.bufferToHex(sigUtil.concatSig(msgSig.v, msgSig.r, msgSig.s))
- return rawMsgSig
+ return Promise.resolve(rawMsgSig)
}
- getWalletForAccount(account) {
- return this.wallets.find((w) => {
- const address = w.getAddress().toString('hex')
- return ((address === account) || (normalize(address) === account))
- })
+ exportAccount (address) {
+ const wallet = this._getWalletForAccount(address)
+ return Promise.resolve(wallet.getPrivateKey().toString('hex'))
}
+ /* PRIVATE METHODS */
-}
+ _initFromMnemonic (mnemonic) {
+ this.mnemonic = mnemonic
+ const seed = bip39.mnemonicToSeed(mnemonic)
+ this.hdWallet = hdkey.fromMasterSeed(seed)
+ this.root = this.hdWallet.derivePath(hdPathString)
+ }
-function normalize(address) {
- return ethUtil.addHexPrefix(address.toLowerCase())
+
+ _getWalletForAccount (account) {
+ return this.wallets.find((w) => {
+ const address = w.getAddress().toString('hex')
+ return ((address === account) || (sigUtil.normalize(address) === account))
+ })
+ }
}
+
+HdKeyring.type = type
+module.exports = HdKeyring
diff --git a/app/scripts/keyrings/simple.js b/app/scripts/keyrings/simple.js
index 9e832f274..d604430b8 100644
--- a/app/scripts/keyrings/simple.js
+++ b/app/scripts/keyrings/simple.js
@@ -4,65 +4,78 @@ const ethUtil = require('ethereumjs-util')
const type = 'Simple Key Pair'
const sigUtil = require('../lib/sig-util')
-module.exports = class SimpleKeyring extends EventEmitter {
+class SimpleKeyring extends EventEmitter {
- static type() {
- return type
- }
+ /* PUBLIC METHODS */
- constructor(opts) {
+ constructor (opts) {
super()
this.type = type
this.opts = opts || {}
this.wallets = []
}
- serialize() {
- return this.wallets.map(w => w.getPrivateKey().toString('hex'))
+ serialize () {
+ return Promise.resolve(this.wallets.map(w => w.getPrivateKey().toString('hex')))
}
- deserialize(wallets = []) {
- this.wallets = wallets.map((w) => {
- var b = new Buffer(w, 'hex')
- const wallet = Wallet.fromPrivateKey(b)
+ deserialize (privateKeys = []) {
+ this.wallets = privateKeys.map((privateKey) => {
+ const stripped = ethUtil.stripHexPrefix(privateKey)
+ const buffer = new Buffer(stripped, 'hex')
+ const wallet = Wallet.fromPrivateKey(buffer)
return wallet
})
+ return Promise.resolve()
}
- addAccounts(n = 1) {
+ addAccounts (n = 1) {
var newWallets = []
for (var i = 0; i < n; i++) {
newWallets.push(Wallet.generate())
}
this.wallets = this.wallets.concat(newWallets)
- return newWallets.map(w => w.getAddress().toString('hex'))
+ const hexWallets = newWallets.map(w => ethUtil.bufferToHex(w.getAddress()))
+ return Promise.resolve(hexWallets)
}
- getAccounts() {
- return this.wallets.map(w => w.getAddress().toString('hex'))
+ getAccounts () {
+ return Promise.resolve(this.wallets.map(w => ethUtil.bufferToHex(w.getAddress())))
}
// tx is an instance of the ethereumjs-transaction class.
- signTransaction(address, tx) {
- const wallet = this.getWalletForAccount(address)
+ signTransaction (address, tx) {
+ const wallet = this._getWalletForAccount(address)
var privKey = wallet.getPrivateKey()
tx.sign(privKey)
- return tx
+ return Promise.resolve(tx)
}
// For eth_sign, we need to sign transactions:
- signMessage(withAccount, data) {
- const wallet = this.getWalletForAccount(withAccount)
- const message = ethUtil.removeHexPrefix(data)
+ signMessage (withAccount, data) {
+ const wallet = this._getWalletForAccount(withAccount)
+ const message = ethUtil.stripHexPrefix(data)
var privKey = wallet.getPrivateKey()
var msgSig = ethUtil.ecsign(new Buffer(message, 'hex'), privKey)
var rawMsgSig = ethUtil.bufferToHex(sigUtil.concatSig(msgSig.v, msgSig.r, msgSig.s))
- return rawMsgSig
+ return Promise.resolve(rawMsgSig)
+ }
+
+ exportAccount (address) {
+ const wallet = this._getWalletForAccount(address)
+ return Promise.resolve(wallet.getPrivateKey().toString('hex'))
}
- getWalletForAccount(account) {
- return this.wallets.find(w => w.getAddress().toString('hex') === account)
+
+ /* PRIVATE METHODS */
+
+ _getWalletForAccount (account) {
+ let wallet = this.wallets.find(w => ethUtil.bufferToHex(w.getAddress()) === account)
+ if (!wallet) throw new Error('Simple Keyring - Unable to find matching address.')
+ return wallet
}
}
+SimpleKeyring.type = type
+module.exports = SimpleKeyring
diff --git a/app/scripts/lib/auto-reload.js b/app/scripts/lib/auto-reload.js
index 3c90905db..1302df35f 100644
--- a/app/scripts/lib/auto-reload.js
+++ b/app/scripts/lib/auto-reload.js
@@ -18,17 +18,16 @@ function setupDappAutoReload (web3) {
return handleResetRequest
- function handleResetRequest() {
+ function handleResetRequest () {
resetWasRequested = true
// ignore if web3 was not used
if (!pageIsUsingWeb3) return
// reload after short timeout
setTimeout(triggerReset, 500)
}
-
}
// reload the page
function triggerReset () {
global.location.reload()
-} \ No newline at end of file
+}
diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js
index f50d95c12..e927c78ec 100644
--- a/app/scripts/lib/config-manager.js
+++ b/app/scripts/lib/config-manager.js
@@ -1,12 +1,12 @@
const Migrator = require('pojo-migrator')
const MetamaskConfig = require('../config.js')
const migrations = require('./migrations')
-const rp = require('request-promise')
const ethUtil = require('ethereumjs-util')
+const normalize = require('./sig-util').normalize
const TESTNET_RPC = MetamaskConfig.network.testnet
const MAINNET_RPC = MetamaskConfig.network.mainnet
-const txLimit = 40
+const MORDEN_RPC = MetamaskConfig.network.morden
/* The config-manager is a convenience object
* wrapping a pojo-migrator.
@@ -17,8 +17,6 @@ const txLimit = 40
*/
module.exports = ConfigManager
function ConfigManager (opts) {
- this.txLimit = txLimit
-
// ConfigManager is observable and will emit updates
this._subs = []
@@ -119,7 +117,7 @@ ConfigManager.prototype.setVault = function (encryptedString) {
ConfigManager.prototype.getVault = function () {
var data = this.getData()
- return ('vault' in data) && data.vault
+ return data.vault
}
ConfigManager.prototype.getKeychains = function () {
@@ -182,6 +180,9 @@ ConfigManager.prototype.getCurrentRpcAddress = function () {
case 'testnet':
return TESTNET_RPC
+ case 'morden':
+ return MORDEN_RPC
+
default:
return provider && provider.rpcTarget ? provider.rpcTarget : TESTNET_RPC
}
@@ -204,61 +205,12 @@ ConfigManager.prototype.getTxList = function () {
}
}
-ConfigManager.prototype.unconfirmedTxs = function () {
- var transactions = this.getTxList()
- return transactions.filter(tx => tx.status === 'unconfirmed')
- .reduce((result, tx) => { result[tx.id] = tx; return result }, {})
-}
-
-ConfigManager.prototype._saveTxList = function (txList) {
+ConfigManager.prototype.setTxList = function (txList) {
var data = this.migrator.getData()
data.transactions = txList
this.setData(data)
}
-ConfigManager.prototype.addTx = function (tx) {
- var transactions = this.getTxList()
- while (transactions.length > this.txLimit - 1) {
- transactions.shift()
- }
- transactions.push(tx)
- this._saveTxList(transactions)
-}
-
-ConfigManager.prototype.getTx = function (txId) {
- var transactions = this.getTxList()
- var matching = transactions.filter(tx => tx.id === txId)
- return matching.length > 0 ? matching[0] : null
-}
-
-ConfigManager.prototype.confirmTx = function (txId) {
- this._setTxStatus(txId, 'confirmed')
-}
-
-ConfigManager.prototype.rejectTx = function (txId) {
- this._setTxStatus(txId, 'rejected')
-}
-
-ConfigManager.prototype._setTxStatus = function (txId, status) {
- var tx = this.getTx(txId)
- tx.status = status
- this.updateTx(tx)
-}
-
-ConfigManager.prototype.updateTx = function (tx) {
- var transactions = this.getTxList()
- var found, index
- transactions.forEach((otherTx, i) => {
- if (otherTx.id === tx.id) {
- found = true
- index = i
- }
- })
- if (found) {
- transactions[index] = tx
- }
- this._saveTxList(transactions)
-}
// wallet nickname methods
@@ -269,13 +221,13 @@ ConfigManager.prototype.getWalletNicknames = function () {
}
ConfigManager.prototype.nicknameForWallet = function (account) {
- const address = ethUtil.addHexPrefix(account.toLowerCase())
+ const address = normalize(account)
const nicknames = this.getWalletNicknames()
return nicknames[address]
}
ConfigManager.prototype.setNicknameForWallet = function (account, nickname) {
- const address = ethUtil.addHexPrefix(account.toLowerCase())
+ const address = normalize(account)
const nicknames = this.getWalletNicknames()
nicknames[address] = nickname
var data = this.getData()
@@ -290,7 +242,7 @@ ConfigManager.prototype.getSalt = function () {
return ('salt' in data) && data.salt
}
-ConfigManager.prototype.setSalt = function(salt) {
+ConfigManager.prototype.setSalt = function (salt) {
var data = this.getData()
data.salt = salt
this.setData(data)
@@ -313,15 +265,15 @@ ConfigManager.prototype._emitUpdates = function (state) {
})
}
-ConfigManager.prototype.setConfirmed = function (confirmed) {
+ConfigManager.prototype.setConfirmedDisclaimer = function (confirmed) {
var data = this.getData()
- data.isConfirmed = confirmed
+ data.isDisclaimerConfirmed = confirmed
this.setData(data)
}
-ConfigManager.prototype.getConfirmed = function () {
+ConfigManager.prototype.getConfirmedDisclaimer = function () {
var data = this.getData()
- return ('isConfirmed' in data) && data.isConfirmed
+ return ('isDisclaimerConfirmed' in data) && data.isDisclaimerConfirmed
}
ConfigManager.prototype.setTOSHash = function (hash) {
@@ -348,17 +300,16 @@ ConfigManager.prototype.getCurrentFiat = function () {
ConfigManager.prototype.updateConversionRate = function () {
var data = this.getData()
- return rp(`https://www.cryptonator.com/api/ticker/eth-${data.fiatCurrency}`)
- .then((response) => {
- const parsedResponse = JSON.parse(response)
+ return fetch(`https://www.cryptonator.com/api/ticker/eth-${data.fiatCurrency}`)
+ .then(response => response.json())
+ .then((parsedResponse) => {
this.setConversionPrice(parsedResponse.ticker.price)
this.setConversionDate(parsedResponse.timestamp)
}).catch((err) => {
- console.error('Error in conversion.', err)
+ console.warn('MetaMask - Failed to query currency conversion.')
this.setConversionPrice(0)
this.setConversionDate('N/A')
})
-
}
ConfigManager.prototype.setConversionPrice = function (price) {
@@ -428,3 +379,14 @@ ConfigManager.prototype.setGasMultiplier = function (gasMultiplier) {
data.gasMultiplier = gasMultiplier
this.setData(data)
}
+
+ConfigManager.prototype.setLostAccounts = function (lostAccounts) {
+ var data = this.getData()
+ data.lostAccounts = lostAccounts
+ this.setData(data)
+}
+
+ConfigManager.prototype.getLostAccounts = function () {
+ var data = this.getData()
+ return data.lostAccounts || []
+}
diff --git a/app/scripts/lib/encryptor.js b/app/scripts/lib/encryptor.js
deleted file mode 100644
index fe83b86dd..000000000
--- a/app/scripts/lib/encryptor.js
+++ /dev/null
@@ -1,149 +0,0 @@
-var ethUtil = require('ethereumjs-util')
-
-module.exports = {
-
- // Simple encryption methods:
- encrypt,
- decrypt,
-
- // More advanced encryption methods:
- keyFromPassword,
- encryptWithKey,
- decryptWithKey,
-
- // Buffer <-> String methods
- convertArrayBufferViewtoString,
- convertStringToArrayBufferView,
-
- // Buffer <-> Hex string methods
- serializeBufferForStorage,
- serializeBufferFromStorage,
-
- // Buffer <-> base64 string methods
- encodeBufferToBase64,
- decodeBase64ToBuffer,
-
- generateSalt,
-}
-
-// Takes a Pojo, returns encrypted text.
-function encrypt (password, dataObj) {
- return keyFromPassword(password)
- .then(function (passwordDerivedKey) {
- return encryptWithKey(passwordDerivedKey, dataObj)
- })
-}
-
-function encryptWithKey (key, dataObj) {
- var data = JSON.stringify(dataObj)
- var dataBuffer = convertStringToArrayBufferView(data)
- var vector = global.crypto.getRandomValues(new Uint8Array(16))
-
- return global.crypto.subtle.encrypt({
- name: 'AES-GCM',
- iv: vector,
- }, key, dataBuffer).then(function(buf){
- var buffer = new Uint8Array(buf)
- var vectorStr = encodeBufferToBase64(vector)
- var vaultStr = encodeBufferToBase64(buffer)
- return `${vaultStr}\\${vectorStr}`
- })
-}
-
-// Takes encrypted text, returns the restored Pojo.
-function decrypt (password, text) {
- return keyFromPassword(password)
- .then(function (key) {
- return decryptWithKey(key, text)
- })
-}
-
-function decryptWithKey (key, text) {
- const parts = text.split('\\')
- const encryptedData = decodeBase64ToBuffer(parts[0])
- const vector = decodeBase64ToBuffer(parts[1])
- return crypto.subtle.decrypt({name: 'AES-GCM', iv: vector}, key, encryptedData)
- .then(function(result){
- const decryptedData = new Uint8Array(result)
- const decryptedStr = convertArrayBufferViewtoString(decryptedData)
- const decryptedObj = JSON.parse(decryptedStr)
- return decryptedObj
- })
- .catch(function(reason) {
- throw new Error('Incorrect password')
- })
-}
-
-function convertStringToArrayBufferView (str) {
- var bytes = new Uint8Array(str.length)
- for (var i = 0; i < str.length; i++) {
- bytes[i] = str.charCodeAt(i)
- }
-
- return bytes
-}
-
-function convertArrayBufferViewtoString (buffer) {
- var str = ''
- for (var i = 0; i < buffer.byteLength; i++) {
- str += String.fromCharCode(buffer[i])
- }
-
- return str
-}
-
-function keyFromPassword (password) {
- var passBuffer = convertStringToArrayBufferView(password)
- return global.crypto.subtle.digest('SHA-256', passBuffer)
- .then(function (passHash){
- return global.crypto.subtle.importKey('raw', passHash, {name: 'AES-GCM'}, false, ['encrypt', 'decrypt'])
- })
-}
-
-function serializeBufferFromStorage (str) {
- str = ethUtil.stripHexPrefix(str)
- var buf = new Uint8Array(str.length / 2)
- for (var i = 0; i < str.length; i += 2) {
- var seg = str.substr(i, 2)
- buf[i / 2] = parseInt(seg, 16)
- }
- return buf
-}
-
-// Should return a string, ready for storage, in hex format.
-function serializeBufferForStorage (buffer) {
- var result = '0x'
- var len = buffer.length || buffer.byteLength
- for (var i = 0; i < len; i++) {
- result += unprefixedHex(buffer[i])
- }
- return result
-}
-
-function unprefixedHex (num) {
- var hex = num.toString(16)
- while (hex.length < 2) {
- hex = '0' + hex
- }
- return hex
-}
-
-function encodeBufferToBase64 (buf) {
- var b64encoded = btoa(String.fromCharCode.apply(null, buf))
- return b64encoded
-}
-
-function decodeBase64ToBuffer (base64) {
- var buf = new Uint8Array(atob(base64).split('')
- .map(function(c) {
- return c.charCodeAt(0)
- }))
- return buf
-}
-
-function generateSalt (byteCount = 32) {
- var view = new Uint8Array(byteCount)
- global.crypto.getRandomValues(view)
- var b64encoded = btoa(String.fromCharCode.apply(null, view))
- return b64encoded
-}
diff --git a/app/scripts/lib/eth-store.js b/app/scripts/lib/eth-store.js
new file mode 100644
index 000000000..7e2caf884
--- /dev/null
+++ b/app/scripts/lib/eth-store.js
@@ -0,0 +1,146 @@
+/* Ethereum Store
+ *
+ * This module is responsible for tracking any number of accounts
+ * and caching their current balances & transaction counts.
+ *
+ * It also tracks transaction hashes, and checks their inclusion status
+ * on each new block.
+ */
+
+const EventEmitter = require('events').EventEmitter
+const inherits = require('util').inherits
+const async = require('async')
+const clone = require('clone')
+const EthQuery = require('eth-query')
+
+module.exports = EthereumStore
+
+
+inherits(EthereumStore, EventEmitter)
+function EthereumStore(engine) {
+ const self = this
+ EventEmitter.call(self)
+ self._currentState = {
+ accounts: {},
+ transactions: {},
+ }
+ self._query = new EthQuery(engine)
+
+ engine.on('block', self._updateForBlock.bind(self))
+}
+
+//
+// public
+//
+
+EthereumStore.prototype.getState = function () {
+ const self = this
+ return clone(self._currentState)
+}
+
+EthereumStore.prototype.addAccount = function (address) {
+ const self = this
+ self._currentState.accounts[address] = {}
+ self._didUpdate()
+ if (!self.currentBlockNumber) return
+ self._updateAccount(address, () => {
+ self._didUpdate()
+ })
+}
+
+EthereumStore.prototype.removeAccount = function (address) {
+ const self = this
+ delete self._currentState.accounts[address]
+ self._didUpdate()
+}
+
+EthereumStore.prototype.addTransaction = function (txHash) {
+ const self = this
+ self._currentState.transactions[txHash] = {}
+ self._didUpdate()
+ if (!self.currentBlockNumber) return
+ self._updateTransaction(self.currentBlockNumber, txHash, noop)
+}
+
+EthereumStore.prototype.removeTransaction = function (address) {
+ const self = this
+ delete self._currentState.transactions[address]
+ self._didUpdate()
+}
+
+
+//
+// private
+//
+
+EthereumStore.prototype._didUpdate = function () {
+ const self = this
+ var state = self.getState()
+ self.emit('update', state)
+}
+
+EthereumStore.prototype._updateForBlock = function (block) {
+ const self = this
+ var blockNumber = '0x' + block.number.toString('hex')
+ self.currentBlockNumber = blockNumber
+ async.parallel([
+ self._updateAccounts.bind(self),
+ self._updateTransactions.bind(self, blockNumber),
+ ], function (err) {
+ if (err) return console.error(err)
+ self.emit('block', self.getState())
+ self._didUpdate()
+ })
+}
+
+EthereumStore.prototype._updateAccounts = function (cb) {
+ var accountsState = this._currentState.accounts
+ var addresses = Object.keys(accountsState)
+ async.each(addresses, this._updateAccount.bind(this), cb)
+}
+
+EthereumStore.prototype._updateAccount = function (address, cb) {
+ var accountsState = this._currentState.accounts
+ this.getAccount(address, function (err, result) {
+ if (err) return cb(err)
+ result.address = address
+ // only populate if the entry is still present
+ if (accountsState[address]) {
+ accountsState[address] = result
+ }
+ cb(null, result)
+ })
+}
+
+EthereumStore.prototype.getAccount = function (address, cb) {
+ const query = this._query
+ async.parallel({
+ balance: query.getBalance.bind(query, address),
+ nonce: query.getTransactionCount.bind(query, address),
+ code: query.getCode.bind(query, address),
+ }, cb)
+}
+
+EthereumStore.prototype._updateTransactions = function (block, cb) {
+ const self = this
+ var transactionsState = self._currentState.transactions
+ var txHashes = Object.keys(transactionsState)
+ async.each(txHashes, self._updateTransaction.bind(self, block), cb)
+}
+
+EthereumStore.prototype._updateTransaction = function (block, txHash, cb) {
+ const self = this
+ // would use the block here to determine how many confirmations the tx has
+ var transactionsState = self._currentState.transactions
+ self._query.getTransaction(txHash, function (err, result) {
+ if (err) return cb(err)
+ // only populate if the entry is still present
+ if (transactionsState[txHash]) {
+ transactionsState[txHash] = result
+ self._didUpdate()
+ }
+ cb(null, result)
+ })
+}
+
+function noop() {}
diff --git a/app/scripts/lib/idStore-migrator.js b/app/scripts/lib/idStore-migrator.js
index 2d1826641..655aed0af 100644
--- a/app/scripts/lib/idStore-migrator.js
+++ b/app/scripts/lib/idStore-migrator.js
@@ -1,5 +1,8 @@
const IdentityStore = require('./idStore')
-
+const HdKeyring = require('../keyrings/hd')
+const sigUtil = require('./sig-util')
+const normalize = sigUtil.normalize
+const denodeify = require('denodeify')
module.exports = class IdentityStoreMigrator {
@@ -11,7 +14,7 @@ module.exports = class IdentityStoreMigrator {
}
}
- oldSeedForPassword( password ) {
+ migratedVaultForPassword (password) {
const hasOldVault = this.hasOldVault()
const configManager = this.configManager
@@ -23,29 +26,54 @@ module.exports = class IdentityStoreMigrator {
return Promise.resolve(null)
}
- return new Promise((resolve, reject) => {
- this.idStore.submitPassword(password, (err) => {
- if (err) return reject(err)
- try {
- resolve(this.serializeVault())
- } catch (e) {
- reject(e)
- }
- })
+ const idStore = this.idStore
+ const submitPassword = denodeify(idStore.submitPassword.bind(idStore))
+
+ return submitPassword(password)
+ .then(() => {
+ const serialized = this.serializeVault()
+ return this.checkForLostAccounts(serialized)
})
}
- serializeVault() {
+ serializeVault () {
const mnemonic = this.idStore._idmgmt.getSeed()
- const n = this.idStore._getAddresses().length
+ const numberOfAccounts = this.idStore._getAddresses().length
return {
type: 'HD Key Tree',
- data: { mnemonic, n },
+ data: { mnemonic, numberOfAccounts },
}
}
- hasOldVault() {
+ checkForLostAccounts (serialized) {
+ const hd = new HdKeyring()
+ return hd.deserialize(serialized.data)
+ .then((hexAccounts) => {
+ const newAccounts = hexAccounts.map(normalize)
+ const oldAccounts = this.idStore._getAddresses().map(normalize)
+ const lostAccounts = oldAccounts.reduce((result, account) => {
+ if (newAccounts.includes(account)) {
+ return result
+ } else {
+ result.push(account)
+ return result
+ }
+ }, [])
+
+ return {
+ serialized,
+ lostAccounts: lostAccounts.map((address) => {
+ return {
+ address,
+ privateKey: this.idStore.exportAccount(address),
+ }
+ }),
+ }
+ })
+ }
+
+ hasOldVault () {
const wallet = this.configManager.getWallet()
return wallet
}
diff --git a/app/scripts/lib/idStore.js b/app/scripts/lib/idStore.js
index c566907b9..e4cbca456 100644
--- a/app/scripts/lib/idStore.js
+++ b/app/scripts/lib/idStore.js
@@ -1,19 +1,14 @@
const EventEmitter = require('events').EventEmitter
const inherits = require('util').inherits
-const async = require('async')
const ethUtil = require('ethereumjs-util')
-const BN = ethUtil.BN
-const EthQuery = require('eth-query')
const KeyStore = require('eth-lightwallet').keystore
const clone = require('clone')
const extend = require('xtend')
-const createId = require('web3-provider-engine/util/random-id')
-const ethBinToOps = require('eth-bin-to-ops')
const autoFaucet = require('./auto-faucet')
-const messageManager = require('./message-manager')
const DEFAULT_RPC = 'https://testrpc.metamask.io/'
const IdManagement = require('./id-management')
+
module.exports = IdentityStore
inherits(IdentityStore, EventEmitter)
@@ -34,17 +29,14 @@ function IdentityStore (opts = {}) {
selectedAddress: null,
identities: {},
}
-
// not part of serilized metamask state - only kept in memory
- this._unconfTxCbs = {}
- this._unconfMsgCbs = {}
}
//
// public
//
-IdentityStore.prototype.createNewVault = function (password, entropy, cb) {
+IdentityStore.prototype.createNewVault = function (password, cb) {
delete this._keyStore
var serializedKeystore = this.configManager.getWallet()
@@ -53,7 +45,7 @@ IdentityStore.prototype.createNewVault = function (password, entropy, cb) {
}
this.purgeCache()
- this._createVault(password, null, entropy, (err) => {
+ this._createVault(password, null, (err) => {
if (err) return cb(err)
this._autoFaucet()
@@ -77,7 +69,7 @@ IdentityStore.prototype.recoverSeed = function (cb) {
IdentityStore.prototype.recoverFromSeed = function (password, seed, cb) {
this.purgeCache()
- this._createVault(password, seed, null, (err) => {
+ this._createVault(password, seed, (err) => {
if (err) return cb(err)
this._loadIdentities()
@@ -102,11 +94,7 @@ IdentityStore.prototype.getState = function () {
isInitialized: !!configManager.getWallet() && !seedWords,
isUnlocked: this._isUnlocked(),
seedWords: seedWords,
- isConfirmed: configManager.getConfirmed(),
- unconfTxs: configManager.unconfirmedTxs(),
- transactions: configManager.getTxList(),
- unconfMsgs: messageManager.unconfirmedMsgs(),
- messages: messageManager.getMsgList(),
+ isDisclaimerConfirmed: configManager.getConfirmedDisclaimer(),
selectedAddress: configManager.getSelectedAccount(),
shapeShiftTxList: configManager.getShapeShiftTxList(),
currentFiat: configManager.getCurrentFiat(),
@@ -202,206 +190,10 @@ IdentityStore.prototype.submitPassword = function (password, cb) {
IdentityStore.prototype.exportAccount = function (address, cb) {
var privateKey = this._idmgmt.exportPrivateKey(address)
- cb(null, privateKey)
-}
-
-//
-// Transactions
-//
-
-// comes from dapp via zero-client hooked-wallet provider
-IdentityStore.prototype.addUnconfirmedTransaction = function (txParams, onTxDoneCb, cb) {
- const configManager = this.configManager
-
- var self = this
- // create txData obj with parameters and meta data
- var time = (new Date()).getTime()
- var txId = createId()
- txParams.metamaskId = txId
- txParams.metamaskNetworkId = self._currentState.network
- var txData = {
- id: txId,
- txParams: txParams,
- time: time,
- status: 'unconfirmed',
- gasMultiplier: configManager.getGasMultiplier() || 1,
- }
-
- console.log('addUnconfirmedTransaction:', txData)
-
- // keep the onTxDoneCb around for after approval/denial (requires user interaction)
- // This onTxDoneCb fires completion to the Dapp's write operation.
- self._unconfTxCbs[txId] = onTxDoneCb
-
- var provider = self._ethStore._query.currentProvider
- var query = new EthQuery(provider)
-
- // calculate metadata for tx
- async.parallel([
- analyzeForDelegateCall,
- estimateGas,
- ], didComplete)
-
- // perform static analyis on the target contract code
- function analyzeForDelegateCall(cb){
- if (txParams.to) {
- query.getCode(txParams.to, (err, result) => {
- if (err) return cb(err)
- var containsDelegateCall = self.checkForDelegateCall(result)
- txData.containsDelegateCall = containsDelegateCall
- cb()
- })
- } else {
- cb()
- }
- }
-
- function estimateGas(cb){
- query.estimateGas(txParams, function(err, result){
- if (err) return cb(err)
- txData.estimatedGas = self.addGasBuffer(result)
- cb()
- })
- }
-
- function didComplete (err) {
- if (err) return cb(err)
- configManager.addTx(txData)
- // signal update
- self._didUpdate()
- // signal completion of add tx
- cb(null, txData)
- }
+ if (cb) cb(null, privateKey)
+ return privateKey
}
-IdentityStore.prototype.checkForDelegateCall = function (codeHex) {
- const code = ethUtil.toBuffer(codeHex)
- if (code !== '0x') {
- const ops = ethBinToOps(code)
- const containsDelegateCall = ops.some((op) => op.name === 'DELEGATECALL')
- return containsDelegateCall
- } else {
- return false
- }
-}
-
-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
-IdentityStore.prototype.approveTransaction = function (txId, cb) {
- const configManager = this.configManager
- var approvalCb = this._unconfTxCbs[txId] || noop
-
- // accept tx
- cb()
- approvalCb(null, true)
- // clean up
- configManager.confirmTx(txId)
- delete this._unconfTxCbs[txId]
- this._didUpdate()
-}
-
-// comes from metamask ui
-IdentityStore.prototype.cancelTransaction = function (txId) {
- const configManager = this.configManager
- var approvalCb = this._unconfTxCbs[txId] || noop
-
- // reject tx
- approvalCb(null, false)
- // clean up
- configManager.rejectTx(txId)
- delete this._unconfTxCbs[txId]
- this._didUpdate()
-}
-
-// performs the actual signing, no autofill of params
-IdentityStore.prototype.signTransaction = function (txParams, cb) {
- try {
- console.log('signing tx...', txParams)
- var rawTx = this._idmgmt.signTx(txParams)
- cb(null, rawTx)
- } catch (err) {
- cb(err)
- }
-}
-
-//
-// Messages
-//
-
-// comes from dapp via zero-client hooked-wallet provider
-IdentityStore.prototype.addUnconfirmedMessage = function (msgParams, cb) {
- // create txData obj with parameters and meta data
- var time = (new Date()).getTime()
- var msgId = createId()
- var msgData = {
- id: msgId,
- msgParams: msgParams,
- time: time,
- status: 'unconfirmed',
- }
- messageManager.addMsg(msgData)
- console.log('addUnconfirmedMessage:', msgData)
-
- // keep the cb around for after approval (requires user interaction)
- // This cb fires completion to the Dapp's write operation.
- this._unconfMsgCbs[msgId] = cb
-
- // signal update
- this._didUpdate()
-
- return msgId
-}
-
-// comes from metamask ui
-IdentityStore.prototype.approveMessage = function (msgId, cb) {
- var approvalCb = this._unconfMsgCbs[msgId] || noop
-
- // accept msg
- cb()
- approvalCb(null, true)
- // clean up
- messageManager.confirmMsg(msgId)
- delete this._unconfMsgCbs[msgId]
- this._didUpdate()
-}
-
-// comes from metamask ui
-IdentityStore.prototype.cancelMessage = function (msgId) {
- var approvalCb = this._unconfMsgCbs[msgId] || noop
-
- // reject tx
- approvalCb(null, false)
- // clean up
- messageManager.rejectMsg(msgId)
- delete this._unconfTxCbs[msgId]
- this._didUpdate()
-}
-
-// performs the actual signing, no autofill of params
-IdentityStore.prototype.signMessage = function (msgParams, cb) {
- try {
- console.log('signing msg...', msgParams.data)
- var rawMsg = this._idmgmt.signMsg(msgParams.from, msgParams.data)
- if ('metamaskId' in msgParams) {
- var id = msgParams.metamaskId
- delete msgParams.metamaskId
-
- this.approveMessage(id, cb)
- } else {
- cb(null, rawMsg)
- }
- } catch (err) {
- cb(err)
- }
-}
-
-//
// private
//
@@ -481,7 +273,7 @@ IdentityStore.prototype.tryPassword = function (password, cb) {
})
}
-IdentityStore.prototype._createVault = function (password, seedPhrase, entropy, cb) {
+IdentityStore.prototype._createVault = function (password, seedPhrase, cb) {
const opts = {
password,
hdPathString: this.hdPathString,
@@ -556,4 +348,3 @@ IdentityStore.prototype._autoFaucet = function () {
// util
-function noop () {}
diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js
index 052a8f5fe..11bd5cc3a 100644
--- a/app/scripts/lib/inpage-provider.js
+++ b/app/scripts/lib/inpage-provider.js
@@ -2,6 +2,7 @@ const Streams = require('mississippi')
const StreamProvider = require('web3-stream-provider')
const ObjectMultiplex = require('./obj-multiplex')
const RemoteStore = require('./remote-store.js').RemoteStore
+const createRandomId = require('./random-id')
module.exports = MetamaskInpageProvider
@@ -39,7 +40,7 @@ function MetamaskInpageProvider (connectionStream) {
self.idMap = {}
// handle sendAsync requests via asyncProvider
- self.sendAsync = function(payload, cb){
+ self.sendAsync = function (payload, cb) {
// rewrite request ids
var request = eachJsonMessage(payload, (message) => {
var newId = createRandomId()
@@ -48,7 +49,7 @@ function MetamaskInpageProvider (connectionStream) {
return message
})
// forward to asyncProvider
- asyncProvider.sendAsync(request, function(err, res){
+ asyncProvider.sendAsync(request, function (err, res) {
if (err) return cb(err)
// transform messages to original ids
eachJsonMessage(res, (message) => {
@@ -65,20 +66,25 @@ function MetamaskInpageProvider (connectionStream) {
MetamaskInpageProvider.prototype.send = function (payload) {
const self = this
- let selectedAddress
+ let selectedAccount
let result = null
switch (payload.method) {
case 'eth_accounts':
// read from localStorage
- selectedAddress = self.publicConfigStore.get('selectedAddress')
- result = selectedAddress ? [selectedAddress] : []
+ selectedAccount = self.publicConfigStore.get('selectedAccount')
+ result = selectedAccount ? [selectedAccount] : []
break
case 'eth_coinbase':
// read from localStorage
- selectedAddress = self.publicConfigStore.get('selectedAddress')
- result = selectedAddress || '0x0000000000000000000000000000000000000000'
+ selectedAccount = self.publicConfigStore.get('selectedAccount')
+ result = selectedAccount || '0x0000000000000000000000000000000000000000'
+ break
+
+ case 'eth_uninstallFilter':
+ self.sendAsync(payload, noop)
+ result = true
break
// throw not-supported Error
@@ -105,6 +111,8 @@ MetamaskInpageProvider.prototype.isConnected = function () {
return true
}
+MetamaskInpageProvider.prototype.isMetaMask = true
+
// util
function remoteStoreWithLocalStorageCache (storageKey) {
@@ -119,20 +127,12 @@ function remoteStoreWithLocalStorageCache (storageKey) {
return store
}
-function createRandomId(){
- const extraDigits = 3
- // 13 time digits
- const datePart = new Date().getTime() * Math.pow(10, extraDigits)
- // 3 random digits
- const extraPart = Math.floor(Math.random() * Math.pow(10, extraDigits))
- // 16 digits
- return datePart + extraPart
-}
-
-function eachJsonMessage(payload, transformFn){
+function eachJsonMessage (payload, transformFn) {
if (Array.isArray(payload)) {
return payload.map(transformFn)
} else {
return transformFn(payload)
}
}
+
+function noop () {}
diff --git a/app/scripts/lib/is-popup-or-notification.js b/app/scripts/lib/is-popup-or-notification.js
index 5c38ac823..693fa8751 100644
--- a/app/scripts/lib/is-popup-or-notification.js
+++ b/app/scripts/lib/is-popup-or-notification.js
@@ -1,4 +1,4 @@
-module.exports = function isPopupOrNotification() {
+module.exports = function isPopupOrNotification () {
const url = window.location.href
if (url.match(/popup.html$/)) {
return 'popup'
diff --git a/app/scripts/lib/nodeify.js b/app/scripts/lib/nodeify.js
new file mode 100644
index 000000000..51d89a8fb
--- /dev/null
+++ b/app/scripts/lib/nodeify.js
@@ -0,0 +1,24 @@
+module.exports = function (promiseFn) {
+ return function () {
+ var args = []
+ for (var i = 0; i < arguments.length - 1; i++) {
+ args.push(arguments[i])
+ }
+ var cb = arguments[arguments.length - 1]
+
+ const nodeified = promiseFn.apply(this, args)
+
+ if (!nodeified) {
+ const methodName = String(promiseFn).split('(')[0]
+ throw new Error(`The ${methodName} did not return a Promise, but was nodeified.`)
+ }
+ nodeified.then(function (result) {
+ cb(null, result)
+ })
+ .catch(function (reason) {
+ cb(reason)
+ })
+
+ return nodeified
+ }
+}
diff --git a/app/scripts/lib/notifications.js b/app/scripts/lib/notifications.js
index cd7535232..3db1ac6b5 100644
--- a/app/scripts/lib/notifications.js
+++ b/app/scripts/lib/notifications.js
@@ -15,12 +15,9 @@ function show () {
if (err) throw err
if (popup) {
-
// bring focus to existing popup
extension.windows.update(popup.id, { focused: true })
-
} else {
-
// create new popup
extension.windows.create({
url: 'notification.html',
@@ -29,12 +26,11 @@ function show () {
width,
height,
})
-
}
})
}
-function getWindows(cb) {
+function getWindows (cb) {
// Ignore in test environment
if (!extension.windows) {
return cb()
@@ -45,14 +41,14 @@ function getWindows(cb) {
})
}
-function getPopup(cb) {
+function getPopup (cb) {
getWindows((err, windows) => {
if (err) throw err
cb(null, getPopupIn(windows))
})
}
-function getPopupIn(windows) {
+function getPopupIn (windows) {
return windows ? windows.find((win) => {
return (win && win.type === 'popup' &&
win.height === height &&
@@ -60,7 +56,7 @@ function getPopupIn(windows) {
}) : null
}
-function closePopup() {
+function closePopup () {
getPopup((err, popup) => {
if (err) throw err
if (!popup) return
diff --git a/app/scripts/lib/port-stream.js b/app/scripts/lib/port-stream.js
index 6f4ccc6ab..607a9c9ed 100644
--- a/app/scripts/lib/port-stream.js
+++ b/app/scripts/lib/port-stream.js
@@ -51,11 +51,11 @@ PortDuplexStream.prototype._write = function (msg, encoding, cb) {
// console.log('PortDuplexStream - sent message', msg)
this._port.postMessage(msg)
}
- cb()
} catch (err) {
// console.error(err)
- cb(new Error('PortDuplexStream - disconnected'))
+ return cb(new Error('PortDuplexStream - disconnected'))
}
+ cb()
}
// util
diff --git a/app/scripts/lib/random-id.js b/app/scripts/lib/random-id.js
new file mode 100644
index 000000000..788f3370f
--- /dev/null
+++ b/app/scripts/lib/random-id.js
@@ -0,0 +1,9 @@
+const MAX = Number.MAX_SAFE_INTEGER
+
+let idCounter = Math.round(Math.random() * MAX)
+function createRandomId () {
+ idCounter = idCounter % MAX
+ return idCounter++
+}
+
+module.exports = createRandomId
diff --git a/app/scripts/lib/sig-util.js b/app/scripts/lib/sig-util.js
index f8748f535..193dda381 100644
--- a/app/scripts/lib/sig-util.js
+++ b/app/scripts/lib/sig-util.js
@@ -12,6 +12,11 @@ module.exports = {
return ethUtil.addHexPrefix(rStr.concat(sStr, vStr)).toString('hex')
},
+ normalize: function (address) {
+ if (!address) return
+ return ethUtil.addHexPrefix(address.toLowerCase())
+ },
+
}
function padWithZeroes (number, length) {
diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js
new file mode 100644
index 000000000..5116cb93b
--- /dev/null
+++ b/app/scripts/lib/tx-utils.js
@@ -0,0 +1,132 @@
+const async = require('async')
+const EthQuery = require('eth-query')
+const ethUtil = require('ethereumjs-util')
+const Transaction = require('ethereumjs-tx')
+const normalize = require('./sig-util').normalize
+const BN = ethUtil.BN
+
+/*
+tx-utils are utility methods for Transaction manager
+its passed a provider and that is passed to ethquery
+and used to do things like calculate gas of a tx.
+*/
+
+module.exports = class txProviderUtils {
+ constructor (provider) {
+ this.provider = provider
+ this.query = new EthQuery(provider)
+ }
+
+ analyzeGasUsage (txData, cb) {
+ var self = this
+ this.query.getBlockByNumber('latest', true, (err, block) => {
+ if (err) return cb(err)
+ async.waterfall([
+ self.estimateTxGas.bind(self, txData, block.gasLimit),
+ self.setTxGas.bind(self, txData, block.gasLimit),
+ ], cb)
+ })
+ }
+
+ estimateTxGas (txData, blockGasLimitHex, cb) {
+ const txParams = txData.txParams
+ // check if gasLimit is already specified
+ txData.gasLimitSpecified = Boolean(txParams.gas)
+ // if not, fallback to block gasLimit
+ if (!txData.gasLimitSpecified) {
+ txParams.gas = blockGasLimitHex
+ }
+ // run tx, see if it will OOG
+ this.query.estimateGas(txParams, cb)
+ }
+
+ setTxGas (txData, blockGasLimitHex, estimatedGasHex, cb) {
+ txData.estimatedGas = estimatedGasHex
+ const txParams = txData.txParams
+
+ // if gasLimit was specified and doesnt OOG,
+ // use original specified amount
+ if (txData.gasLimitSpecified) {
+ txData.estimatedGas = txParams.gas
+ cb()
+ return
+ }
+ // if gasLimit not originally specified,
+ // try adding an additional gas buffer to our estimation for safety
+ const estimatedGasBn = new BN(ethUtil.stripHexPrefix(txData.estimatedGas), 16)
+ const blockGasLimitBn = new BN(ethUtil.stripHexPrefix(blockGasLimitHex), 16)
+ const estimationWithBuffer = new BN(this.addGasBuffer(estimatedGasBn), 16)
+ // added gas buffer is too high
+ if (estimationWithBuffer.gt(blockGasLimitBn)) {
+ txParams.gas = txData.estimatedGas
+ // added gas buffer is safe
+ } else {
+ const gasWithBufferHex = ethUtil.intToHex(estimationWithBuffer)
+ txParams.gas = gasWithBufferHex
+ }
+ cb()
+ return
+ }
+
+ addGasBuffer (gas) {
+ const gasBuffer = new BN('100000', 10)
+ const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16)
+ const correct = bnGas.add(gasBuffer)
+ return ethUtil.addHexPrefix(correct.toString(16))
+ }
+
+ fillInTxParams (txParams, cb) {
+ let fromAddress = txParams.from
+ let reqs = {}
+
+ if (isUndef(txParams.gas)) reqs.gas = (cb) => this.query.estimateGas(txParams, cb)
+ if (isUndef(txParams.gasPrice)) reqs.gasPrice = (cb) => this.query.gasPrice(cb)
+ if (isUndef(txParams.nonce)) reqs.nonce = (cb) => this.query.getTransactionCount(fromAddress, 'pending', cb)
+
+ async.parallel(reqs, function(err, result) {
+ if (err) return cb(err)
+ // write results to txParams obj
+ Object.assign(txParams, result)
+ cb()
+ })
+ }
+
+ // builds ethTx from txParams object
+ buildEthTxFromParams (txParams, gasMultiplier = 1) {
+ // apply gas multiplyer
+ let gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice), 16)
+ // multiply and divide by 100 so as to add percision to integer mul
+ gasPrice = gasPrice.mul(new BN(gasMultiplier * 100, 10)).div(new BN(100, 10))
+ txParams.gasPrice = ethUtil.intToHex(gasPrice.toNumber())
+ // normalize values
+ txParams.to = normalize(txParams.to)
+ txParams.from = normalize(txParams.from)
+ txParams.value = normalize(txParams.value)
+ txParams.data = normalize(txParams.data)
+ txParams.gasLimit = normalize(txParams.gasLimit || txParams.gas)
+ txParams.nonce = normalize(txParams.nonce)
+ // build ethTx
+ const ethTx = new Transaction(txParams)
+ return ethTx
+ }
+
+ publishTransaction (rawTx, cb) {
+ this.query.sendRawTransaction(rawTx, cb)
+ }
+
+ validateTxParams (txParams, cb) {
+ if (('value' in txParams) && txParams.value.indexOf('-') === 0) {
+ cb(new Error(`Invalid transaction value of ${txParams.value} not a positive number.`))
+ } else {
+ cb()
+ }
+ }
+
+
+}
+
+// util
+
+function isUndef(value) {
+ return value === undefined
+}
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index a165a2e2a..b94b98eac 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -1,28 +1,54 @@
+const EventEmitter = require('events')
const extend = require('xtend')
-const EthStore = require('eth-store')
+const EthStore = require('./lib/eth-store')
const MetaMaskProvider = require('web3-provider-engine/zero.js')
const KeyringController = require('./keyring-controller')
+const NoticeController = require('./notice-controller')
const messageManager = require('./lib/message-manager')
+const TxManager = require('./transaction-manager')
const HostStore = require('./lib/remote-store.js').HostStore
const Web3 = require('web3')
const ConfigManager = require('./lib/config-manager')
const extension = require('./lib/extension')
+const autoFaucet = require('./lib/auto-faucet')
+const nodeify = require('./lib/nodeify')
+const IdStoreMigrator = require('./lib/idStore-migrator')
+const version = require('../manifest.json').version
-module.exports = class MetamaskController {
+module.exports = class MetamaskController extends EventEmitter {
constructor (opts) {
+ super()
this.state = { network: 'loading' }
this.opts = opts
- this.listeners = []
this.configManager = new ConfigManager(opts)
this.keyringController = new KeyringController({
configManager: this.configManager,
+ getNetwork: this.getStateNetwork.bind(this),
})
+ // notices
+ this.noticeController = new NoticeController({
+ configManager: this.configManager,
+ })
+ this.noticeController.updateNoticesList()
+ // to be uncommented when retrieving notices from a remote server.
+ // this.noticeController.startPolling()
this.provider = this.initializeProvider(opts)
this.ethStore = new EthStore(this.provider)
this.keyringController.setStore(this.ethStore)
this.getNetwork()
this.messageManager = messageManager
+ this.txManager = new TxManager({
+ txList: this.configManager.getTxList(),
+ txHistoryLimit: 40,
+ setTxList: this.configManager.setTxList.bind(this.configManager),
+ getSelectedAccount: this.configManager.getSelectedAccount.bind(this.configManager),
+ getGasMultiplier: this.configManager.getGasMultiplier.bind(this.configManager),
+ getNetwork: this.getStateNetwork.bind(this),
+ signTransaction: this.keyringController.signTransaction.bind(this.keyringController),
+ provider: this.provider,
+ blockTracker: this.provider,
+ })
this.publicConfigStore = this.initPublicConfigStore()
var currentFiat = this.configManager.getCurrentFiat() || 'USD'
@@ -32,22 +58,40 @@ module.exports = class MetamaskController {
this.checkTOSChange()
this.scheduleConversionInterval()
+
+ // TEMPORARY UNTIL FULL DEPRECATION:
+ this.idStoreMigrator = new IdStoreMigrator({
+ configManager: this.configManager,
+ })
+
+ this.ethStore.on('update', this.sendUpdate.bind(this))
+ this.keyringController.on('update', this.sendUpdate.bind(this))
+ this.txManager.on('update', this.sendUpdate.bind(this))
}
getState () {
- return extend(
- this.state,
- this.ethStore.getState(),
- this.configManager.getConfig(),
- this.keyringController.getState()
- )
+ return this.keyringController.getState()
+ .then((keyringControllerState) => {
+ return extend(
+ this.state,
+ this.ethStore.getState(),
+ this.configManager.getConfig(),
+ this.txManager.getState(),
+ keyringControllerState,
+ this.noticeController.getState(), {
+ lostAccounts: this.configManager.getLostAccounts(),
+ }
+ )
+ })
}
getApi () {
const keyringController = this.keyringController
+ const txManager = this.txManager
+ const noticeController = this.noticeController
return {
- getState: (cb) => { cb(null, this.getState()) },
+ getState: nodeify(this.getState.bind(this)),
setRpcTarget: this.setRpcTarget.bind(this),
setProviderType: this.setProviderType.bind(this),
useEtherscanProvider: this.useEtherscanProvider.bind(this),
@@ -57,27 +101,39 @@ module.exports = class MetamaskController {
setTOSHash: this.setTOSHash.bind(this),
checkTOSChange: this.checkTOSChange.bind(this),
setGasMultiplier: this.setGasMultiplier.bind(this),
+ markAccountsFound: this.markAccountsFound.bind(this),
// forward directly to keyringController
- placeSeedWords: keyringController.placeSeedWords.bind(keyringController),
- createNewVaultAndKeychain: keyringController.createNewVaultAndKeychain.bind(keyringController),
- createNewVaultAndRestore: keyringController.createNewVaultAndRestore.bind(keyringController),
- clearSeedWordCache: keyringController.clearSeedWordCache.bind(keyringController),
- addNewKeyring: keyringController.addNewKeyring.bind(keyringController),
- addNewAccount: keyringController.addNewAccount.bind(keyringController),
- submitPassword: keyringController.submitPassword.bind(keyringController),
- setSelectedAddress: keyringController.setSelectedAddress.bind(keyringController),
- approveTransaction: keyringController.approveTransaction.bind(keyringController),
- cancelTransaction: keyringController.cancelTransaction.bind(keyringController),
+ createNewVaultAndKeychain: nodeify(keyringController.createNewVaultAndKeychain).bind(keyringController),
+ createNewVaultAndRestore: nodeify(keyringController.createNewVaultAndRestore).bind(keyringController),
+ placeSeedWords: nodeify(keyringController.placeSeedWords).bind(keyringController),
+ clearSeedWordCache: nodeify(keyringController.clearSeedWordCache).bind(keyringController),
+ setLocked: nodeify(keyringController.setLocked).bind(keyringController),
+ submitPassword: (password, cb) => {
+ this.migrateOldVaultIfAny(password)
+ .then(keyringController.submitPassword.bind(keyringController, password))
+ .then((newState) => { cb(null, newState) })
+ .catch((reason) => { cb(reason) })
+ },
+ addNewKeyring: nodeify(keyringController.addNewKeyring).bind(keyringController),
+ addNewAccount: nodeify(keyringController.addNewAccount).bind(keyringController),
+ setSelectedAccount: nodeify(keyringController.setSelectedAccount).bind(keyringController),
+ saveAccountLabel: nodeify(keyringController.saveAccountLabel).bind(keyringController),
+ exportAccount: nodeify(keyringController.exportAccount).bind(keyringController),
+
+ // signing methods
+ approveTransaction: txManager.approveTransaction.bind(txManager),
+ cancelTransaction: txManager.cancelTransaction.bind(txManager),
signMessage: keyringController.signMessage.bind(keyringController),
cancelMessage: keyringController.cancelMessage.bind(keyringController),
- setLocked: keyringController.setLocked.bind(keyringController),
- exportAccount: keyringController.exportAccount.bind(keyringController),
- saveAccountLabel: keyringController.saveAccountLabel.bind(keyringController),
+
// coinbase
buyEth: this.buyEth.bind(this),
// shapeshift
createShapeShiftTx: this.createShapeShiftTx.bind(this),
+ // notices
+ checkNotices: noticeController.updateNoticesList.bind(noticeController),
+ markNoticeRead: noticeController.markNoticeRead.bind(noticeController),
}
}
@@ -86,23 +142,6 @@ module.exports = class MetamaskController {
}
onRpcRequest (stream, originDomain, request) {
-
- /* Commented out for Parity compliance
- * Parity does not permit additional keys, like `origin`,
- * and Infura is not currently filtering this key out.
- var payloads = Array.isArray(request) ? request : [request]
- payloads.forEach(function (payload) {
- // Append origin to rpc payload
- payload.origin = originDomain
- // Append origin to signature request
- if (payload.method === 'eth_sendTransaction') {
- payload.params[0].origin = originDomain
- } else if (payload.method === 'eth_sign') {
- payload.params.push({ origin: originDomain })
- }
- })
- */
-
// handle rpc request
this.provider.sendAsync(request, function onPayloadHandled (err, response) {
logger(err, request, response)
@@ -129,8 +168,9 @@ module.exports = class MetamaskController {
}
sendUpdate () {
- this.listeners.forEach((remote) => {
- remote.sendUpdate(this.getState())
+ this.getState()
+ .then((state) => {
+ this.emit('update', state)
})
}
@@ -138,20 +178,19 @@ module.exports = class MetamaskController {
const keyringController = this.keyringController
var providerOpts = {
+ static: {
+ eth_syncing: false,
+ web3_clientVersion: `MetaMask/v${version}`,
+ },
rpcUrl: this.configManager.getCurrentRpcAddress(),
// account mgmt
getAccounts: (cb) => {
- var selectedAddress = this.configManager.getSelectedAccount()
- var result = selectedAddress ? [selectedAddress] : []
+ var selectedAccount = this.configManager.getSelectedAccount()
+ var result = selectedAccount ? [selectedAccount] : []
cb(null, result)
},
// tx signing
- approveTransaction: this.newUnsignedTransaction.bind(this),
- signTransaction: (...args) => {
- keyringController.signTransaction(...args)
- this.sendUpdate()
- },
-
+ processTransaction: (txParams, cb) => this.newUnapprovedTransaction(txParams, cb),
// msg signing
approveMessage: this.newUnsignedMessage.bind(this),
signMessage: (...args) => {
@@ -164,7 +203,6 @@ module.exports = class MetamaskController {
var web3 = new Web3(provider)
this.web3 = web3
keyringController.web3 = web3
-
provider.on('block', this.processBlock.bind(this))
provider.on('error', this.getNetwork.bind(this))
@@ -173,34 +211,22 @@ module.exports = class MetamaskController {
initPublicConfigStore () {
// get init state
- var initPublicState = extend(
- keyringControllerToPublic(this.keyringController.getState()),
- configToPublic(this.configManager.getConfig())
- )
-
+ var initPublicState = configToPublic(this.configManager.getConfig())
var publicConfigStore = new HostStore(initPublicState)
// subscribe to changes
this.configManager.subscribe(function (state) {
storeSetFromObj(publicConfigStore, configToPublic(state))
})
- this.keyringController.on('update', () => {
- const state = this.keyringController.getState()
- storeSetFromObj(publicConfigStore, keyringControllerToPublic(state))
- this.sendUpdate()
+
+ this.keyringController.on('newAccount', (account) => {
+ autoFaucet(account)
})
- // keyringController substate
- function keyringControllerToPublic (state) {
- return {
- selectedAddress: state.selectedAddress,
- }
- }
// config substate
function configToPublic (state) {
return {
- provider: state.provider,
- selectedAddress: state.selectedAccount,
+ selectedAccount: state.selectedAccount,
}
}
// dump obj into store
@@ -213,26 +239,26 @@ module.exports = class MetamaskController {
return publicConfigStore
}
- newUnsignedTransaction (txParams, onTxDoneCb) {
- const keyringController = this.keyringController
-
- let err = this.enforceTxValidations(txParams)
- if (err) return onTxDoneCb(err)
-
- keyringController.addUnconfirmedTransaction(txParams, onTxDoneCb, (err, txData) => {
- if (err) return onTxDoneCb(err)
- this.sendUpdate()
- this.opts.showUnconfirmedTx(txParams, txData, onTxDoneCb)
+ newUnapprovedTransaction (txParams, cb) {
+ const self = this
+ self.txManager.addUnapprovedTransaction(txParams, (err, txMeta) => {
+ if (err) return cb(err)
+ self.sendUpdate()
+ self.opts.showUnapprovedTx(txMeta)
+ // listen for tx completion (success, fail)
+ self.txManager.once(`${txMeta.id}:finished`, (status) => {
+ switch (status) {
+ case 'submitted':
+ return cb(null, txMeta.hash)
+ case 'rejected':
+ return cb(new Error('MetaMask Tx Signature: User denied transaction signature.'))
+ default:
+ return cb(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(txMeta.txParams)}`))
+ }
+ })
})
}
- enforceTxValidations (txParams) {
- if (('value' in txParams) && txParams.value.indexOf('-') === 0) {
- const msg = `Invalid transaction value of ${txParams.value} not a positive number.`
- return new Error(msg)
- }
- }
-
newUnsignedMessage (msgParams, cb) {
var state = this.keyringController.getState()
if (!state.isUnlocked) {
@@ -276,7 +302,7 @@ module.exports = class MetamaskController {
setTOSHash (hash) {
try {
this.configManager.setTOSHash(hash)
- } catch (e) {
+ } catch (err) {
console.error('Error in setting terms of service hash.')
}
}
@@ -288,24 +314,25 @@ module.exports = class MetamaskController {
this.resetDisclaimer()
this.setTOSHash(global.TOS_HASH)
}
- } catch (e) {
+ } catch (err) {
console.error('Error in checking TOS change.')
}
-
}
+ // disclaimer
+
agreeToDisclaimer (cb) {
try {
- this.configManager.setConfirmed(true)
+ this.configManager.setConfirmedDisclaimer(true)
cb()
- } catch (e) {
- cb(e)
+ } catch (err) {
+ cb(err)
}
}
resetDisclaimer () {
try {
- this.configManager.setConfirmed(false)
+ this.configManager.setConfirmedDisclaimer(false)
} catch (e) {
console.error(e)
}
@@ -322,8 +349,8 @@ module.exports = class MetamaskController {
conversionDate: this.configManager.getConversionDate(),
}
cb(data)
- } catch (e) {
- cb(null, e)
+ } catch (err) {
+ cb(null, err)
}
}
@@ -360,7 +387,7 @@ module.exports = class MetamaskController {
var network = this.state.network
var url = `https://buy.coinbase.com/?code=9ec56d01-7e81-5017-930c-513daa27bb6a&amount=${amount}&address=${address}&crypto_currency=ETH`
- if (network === '2') {
+ if (network === '3') {
url = 'https://faucet.metamask.io/'
}
@@ -373,7 +400,7 @@ module.exports = class MetamaskController {
this.configManager.createShapeShiftTx(depositAddress, depositType)
}
- getNetwork(err) {
+ getNetwork (err) {
if (err) {
this.state.network = 'loading'
this.sendUpdate()
@@ -396,8 +423,74 @@ module.exports = class MetamaskController {
try {
this.configManager.setGasMultiplier(gasMultiplier)
cb()
- } catch (e) {
- cb(e)
+ } catch (err) {
+ cb(err)
+ }
+ }
+
+ getStateNetwork () {
+ return this.state.network
+ }
+
+ markAccountsFound (cb) {
+ this.configManager.setLostAccounts([])
+ this.sendUpdate()
+ cb(null, this.getState())
+ }
+
+ // Migrate Old Vault If Any
+ // @string password
+ //
+ // returns Promise()
+ //
+ // Temporary step used when logging in.
+ // Checks if old style (pre-3.0.0) Metamask Vault exists.
+ // If so, persists that vault in the new vault format
+ // with the provided password, so the other unlock steps
+ // may be completed without interruption.
+ migrateOldVaultIfAny (password) {
+
+ if (!this.checkIfShouldMigrate()) {
+ return Promise.resolve(password)
+ }
+
+ const keyringController = this.keyringController
+
+ return this.idStoreMigrator.migratedVaultForPassword(password)
+ .then(this.restoreOldVaultAccounts.bind(this))
+ .then(this.restoreOldLostAccounts.bind(this))
+ .then(keyringController.persistAllKeyrings.bind(keyringController, password))
+ .then(() => password)
+ }
+
+ checkIfShouldMigrate() {
+ return !!this.configManager.getWallet() && !this.configManager.getVault()
+ }
+
+ restoreOldVaultAccounts(migratorOutput) {
+ const { serialized } = migratorOutput
+ return this.keyringController.restoreKeyring(serialized)
+ .then(() => migratorOutput)
+ }
+
+ restoreOldLostAccounts(migratorOutput) {
+ const { lostAccounts } = migratorOutput
+ if (lostAccounts) {
+ this.configManager.setLostAccounts(lostAccounts.map(acct => acct.address))
+ return this.importLostAccounts(migratorOutput)
}
+ return Promise.resolve(migratorOutput)
+ }
+
+ // IMPORT LOST ACCOUNTS
+ // @Object with key lostAccounts: @Array accounts <{ address, privateKey }>
+ // Uses the array's private keys to create a new Simple Key Pair keychain
+ // and add it to the keyring controller.
+ importLostAccounts ({ lostAccounts }) {
+ const privKeys = lostAccounts.map(acct => acct.privateKey)
+ return this.keyringController.restoreKeyring({
+ type: 'Simple Key Pair',
+ data: privKeys,
+ })
}
}
diff --git a/app/scripts/notice-controller.js b/app/scripts/notice-controller.js
new file mode 100644
index 000000000..00c87c670
--- /dev/null
+++ b/app/scripts/notice-controller.js
@@ -0,0 +1,96 @@
+const EventEmitter = require('events').EventEmitter
+const hardCodedNotices = require('../../development/notices.json')
+
+module.exports = class NoticeController extends EventEmitter {
+
+ constructor (opts) {
+ super()
+ this.configManager = opts.configManager
+ this.noticePoller = null
+ }
+
+ getState () {
+ var lastUnreadNotice = this.getLatestUnreadNotice()
+
+ return {
+ lastUnreadNotice: lastUnreadNotice,
+ noActiveNotices: !lastUnreadNotice,
+ }
+ }
+
+ getNoticesList () {
+ var data = this.configManager.getData()
+ if ('noticesList' in data) {
+ return data.noticesList
+ } else {
+ return []
+ }
+ }
+
+ setNoticesList (list) {
+ var data = this.configManager.getData()
+ data.noticesList = list
+ this.configManager.setData(data)
+ return Promise.resolve(true)
+ }
+
+ markNoticeRead (notice, cb) {
+ cb = cb || function (err) { if (err) throw err }
+ try {
+ var notices = this.getNoticesList()
+ var id = notice.id
+ notices[id].read = true
+ this.setNoticesList(notices)
+ const latestNotice = this.getLatestUnreadNotice()
+ cb(null, latestNotice)
+ } catch (err) {
+ cb(err)
+ }
+ }
+
+ updateNoticesList () {
+ return this._retrieveNoticeData().then((newNotices) => {
+ var oldNotices = this.getNoticesList()
+ var combinedNotices = this._mergeNotices(oldNotices, newNotices)
+ return Promise.resolve(this.setNoticesList(combinedNotices))
+ })
+ }
+
+ getLatestUnreadNotice () {
+ var notices = this.getNoticesList()
+ var filteredNotices = notices.filter((notice) => {
+ return notice.read === false
+ })
+ return filteredNotices[filteredNotices.length - 1]
+ }
+
+ startPolling () {
+ if (this.noticePoller) {
+ clearInterval(this.noticePoller)
+ }
+ this.noticePoller = setInterval(() => {
+ this.noticeController.updateNoticesList()
+ }, 300000)
+ }
+
+ _mergeNotices (oldNotices, newNotices) {
+ var noticeMap = this._mapNoticeIds(oldNotices)
+ newNotices.forEach((notice) => {
+ if (noticeMap.indexOf(notice.id) === -1) {
+ oldNotices.push(notice)
+ }
+ })
+ return oldNotices
+ }
+
+ _mapNoticeIds (notices) {
+ return notices.map((notice) => notice.id)
+ }
+
+ _retrieveNoticeData () {
+ // Placeholder for the API.
+ return Promise.resolve(hardCodedNotices)
+ }
+
+
+}
diff --git a/app/scripts/popup-core.js b/app/scripts/popup-core.js
index 94413a1c4..0c97a5d19 100644
--- a/app/scripts/popup-core.js
+++ b/app/scripts/popup-core.js
@@ -9,7 +9,7 @@ const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
module.exports = initializePopup
-function initializePopup(connectionStream){
+function initializePopup (connectionStream) {
// setup app
connectToAccountManager(connectionStream, setupApp)
}
@@ -32,7 +32,7 @@ function setupWeb3Connection (connectionStream) {
}
function setupControllerConnection (connectionStream, cb) {
- // this is a really sneaky way of adding EventEmitter api
+ // this is a really sneaky way of adding EventEmitter api
// to a bi-directional dnode instance
var eventEmitter = new EventEmitter()
var accountManagerDnode = Dnode({
diff --git a/app/scripts/popup.js b/app/scripts/popup.js
index e6f149f96..62db68c10 100644
--- a/app/scripts/popup.js
+++ b/app/scripts/popup.js
@@ -18,7 +18,7 @@ var portStream = new PortStream(pluginPort)
startPopup(portStream)
-function closePopupIfOpen(name) {
+function closePopupIfOpen (name) {
if (name !== 'notification') {
notification.closePopup()
}
diff --git a/app/scripts/transaction-manager.js b/app/scripts/transaction-manager.js
new file mode 100644
index 000000000..87f99ce62
--- /dev/null
+++ b/app/scripts/transaction-manager.js
@@ -0,0 +1,362 @@
+const EventEmitter = require('events')
+const async = require('async')
+const extend = require('xtend')
+const Semaphore = require('semaphore')
+const ethUtil = require('ethereumjs-util')
+const BN = require('ethereumjs-util').BN
+const TxProviderUtil = require('./lib/tx-utils')
+const createId = require('./lib/random-id')
+
+module.exports = class TransactionManager extends EventEmitter {
+ constructor (opts) {
+ super()
+ this.txList = opts.txList || []
+ this._setTxList = opts.setTxList
+ this.txHistoryLimit = opts.txHistoryLimit
+ this.getSelectedAccount = opts.getSelectedAccount
+ this.provider = opts.provider
+ this.blockTracker = opts.blockTracker
+ this.txProviderUtils = new TxProviderUtil(this.provider)
+ this.blockTracker.on('block', this.checkForTxInBlock.bind(this))
+ this.getGasMultiplier = opts.getGasMultiplier
+ this.getNetwork = opts.getNetwork
+ this.signEthTx = opts.signTransaction
+ this.nonceLock = Semaphore(1)
+ }
+
+ getState () {
+ var selectedAccount = this.getSelectedAccount()
+ return {
+ transactions: this.getTxList(),
+ unconfTxs: this.getUnapprovedTxList(),
+ selectedAccountTxList: this.getFilteredTxList({metamaskNetworkId: this.getNetwork(), from: selectedAccount}),
+ }
+ }
+
+// Returns the tx list
+ getTxList () {
+ let network = this.getNetwork()
+ return this.txList.filter(txMeta => txMeta.metamaskNetworkId === network)
+ }
+
+ // Adds a tx to the txlist
+ addTx (txMeta) {
+ var txList = this.getTxList()
+ var txHistoryLimit = this.txHistoryLimit
+
+ // checks if the length of th tx history is
+ // longer then desired persistence limit
+ // and then if it is removes only confirmed
+ // or rejected tx's.
+ // not tx's that are pending or unapproved
+ if (txList.length > txHistoryLimit - 1) {
+ var index = txList.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected')
+ txList.splice(index, 1)
+ }
+ txList.push(txMeta)
+
+ this._saveTxList(txList)
+ this.once(`${txMeta.id}:signed`, function (txId) {
+ this.removeAllListeners(`${txMeta.id}:rejected`)
+ })
+ this.once(`${txMeta.id}:rejected`, function (txId) {
+ this.removeAllListeners(`${txMeta.id}:signed`)
+ })
+
+ this.emit('updateBadge')
+ this.emit(`${txMeta.id}:unapproved`, txMeta)
+ }
+
+ // gets tx by Id and returns it
+ getTx (txId, cb) {
+ var txList = this.getTxList()
+ var txMeta = txList.find(txData => txData.id === txId)
+ return cb ? cb(txMeta) : txMeta
+ }
+
+ //
+ updateTx (txMeta) {
+ var txId = txMeta.id
+ var txList = this.getTxList()
+ var index = txList.findIndex(txData => txData.id === txId)
+ txList[index] = txMeta
+ this._saveTxList(txList)
+ this.emit('update')
+ }
+
+ get unapprovedTxCount () {
+ return Object.keys(this.getUnapprovedTxList()).length
+ }
+
+ get pendingTxCount () {
+ return this.getTxsByMetaData('status', 'signed').length
+ }
+
+ addUnapprovedTransaction (txParams, done) {
+ let txMeta
+ async.waterfall([
+ // validate
+ (cb) => this.txProviderUtils.validateTxParams(txParams, cb),
+ // prepare txMeta
+ (cb) => {
+ // create txMeta obj with parameters and meta data
+ let time = (new Date()).getTime()
+ let txId = createId()
+ txParams.metamaskId = txId
+ txParams.metamaskNetworkId = this.getNetwork()
+ txMeta = {
+ id: txId,
+ time: time,
+ status: 'unapproved',
+ gasMultiplier: this.getGasMultiplier() || 1,
+ metamaskNetworkId: this.getNetwork(),
+ txParams: txParams,
+ }
+ // calculate metadata for tx
+ this.txProviderUtils.analyzeGasUsage(txMeta, cb)
+ },
+ // save txMeta
+ (cb) => {
+ this.addTx(txMeta)
+ this.setMaxTxCostAndFee(txMeta)
+ cb(null, txMeta)
+ },
+ ], done)
+ }
+
+ setMaxTxCostAndFee (txMeta) {
+ var txParams = txMeta.txParams
+ var gasMultiplier = txMeta.gasMultiplier
+ var gasCost = new BN(ethUtil.stripHexPrefix(txParams.gas || txMeta.estimatedGas), 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 txValue = new BN(ethUtil.stripHexPrefix(txParams.value || '0x0'), 16)
+ var maxCost = txValue.add(txFee)
+ txMeta.txFee = txFee
+ txMeta.txValue = txValue
+ txMeta.maxCost = maxCost
+ this.updateTx(txMeta)
+ }
+
+ getUnapprovedTxList () {
+ var txList = this.getTxList()
+ return txList.filter((txMeta) => txMeta.status === 'unapproved')
+ .reduce((result, tx) => {
+ result[tx.id] = tx
+ return result
+ }, {})
+ }
+
+ approveTransaction (txId, cb = warn) {
+ const self = this
+ // approve
+ self.setTxStatusApproved(txId)
+ // only allow one tx at a time for atomic nonce usage
+ self.nonceLock.take(() => {
+ // begin signature process
+ async.waterfall([
+ (cb) => self.fillInTxParams(txId, cb),
+ (cb) => self.signTransaction(txId, cb),
+ (rawTx, cb) => self.publishTransaction(txId, rawTx, cb),
+ ], (err) => {
+ self.nonceLock.leave()
+ if (err) {
+ this.setTxStatusFailed(txId)
+ return cb(err)
+ }
+ cb()
+ })
+ })
+ }
+
+ cancelTransaction (txId, cb = warn) {
+ this.setTxStatusRejected(txId)
+ cb()
+ }
+
+ fillInTxParams (txId, cb) {
+ let txMeta = this.getTx(txId)
+ this.txProviderUtils.fillInTxParams(txMeta.txParams, (err) => {
+ if (err) return cb(err)
+ this.updateTx(txMeta)
+ cb()
+ })
+ }
+
+ signTransaction (txId, cb) {
+ let txMeta = this.getTx(txId)
+ let txParams = txMeta.txParams
+ let fromAddress = txParams.from
+ let ethTx = this.txProviderUtils.buildEthTxFromParams(txParams, txMeta.gasMultiplier)
+ this.signEthTx(ethTx, fromAddress).then(() => {
+ this.updateTxAsSigned(txMeta.id, ethTx)
+ cb(null, ethUtil.bufferToHex(ethTx.serialize()))
+ }).catch((err) => {
+ cb(err)
+ })
+ }
+
+ publishTransaction (txId, rawTx, cb) {
+ this.txProviderUtils.publishTransaction(rawTx, (err) => {
+ if (err) return cb(err)
+ this.setTxStatusSubmitted(txId)
+ cb()
+ })
+ }
+
+ // receives a signed tx object and updates the tx hash
+ updateTxAsSigned (txId, ethTx) {
+ // Add the tx hash to the persisted meta-tx object
+ let txHash = ethUtil.bufferToHex(ethTx.hash())
+ let txMeta = this.getTx(txId)
+ txMeta.hash = txHash
+ this.updateTx(txMeta)
+ this.setTxStatusSigned(txMeta.id)
+ }
+
+ /*
+ Takes an object of fields to search for eg:
+ var thingsToLookFor = {
+ to: '0x0..',
+ from: '0x0..',
+ status: 'signed',
+ }
+ and returns a list of tx with all
+ options matching
+
+ this is for things like filtering a the tx list
+ for only tx's from 1 account
+ or for filltering for all txs from one account
+ and that have been 'confirmed'
+ */
+ getFilteredTxList (opts) {
+ var filteredTxList
+ Object.keys(opts).forEach((key) => {
+ filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList)
+ })
+ return filteredTxList
+ }
+
+ getTxsByMetaData (key, value, txList = this.getTxList()) {
+ return txList.filter((txMeta) => {
+ if (key in txMeta.txParams) {
+ return txMeta.txParams[key] === value
+ } else {
+ return txMeta[key] === value
+ }
+ })
+ }
+
+ // STATUS METHODS
+ // get::set status
+
+ // should return the status of the tx.
+ getTxStatus (txId) {
+ const txMeta = this.getTx(txId)
+ return txMeta.status
+ }
+
+ // should update the status of the tx to 'rejected'.
+ setTxStatusRejected (txId) {
+ this._setTxStatus(txId, 'rejected')
+ }
+
+ // should update the status of the tx to 'approved'.
+ setTxStatusApproved (txId) {
+ this._setTxStatus(txId, 'approved')
+ }
+
+ // should update the status of the tx to 'signed'.
+ setTxStatusSigned (txId) {
+ this._setTxStatus(txId, 'signed')
+ }
+
+ // should update the status of the tx to 'submitted'.
+ setTxStatusSubmitted (txId) {
+ this._setTxStatus(txId, 'submitted')
+ }
+
+ // should update the status of the tx to 'confirmed'.
+ setTxStatusConfirmed (txId) {
+ this._setTxStatus(txId, 'confirmed')
+ }
+
+ setTxStatusFailed (txId) {
+ this._setTxStatus(txId, 'failed')
+ }
+
+ // merges txParams obj onto txData.txParams
+ // use extend to ensure that all fields are filled
+ updateTxParams (txId, txParams) {
+ var txMeta = this.getTx(txId)
+ txMeta.txParams = extend(txMeta.txParams, txParams)
+ this.updateTx(txMeta)
+ }
+
+ // checks if a signed tx is in a block and
+ // if included sets the tx status as 'confirmed'
+ checkForTxInBlock () {
+ var signedTxList = this.getFilteredTxList({status: 'signed'})
+ if (!signedTxList.length) return
+ signedTxList.forEach((txMeta) => {
+ var txHash = txMeta.hash
+ var txId = txMeta.id
+ if (!txHash) {
+ txMeta.err = {
+ errCode: 'No hash was provided',
+ message: 'We had an error while submitting this transaction, please try again.',
+ }
+ this.updateTx(txMeta)
+ return this.setTxStatusFailed(txId)
+ }
+ this.txProviderUtils.query.getTransactionByHash(txHash, (err, txParams) => {
+ if (err || !txParams) {
+ if (!txParams) return
+ txMeta.err = {
+ isWarning: true,
+ errorCode: err,
+ message: 'There was a problem loading this transaction.',
+ }
+ this.updateTx(txMeta)
+ return console.error(err)
+ }
+ if (txParams.blockNumber) {
+ this.setTxStatusConfirmed(txId)
+ }
+ })
+ })
+ }
+
+ // PRIVATE METHODS
+
+ // Should find the tx in the tx list and
+ // update it.
+ // should set the status in txData
+ // - `'unapproved'` the user has not responded
+ // - `'rejected'` the user has responded no!
+ // - `'approved'` the user has approved the tx
+ // - `'signed'` the tx is signed
+ // - `'submitted'` the tx is sent to a server
+ // - `'confirmed'` the tx has been included in a block.
+ _setTxStatus (txId, status) {
+ var txMeta = this.getTx(txId)
+ txMeta.status = status
+ this.emit(`${txMeta.id}:${status}`, txId)
+ if (status === 'submitted' || status === 'rejected') {
+ this.emit(`${txMeta.id}:finished`, status)
+ }
+ this.updateTx(txMeta)
+ this.emit('updateBadge')
+ }
+
+ // Saves the new/updated txList.
+ // Function is intended only for internal use
+ _saveTxList (txList) {
+ this.txList = txList
+ this._setTxList(txList)
+ }
+}
+
+
+const warn = () => console.warn('warn was used no cb provided')