1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Merge pull request #1005 from MetaMask/dev

Merge dev into master!
This commit is contained in:
kumavis 2017-01-16 15:02:24 -08:00 committed by GitHub
commit ee62a6a391
132 changed files with 10273 additions and 1613 deletions

View File

@ -1,2 +1 @@
app/scripts/lib/extension-instance.js
ui/app/conversion-util.js

View File

@ -127,9 +127,9 @@
"no-whitespace-before-property": 2,
"no-with": 2,
"one-var": [2, { "initialized": "never" }],
"operator-linebreak": [1, "after", { "overrides": { "?": "before", ":": "before" } }],
"operator-linebreak": [1, "after", { "overrides": { "?": "ignore", ":": "ignore" } }],
"padded-blocks": [1, "never"],
"quotes": [2, "single", "avoid-escape"],
"quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}],
"semi": [2, "never"],
"semi-spacing": [2, { "before": false, "after": true }],
"space-before-blocks": [1, "always"],

3
.gitignore vendored
View File

@ -1,5 +1,4 @@
dist
npm-debug.log
node_modules
temp
@ -8,9 +7,9 @@ temp
app/bower_components
test/bower_components
package
.DS_Store
builds/
disc/
notes.txt
app/.DS_Store
development/bundle.js

View File

@ -2,9 +2,27 @@
## Current Master
## 3.0.0 2017-1-16
- Fix seed word account generation (https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd#.t4i1qmmsz).
- Fix Bug where you see a empty transaction flash by on the confirm transaction view.
- Create visible difference in transaction history between a approved but not yet included in a block transaction and a transaction who has been confirmed.
- Fix memory leak in RPC Cache
- Override RPC commands eth_syncing and web3_clientVersion
- Remove certain non-essential permissions from certain builds.
- Add a check for when a tx is included in a block.
- Fix bug where browser-solidity would sometimes warn of a contract creation error when there was none.
- Minor modifications to network display.
- Network now displays properly for pending transactions.
- Implement replay attack protections allowed by EIP 155.
- Fix bug where sometimes loading account data would fail by querying a future block.
## 2.14.1 2016-12-20
- Update Coinbase info. and increase the buy amount to $15
- Fixed ropsten transaction links
- Temporarily disable extension reload detection causing infinite reload bug.
- Implemented basic checking for valid RPC URIs.
## 2.14.0 2016-12-16
@ -16,6 +34,7 @@
## 2.13.11 2016-11-23
- Add support for synchronous RPC method "eth_uninstallFilter".
- Forgotten password prompts now send users directly to seed word restoration.
## 2.13.10 2016-11-22
@ -28,6 +47,9 @@
- Add support for the new, default Ropsten Test Network.
- Fix bug that would cause MetaMask to occasionally lose its StreamProvider connection and drop requests.
- Fix bug that would cause the Custom RPC menu item to not appear when Localhost 8545 was selected.
- Point ropsten faucet button to actual faucet.
- Phase out ethereumjs-util from our encryptor module.
## 2.13.8 2016-11-16

View File

@ -18,6 +18,12 @@ If you're a web dapp developer, we've got two types of guides for you:
Uncompressed builds can be found in `/dist`, compressed builds can be found in `/builds` once they're built.
## Installing Local Builds on Chrome
To install your locally built extension on Chrome, [follow this guide](http://stackoverflow.com/a/24577660/272576).
The built extension is stored in `./dist/chrome/`.
## Architecture
[![Architecture Diagram](./docs/architecture.png)][1]
@ -26,6 +32,13 @@ If you're a web dapp developer, we've got two types of guides for you:
```bash
npm install
npm start
```
## Build for Publishing
```bash
npm run dist
```
#### In Chrome
@ -78,7 +91,7 @@ To enjoy the live-reloading that `gulp dev` offers while working on the `web3-pr
2. `npm install` in its folder.
3. Run `npm link` in its folder.
4. Run `npm link $DEP_NAME` in this project folder.
5. Next time you `gulp dev` it will watch the dependency for changes as well!
5. Next time you `npm start` it will watch the dependency for changes as well!
### Running Tests
@ -90,6 +103,10 @@ You can also test with a continuously watching process, via `npm run watch`.
You can run the linter by itself with `gulp lint`.
#### Writing Browser Tests
To write tests that will be run in the browser using QUnit, add your test files to `test/integration/lib`.
### Deploying the UI
You must be authorized already on the MetaMask plugin.
@ -100,3 +117,39 @@ You can run the linter by itself with `gulp lint`.
3. Upload the latest zip file from `builds/metamask-$PLATFORM-$VERSION.zip` as the updated package.
[1]: http://www.nomnoml.com/#view/%5B%3Cactor%3Euser%5D%0A%0A%5Bmetamask-ui%7C%0A%20%20%20%5Btools%7C%0A%20%20%20%20%20react%0A%20%20%20%20%20redux%0A%20%20%20%20%20thunk%0A%20%20%20%20%20ethUtils%0A%20%20%20%20%20jazzicon%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20account-detail%0A%20%20%20%20%20accounts%0A%20%20%20%20%20locked-screen%0A%20%20%20%20%20restore-vault%0A%20%20%20%20%20identicon%0A%20%20%20%20%20config%0A%20%20%20%20%20info%0A%20%20%20%5D%0A%20%20%20%5Breducers%7C%0A%20%20%20%20%20app%0A%20%20%20%20%20metamask%0A%20%20%20%20%20identities%0A%20%20%20%5D%0A%20%20%20%5Bactions%7C%0A%20%20%20%20%20%5BaccountManager%5D%0A%20%20%20%5D%0A%20%20%20%5Bcomponents%5D%3A-%3E%5Bactions%5D%0A%20%20%20%5Bactions%5D%3A-%3E%5Breducers%5D%0A%20%20%20%5Breducers%5D%3A-%3E%5Bcomponents%5D%0A%5D%0A%0A%5Bweb%20dapp%7C%0A%20%20%5Bui%20code%5D%0A%20%20%5Bweb3%5D%0A%20%20%5Bmetamask-inpage%5D%0A%20%20%0A%20%20%5B%3Cactor%3Eui%20developer%5D%0A%20%20%5Bui%20developer%5D-%3E%5Bui%20code%5D%0A%20%20%5Bui%20code%5D%3C-%3E%5Bweb3%5D%0A%20%20%5Bweb3%5D%3C-%3E%5Bmetamask-inpage%5D%0A%5D%0A%0A%5Bmetamask-background%7C%0A%20%20%5Bprovider-engine%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bid%20store%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%3E%5Bhooked%20wallet%20subprovider%5D%0A%20%20%5Bhooked%20wallet%20subprovider%5D%3C-%3E%5Bid%20store%5D%0A%20%20%5Bconfig%20manager%7C%0A%20%20%20%20%5Brpc%20configuration%5D%0A%20%20%20%20%5Bencrypted%20keys%5D%0A%20%20%20%20%5Bwallet%20nicknames%5D%0A%20%20%5D%0A%20%20%0A%20%20%5Bprovider-engine%5D%3C-%5Bconfig%20manager%5D%0A%20%20%5Bid%20store%5D%3C-%3E%5Bconfig%20manager%5D%0A%5D%0A%0A%5Buser%5D%3C-%3E%5Bmetamask-ui%5D%0A%0A%5Buser%5D%3C%3A--%3A%3E%5Bweb%20dapp%5D%0A%0A%5Bmetamask-contentscript%7C%0A%20%20%5Bplugin%20restart%20detector%5D%0A%20%20%5Brpc%20passthrough%5D%0A%5D%0A%0A%5Brpc%20%7C%0A%20%20%5Bethereum%20blockchain%20%7C%0A%20%20%20%20%5Bcontracts%5D%0A%20%20%20%20%5Baccounts%5D%0A%20%20%5D%0A%5D%0A%0A%5Bweb%20dapp%5D%3C%3A--%3A%3E%5Bmetamask-contentscript%5D%0A%5Bmetamask-contentscript%5D%3C-%3E%5Bmetamask-background%5D%0A%5Bmetamask-background%5D%3C-%3E%5Bmetamask-ui%5D%0A%5Bmetamask-background%5D%3C-%3E%5Brpc%5D%0A
### Generate Development Visualization
This will generate a video of the repo commit history.
Install preqs:
```
brew install gource
brew install ffmpeg
```
From the repo dir, pipe `gource` into `ffmpeg`:
```
gource \
--seconds-per-day .1 \
--user-scale 1.5 \
--default-user-image "./images/icon-512.png" \
--viewport 1280x720 \
--auto-skip-seconds .1 \
--multi-sampling \
--stop-at-end \
--highlight-users \
--hide mouse,progress \
--file-idle-time 0 \
--max-files 0 \
--background-colour 000000 \
--font-size 18 \
--date-format "%b %d, %Y" \
--highlight-dirs \
--user-friction 0.1 \
--title "MetaMask Development History" \
--output-ppm-stream - \
--output-framerate 30 \
| ffmpeg -y -r 30 -f image2pipe -vcodec ppm -i - -b 65536K metamask-dev-history.mp4
```

View File

@ -1,7 +1,7 @@
{
"name": "MetaMask",
"short_name": "Metamask",
"version": "2.14.0",
"version": "2.15.0",
"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": [

View File

@ -10,26 +10,25 @@ const MetamaskController = require('./metamask-controller')
const extension = require('./lib/extension')
const STORAGE_KEY = 'metamask-config'
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
var popupIsOpen = false
const controller = new MetamaskController({
// User confirmation callbacks:
showUnconfirmedMessage: triggerUi,
unlockAccountMessage: triggerUi,
showUnconfirmedTx: triggerUi,
showUnapprovedTx: triggerUi,
// Persistence Methods:
setData,
loadData,
})
const idStore = controller.idStore
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') {
if ((details.reason === 'install') && (!METAMASK_DEBUG)) {
extension.tabs.create({url: 'https://metamask.io/#how-it-works'})
}
})
@ -80,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)
idStore.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
})
})
@ -96,15 +93,15 @@ function setupControllerConnection (stream) {
// plugin badge text
//
idStore.on('update', updateBadge)
controller.txManager.on('updateBadge', updateBadge)
updateBadge()
function updateBadge (state) {
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)
}
@ -112,6 +109,8 @@ function updateBadge (state) {
extension.browserAction.setBadgeBackgroundColor({ color: '#506F8B' })
}
// data :: setters/getters
function loadData () {
var oldData = getOldStyleData()
var newData

View File

@ -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

View File

@ -43,6 +43,7 @@ reloadStream.once('data', triggerReload)
// var pingChannel = inpageProvider.multiStream.createStream('pingpong')
// var pingStream = new PingStream({ objectMode: true })
// wait for first successful reponse
// disable pingStream until https://github.com/MetaMask/metamask-plugin/issues/746 is resolved more gracefully
// metamaskStream.once('data', function(){
// pingStream.pipe(pingChannel).pipe(pingStream)
@ -51,7 +52,7 @@ reloadStream.once('data', triggerReload)
// set web3 defaultAcount
inpageProvider.publicConfigStore.subscribe(function (state) {
web3.eth.defaultAccount = state.selectedAddress
web3.eth.defaultAccount = state.selectedAccount
})
//

View File

@ -0,0 +1,680 @@
const ethUtil = require('ethereumjs-util')
const bip39 = require('bip39')
const EventEmitter = require('events').EventEmitter
const filter = require('promise-filter')
const encryptor = require('browser-passworder')
const normalize = require('./lib/sig-util').normalize
const messageManager = require('./lib/message-manager')
const BN = ethUtil.BN
// Keyrings:
const SimpleKeyring = require('./keyrings/simple')
const HdKeyring = require('./keyrings/hd')
const keyringTypes = [
SimpleKeyring,
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.configManager = opts.configManager
this.ethStore = opts.ethStore
this.encryptor = encryptor
this.keyringTypes = keyringTypes
this.keyrings = []
this.identities = {} // Essentially a name hash
this._unconfMsgCbs = {}
this.getNetwork = opts.getNetwork
}
// 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
}
// 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 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,
}
})
}
// 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 Promise.reject('Password must be text.')
}
if (!bip39.validateMnemonic(seed)) {
return Promise.reject('Seed phrase is invalid.')
}
this.clearKeyrings()
return this.persistAllKeyrings(password)
.then(() => {
return this.addNewKeyring('HD Key Tree', {
mnemonic: seed,
numberOfAccounts: 1,
})
}).then(() => {
const firstKeyring = this.keyrings[0]
return firstKeyring.getAccounts()
})
.then((accounts) => {
const firstAccount = accounts[0]
const hexAccount = normalize(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)
return this.fullUpdate()
})
}
// 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())
}
// 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
return this.fullUpdate()
})
}
// 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)
return keyring.getAccounts()
.then((accounts) => {
this.keyrings.push(keyring)
return this.setupAccounts(accounts)
})
.then(() => { return this.password })
.then(this.persistAllKeyrings.bind(this))
.then(() => {
return keyring
})
}
// 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()
}
// 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
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)
}
}
// SIGNING METHODS
//
// This method signs tx and returns a promise for
// TX Manager to update the state after signing
signTransaction (ethTx, _fromAddress) {
const fromAddress = normalize(_fromAddress)
return this.getKeyringForAccount(fromAddress)
.then((keyring) => {
return keyring.signTransaction(fromAddress, ethTx)
})
}
// 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()
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.emit('update')
return msgId
}
// 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
messageManager.rejectMsg(msgId)
delete this._unconfTxCbs[msgId]
if (cb && typeof cb === 'function') {
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 msgId = msgParams.metamaskId
delete msgParams.metamaskId
const approvalCb = this._unconfMsgCbs[msgId] || noop
const address = normalize(msgParams.from)
return this.getKeyringForAccount(address)
.then((keyring) => {
return keyring.signMessage(address, msgParams.data)
}).then((rawSig) => {
cb(null, rawSig)
approvalCb(null, true)
return rawSig
})
} catch (e) {
cb(e)
}
}
// 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)
}))
})
}
// 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)
}
// 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
})
}
// 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
})
}
// 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
})
}
// 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)
}, [])
})
}
// 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)
} catch (e) {
accounts = []
}
accounts.forEach((address) => {
this.ethStore.removeAccount(address)
})
this.keyrings = []
this.identities = {}
this.configManager.setSelectedAccount()
}
}
function noop () {}

111
app/scripts/keyrings/hd.js Normal file
View File

@ -0,0 +1,111 @@
const EventEmitter = require('events').EventEmitter
const hdkey = require('ethereumjs-wallet/hdkey')
const bip39 = require('bip39')
const ethUtil = require('ethereumjs-util')
// *Internal Deps
const sigUtil = require('../lib/sig-util')
// Options:
const hdPathString = `m/44'/60'/0'/0`
const type = 'HD Key Tree'
class HdKeyring extends EventEmitter {
/* PUBLIC METHODS */
constructor (opts = {}) {
super()
this.type = type
this.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 (opts.mnemonic) {
this._initFromMnemonic(opts.mnemonic)
}
if (opts.numberOfAccounts) {
return this.addAccounts(opts.numberOfAccounts)
}
return Promise.resolve([])
}
addAccounts (numberOfAccounts = 1) {
if (!this.root) {
this._initFromMnemonic(bip39.generateMnemonic())
}
const oldLen = this.wallets.length
const newWallets = []
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)
}
const hexWallets = newWallets.map(w => w.getAddress().toString('hex'))
return Promise.resolve(hexWallets)
}
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)
var privKey = wallet.getPrivateKey()
tx.sign(privKey)
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)
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 Promise.resolve(rawMsgSig)
}
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)
}
_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

View File

@ -0,0 +1,82 @@
const EventEmitter = require('events').EventEmitter
const Wallet = require('ethereumjs-wallet')
const ethUtil = require('ethereumjs-util')
const type = 'Simple Key Pair'
const sigUtil = require('../lib/sig-util')
class SimpleKeyring extends EventEmitter {
/* PUBLIC METHODS */
constructor (opts) {
super()
this.type = type
this.opts = opts || {}
this.wallets = []
}
serialize () {
return Promise.resolve(this.wallets.map(w => w.getPrivateKey().toString('hex')))
}
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) {
var newWallets = []
for (var i = 0; i < n; i++) {
newWallets.push(Wallet.generate())
}
this.wallets = this.wallets.concat(newWallets)
const hexWallets = newWallets.map(w => ethUtil.bufferToHex(w.getAddress()))
return Promise.resolve(hexWallets)
}
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)
var privKey = wallet.getPrivateKey()
tx.sign(privKey)
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)
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 Promise.resolve(rawMsgSig)
}
exportAccount (address) {
const wallet = this._getWalletForAccount(address)
return Promise.resolve(wallet.getPrivateKey().toString('hex'))
}
/* 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

View File

@ -1,6 +1,9 @@
var uri = 'https://faucet.metamask.io/'
const uri = 'https://faucet.metamask.io/'
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
const env = process.env.METAMASK_ENV
module.exports = function (address) {
if (METAMASK_DEBUG || env === 'test') return // Don't faucet in development or test
var http = new XMLHttpRequest()
var data = address
http.open('POST', uri, true)

View File

@ -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()
}
}

View File

@ -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 MORDEN_RPC = MetamaskConfig.network.morden
const txLimit = 40
/* 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 = []
@ -111,6 +109,27 @@ ConfigManager.prototype.setWallet = function (wallet) {
this.setData(data)
}
ConfigManager.prototype.setVault = function (encryptedString) {
var data = this.getData()
data.vault = encryptedString
this.setData(data)
}
ConfigManager.prototype.getVault = function () {
var data = this.getData()
return data.vault
}
ConfigManager.prototype.getKeychains = function () {
return this.migrator.getData().keychains || []
}
ConfigManager.prototype.setKeychains = function (keychains) {
var data = this.migrator.getData()
data.keychains = keychains
this.setData(data)
}
ConfigManager.prototype.getSelectedAccount = function () {
var config = this.getConfig()
return config.selectedAccount
@ -118,7 +137,7 @@ ConfigManager.prototype.getSelectedAccount = function () {
ConfigManager.prototype.setSelectedAccount = function (address) {
var config = this.getConfig()
config.selectedAccount = address
config.selectedAccount = ethUtil.addHexPrefix(address)
this.setConfig(config)
}
@ -133,11 +152,23 @@ ConfigManager.prototype.setShowSeedWords = function (should) {
this.setData(data)
}
ConfigManager.prototype.getShouldShowSeedWords = function () {
var data = this.migrator.getData()
return data.showSeedWords
}
ConfigManager.prototype.setSeedWords = function (words) {
var data = this.getData()
data.seedWords = words
this.setData(data)
}
ConfigManager.prototype.getSeedWords = function () {
var data = this.getData()
return ('seedWords' in data) && data.seedWords
}
ConfigManager.prototype.getCurrentRpcAddress = function () {
var provider = this.getProvider()
if (!provider) return null
@ -174,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
@ -239,13 +221,15 @@ ConfigManager.prototype.getWalletNicknames = function () {
}
ConfigManager.prototype.nicknameForWallet = function (account) {
const address = normalize(account)
const nicknames = this.getWalletNicknames()
return nicknames[account]
return nicknames[address]
}
ConfigManager.prototype.setNicknameForWallet = function (account, nickname) {
const address = normalize(account)
const nicknames = this.getWalletNicknames()
nicknames[account] = nickname
nicknames[address] = nickname
var data = this.getData()
data.walletNicknames = nicknames
this.setData(data)
@ -253,6 +237,17 @@ ConfigManager.prototype.setNicknameForWallet = function (account, nickname) {
// observable
ConfigManager.prototype.getSalt = function () {
var data = this.getData()
return ('salt' in data) && data.salt
}
ConfigManager.prototype.setSalt = function (salt) {
var data = this.getData()
data.salt = salt
this.setData(data)
}
ConfigManager.prototype.subscribe = function (fn) {
this._subs.push(fn)
var unsubscribe = this.unsubscribe.bind(this, fn)
@ -270,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) {
@ -305,9 +300,9 @@ 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) => {
@ -315,7 +310,6 @@ ConfigManager.prototype.updateConversionRate = function () {
this.setConversionPrice(0)
this.setConversionDate('N/A')
})
}
ConfigManager.prototype.setConversionPrice = function (price) {
@ -340,21 +334,6 @@ ConfigManager.prototype.getConversionDate = function () {
return (('conversionDate' in data) && data.conversionDate) || 'N/A'
}
ConfigManager.prototype.setShouldntShowWarning = function () {
var data = this.getData()
if (data.isEthConfirmed) {
data.isEthConfirmed = !data.isEthConfirmed
} else {
data.isEthConfirmed = true
}
this.setData(data)
}
ConfigManager.prototype.getShouldntShowWarning = function () {
var data = this.getData()
return ('isEthConfirmed' in data) && data.isEthConfirmed
}
ConfigManager.prototype.getShapeShiftTxList = function () {
var data = this.getData()
var shapeShiftTxList = data.shapeShiftTxList ? data.shapeShiftTxList : []
@ -400,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 || []
}

View File

@ -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() {}

View File

@ -0,0 +1,80 @@
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 {
constructor ({ configManager }) {
this.configManager = configManager
const hasOldVault = this.hasOldVault()
if (!hasOldVault) {
this.idStore = new IdentityStore({ configManager })
}
}
migratedVaultForPassword (password) {
const hasOldVault = this.hasOldVault()
const configManager = this.configManager
if (!this.idStore) {
this.idStore = new IdentityStore({ configManager })
}
if (!hasOldVault) {
return Promise.resolve(null)
}
const idStore = this.idStore
const submitPassword = denodeify(idStore.submitPassword.bind(idStore))
return submitPassword(password)
.then(() => {
const serialized = this.serializeVault()
return this.checkForLostAccounts(serialized)
})
}
serializeVault () {
const mnemonic = this.idStore._idmgmt.getSeed()
const numberOfAccounts = this.idStore._getAddresses().length
return {
type: 'HD Key Tree',
data: { mnemonic, numberOfAccounts },
}
}
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
}
}

View File

@ -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('./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,19 +94,13 @@ IdentityStore.prototype.getState = function () {
isInitialized: !!configManager.getWallet() && !seedWords,
isUnlocked: this._isUnlocked(),
seedWords: seedWords,
isConfirmed: configManager.getConfirmed(),
isEthConfirmed: configManager.getShouldntShowWarning(),
unconfTxs: configManager.unconfirmedTxs(),
transactions: configManager.getTxList(),
unconfMsgs: messageManager.unconfirmedMsgs(),
messages: messageManager.getMsgList(),
isDisclaimerConfirmed: configManager.getConfirmedDisclaimer(),
selectedAddress: configManager.getSelectedAccount(),
shapeShiftTxList: configManager.getShapeShiftTxList(),
currentFiat: configManager.getCurrentFiat(),
conversionRate: configManager.getConversionRate(),
conversionDate: configManager.getConversionDate(),
gasMultiplier: configManager.getGasMultiplier(),
}))
}
@ -204,248 +190,10 @@ IdentityStore.prototype.submitPassword = function (password, cb) {
IdentityStore.prototype.exportAccount = function (address, cb) {
var privateKey = this._idmgmt.exportPrivateKey(address)
cb(null, privateKey)
if (cb) cb(null, privateKey)
return 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.message || err)
var containsDelegateCall = self.checkForDelegateCall(result)
txData.containsDelegateCall = containsDelegateCall
cb()
})
} else {
cb()
}
}
function estimateGas(cb){
var estimationParams = extend(txParams)
query.getBlockByNumber('latest', true, function(err, block){
if (err) return cb(err)
// check if gasLimit is already specified
const gasLimitSpecified = Boolean(txParams.gas)
// if not, fallback to block gasLimit
if (!gasLimitSpecified) {
estimationParams.gas = block.gasLimit
}
// run tx, see if it will OOG
query.estimateGas(estimationParams, function(err, estimatedGasHex){
if (err) return cb(err.message || err)
// all gas used - must be an error
if (estimatedGasHex === estimationParams.gas) {
txData.simulationFails = true
txData.estimatedGas = estimatedGasHex
txData.txParams.gas = estimatedGasHex
cb()
return
}
// otherwise, did not use all gas, must be ok
// if specified gasLimit and no error, we're done
if (gasLimitSpecified) {
txData.estimatedGas = txParams.gas
cb()
return
}
// try adding an additional gas buffer to our estimation for safety
const estimatedGasBn = new BN(ethUtil.stripHexPrefix(estimatedGasHex), 16)
const blockGasLimitBn = new BN(ethUtil.stripHexPrefix(block.gasLimit), 16)
const estimationWithBuffer = self.addGasBuffer(estimatedGasBn)
// added gas buffer is too high
if (estimationWithBuffer.gt(blockGasLimitBn)) {
txData.estimatedGas = estimatedGasHex
txData.txParams.gas = estimatedGasHex
// added gas buffer is safe
} else {
const gasWithBufferHex = ethUtil.intToHex(estimationWithBuffer)
txData.estimatedGas = gasWithBufferHex
txData.txParams.gas = gasWithBufferHex
}
cb()
return
})
})
}
function didComplete (err) {
if (err) return cb(err.message || err)
configManager.addTx(txData)
// signal update
self._didUpdate()
// signal completion of add tx
cb(null, txData)
}
}
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 (gasBn) {
// add 20% to specified gas
const gasBuffer = gasBn.div(new BN('5', 10))
const gasWithBuffer = gasBn.add(gasBuffer)
return gasWithBuffer
}
// 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
//
@ -466,7 +214,9 @@ IdentityStore.prototype._loadIdentities = function () {
var addresses = this._getAddresses()
addresses.forEach((address, i) => {
// // add to ethStore
this._ethStore.addAccount(ethUtil.addHexPrefix(address))
if (this._ethStore) {
this._ethStore.addAccount(ethUtil.addHexPrefix(address))
}
// add to identities
const defaultLabel = 'Account ' + (i + 1)
const nickname = configManager.nicknameForWallet(address)
@ -523,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,
@ -598,4 +348,3 @@ IdentityStore.prototype._autoFaucet = function () {
// util
function noop () {}

View File

@ -40,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()
@ -49,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) => {
@ -66,20 +66,20 @@ 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':
@ -111,6 +111,8 @@ MetamaskInpageProvider.prototype.isConnected = function () {
return true
}
MetamaskInpageProvider.prototype.isMetaMask = true
// util
function remoteStoreWithLocalStorageCache (storageKey) {
@ -125,7 +127,7 @@ function remoteStoreWithLocalStorageCache (storageKey) {
return store
}
function eachJsonMessage(payload, transformFn){
function eachJsonMessage (payload, transformFn) {
if (Array.isArray(payload)) {
return payload.map(transformFn)
} else {

View File

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

View File

@ -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
}
}

View File

@ -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

View File

@ -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

View File

@ -1,7 +1,7 @@
const MAX = 1000000000
const MAX = Number.MAX_SAFE_INTEGER
let idCounter = Math.round( Math.random() * MAX )
function createRandomId() {
let idCounter = Math.round(Math.random() * MAX)
function createRandomId () {
idCounter = idCounter % MAX
return idCounter++
}

View File

@ -0,0 +1,28 @@
const ethUtil = require('ethereumjs-util')
module.exports = {
concatSig: function (v, r, s) {
const rSig = ethUtil.fromSigned(r)
const sSig = ethUtil.fromSigned(s)
const vSig = ethUtil.bufferToInt(v)
const rStr = padWithZeroes(ethUtil.toUnsigned(rSig).toString('hex'), 64)
const sStr = padWithZeroes(ethUtil.toUnsigned(sSig).toString('hex'), 64)
const vStr = ethUtil.stripHexPrefix(ethUtil.intToHex(vSig))
return ethUtil.addHexPrefix(rStr.concat(sStr, vStr)).toString('hex')
},
normalize: function (address) {
if (!address) return
return ethUtil.addHexPrefix(address.toLowerCase())
},
}
function padWithZeroes (number, length) {
var myString = '' + number
while (myString.length < length) {
myString = '0' + myString
}
return myString
}

132
app/scripts/lib/tx-utils.js Normal file
View File

@ -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
}

View File

@ -1,22 +1,30 @@
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 IdentityStore = require('./lib/idStore')
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.idStore = new IdentityStore({
this.keyringController = new KeyringController({
configManager: this.configManager,
getNetwork: this.getStateNetwork.bind(this),
})
// notices
this.noticeController = new NoticeController({
@ -27,8 +35,20 @@ module.exports = class MetamaskController {
// this.noticeController.startPolling()
this.provider = this.initializeProvider(opts)
this.ethStore = new EthStore(this.provider)
this.idStore.setStore(this.ethStore)
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'
@ -38,50 +58,75 @@ 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.ethStore.getState(),
this.idStore.getState(),
this.configManager.getConfig(),
this.noticeController.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 idStore = this.idStore
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),
agreeToDisclaimer: this.agreeToDisclaimer.bind(this),
resetDisclaimer: this.resetDisclaimer.bind(this),
setCurrentFiat: this.setCurrentFiat.bind(this),
agreeToEthWarning: this.agreeToEthWarning.bind(this),
setTOSHash: this.setTOSHash.bind(this),
checkTOSChange: this.checkTOSChange.bind(this),
setGasMultiplier: this.setGasMultiplier.bind(this),
markAccountsFound: this.markAccountsFound.bind(this),
// forward directly to 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),
// forward directly to idStore
createNewVault: idStore.createNewVault.bind(idStore),
recoverFromSeed: idStore.recoverFromSeed.bind(idStore),
submitPassword: idStore.submitPassword.bind(idStore),
setSelectedAddress: idStore.setSelectedAddress.bind(idStore),
approveTransaction: idStore.approveTransaction.bind(idStore),
cancelTransaction: idStore.cancelTransaction.bind(idStore),
signMessage: idStore.signMessage.bind(idStore),
cancelMessage: idStore.cancelMessage.bind(idStore),
setLocked: idStore.setLocked.bind(idStore),
clearSeedWordCache: idStore.clearSeedWordCache.bind(idStore),
exportAccount: idStore.exportAccount.bind(idStore),
revealAccount: idStore.revealAccount.bind(idStore),
saveAccountLabel: idStore.saveAccountLabel.bind(idStore),
tryPassword: idStore.tryPassword.bind(idStore),
recoverSeed: idStore.recoverSeed.bind(idStore),
// coinbase
buyEth: this.buyEth.bind(this),
// shapeshift
@ -97,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)
@ -140,76 +168,65 @@ module.exports = class MetamaskController {
}
sendUpdate () {
this.listeners.forEach((remote) => {
remote.sendUpdate(this.getState())
this.getState()
.then((state) => {
this.emit('update', state)
})
}
initializeProvider (opts) {
const idStore = this.idStore
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 = idStore.getSelectedAddress()
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) => {
idStore.signTransaction(...args)
this.sendUpdate()
},
processTransaction: (txParams, cb) => this.newUnapprovedTransaction(txParams, cb),
// msg signing
approveMessage: this.newUnsignedMessage.bind(this),
signMessage: (...args) => {
idStore.signMessage(...args)
keyringController.signMessage(...args)
this.sendUpdate()
},
}
var provider = MetaMaskProvider(providerOpts)
var web3 = new Web3(provider)
idStore.web3 = web3
idStore.getNetwork()
this.web3 = web3
keyringController.web3 = web3
provider.on('block', this.processBlock.bind(this))
provider.on('error', idStore.getNetwork.bind(idStore))
provider.on('error', this.getNetwork.bind(this))
return provider
}
initPublicConfigStore () {
// get init state
var initPublicState = extend(
idStoreToPublic(this.idStore.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.idStore.on('update', function (state) {
storeSetFromObj(publicConfigStore, idStoreToPublic(state))
this.keyringController.on('newAccount', (account) => {
autoFaucet(account)
})
// idStore substate
function idStoreToPublic (state) {
return {
selectedAddress: state.selectedAddress,
}
}
// config substate
function configToPublic (state) {
return {
provider: state.provider,
selectedAddress: state.selectedAccount,
selectedAccount: state.selectedAccount,
}
}
// dump obj into store
@ -222,28 +239,30 @@ module.exports = class MetamaskController {
return publicConfigStore
}
newUnsignedTransaction (txParams, onTxDoneCb) {
const idStore = this.idStore
let err = this.enforceTxValidations(txParams)
if (err) return onTxDoneCb(err)
idStore.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.idStore.getState()
var state = this.keyringController.getState()
if (!state.isUnlocked) {
this.idStore.addUnconfirmedMessage(msgParams, cb)
this.keyringController.addUnconfirmedMessage(msgParams, cb)
this.opts.unlockAccountMessage()
} else {
this.addUnconfirmedMessage(msgParams, cb)
@ -252,8 +271,8 @@ module.exports = class MetamaskController {
}
addUnconfirmedMessage (msgParams, cb) {
const idStore = this.idStore
const msgId = idStore.addUnconfirmedMessage(msgParams, cb)
const keyringController = this.keyringController
const msgId = keyringController.addUnconfirmedMessage(msgParams, cb)
this.opts.showUnconfirmedMessage(msgParams, msgId)
}
@ -272,8 +291,8 @@ module.exports = class MetamaskController {
verifyNetwork () {
// Check network when restoring connectivity:
if (this.idStore._currentState.network === 'loading') {
this.idStore.getNetwork()
if (this.state.network === 'loading') {
this.getNetwork()
}
}
@ -298,14 +317,13 @@ module.exports = class MetamaskController {
} catch (err) {
console.error('Error in checking TOS change.')
}
}
// disclaimer
agreeToDisclaimer (cb) {
try {
this.configManager.setConfirmed(true)
this.configManager.setConfirmedDisclaimer(true)
cb()
} catch (err) {
cb(err)
@ -314,9 +332,9 @@ module.exports = class MetamaskController {
resetDisclaimer () {
try {
this.configManager.setConfirmed(false)
} catch (err) {
console.error(err)
this.configManager.setConfirmedDisclaimer(false)
} catch (e) {
console.error(e)
}
}
@ -345,26 +363,17 @@ module.exports = class MetamaskController {
}, 300000)
}
agreeToEthWarning (cb) {
try {
this.configManager.setShouldntShowWarning()
cb()
} catch (err) {
cb(err)
}
}
// called from popup
setRpcTarget (rpcTarget) {
this.configManager.setRpcTarget(rpcTarget)
extension.runtime.reload()
this.idStore.getNetwork()
this.getNetwork()
}
setProviderType (type) {
this.configManager.setProviderType(type)
extension.runtime.reload()
this.idStore.getNetwork()
this.getNetwork()
}
useEtherscanProvider () {
@ -375,7 +384,7 @@ module.exports = class MetamaskController {
buyEth (address, amount) {
if (!amount) amount = '5'
var network = this.idStore._currentState.network
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 === '3') {
@ -391,6 +400,25 @@ module.exports = class MetamaskController {
this.configManager.createShapeShiftTx(depositAddress, depositType)
}
getNetwork (err) {
if (err) {
this.state.network = 'loading'
this.sendUpdate()
}
this.web3.version.getNetwork((err, network) => {
if (err) {
this.state.network = 'loading'
return this.sendUpdate()
}
if (global.METAMASK_DEBUG) {
console.log('web3.getNetwork returned ' + network)
}
this.state.network = network
this.sendUpdate()
})
}
setGasMultiplier (gasMultiplier, cb) {
try {
this.configManager.setGasMultiplier(gasMultiplier)
@ -399,4 +427,70 @@ module.exports = class MetamaskController {
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,
})
}
}

View File

@ -9,7 +9,7 @@ module.exports = class NoticeController extends EventEmitter {
this.noticePoller = null
}
getState() {
getState () {
var lastUnreadNotice = this.getLatestUnreadNotice()
return {
@ -18,7 +18,7 @@ module.exports = class NoticeController extends EventEmitter {
}
}
getNoticesList() {
getNoticesList () {
var data = this.configManager.getData()
if ('noticesList' in data) {
return data.noticesList
@ -27,28 +27,28 @@ module.exports = class NoticeController extends EventEmitter {
}
}
setNoticesList(list) {
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 }
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)
let latestNotice = this.getLatestUnreadNotice()
const latestNotice = this.getLatestUnreadNotice()
cb(null, latestNotice)
} catch (err) {
cb(err)
}
}
updateNoticesList() {
updateNoticesList () {
return this._retrieveNoticeData().then((newNotices) => {
var oldNotices = this.getNoticesList()
var combinedNotices = this._mergeNotices(oldNotices, newNotices)
@ -56,7 +56,7 @@ module.exports = class NoticeController extends EventEmitter {
})
}
getLatestUnreadNotice() {
getLatestUnreadNotice () {
var notices = this.getNoticesList()
var filteredNotices = notices.filter((notice) => {
return notice.read === false
@ -73,7 +73,7 @@ module.exports = class NoticeController extends EventEmitter {
}, 300000)
}
_mergeNotices(oldNotices, newNotices) {
_mergeNotices (oldNotices, newNotices) {
var noticeMap = this._mapNoticeIds(oldNotices)
newNotices.forEach((notice) => {
if (noticeMap.indexOf(notice.id) === -1) {
@ -83,11 +83,11 @@ module.exports = class NoticeController extends EventEmitter {
return oldNotices
}
_mapNoticeIds(notices) {
_mapNoticeIds (notices) {
return notices.map((notice) => notice.id)
}
_retrieveNoticeData() {
_retrieveNoticeData () {
// Placeholder for the API.
return Promise.resolve(hardCodedNotices)
}

View File

@ -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({

View File

@ -18,7 +18,7 @@ var portStream = new PortStream(pluginPort)
startPopup(portStream)
function closePopupIfOpen(name) {
function closePopupIfOpen (name) {
if (name !== 'notification') {
notification.closePopup()
}

View File

@ -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')

File diff suppressed because one or more lines are too long

View File

@ -133,10 +133,10 @@
"address": "0x704107d04affddd9b66ab9de3dd7b095852e9b69"
}
},
"selectedAddress": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"selectedAccount": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"network": "1",
"seedWords": null,
"isConfirmed": true,
"isDisclaimerConfirmed": true,
"unconfMsgs": {},
"messages": [],
"provider": {

View File

@ -99,10 +99,10 @@
"status": "confirmed",
"containsDelegateCall": false
}],
"selectedAddress": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"selectedAccount": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"network": "2",
"seedWords": null,
"isConfirmed": true,
"isDisclaimerConfirmed": true,
"unconfMsgs": {},
"messages": [],
"provider": {

View File

@ -57,10 +57,10 @@
}
},
"transactions": [],
"selectedAddress": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"selectedAccount": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"network": "2",
"seedWords": null,
"isConfirmed": true,
"isDisclaimerConfirmed": true,
"unconfMsgs": {},
"messages": [],
"provider": {

View File

@ -0,0 +1,126 @@
{
"metamask": {
"isInitialized": true,
"isUnlocked": true,
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {
"0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9": {
"address": "0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9",
"name": "Account 1"
},
"0xd7c0cd9e7d2701c710d64fc492c7086679bdf7b4": {
"address": "0xd7c0cd9e7d2701c710d64fc492c7086679bdf7b4",
"name": "Account 2"
},
"0x1acfb961c5a8268eac8e09d6241a26cbeff42241": {
"address": "0x1acfb961c5a8268eac8e09d6241a26cbeff42241",
"name": "Account 3"
},
"0xe15d894becb0354c501ae69429b05143679f39e0": {
"address": "0xe15d894becb0354c501ae69429b05143679f39e0",
"name": "Account 4"
},
"0x87658c15aefe7448008a28513a11b6b130ef4cd0": {
"address": "0x87658c15aefe7448008a28513a11b6b130ef4cd0",
"name": "Account 5"
},
"0xaa25854c0379e53c957ac9382e720c577fa31fd5": {
"address": "0xaa25854c0379e53c957ac9382e720c577fa31fd5",
"name": "Account 6"
}
},
"unconfTxs": {},
"currentFiat": "USD",
"conversionRate": 0,
"conversionDate": "N/A",
"noActiveNotices": true,
"network": "3",
"accounts": {
"0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9": {
"code": "0x",
"balance": "0x11f646fe14c9c000",
"nonce": "0x3",
"address": "0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9"
},
"0xd7c0cd9e7d2701c710d64fc492c7086679bdf7b4": {
"code": "0x",
"balance": "0x0",
"nonce": "0x0",
"address": "0xd7c0cd9e7d2701c710d64fc492c7086679bdf7b4"
},
"0x1acfb961c5a8268eac8e09d6241a26cbeff42241": {
"code": "0x",
"balance": "0x0",
"nonce": "0x0",
"address": "0x1acfb961c5a8268eac8e09d6241a26cbeff42241"
},
"0xe15d894becb0354c501ae69429b05143679f39e0": {
"code": "0x",
"balance": "0x0",
"nonce": "0x0",
"address": "0xe15d894becb0354c501ae69429b05143679f39e0"
},
"0x87658c15aefe7448008a28513a11b6b130ef4cd0": {
"code": "0x",
"balance": "0x0",
"nonce": "0x0",
"address": "0x87658c15aefe7448008a28513a11b6b130ef4cd0"
},
"0xaa25854c0379e53c957ac9382e720c577fa31fd5": {
"code": "0x",
"balance": "0x0",
"nonce": "0x0",
"address": "0xaa25854c0379e53c957ac9382e720c577fa31fd5"
}
},
"transactions": [],
"provider": {
"type": "testnet"
},
"selectedAccount": "0x87658c15aefe7448008a28513a11b6b130ef4cd0",
"isDisclaimerConfirmed": true,
"unconfMsgs": {},
"messages": [],
"shapeShiftTxList": [],
"keyringTypes": [
"Simple Key Pair",
"HD Key Tree"
],
"keyrings": [
{
"type": "HD Key Tree",
"accounts": [
"ac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9",
"d7c0cd9e7d2701c710d64fc492c7086679bdf7b4",
"1acfb961c5a8268eac8e09d6241a26cbeff42241"
]
},
{
"type": "Simple Key Pair",
"accounts": [
"e15d894becb0354c501ae69429b05143679f39e0",
"87658c15aefe7448008a28513a11b6b130ef4cd0",
"aa25854c0379e53c957ac9382e720c577fa31fd5"
]
}
],
"lostAccounts": []
},
"appState": {
"menuOpen": false,
"currentView": {
"name": "accounts"
},
"accountDetail": {
"subview": "transactions",
"accountExport": "none",
"privateKey": ""
},
"transForward": true,
"isLoading": false,
"warning": null,
"scrollToBottom": false,
"forgottenPassword": false
},
"identities": {}
}

View File

@ -2,7 +2,6 @@
"metamask": {
"isInitialized": true,
"isUnlocked": true,
"isEthConfirmed": true,
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {
"0x0abdd95cafcabec9b3e99dcd09fc4b441037cb80": {
@ -90,9 +89,9 @@
}
},
"transactions": [],
"selectedAddress": "0x0abdd95cafcabec9b3e99dcd09fc4b441037cb80",
"selectedAccount": "0x0abdd95cafcabec9b3e99dcd09fc4b441037cb80",
"network": "2",
"isConfirmed": true,
"isDisclaimerConfirmed": true,
"unconfMsgs": {},
"messages": [],
"shapeShiftTxList": [],
@ -116,4 +115,4 @@
"scrollToBottom": true
},
"identities": {}
}
}

View File

@ -57,9 +57,9 @@
}
},
"transactions": [],
"selectedAddress": "0x843963b837841dad3b0f5969ff271108776616df",
"selectedAccount": "0x843963b837841dad3b0f5969ff271108776616df",
"network": "2",
"isConfirmed": true,
"isDisclaimerConfirmed": true,
"unconfMsgs": {},
"messages": [],
"provider": {

View File

@ -10,7 +10,7 @@
"transactions": [],
"network": "2",
"seedWords": null,
"isConfirmed": true,
"isDisclaimerConfirmed": true,
"unconfMsgs": {},
"messages": [],
"provider": {

View File

@ -157,11 +157,10 @@
"estimatedGas": "0x5208"
}
],
"selectedAddress": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"selectedAccount": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"network": "166",
"seedWords": null,
"isConfirmed": true,
"isEthConfirmed": true,
"isDisclaimerConfirmed": true,
"unconfMsgs": {},
"messages": [],
"provider": {

View File

@ -54,10 +54,10 @@
}
},
"transactions": [],
"selectedAddress": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"selectedAccount": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"network": "2",
"seedWords": null,
"isConfirmed": true,
"isDisclaimerConfirmed": true,
"unconfMsgs": {},
"messages": [],
"provider": {

View File

@ -2,18 +2,22 @@
"metamask": {
"isInitialized": false,
"isUnlocked": false,
"currentDomain": "example.com",
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {},
"unconfTxs": {},
"currentFiat": "USD",
"conversionRate": 11.47635827,
"conversionDate": 1477606503,
"network": null,
"accounts": {},
"transactions": [],
"network": "2",
"seedWords": null,
"isConfirmed": false,
"isEthConfirmed": false,
"isDisclaimerConfirmed": true,
"unconfMsgs": {},
"messages": [],
"shapeShiftTxList": [],
"keyringTypes": [
"Simple Key Pair"
],
"provider": {
"type": "testnet"
}
@ -21,15 +25,15 @@
"appState": {
"menuOpen": false,
"currentView": {
"name": "EthStoreWarning"
"name": "accounts",
"detailView": null
},
"accountDetail": {
"subview": "transactions"
},
"currentDomain": "127.0.0.1:9966",
"transForward": true,
"isLoading": false,
"warning": null
},
"identities": {}
}
}

View File

@ -55,14 +55,14 @@
},
"transactions": [],
"network": "2",
"isConfirmed": true,
"isDisclaimerConfirmed": true,
"unconfMsgs": {},
"messages": [],
"provider": {
"type": "testnet"
},
"selectedAccount": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"selectedAddress": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"selectedAccount": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"seedWords": null
},
"appState": {

View File

@ -2,7 +2,6 @@
"metamask": {
"isInitialized": true,
"isUnlocked": false,
"isEthConfirmed": true,
"currentDomain": "example.com",
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {},
@ -12,10 +11,10 @@
"conversionDate": 1473358355,
"accounts": {},
"transactions": [],
"selectedAddress": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"selectedAccount": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"network": "1473186153102",
"seedWords": null,
"isConfirmed": true,
"isDisclaimerConfirmed": true,
"unconfMsgs": {},
"messages": [],
"shapeShiftTxList": [],

View File

@ -0,0 +1,91 @@
{
"metamask": {
"currentFiat": "USD",
"lostAccounts": [
"0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b"
],
"conversionRate": 11.06608791,
"conversionDate": 1470421024,
"isInitialized": true,
"isUnlocked": true,
"currentDomain": "example.com",
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {
"0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": {
"name": "Wallet 1",
"address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"mayBeFauceting": false
},
"0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b": {
"name": "Wallet 2",
"address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b",
"mayBeFauceting": false
},
"0xeb9e64b93097bc15f01f13eae97015c57ab64823": {
"name": "Wallet 3",
"address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823",
"mayBeFauceting": false
},
"0x704107d04affddd9b66ab9de3dd7b095852e9b69": {
"name": "Wallet 4",
"address": "0x704107d04affddd9b66ab9de3dd7b095852e9b69",
"mayBeFauceting": false
}
},
"unconfTxs": {},
"accounts": {
"0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": {
"code": "0x",
"balance": "0x100000000000",
"nonce": "0x0",
"address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
},
"0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b": {
"code": "0x",
"nonce": "0x0",
"balance": "0x100000000000",
"address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b"
},
"0xeb9e64b93097bc15f01f13eae97015c57ab64823": {
"code": "0x",
"nonce": "0x0",
"balance": "0x100000000000",
"address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823"
},
"0x704107d04affddd9b66ab9de3dd7b095852e9b69": {
"code": "0x",
"balance": "0x0",
"nonce": "0x0",
"address": "0x704107d04affddd9b66ab9de3dd7b095852e9b69"
}
},
"transactions": [],
"selectedAccount": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"network": "2",
"seedWords": null,
"isDisclaimerConfirmed": true,
"unconfMsgs": {},
"messages": [],
"provider": {
"type": "testnet"
},
"selectedAccount": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
},
"appState": {
"menuOpen": false,
"currentView": {
"name": "accountDetail",
"detailView": null,
"context": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
},
"accountDetail": {
"subview": "transactions"
},
"currentDomain": "127.0.0.1:9966",
"transForward": true,
"isLoading": false,
"warning": null
},
"identities": {}
}

View File

@ -10,7 +10,7 @@
"transactions": [],
"network": "2",
"seedWords": null,
"isConfirmed": false,
"isDisclaimerConfirmed": false,
"unconfMsgs": {},
"messages": [],
"provider": {

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,6 @@
"metamask": {
"isInitialized": true,
"isUnlocked": true,
"isEthConfirmed": true,
"currentDomain": "example.com",
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {
@ -352,10 +351,10 @@
"hash": "0xb6e6ff57e7b5f6bd7f2e6dc44c39f4e858a227c9509586634ca547179345a13e"
}
],
"selectedAddress": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"selectedAccount": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"network": "1471904489432",
"seedWords": null,
"isConfirmed": true,
"isDisclaimerConfirmed": true,
"unconfMsgs": {
"1472076978535283": {
"id": 1472076978535283,
@ -403,4 +402,4 @@
"warning": null
},
"identities": {}
}
}

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{"metamask":{"isInitialized":true,"isUnlocked":true,"currentDomain":"example.com","rpcTarget":"https://rawtestrpc.metamask.io/","identities":{"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825":{"name":"Wallet 1","address":"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825","mayBeFauceting":false},"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb":{"name":"Wallet 2","address":"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb","mayBeFauceting":false},"0x2f8d4a878cfa04a6e60d46362f5644deab66572d":{"name":"Wallet 3","address":"0x2f8d4a878cfa04a6e60d46362f5644deab66572d","mayBeFauceting":false}},"unconfTxs":{"1467868023090690":{"id":1467868023090690,"txParams":{"data":"0xa9059cbb0000000000000000000000008deb4d106090c3eb8f1950f727e87c4f884fb06f0000000000000000000000000000000000000000000000000000000000000064","from":"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825","value":"0x16345785d8a0000","to":"0xbeb0ed3034c4155f3d16a64a5c5e7c8d4ea9e9c9","origin":"MetaMask","metamaskId":1467868023090690,"metamaskNetworkId":"2"},"time":1467868023090,"status":"unconfirmed","containsDelegateCall":false}},"accounts":{"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825":{"code":"0x","balance":"0x38326dc32cf80800","nonce":"0x10000c","address":"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"},"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb":{"code":"0x","balance":"0x15e578bd8e9c8000","nonce":"0x100000","address":"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb"},"0x2f8d4a878cfa04a6e60d46362f5644deab66572d":{"code":"0x","nonce":"0x100000","balance":"0x2386f26fc10000","address":"0x2f8d4a878cfa04a6e60d46362f5644deab66572d"}},"transactions":[],"selectedAddress":"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825","network":"2","seedWords":null,"isConfirmed":true,"unconfMsgs":{},"messages":[],"provider":{"type":"testnet"},"selectedAccount":"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"},"appState":{"menuOpen":false,"currentView":{"name":"confTx","context":0},"accountDetail":{"subview":"transactions"},"currentDomain":"extensions","transForward":true,"isLoading":false,"warning":null},"identities":{}}
{"metamask":{"isInitialized":true,"isUnlocked":true,"currentDomain":"example.com","rpcTarget":"https://rawtestrpc.metamask.io/","identities":{"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825":{"name":"Wallet 1","address":"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825","mayBeFauceting":false},"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb":{"name":"Wallet 2","address":"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb","mayBeFauceting":false},"0x2f8d4a878cfa04a6e60d46362f5644deab66572d":{"name":"Wallet 3","address":"0x2f8d4a878cfa04a6e60d46362f5644deab66572d","mayBeFauceting":false}},"unconfTxs":{"1467868023090690":{"id":1467868023090690,"txParams":{"data":"0xa9059cbb0000000000000000000000008deb4d106090c3eb8f1950f727e87c4f884fb06f0000000000000000000000000000000000000000000000000000000000000064","from":"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825","value":"0x16345785d8a0000","to":"0xbeb0ed3034c4155f3d16a64a5c5e7c8d4ea9e9c9","origin":"MetaMask","metamaskId":1467868023090690,"metamaskNetworkId":"2"},"time":1467868023090,"status":"unconfirmed","containsDelegateCall":false}},"accounts":{"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825":{"code":"0x","balance":"0x38326dc32cf80800","nonce":"0x10000c","address":"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"},"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb":{"code":"0x","balance":"0x15e578bd8e9c8000","nonce":"0x100000","address":"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb"},"0x2f8d4a878cfa04a6e60d46362f5644deab66572d":{"code":"0x","nonce":"0x100000","balance":"0x2386f26fc10000","address":"0x2f8d4a878cfa04a6e60d46362f5644deab66572d"}},"transactions":[],"selectedAccount":"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825","network":"2","seedWords":null,"isDisclaimerConfirmed":true,"unconfMsgs":{},"messages":[],"provider":{"type":"testnet"},"selectedAccount":"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"},"appState":{"menuOpen":false,"currentView":{"name":"confTx","context":0},"accountDetail":{"subview":"transactions"},"currentDomain":"extensions","transForward":true,"isLoading":false,"warning":null},"identities":{}}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,86 @@
{
"metamask": {
"isInitialized": true,
"isUnlocked": true,
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {
"name": "Account 1",
"address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"mayBeFauceting": false
},
"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": {
"name": "Account 2",
"address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb",
"mayBeFauceting": false
}
},
"unconfTxs": {},
"currentFiat": "USD",
"conversionRate": 9.52855776,
"conversionDate": 1479756513,
"accounts": {
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {
"balance": "0x0",
"nonce": "0x0",
"code": "0x0",
"address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
},
"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": {
"balance": "0x0",
"nonce": "0x0",
"code": "0x0",
"address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb"
}
},
"transactions": [
{
"id": 5551995700357153,
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0x48ff0cbac0acefedf152281ee80e9a0a01d5da63",
"data": "0x90b98a11000000000000000000000000c5b8dbac4c1d3f152cdeb400e2313f309c410acb000000000000000000000000000000000000000000000000000000000000000a",
"metamaskId": 5551995700357153,
"metamaskNetworkId": "1479490588308"
},
"time": 1479498745949,
"status": "confirmed",
"gasMultiplier": 1,
"metamaskNetworkId": "1479490588308",
"containsDelegateCall": true,
"estimatedGas": "0x24b33",
"hash": "0xad609a6931f54a575ad71222ffc27cd6746017106d5b89f4ad300b37b273f8ac"
}
],
"selectedAccount": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"network": "1479753732793",
"isConfirmed": true,
"isEthConfirmed": true,
"unconfMsgs": {},
"messages": [],
"shapeShiftTxList": [],
"gasMultiplier": false,
"provider": {
"type": "rpc",
"rpcTarget": "http://localhost:8545"
},
"selectedAccount": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"isDisclaimerConfirmed": true
},
"appState": {
"menuOpen": false,
"currentView": {
"name": "accountDetail"
},
"accountDetail": {
"subview": "transactions",
"accountExport": "none",
"privateKey": ""
},
"transForward": false,
"isLoading": false,
"warning": null,
"scrollToBottom": false
},
"identities": {}
}

View File

@ -2,7 +2,6 @@
"metamask": {
"isInitialized": false,
"isUnlocked": false,
"isEthConfirmed": false,
"currentDomain": "example.com",
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {},
@ -13,7 +12,7 @@
"accounts": {},
"transactions": [],
"seedWords": null,
"isConfirmed": true,
"isDisclaimerConfirmed": true,
"unconfMsgs": {},
"messages": [],
"shapeShiftTxList": [],
@ -36,4 +35,4 @@
"warning": null
},
"identities": {}
}
}

View File

@ -2,7 +2,6 @@
"metamask": {
"isInitialized": true,
"isUnlocked": true,
"isEthConfirmed": false,
"currentDomain": "example.com",
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {
@ -49,7 +48,7 @@
"transactions": [],
"network": "2",
"seedWords": null,
"isConfirmed": true,
"isDisclaimerConfirmed": true,
"unconfMsgs": {},
"messages": [],
"shapeShiftTxList": [],
@ -73,4 +72,4 @@
"detailView": {}
},
"identities": {}
}
}

View File

@ -2,7 +2,6 @@
"metamask": {
"isInitialized": true,
"isUnlocked": true,
"isEthConfirmed": true,
"currentDomain": "example.com",
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {
@ -47,10 +46,10 @@
}
},
"transactions": [],
"selectedAddress": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"selectedAccount": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"network": "1",
"seedWords": null,
"isConfirmed": true,
"isDisclaimerConfirmed": true,
"unconfMsgs": {},
"messages": [],
"shapeShiftTxList": [],
@ -345,4 +344,4 @@
"isSubLoading": false
},
"identities": {}
}
}

View File

@ -45,7 +45,7 @@
"transactions": [],
"network": "2",
"seedWords": "debris dizzy just program just float decrease vacant alarm reduce speak stadium",
"isConfirmed": true,
"isDisclaimerConfirmed": true,
"unconfMsgs": {},
"messages": [],
"provider": {

31
development/test.html Normal file
View File

@ -0,0 +1,31 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>MetaMask</title>
<script>
window.METAMASK_DEBUG = true
window.TEST_MODE = true
</script>
</head>
<body>
<!-- app content -->
<div id="app-content" style="height: 100%"></div>
<script src="./bundle.js" type="text/javascript" charset="utf-8"></script>
</body>
<style>
html, body, #app-content, .super-dev-container {
height: 100%;
width: 100%;
position: relative;
background: white;
}
.mock-app-root {
background: #F7F7F7;
}
</style>
</html>

View File

@ -0,0 +1,188 @@
https://hackmd.io/JwIwDMDGKQZgtAFgKZjEgbARhPAhgKxZbwAcA7LAWOQCaKEgFA==?edit
Subscribablez(initState)
.subscribe()
.emitUpdate(newState)
//.getState()
var initState = fromDisk()
ReduxStore(reducer, initState)
.reduce(action) -> .emitUpdate()
ReduxStore.subscribe(toDisk)
### KeyChainManager / idStore 2.0 (maybe just in MetaMaskController)
keychains: []
getAllAccounts(cb)
getAllKeychainViewStates(cb) -> returns [ KeyChainViewState]
#### Old idStore external methods, for feature parity:
- init(configManager)
- setStore(ethStore)
- getState()
- getSelectedAddres()
- setSelectedAddress()
- createNewVault()
- recoverFromSeed()
- submitPassword()
- approveTransaction()
- cancelTransaction()
- addUnconfirmedMessage(msgParams, cb)
- signMessage()
- cancelMessage()
- setLocked()
- clearSeedWordCache()
- exportAccount()
- revealAccount()
- saveAccountLabel()
- tryPassword()
- recoverSeed()
- getNetwork()
##### Of those methods
Where they should end up:
##### MetaMaskController
- getNetwork()
##### KeyChainManager
- init(configManager)
- setStore(ethStore)
- getState() // Deprecate for unidirectional flow
- on('update', cb)
- createNewVault(password)
- getSelectedAddres()
- setSelectedAddress()
- submitPassword()
- tryPassword()
- approveTransaction()
- cancelTransaction()
- signMessage()
- cancelMessage()
- setLocked()
- exportAccount()
##### Bip44 KeyChain
- getState() // Deprecate for unidirectional flow
- on('update', cb)
If we adopt a ReactStore style unidirectional action dispatching data flow, these methods will be unified under a `dispatch` method, and rather than having a cb will emit an update to the UI:
- createNewKeyChain(entropy)
- recoverFromSeed()
- approveTransaction()
- signMessage()
- clearSeedWordCache()
- exportAccount()
- revealAccount()
- saveAccountLabel()
- recoverSeed()
Additional methods, new to this:
- serialize()
- Returns pojo with optional `secret` key whose contents will be encrypted with the users' password and salt when written to disk.
- The isolation of secrets is to preserve performance when decrypting user data.
- deserialize(pojo)
### KeyChain (ReduxStore?)
// attributes
@name
signTx(txParams, cb)
signMsg(msg, cb)
getAddressList(cb)
getViewState(cb) -> returns KeyChainViewState
serialize(cb) -> obj
deserialize(obj)
dispatch({ type: <str>, value: <pojo> })
### KeyChainViewState
// The serialized, renderable keychain data
accountList: [],
typeName: 'uPort',
iconAddress: 'uport.gif',
internal: {} // Subclass-defined metadata
### KeyChainReactComponent
// takes a KeyChainViewState
// Subclasses of this:
- KeyChainListItemComponent
- KeyChainInitComponent - Maybe part of the List Item
- KeyChainAccountHeaderComponent
- KeyChainConfirmationComponent
// Account list item, tx confirmation extra data (like a QR code),
// Maybe an options screen, init screen,
how to send actions?
emitAction(keychains.<id>.didInit)
gimmeRemoteKeychain((err, remoteKeychain)=>
)
KeyChainReactComponent({
keychain
})
Keychain:
methods:{},
cachedAccountList: [],
name: '',
CoinbaseKeychain
getAccountList
CoinbaseKeychainComponent
isLoading = true
keychain.getAccountList(()=>{
isLoading=false
accountList=accounts
})
KeyChainViewState {
attributes: {
//mandatory:
accountList: [],
typeName: 'uPort',
iconAddress: 'uport.gif',
internal: {
// keychain-specific metadata
proxyAddresses: {
0xReal: '0xProxy'
}
},
},
methods: {
// arbitrary, internal
}
}
## A note on the security of arbitrary action dispatchers
Since keychains will be dispatching actions that are then passed through the background process to be routed, we should not trust or require them to include their own keychain ID as a prefix to their action, but we should tack it on ourselves, so that no action dispatched by a KeyChainComponent ever reaches any KeyChain other than its own.

View File

@ -1,5 +1,6 @@
var watchify = require('watchify')
var browserify = require('browserify')
var disc = require('disc')
var gulp = require('gulp')
var source = require('vinyl-source-stream')
var buffer = require('vinyl-buffer')
@ -10,7 +11,6 @@ var jsoneditor = require('gulp-json-editor')
var zip = require('gulp-zip')
var assign = require('lodash.assign')
var livereload = require('gulp-livereload')
var brfs = require('gulp-brfs')
var del = require('del')
var eslint = require('gulp-eslint')
var fs = require('fs')
@ -21,6 +21,7 @@ var replace = require('gulp-replace')
var disclaimer = fs.readFileSync(path.join(__dirname, 'USER_AGREEMENT.md')).toString()
var crypto = require('crypto')
var hash = crypto.createHash('sha256')
var mkdirp = require('mkdirp')
hash.update(disclaimer)
var tosHash = hash.digest('hex')
@ -33,7 +34,6 @@ var debug = gutil.env.debug
gulp.task('dev:reload', function() {
livereload.listen({
port: 35729,
// basePath: './dist/firefox/'
})
})
@ -46,6 +46,7 @@ gulp.task('copy:locales', copyTask({
'./dist/firefox/_locales',
'./dist/chrome/_locales',
'./dist/edge/_locales',
'./dist/opera/_locales',
]
}))
gulp.task('copy:images', copyTask({
@ -54,6 +55,7 @@ gulp.task('copy:images', copyTask({
'./dist/firefox/images',
'./dist/chrome/images',
'./dist/edge/images',
'./dist/opera/images',
],
}))
gulp.task('copy:fonts', copyTask({
@ -62,6 +64,7 @@ gulp.task('copy:fonts', copyTask({
'./dist/firefox/fonts',
'./dist/chrome/fonts',
'./dist/edge/fonts',
'./dist/opera/fonts',
],
}))
gulp.task('copy:reload', copyTask({
@ -70,6 +73,7 @@ gulp.task('copy:reload', copyTask({
'./dist/firefox/scripts',
'./dist/chrome/scripts',
'./dist/edge/scripts',
'./dist/opera/scripts',
],
pattern: '/chromereload.js',
}))
@ -79,6 +83,7 @@ gulp.task('copy:root', copyTask({
'./dist/firefox',
'./dist/chrome',
'./dist/edge',
'./dist/opera',
],
pattern: '/*',
}))
@ -92,6 +97,21 @@ gulp.task('manifest:chrome', function() {
.pipe(gulp.dest('./dist/chrome', { overwrite: true }))
})
gulp.task('manifest:opera', function() {
return gulp.src('./dist/opera/manifest.json')
.pipe(jsoneditor(function(json) {
json.permissions = [
"storage",
"tabs",
"clipboardWrite",
"clipboardRead",
"http://localhost:8545/"
]
return json
}))
.pipe(gulp.dest('./dist/opera', { overwrite: true }))
})
gulp.task('manifest:production', function() {
return gulp.src([
'./dist/firefox/manifest.json',
@ -118,7 +138,7 @@ if (!disableLiveReload) {
copyStrings.push('copy:reload')
}
gulp.task('copy', gulp.series(gulp.parallel(...copyStrings), 'manifest:production', 'manifest:chrome'))
gulp.task('copy', gulp.series(gulp.parallel(...copyStrings), 'manifest:production', 'manifest:chrome', 'manifest:opera'))
gulp.task('copy:watch', function(){
gulp.watch(['./app/{_locales,images}/*', './app/scripts/chromereload.js', './app/*.{html,json}'], gulp.series('copy'))
})
@ -152,18 +172,27 @@ const jsFiles = [
'popup',
]
// bundle tasks
var jsDevStrings = jsFiles.map(jsFile => `dev:js:${jsFile}`)
var jsBuildStrings = jsFiles.map(jsFile => `build:js:${jsFile}`)
jsFiles.forEach((jsFile) => {
gulp.task(`dev:js:${jsFile}`, bundleTask({ watch: true, filename: `${jsFile}.js` }))
gulp.task(`build:js:${jsFile}`, bundleTask({ watch: false, filename: `${jsFile}.js` }))
gulp.task(`dev:js:${jsFile}`, bundleTask({ watch: true, label: jsFile, filename: `${jsFile}.js` }))
gulp.task(`build:js:${jsFile}`, bundleTask({ watch: false, label: jsFile, filename: `${jsFile}.js` }))
})
gulp.task('dev:js', gulp.parallel(...jsDevStrings))
gulp.task('build:js', gulp.parallel(...jsBuildStrings))
// disc bundle analyzer tasks
jsFiles.forEach((jsFile) => {
gulp.task(`disc:${jsFile}`, bundleTask({ label: jsFile, filename: `${jsFile}.js` }))
})
gulp.task('disc', gulp.parallel(jsFiles.map(jsFile => `disc:${jsFile}`)))
// clean dist
@ -173,22 +202,11 @@ gulp.task('clean', function clean() {
})
// zip tasks for distribution
gulp.task('zip:chrome', () => {
return gulp.src('dist/chrome/**')
.pipe(zip(`metamask-chrome-${manifest.version}.zip`))
.pipe(gulp.dest('builds'));
})
gulp.task('zip:firefox', () => {
return gulp.src('dist/firefox/**')
.pipe(zip(`metamask-firefox-${manifest.version}.zip`))
.pipe(gulp.dest('builds'));
})
gulp.task('zip:edge', () => {
return gulp.src('dist/edge/**')
.pipe(zip(`metamask-edge-${manifest.version}.zip`))
.pipe(gulp.dest('builds'));
})
gulp.task('zip', gulp.parallel('zip:chrome', 'zip:firefox', 'zip:edge'))
gulp.task('zip:chrome', zipTask('chrome'))
gulp.task('zip:firefox', zipTask('firefox'))
gulp.task('zip:edge', zipTask('edge'))
gulp.task('zip:opera', zipTask('opera'))
gulp.task('zip', gulp.parallel('zip:chrome', 'zip:firefox', 'zip:edge', 'zip:opera'))
// high level tasks
@ -218,21 +236,65 @@ function copyTask(opts){
}
}
function bundleTask(opts) {
function zipTask(target) {
return () => {
return gulp.src(`dist/${target}/**`)
.pipe(zip(`metamask-${target}-${manifest.version}.zip`))
.pipe(gulp.dest('builds'));
}
}
function generateBundler(opts) {
var browserifyOpts = assign({}, watchify.args, {
entries: ['./app/scripts/'+opts.filename],
debug: true,
plugin: 'browserify-derequire',
debug: debug,
fullPaths: debug,
})
var bundler = browserify(browserifyOpts)
bundler.transform('brfs')
return browserify(browserifyOpts)
}
function discTask(opts) {
let bundler = generateBundler(opts)
if (opts.watch) {
bundler = watchify(bundler)
bundler.on('update', performBundle) // on any dep update, runs the bundler
// on any dep update, runs the bundler
bundler.on('update', performBundle)
}
bundler.on('log', gutil.log) // output build logs to terminal
// output build logs to terminal
bundler.on('log', gutil.log)
return performBundle
function performBundle(){
// start "disc" build
let discDir = path.join(__dirname, 'disc')
mkdirp.sync(discDir)
let discPath = path.join(discDir, `${opts.label}.html`)
return (
bundler.bundle()
.pipe(disc())
.pipe(fs.createWriteStream(discPath))
)
}
}
function bundleTask(opts) {
let bundler = generateBundler(opts)
if (opts.watch) {
bundler = watchify(bundler)
// on any file update, re-runs the bundler
bundler.on('update', performBundle)
}
// output build logs to terminal
bundler.on('log', gutil.log)
return performBundle
@ -242,20 +304,25 @@ function bundleTask(opts) {
bundler.bundle()
// log errors if they happen
.on('error', gutil.log.bind(gutil, 'Browserify Error'))
// convert bundle stream to gulp vinyl stream
.pipe(source(opts.filename))
.pipe(brfs())
// inject variables into bundle
.pipe(replace('GULP_TOS_HASH', tosHash))
.pipe(replace('\'GULP_METAMASK_DEBUG\'', debug))
// optional, remove if you don't need to buffer file contents
// buffer file contents (?)
.pipe(buffer())
// optional, remove if you dont want sourcemaps
.pipe(sourcemaps.init({loadMaps: true})) // loads map from browserify file
// Add transformation tasks to the pipeline here.
.pipe(sourcemaps.write('./')) // writes .map file
// sourcemaps
// loads map from browserify file
.pipe(sourcemaps.init({loadMaps: true}))
// writes .map file
.pipe(sourcemaps.write('./'))
// write completed bundles
.pipe(gulp.dest('./dist/firefox/scripts'))
.pipe(gulp.dest('./dist/chrome/scripts'))
.pipe(gulp.dest('./dist/edge/scripts'))
.pipe(gulpif(!disableLiveReload,livereload()))
.pipe(gulp.dest('./dist/opera/scripts'))
// finally, trigger live reload
.pipe(gulpif(!disableLiveReload, livereload()))
)
}

View File

@ -21,7 +21,7 @@ function initializeZeroClient() {
// User confirmation callbacks:
showUnconfirmedMessage,
unlockAccountMessage,
showUnconfirmedTx,
showUnapprovedTx,
// Persistence Methods:
setData,
loadData,
@ -36,8 +36,8 @@ function initializeZeroClient() {
console.log('notif stub - showUnconfirmedMessage')
}
function showUnconfirmedTx (txParams, txData, onTxDoneCb) {
console.log('notif stub - showUnconfirmedTx')
function showUnapprovedTx (txParams, txData, onTxDoneCb) {
console.log('notif stub - showUnapprovedTx')
}
//
@ -156,4 +156,4 @@ function initializeZeroClient() {
}
}
}
}

View File

@ -26,7 +26,7 @@ const extension = require('./development/mockExtension')
// Query String
const qs = require('qs')
let queryString = qs.parse(window.location.href.split('#')[1])
let selectedView = queryString.view || 'terms'
let selectedView = queryString.view || 'first time'
const firstState = states[selectedView]
updateQueryParams(selectedView)
@ -46,7 +46,7 @@ const controller = new MetamaskController({
// User confirmation callbacks:
showUnconfirmedMessage: noop,
unlockAccountMessage: noop,
showUnconfirmedTx: noop,
showUnapprovedTx: noop,
// Persistence Methods:
setData,
loadData,
@ -107,7 +107,7 @@ function getOldStyleData () {
return result
}
actions._setAccountManager(controller.getApi())
actions._setBackgroundConnection(controller.getApi())
actions.update = function(stateName) {
selectedView = stateName
updateQueryParams(stateName)

View File

@ -4,19 +4,21 @@
"public": false,
"private": true,
"scripts": {
"start": "gulp dev",
"start": "npm run dev",
"lint": "gulp lint",
"dev": "gulp dev",
"buildCiUnits": "node test/integration/index.js",
"dev": "gulp dev --debug",
"disc": "gulp disc --debug",
"dist": "gulp dist --disableLiveReload",
"test": "npm run fastTest && npm run ci && npm run lint",
"fastTest": "mocha --require test/helper.js --compilers js:babel-register --recursive \"test/unit/**/*.js\"",
"fastTest": "METAMASK_ENV=test mocha --require test/helper.js --compilers js:babel-register --recursive \"test/unit/**/*.js\"",
"watch": "mocha watch --compilers js:babel-register --recursive \"test/unit/**/*.js\"",
"genStates": "node development/genStates.js",
"ui": "npm run genStates && beefy ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
"mock": "beefy mock-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
"buildMock": "npm run genStates && browserify ./mock-dev.js -o ./development/bundle.js",
"testem": "npm run buildMock && testem",
"ci": "npm run buildMock && testem ci -P 2",
"ci": "npm run buildMock && npm run buildCiUnits && testem ci -P 2",
"announce": "node development/announcer.js",
"generateNotice": "node development/notice-generator.js"
},
@ -35,19 +37,23 @@
},
"dependencies": {
"async": "^1.5.2",
"bip39": "^2.2.0",
"browser-passworder": "^2.0.3",
"browserify-derequire": "^0.9.4",
"clone": "^1.0.2",
"copy-to-clipboard": "^2.0.0",
"debounce": "^1.0.0",
"denodeify": "^1.2.1",
"disc": "^1.3.2",
"dnode": "^1.2.2",
"end-of-stream": "^1.1.0",
"ensnare": "^1.0.0",
"eth-bin-to-ops": "^1.0.1",
"eth-lightwallet": "^2.3.3",
"eth-query": "^1.0.3",
"eth-store": "^1.1.0",
"ethereumjs-tx": "^1.0.0",
"ethereumjs-util": "^4.4.0",
"ethereumjs-wallet": "^0.6.0",
"express": "^4.14.0",
"extension-link-enabler": "^1.0.0",
"extensionizer": "^1.0.0",
@ -57,16 +63,18 @@
"iframe": "^1.0.0",
"iframe-stream": "^1.0.2",
"inject-css": "^0.1.1",
"jazzicon": "1.1.5",
"jazzicon": "^1.2.0",
"menu-droppo": "^1.1.0",
"metamask-logo": "^2.1.2",
"mississippi": "^1.2.0",
"mkdirp": "^0.5.1",
"multiplex": "^6.7.0",
"once": "^1.3.3",
"ping-pong-stream": "^1.0.0",
"pojo-migrator": "^2.1.0",
"polyfill-crypto.getrandomvalues": "^1.0.0",
"post-message-stream": "^1.0.0",
"promise-filter": "^1.1.0",
"pumpify": "^1.3.4",
"qrcode-npm": "0.0.3",
"react": "^15.0.2",
@ -80,14 +88,15 @@
"redux": "^3.0.5",
"redux-logger": "^2.3.1",
"redux-thunk": "^1.0.2",
"request-promise": "^4.1.1",
"sandwich-expando": "^1.0.5",
"semaphore": "^1.0.5",
"textarea-caret": "^3.0.1",
"three.js": "^0.73.2",
"through2": "^2.0.1",
"valid-url": "^1.0.9",
"vreme": "^3.0.2",
"web3": "0.17.0-beta",
"web3-provider-engine": "^8.1.14",
"web3-provider-engine": "^8.4.0",
"web3-stream-provider": "^2.0.6",
"xtend": "^4.0.1"
},
@ -103,7 +112,6 @@
"del": "^2.2.0",
"fs-promise": "^1.0.0",
"gulp": "github:gulpjs/gulp#4.0",
"gulp-brfs": "^0.1.0",
"gulp-if": "^2.0.1",
"gulp-json-editor": "^2.2.1",
"gulp-livereload": "^3.8.1",
@ -112,6 +120,7 @@
"gulp-util": "^3.0.7",
"gulp-watch": "^4.3.5",
"gulp-zip": "^3.2.0",
"isomorphic-fetch": "^2.2.1",
"jsdom": "^8.1.0",
"jsdom-global": "^1.7.0",
"jshint-stylish": "~0.1.5",

View File

@ -1,7 +1,7 @@
function wait() {
function wait(time) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve()
}, 500)
}, time * 3 || 1500)
})
}

View File

@ -12,10 +12,10 @@
<script src="https://code.jquery.com/qunit/qunit-2.0.0.js"></script>
<script src="./jquery-3.1.0.min.js"></script>
<script src="helpers.js"></script>
<script src="tests.js"></script>
<script src="bundle.js"></script>
<script src="/testem.js"></script>
<iframe src="/development/index.html" height="500px" width="360px">
<iframe src="/development/test.html" height="500px" width="360px">
<p>Your browser does not support iframes</p>
</iframe>
</body>

21
test/integration/index.js Normal file
View File

@ -0,0 +1,21 @@
var fs = require('fs')
var path = require('path')
var browserify = require('browserify');
var tests = fs.readdirSync(path.join(__dirname, 'lib'))
var bundlePath = path.join(__dirname, 'bundle.js')
var b = browserify();
// Remove old bundle
try {
fs.unlinkSync(bundlePath)
} catch (e) {}
var writeStream = fs.createWriteStream(bundlePath)
tests.forEach(function(fileName) {
b.add(path.join(__dirname, 'lib', fileName))
})
b.bundle().pipe(writeStream);

View File

@ -0,0 +1,90 @@
const PASSWORD = 'password123'
QUnit.module('first time usage')
QUnit.test('agree to terms', function (assert) {
var done = assert.async()
let app
wait().then(function() {
app = $('iframe').contents().find('#app-content .mock-app-root')
app.find('.markdown').prop('scrollTop', 100000000)
return wait()
}).then(function() {
var title = app.find('h1').text()
assert.equal(title, 'MetaMask', 'title screen')
var pwBox = app.find('#password-box')[0]
var confBox = app.find('#password-box-confirm')[0]
pwBox.value = PASSWORD
confBox.value = PASSWORD
return wait()
}).then(function() {
var createButton = app.find('button.primary')[0]
createButton.click()
return wait(1500)
}).then(function() {
var terms = app.find('h3.terms-header')[0]
assert.equal(terms.textContent, 'MetaMask Terms & Conditions', 'Showing TOS')
// Scroll through terms
var scrollable = app.find('.markdown')[0]
scrollable.scrollTop = scrollable.scrollHeight
return wait(10)
}).then(function() {
var button = app.find('button')[0] // Agree button
button.click()
return wait(1000)
}).then(function() {
var created = app.find('h3')[0]
assert.equal(created.textContent, 'Vault Created', 'Vault created screen')
var button = app.find('button')[0] // Agree button
button.click()
return wait(1000)
}).then(function() {
var detail = app.find('.account-detail-section')[0]
assert.ok(detail, 'Account detail section loaded.')
var sandwich = app.find('.sandwich-expando')[0]
sandwich.click()
return wait()
}).then(function() {
var sandwich = app.find('.menu-droppo')[0]
var lock = sandwich.children[2]
assert.ok(lock, 'Lock menu item found')
lock.click()
return wait(1000)
}).then(function() {
var pwBox = app.find('#password-box')[0]
pwBox.value = PASSWORD
var createButton = app.find('button.primary')[0]
createButton.click()
return wait(1000)
}).then(function() {
var detail = app.find('.account-detail-section')[0]
assert.ok(detail, 'Account detail section loaded again.')
done()
})
})

View File

@ -0,0 +1,91 @@
var ConfigManager = require('../../../app/scripts/lib/config-manager')
var IdStoreMigrator = require('../../../app/scripts/lib/idStore-migrator')
var SimpleKeyring = require('../../../app/scripts/keyrings/simple')
var normalize = require('../../../app/scripts/lib/sig-util').normalize
var oldStyleVault = require('../mocks/oldVault.json')
var badStyleVault = require('../mocks/badVault.json')
var STORAGE_KEY = 'metamask-config'
var PASSWORD = '12345678'
var FIRST_ADDRESS = '0x4dd5d356c5A016A220bCD69e82e5AF680a430d00'.toLowerCase()
var SEED = 'fringe damage bounce extend tunnel afraid alert sound all soldier all dinner'
var BAD_STYLE_FIRST_ADDRESS = '0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9'
QUnit.module('Old Style Vaults', {
beforeEach: function () {
window.localStorage[STORAGE_KEY] = JSON.stringify(oldStyleVault)
this.configManager = new ConfigManager({
loadData: () => { return JSON.parse(window.localStorage[STORAGE_KEY]) },
setData: (data) => { window.localStorage[STORAGE_KEY] = JSON.stringify(data) },
})
this.migrator = new IdStoreMigrator({
configManager: this.configManager,
})
}
})
QUnit.test('migrator:isInitialized', function (assert) {
assert.ok(this.migrator)
})
QUnit.test('migrator:migratedVaultForPassword', function (assert) {
var done = assert.async()
this.migrator.migratedVaultForPassword(PASSWORD)
.then((result) => {
const { serialized, lostAccounts } = result
assert.equal(serialized.data.mnemonic, SEED, 'seed phrase recovered')
assert.equal(lostAccounts.length, 0, 'no lost accounts')
done()
})
})
QUnit.module('Old Style Vaults with bad HD seed', {
beforeEach: function () {
window.localStorage[STORAGE_KEY] = JSON.stringify(badStyleVault)
this.configManager = new ConfigManager({
loadData: () => { return JSON.parse(window.localStorage[STORAGE_KEY]) },
setData: (data) => { window.localStorage[STORAGE_KEY] = JSON.stringify(data) },
})
this.migrator = new IdStoreMigrator({
configManager: this.configManager,
})
}
})
QUnit.test('migrator:migratedVaultForPassword', function (assert) {
var done = assert.async()
this.migrator.migratedVaultForPassword(PASSWORD)
.then((result) => {
const { serialized, lostAccounts } = result
assert.equal(lostAccounts.length, 1, 'one lost account')
assert.equal(lostAccounts[0].address, '0xe15D894BeCB0354c501AE69429B05143679F39e0'.toLowerCase())
assert.ok(lostAccounts[0].privateKey, 'private key exported')
var lostAccount = lostAccounts[0]
var privateKey = lostAccount.privateKey
var simple = new SimpleKeyring()
simple.deserialize([privateKey])
.then(() => {
return simple.getAccounts()
})
.then((accounts) => {
assert.equal(normalize(accounts[0]), lostAccount.address, 'recovered address.')
done()
})
.catch((reason) => {
assert.ifError(reason)
done(reason)
})
})
})

View File

@ -0,0 +1 @@
{"meta":{"version":4},"data":{"fiatCurrency":"USD","conversionRate":8.34908448,"conversionDate":1481227505,"isConfirmed":true,"wallet":"{\"encSeed\":{\"encStr\":\"Te2KyAGY3S01bgUJ+7d4y3BOvr/8TKrXrkRZ29cGI6dgyedtN+YgTQxElC2td/pzuoXm7KeSfr+yAoFCvMgqFAJwRcX3arHOsMFQie8kp8mL5I65zwdg/HB2QecB4OJHytrxgApv2zZiKEo0kbu2cs8zYIn5wNlCBIHwgylYmHpUDIJcO1B4zg==\",\"nonce\":\"xnxqk4iy70bjt721F+KPLV4PNfBFNyct\"},\"ksData\":{\"m/44'/60'/0'/0\":{\"info\":{\"curve\":\"secp256k1\",\"purpose\":\"sign\"},\"encHdPathPriv\":{\"encStr\":\"vNrSjekRKLmaGFf77Uca9+aAebmDlvrBwtAV8YthpQ4OX/mXtLSycmnLsYdk4schaByfJvrm6/Mf9fxzOSaScJk+XvKw5XqNXedkDHtbWrmNnxFpuT+9tuB8Nupr3D9GZK9PgXhJD99/7Bn6Wk7/ne+PIDmbtdmx/SWmrdo3pg==\",\"nonce\":\"zqWq/gtJ5zfUVRWQQJkP/zoYjer6Rozj\"},\"hdIndex\":1,\"encPrivKeys\":{\"e15d894becb0354c501ae69429b05143679f39e0\":{\"key\":\"jBLQ9v1l5LOEY1C3kI8z7LpbJKHP1vpVfPAlz90MNSfa8Oe+XlxKQAGYs8Zb4fWm\",\"nonce\":\"fJyrSRo1t0RMNqp2MsneoJnYJWHQnSVY\"}},\"addresses\":[\"e15d894becb0354c501ae69429b05143679f39e0\"]}},\"encHdRootPriv\":{\"encStr\":\"mbvwiFBQGbjj4BJLmdeYzfYi8jb7gtFtwiCQOPfvmyz4h2/KMbHNGzumM16qRKpifioQXkhnBulMIQHaYg0Jwv1MoFsqHxHmuIAT+QP5XvJjz0MRl6708pHowmIVG+R8CZNTLqzE7XS8YkZ4ElRpTvLEM8Wngi5Sg287mQMP9w==\",\"nonce\":\"i5Tp2lQe92rXQzNhjZcu9fNNhfux6Wf4\"},\"salt\":\"FQpA8D9R/5qSp9WtQ94FILyfWZHMI6YZw6RmBYqK0N0=\",\"version\":2}","config":{"provider":{"type":"testnet"},"selectedAccount":"0xe15d894becb0354c501ae69429b05143679f39e0"},"isEthConfirmed":true,"transactions":[],"TOSHash":"a4f4e23f823a7ac51783e7ffba7914a911b09acdb97263296b7e14b527f80c5b","gasMultiplier":1}}

View File

@ -0,0 +1 @@
{"meta":{"version":4},"data":{"fiatCurrency":"USD","isConfirmed":true,"TOSHash":"a4f4e23f823a7ac51783e7ffba7914a911b09acdb97263296b7e14b527f80c5b","noticesList":[{"read":true,"date":"Fri Dec 16 2016","title":"Ending Morden Support","body":"Due to [recent events](https://blog.ethereum.org/2016/11/20/from-morden-to-ropsten/), MetaMask is now deprecating support for the Morden Test Network.\n\nUsers will still be able to access Morden through a locally hosted node, but we will no longer be providing hosted access to this network through [Infura](http://infura.io/).\n\nPlease use the new Ropsten Network as your new default test network.\n\nYou can fund your Ropsten account using the buy button on your account page.\n\nBest wishes!\nThe MetaMask Team\n\n","id":0}],"conversionRate":7.07341909,"conversionDate":1482539284,"wallet":"{\"encSeed\":{\"encStr\":\"LZsdN8lJzYkUe1UpmAalnERdgkBFt25gWDdK8kfQUwMAk/27XR+dc+8n5swgoF5qgwhc9LBgliEGNDs1Q/lnuld3aQLabkOeAW4BHS1vS7FxqKrzDS3iyzSuQO6wDQmGno/buuknVgDsKiyjW22hpt7vtVVWA+ZL1P3x6M0+AxGJjeGVrG+E8Q==\",\"nonce\":\"T6O9BmwmTj214XUK3KF0s3iCKo3OlrUD\"},\"ksData\":{\"m/44'/60'/0'/0\":{\"info\":{\"curve\":\"secp256k1\",\"purpose\":\"sign\"},\"encHdPathPriv\":{\"encStr\":\"GNNfZevCMlgMVh9y21y1UwrC9qcmH6XYq7v+9UoqbHnzPQJFlxidN5+x/Sldo72a6+5zJpQkkdZ+Q0lePrzvXfuSd3D/RO7WKFIKo9nAQI5+JWwz4INuCmVcmqCv2J4BTLGjrG8fp5pDJ62Bn0XHqkJo3gx3fpvs3cS66+ZKwg==\",\"nonce\":\"HRTlGj44khQs2veYHEF/GqTI1t0yYvyd\"},\"hdIndex\":3,\"encPrivKeys\":{\"e15d894becb0354c501ae69429b05143679f39e0\":{\"key\":\"ZAeZL9VcRUtiiO4VXOQKBFg787PR5R3iymjUeU5vpDRIqOXbjWN6N4ZNR8YpSXl+\",\"nonce\":\"xLsADagS8uqDYae6cImyhxF7o1kBDbPe\"},\"87658c15aefe7448008a28513a11b6b130ef4cd0\":{\"key\":\"ku0mm5s1agRJNAMYIJO0qeoDe+FqcbqdQI6azXF3GL1OLo6uMlt6I4qS+eeravFi\",\"nonce\":\"xdGfSUPKtkW8ge0SWIbbpahs/NyEMzn5\"},\"aa25854c0379e53c957ac9382e720c577fa31fd5\":{\"key\":\"NjpYC9FbiC95CTx/1kwgOHk5LSN9vl4RULEBbvwfVOjqSH8WixNoP3R6I/QyNIs2\",\"nonce\":\"M/HWpXXA9QvuZxEykkGQPJKKdz33ovQr\"}},\"addresses\":[\"e15d894becb0354c501ae69429b05143679f39e0\",\"87658c15aefe7448008a28513a11b6b130ef4cd0\",\"aa25854c0379e53c957ac9382e720c577fa31fd5\"]}},\"encHdRootPriv\":{\"encStr\":\"f+3prUOzl+95aNAV+ad6lZdsYZz120ZsL67ucjj3tiMXf/CC4X8XB9N2QguhoMy6fW+fATUsTdJe8+CbAAyb79V9HY0Pitzq9Yw/g1g0/Ii2JzsdGBriuMsPdwZSVqz+rvQFw/6Qms1xjW6cqa8S7kM2WA5l8RB1Ck6r5zaqbA==\",\"nonce\":\"oGahxNFekVxH9sg6PUCCHIByvo4WFSqm\"},\"salt\":\"N7xYoEA53yhSweOsEphku1UKkIEuZtX2MwLBhVM6RR8=\",\"version\":2}","config":{"provider":{"type":"testnet"},"selectedAccount":"0xe15d894becb0354c501ae69429b05143679f39e0"},"isDisclaimerConfirmed":true,"walletNicknames":{"0xac39b311dceb2a4b2f5d8461c1cdaf756f4f7ae9":"Account 1","0xd7c0cd9e7d2701c710d64fc492c7086679bdf7b4":"Account 2","0x1acfb961c5a8268eac8e09d6241a26cbeff42241":"Account 3"},"lostAccounts":["0xe15d894becb0354c501ae69429b05143679f39e0","0x87658c15aefe7448008a28513a11b6b130ef4cd0","0xaa25854c0379e53c957ac9382e720c577fa31fd5"]}}

View File

@ -0,0 +1,21 @@
{
"meta": {
"version": 4
},
"data": {
"fiatCurrency": "USD",
"isConfirmed": true,
"TOSHash": "a4f4e23f823a7ac51783e7ffba7914a911b09acdb97263296b7e14b527f80c5b",
"conversionRate": 9.47316629,
"conversionDate": 1479510994,
"wallet": "{\"encSeed\":{\"encStr\":\"a5tjKtDGlHkua+6Ta5s3wMFWPmsBqaPdMKGmqeI2z1kMbNs3V03HBaCptU7NtMra1DjHKbSNsUToxFUrmrvWBmUejamN16+l1CviwqASsv7kKzpot00/dfyyJgtZwwFP5Je+TAB1V231nRbPidOfeE1cDec5V8KTF8epl6qzsbA25pjeW76Dfw==\",\"nonce\":\"RzID6bAhWfGTSR74xdIh3RaT1+1sLk6F\"},\"ksData\":{\"m/44'/60'/0'/0\":{\"info\":{\"curve\":\"secp256k1\",\"purpose\":\"sign\"},\"encHdPathPriv\":{\"encStr\":\"6nlYAopRbmGcqerRZO08XwgeYaCJg9XRhh4oiYiVVdQtyNPdxvOI9TcE/mqvBiatMwBwA+TmsqTV6eZZe/VDZKYIGajKulQbScd0xQ71JhYfqqmzSG6EH2Pnzwa+aSAsfARgN1JJSaff2+p6wV6Zg5BUDtl72OGEIEfXhcUGwg==\",\"nonce\":\"Ee1KiDqtx7NvYToQUFvjEhKNinNQcXlK\"},\"hdIndex\":1,\"encPrivKeys\":{\"4dd5d356c5a016a220bcd69e82e5af680a430d00\":{\"key\":\"htGRGAH10lGF4M+fvioznmYVIUSWAzwp/yWSIo85psgZZwmCdJY72oyGanYsrFO8\",\"nonce\":\"PkP8XeZ+ok215rzEorvJu9nYTWzkOVr0\"}},\"addresses\":[\"4dd5d356c5a016a220bcd69e82e5af680a430d00\"]}},\"encHdRootPriv\":{\"encStr\":\"TAZAo71a+4IlAaoA66f0w4ts2f+V7ArTSUHRIrMltfAPXz7GfJBmKXNtHPORUYAjRiKqWK6FZnhKLf7Vcng2LG7VnDQwC4xPxzSRZzSEilnoY3V+zRY0HD7Wb/pndb4FliA/buZQmjohO4vezeX0hl70rJlPJEZTyYoWgxbxFA==\",\"nonce\":\"FlJOaLyBEHMaH5fEnYjdHc6nn18+WkRj\"},\"salt\":\"CmuCcWpbqpKUUv+1aE2ZwvQl7EIQ731uFibSq++vwtY=\",\"version\":2}",
"config": {
"provider": {
"type": "testnet"
},
"selectedAccount": "0x4dd5d356c5a016a220bcd69e82e5af680a430d00"
},
"showSeedWords": false,
"isEthConfirmed": true
}
}

View File

@ -1,24 +0,0 @@
QUnit.test('agree to terms', function (assert) {
var done = assert.async()
// Select the mock app root
var app = $('iframe').contents().find('#app-content .mock-app-root')
app.find('.markdown').prop('scrollTop', 100000000)
wait().then(function() {
app.find('button').click()
}).then(function() {
return wait()
}).then(function() {
var title = app.find('h1').text()
assert.equal(title, 'MetaMask', 'title screen')
var buttons = app.find('button')
assert.equal(buttons.length, 2, 'two buttons: create and restore')
done()
})
// Wait for view to transition:
})

View File

@ -1,5 +1,5 @@
var ConfigManager = require('../../app/scripts/lib/config-manager')
const STORAGE_KEY = 'metamask-persistance-key'
const STORAGE_KEY = 'metamask-config'
const extend = require('xtend')
module.exports = function() {
@ -9,6 +9,7 @@ module.exports = function() {
function loadData () {
var oldData = getOldStyleData()
var newData
try {
newData = JSON.parse(window.localStorage[STORAGE_KEY])
} catch (e) {}

View File

@ -0,0 +1,32 @@
var mockHex = '0xabcdef0123456789'
var mockKey = new Buffer(32)
let cacheVal
module.exports = {
encrypt(password, dataObj) {
cacheVal = dataObj
return Promise.resolve(mockHex)
},
decrypt(password, text) {
return Promise.resolve(cacheVal || {})
},
encryptWithKey(key, dataObj) {
return this.encrypt(key, dataObj)
},
decryptWithKey(key, text) {
return this.decrypt(key, text)
},
keyFromPassword(password) {
return Promise.resolve(mockKey)
},
generateSalt() {
return 'WHADDASALT!'
},
}

View File

@ -0,0 +1,38 @@
var fakeWallet = {
privKey: '0x123456788890abcdef',
address: '0xfedcba0987654321',
}
const type = 'Simple Key Pair'
module.exports = class MockSimpleKeychain {
static type() { return type }
constructor(opts) {
this.type = type
this.opts = opts || {}
this.wallets = []
}
serialize() {
return [ fakeWallet.privKey ]
}
deserialize(data) {
if (!Array.isArray(data)) {
throw new Error('Simple keychain deserialize requires a privKey array.')
}
this.wallets = [ fakeWallet ]
}
addAccounts(n = 1) {
for(var i = 0; i < n; i++) {
this.wallets.push(fakeWallet)
}
}
getAccounts() {
return this.wallets.map(w => w.address)
}
}

View File

@ -1,60 +0,0 @@
var jsdom = require('mocha-jsdom')
var assert = require('assert')
var freeze = require('deep-freeze-strict')
var path = require('path')
var sinon = require('sinon')
var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js'))
var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js'))
describe('#recoverFromSeed(password, seed)', function() {
beforeEach(function() {
// sinon allows stubbing methods that are easily verified
this.sinon = sinon.sandbox.create()
})
afterEach(function() {
// sinon requires cleanup otherwise it will overwrite context
this.sinon.restore()
})
// stub out account manager
actions._setAccountManager({
recoverFromSeed(pw, seed, cb) {
cb(null, {
identities: {
foo: 'bar'
}
})
},
})
it('sets metamask.isUnlocked to true', function() {
var initialState = {
metamask: {
isUnlocked: false,
isInitialized: false,
}
}
freeze(initialState)
const restorePhrase = 'invite heavy among daring outdoor dice jelly coil stable note seat vicious'
const password = 'foo'
const dispatchFunc = actions.recoverFromSeed(password, restorePhrase)
var dispatchStub = this.sinon.stub()
dispatchStub.withArgs({ TYPE: actions.unlockMetamask() }).onCall(0)
dispatchStub.withArgs({ TYPE: actions.showAccountsPage() }).onCall(1)
var action
var resultingState = initialState
dispatchFunc((newAction) => {
action = newAction
resultingState = reducers(resultingState, action)
})
assert.equal(resultingState.metamask.isUnlocked, true, 'was unlocked')
assert.equal(resultingState.metamask.isInitialized, true, 'was initialized')
});
});

View File

@ -44,6 +44,5 @@ describe('SHOW_ACCOUNT_DETAIL', function() {
var resultingState = reducers(initialState, action)
assert.equal(resultingState.metamask.selectedAccount, action.value)
assert.equal(resultingState.metamask.selectedAddress, action.value)
})
})

View File

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

View File

@ -1,8 +1,10 @@
// polyfill fetch
global.fetch = global.fetch || require('isomorphic-fetch')
const assert = require('assert')
const extend = require('xtend')
const rp = require('request-promise')
const nock = require('nock')
var configManagerGen = require('../lib/mock-config-manager')
const configManagerGen = require('../lib/mock-config-manager')
const STORAGE_KEY = 'metamask-persistance-key'
describe('config-manager', function() {
@ -100,31 +102,31 @@ describe('config-manager', function() {
describe('confirmation', function() {
describe('#getConfirmed', function() {
describe('#getConfirmedDisclaimer', function() {
it('should return false if no previous key exists', function() {
var result = configManager.getConfirmed()
var result = configManager.getConfirmedDisclaimer()
assert.ok(!result)
})
})
describe('#setConfirmed', function() {
it('should make getConfirmed return true once set', function() {
assert.equal(configManager.getConfirmed(), false)
configManager.setConfirmed(true)
var result = configManager.getConfirmed()
describe('#setConfirmedDisclaimer', function() {
it('should make getConfirmedDisclaimer return true once set', function() {
assert.equal(configManager.getConfirmedDisclaimer(), false)
configManager.setConfirmedDisclaimer(true)
var result = configManager.getConfirmedDisclaimer()
assert.equal(result, true)
})
it('should be able to set false', function() {
configManager.setConfirmed(false)
var result = configManager.getConfirmed()
configManager.setConfirmedDisclaimer(false)
var result = configManager.getConfirmedDisclaimer()
assert.equal(result, false)
})
it('should persist to local storage', function() {
configManager.setConfirmed(true)
configManager.setConfirmedDisclaimer(true)
var data = configManager.getData()
assert.equal(data.isConfirmed, true)
assert.equal(data.isDisclaimerConfirmed, true)
})
})
})
@ -153,7 +155,7 @@ describe('config-manager', function() {
rpcTarget: 'foobar'
},
}
configManager.setConfirmed(true)
configManager.setConfirmedDisclaimer(true)
configManager.setConfig(testConfig)
var testWallet = {
@ -164,7 +166,7 @@ describe('config-manager', function() {
var result = configManager.getData()
assert.equal(result.wallet.name, testWallet.name, 'wallet name is set')
assert.equal(result.config.provider.rpcTarget, testConfig.provider.rpcTarget)
assert.equal(configManager.getConfirmed(), true)
assert.equal(configManager.getConfirmedDisclaimer(), true)
testConfig.provider.type = 'something else!'
configManager.setConfig(testConfig)
@ -173,7 +175,7 @@ describe('config-manager', function() {
assert.equal(result.wallet.name, testWallet.name, 'wallet name is set')
assert.equal(result.config.provider.rpcTarget, testConfig.provider.rpcTarget)
assert.equal(result.config.provider.type, testConfig.provider.type)
assert.equal(configManager.getConfirmed(), true)
assert.equal(configManager.getConfirmedDisclaimer(), true)
})
})
@ -215,7 +217,7 @@ describe('config-manager', function() {
describe('transactions', function() {
beforeEach(function() {
configManager._saveTxList([])
configManager.setTxList([])
})
describe('#getTxList', function() {
@ -226,90 +228,13 @@ describe('config-manager', function() {
})
})
describe('#_saveTxList', function() {
describe('#setTxList', function() {
it('saves the submitted data to the tx list', function() {
var target = [{ foo: 'bar' }]
configManager._saveTxList(target)
configManager.setTxList(target)
var result = configManager.getTxList()
assert.equal(result[0].foo, 'bar')
})
})
describe('#addTx', function() {
it('adds a tx returned in getTxList', function() {
var tx = { id: 1 }
configManager.addTx(tx)
var result = configManager.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 1)
assert.equal(result[0].id, 1)
})
it('cuts off early txs beyond a limit', function() {
const limit = configManager.txLimit
for (let i = 0; i < limit + 1; i++) {
let tx = { id: i }
configManager.addTx(tx)
}
var result = configManager.getTxList()
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
assert.equal(result[0].id, 1, 'early txs truncted')
})
})
describe('#confirmTx', function() {
it('sets the tx status to confirmed', function() {
var tx = { id: 1, status: 'unconfirmed' }
configManager.addTx(tx)
configManager.confirmTx(1)
var result = configManager.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 1)
assert.equal(result[0].status, 'confirmed')
})
})
describe('#rejectTx', function() {
it('sets the tx status to rejected', function() {
var tx = { id: 1, status: 'unconfirmed' }
configManager.addTx(tx)
configManager.rejectTx(1)
var result = configManager.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 1)
assert.equal(result[0].status, 'rejected')
})
})
describe('#updateTx', function() {
it('replaces the tx with the same id', function() {
configManager.addTx({ id: '1', status: 'unconfirmed' })
configManager.addTx({ id: '2', status: 'confirmed' })
configManager.updateTx({ id: '1', status: 'blah', hash: 'foo' })
var result = configManager.getTx('1')
assert.equal(result.hash, 'foo')
})
})
describe('#unconfirmedTxs', function() {
it('returns unconfirmed txs in a hash', function() {
configManager.addTx({ id: '1', status: 'unconfirmed' })
configManager.addTx({ id: '2', status: 'confirmed' })
let result = configManager.unconfirmedTxs()
assert.equal(typeof result, 'object')
assert.equal(result['1'].status, 'unconfirmed')
assert.equal(result['0'], undefined)
assert.equal(result['2'], undefined)
})
})
describe('#getTx', function() {
it('returns a tx with the requested id', function() {
configManager.addTx({ id: '1', status: 'unconfirmed' })
configManager.addTx({ id: '2', status: 'confirmed' })
assert.equal(configManager.getTx('1').status, 'unconfirmed')
assert.equal(configManager.getTx('2').status, 'confirmed')
})
})
})
})

View File

@ -0,0 +1,146 @@
const async = require('async')
const assert = require('assert')
const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const ConfigManager = require('../../app/scripts/lib/config-manager')
const delegateCallCode = require('../lib/example-code.json').delegateCallCode
// The old way:
const IdentityStore = require('../../app/scripts/lib/idStore')
const STORAGE_KEY = 'metamask-config'
const extend = require('xtend')
// The new ways:
var KeyringController = require('../../app/scripts/keyring-controller')
const mockEncryptor = require('../lib/mock-encryptor')
const MockSimpleKeychain = require('../lib/mock-simple-keychain')
const sinon = require('sinon')
const mockVault = {
seed: 'picnic injury awful upper eagle junk alert toss flower renew silly vague',
account: '0x5d8de92c205279c10e5669f797b853ccef4f739a',
}
const badVault = {
seed: 'radar blur cabbage chef fix engine embark joy scheme fiction master release',
}
describe('IdentityStore to KeyringController migration', function() {
// The stars of the show:
let idStore, keyringController, seedWords, configManager
let password = 'password123'
let entropy = 'entripppppyy duuude'
let accounts = []
let newAccounts = []
let originalKeystore
// This is a lot of setup, I know!
// We have to create an old style vault, populate it,
// and THEN create a new one, before we can run tests on it.
beforeEach(function(done) {
this.sinon = sinon.sandbox.create()
window.localStorage = {} // Hacking localStorage support into JSDom
configManager = new ConfigManager({
loadData,
setData: (d) => { window.localStorage = d }
})
idStore = new IdentityStore({
configManager: configManager,
ethStore: {
addAccount(acct) { accounts.push(ethUtil.addHexPrefix(acct)) },
del(acct) { delete accounts[acct] },
},
})
idStore._createVault(password, mockVault.seed, (err) => {
assert.ifError(err, 'createNewVault threw error')
originalKeystore = idStore._idmgmt.keyStore
idStore.setLocked((err) => {
assert.ifError(err, 'createNewVault threw error')
keyringController = new KeyringController({
configManager,
ethStore: {
addAccount(acct) { newAccounts.push(ethUtil.addHexPrefix(acct)) },
del(acct) { delete newAccounts[acct] },
},
txManager: {
getTxList: () => [],
getUnapprovedTxList: () => []
},
})
// Stub out the browser crypto for a mock encryptor.
// Browser crypto is tested in the integration test suite.
keyringController.encryptor = mockEncryptor
done()
})
})
})
describe('entering a password', function() {
it('should identify an old wallet as an initialized keyring', function(done) {
keyringController.configManager.setWallet('something')
keyringController.getState()
.then((state) => {
assert(state.isInitialized, 'old vault counted as initialized.')
assert(!state.lostAccounts, 'no lost accounts')
done()
})
})
})
})
function loadData () {
var oldData = getOldStyleData()
var newData
try {
newData = JSON.parse(window.localStorage[STORAGE_KEY])
} catch (e) {}
var data = extend({
meta: {
version: 0,
},
data: {
config: {
provider: {
type: 'testnet',
},
},
},
}, oldData || null, newData || null)
return data
}
function setData (data) {
window.localStorage[STORAGE_KEY] = JSON.stringify(data)
}
function getOldStyleData () {
var config, wallet, seedWords
var result = {
meta: { version: 0 },
data: {},
}
try {
config = JSON.parse(window.localStorage['config'])
result.data.config = config
} catch (e) {}
try {
wallet = JSON.parse(window.localStorage['lightwallet'])
result.data.wallet = wallet
} catch (e) {}
try {
seedWords = window.localStorage['seedWords']
result.data.seedWords = seedWords
} catch (e) {}
return result
}

View File

@ -11,7 +11,6 @@ describe('IdentityStore', function() {
describe('#createNewVault', function () {
let idStore
let password = 'password123'
let entropy = 'entripppppyy duuude'
let seedWords
let accounts = []
let originalKeystore
@ -26,7 +25,7 @@ describe('IdentityStore', function() {
},
})
idStore.createNewVault(password, entropy, (err, seeds) => {
idStore.createNewVault(password, (err, seeds) => {
assert.ifError(err, 'createNewVault threw error')
seedWords = seeds
originalKeystore = idStore._idmgmt.keyStore
@ -140,54 +139,4 @@ describe('IdentityStore', function() {
})
})
})
describe('#addGasBuffer', function() {
it('formats the result correctly', function() {
const idStore = new IdentityStore({
configManager: configManagerGen(),
ethStore: {
addAccount(acct) { accounts.push(ethUtil.addHexPrefix(acct)) },
},
})
const gas = '0x01'
const bnGas = new BN(gas, 16)
const bnResult = idStore.addGasBuffer(bnGas)
assert.ok(bnResult.gt(gas), 'added more gas as buffer.')
})
it('buffers 20%', function() {
const idStore = new IdentityStore({
configManager: configManagerGen(),
ethStore: {
addAccount(acct) { accounts.push(ethUtil.addHexPrefix(acct)) },
},
})
const gas = '0x04ee59' // Actual estimated gas example
const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16)
const five = new BN('5', 10)
const correctBuffer = bnGas.div(five)
const correct = bnGas.add(correctBuffer)
const bnResult = idStore.addGasBuffer(bnGas)
assert(bnResult.gt(bnGas), 'Estimate increased in value.')
assert.equal(bnResult.sub(bnGas).toString(10), correctBuffer.toString(10), 'added 20% gas')
})
})
describe('#checkForDelegateCall', function() {
const idStore = new IdentityStore({
configManager: configManagerGen(),
ethStore: {
addAccount(acct) { accounts.push(ethUtil.addHexPrefix(acct)) },
},
})
var result = idStore.checkForDelegateCall(delegateCallCode)
assert.equal(result, true, 'no delegate call in provided code')
})
})

View File

@ -0,0 +1,189 @@
var assert = require('assert')
var KeyringController = require('../../app/scripts/keyring-controller')
var configManagerGen = require('../lib/mock-config-manager')
const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const async = require('async')
const mockEncryptor = require('../lib/mock-encryptor')
const MockSimpleKeychain = require('../lib/mock-simple-keychain')
const sinon = require('sinon')
describe('KeyringController', function() {
let keyringController, state
let password = 'password123'
let seedWords = 'puzzle seed penalty soldier say clay field arctic metal hen cage runway'
let addresses = ['eF35cA8EbB9669A35c31b5F6f249A9941a812AC1'.toLowerCase()]
let accounts = []
let originalKeystore
beforeEach(function(done) {
this.sinon = sinon.sandbox.create()
window.localStorage = {} // Hacking localStorage support into JSDom
keyringController = new KeyringController({
configManager: configManagerGen(),
txManager: {
getTxList: () => [],
getUnapprovedTxList: () => []
},
ethStore: {
addAccount(acct) { accounts.push(ethUtil.addHexPrefix(acct)) },
},
})
// Stub out the browser crypto for a mock encryptor.
// Browser crypto is tested in the integration test suite.
keyringController.encryptor = mockEncryptor
keyringController.createNewVaultAndKeychain(password)
.then(function (newState) {
state = newState
done()
})
})
afterEach(function() {
// Cleanup mocks
this.sinon.restore()
})
describe('#createNewVaultAndKeychain', function () {
this.timeout(10000)
it('should set a vault on the configManager', function(done) {
keyringController.configManager.setVault(null)
assert(!keyringController.configManager.getVault(), 'no previous vault')
keyringController.createNewVaultAndKeychain(password)
.then(() => {
const vault = keyringController.configManager.getVault()
assert(vault, 'vault created')
done()
})
.catch((reason) => {
assert.ifError(reason)
done()
})
})
})
describe('#restoreKeyring', function() {
it(`should pass a keyring's serialized data back to the correct type.`, function(done) {
const mockSerialized = {
type: 'HD Key Tree',
data: {
mnemonic: seedWords,
numberOfAccounts: 1,
}
}
const mock = this.sinon.mock(keyringController)
mock.expects('getBalanceAndNickname')
.exactly(1)
keyringController.restoreKeyring(mockSerialized)
.then((keyring) => {
assert.equal(keyring.wallets.length, 1, 'one wallet restored')
return keyring.getAccounts()
})
.then((accounts) => {
assert.equal(accounts[0], addresses[0])
mock.verify()
done()
})
.catch((reason) => {
assert.ifError(reason)
done()
})
})
})
describe('#createNickname', function() {
it('should add the address to the identities hash', function() {
const fakeAddress = '0x12345678'
keyringController.createNickname(fakeAddress)
const identities = keyringController.identities
const identity = identities[fakeAddress]
assert.equal(identity.address, fakeAddress)
const nick = keyringController.configManager.nicknameForWallet(fakeAddress)
assert.equal(typeof nick, 'string')
})
})
describe('#saveAccountLabel', function() {
it ('sets the nickname', function(done) {
const account = addresses[0]
var nick = 'Test nickname'
keyringController.identities[ethUtil.addHexPrefix(account)] = {}
keyringController.saveAccountLabel(account, nick)
.then((label) => {
assert.equal(label, nick)
const persisted = keyringController.configManager.nicknameForWallet(account)
assert.equal(persisted, nick)
done()
})
.catch((reason) => {
assert.ifError(reason)
done()
})
})
this.timeout(10000)
it('retrieves the persisted nickname', function(done) {
const account = addresses[0]
var nick = 'Test nickname'
keyringController.configManager.setNicknameForWallet(account, nick)
keyringController.createNewVaultAndRestore(password, seedWords)
.then((state) => {
const identity = keyringController.identities['0x' + account]
assert.equal(identity.name, nick)
assert(accounts)
done()
})
.catch((reason) => {
assert.ifError(reason)
done()
})
})
})
describe('#getAccounts', function() {
it('returns the result of getAccounts for each keyring', function() {
keyringController.keyrings = [
{ getAccounts() { return Promise.resolve([1,2,3]) } },
{ getAccounts() { return Promise.resolve([4,5,6]) } },
]
keyringController.getAccounts()
.then((result) => {
assert.deepEqual(result, [1,2,3,4,5,6])
done()
})
})
})
describe('#addGasBuffer', function() {
it('adds 100k gas buffer to estimates', function() {
const gas = '0x04ee59' // Actual estimated gas example
const tooBigOutput = '0x80674f9' // Actual bad output
const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16)
const correctBuffer = new BN('100000', 10)
const correct = bnGas.add(correctBuffer)
const tooBig = new BN(tooBigOutput, 16)
const result = keyringController.addGasBuffer(gas)
const bnResult = new BN(ethUtil.stripHexPrefix(result), 16)
assert.equal(result.indexOf('0x'), 0, 'included hex prefix')
assert(bnResult.gt(bnGas), 'Estimate increased in value.')
assert.equal(bnResult.sub(bnGas).toString(10), '100000', 'added 100k gas')
assert.equal(result, '0x' + correct.toString(16), 'Added the right amount')
assert.notEqual(result, tooBigOutput, 'not that bad estimate')
})
})
})

View File

@ -0,0 +1,127 @@
const assert = require('assert')
const extend = require('xtend')
const HdKeyring = require('../../../app/scripts/keyrings/hd')
// Sample account:
const privKeyHex = 'b8a9c05beeedb25df85f8d641538cbffedf67216048de9c678ee26260eb91952'
const sampleMnemonic = 'finish oppose decorate face calm tragic certain desk hour urge dinosaur mango'
const firstAcct = '1c96099350f13d558464ec79b9be4445aa0ef579'
const secondAcct = '1b00aed43a693f3a957f9feb5cc08afa031e37a0'
describe('hd-keyring', function() {
let keyring
beforeEach(function() {
keyring = new HdKeyring()
})
describe('constructor', function(done) {
keyring = new HdKeyring({
mnemonic: sampleMnemonic,
numberOfAccounts: 2,
})
const accounts = keyring.getAccounts()
.then((accounts) => {
assert.equal(accounts[0], firstAcct)
assert.equal(accounts[1], secondAcct)
done()
})
})
describe('Keyring.type', function() {
it('is a class property that returns the type string.', function() {
const type = HdKeyring.type
assert.equal(typeof type, 'string')
})
})
describe('#type', function() {
it('returns the correct value', function() {
const type = keyring.type
const correct = HdKeyring.type
assert.equal(type, correct)
})
})
describe('#serialize empty wallets.', function() {
it('serializes a new mnemonic', function() {
keyring.serialize()
.then((output) => {
assert.equal(output.numberOfAccounts, 0)
assert.equal(output.mnemonic, null)
})
})
})
describe('#deserialize a private key', function() {
it('serializes what it deserializes', function(done) {
keyring.deserialize({
mnemonic: sampleMnemonic,
numberOfAccounts: 1
})
.then(() => {
assert.equal(keyring.wallets.length, 1, 'restores two accounts')
return keyring.addAccounts(1)
}).then(() => {
return keyring.getAccounts()
}).then((accounts) => {
assert.equal(accounts[0], firstAcct)
assert.equal(accounts[1], secondAcct)
assert.equal(accounts.length, 2)
return keyring.serialize()
}).then((serialized) => {
assert.equal(serialized.mnemonic, sampleMnemonic)
done()
})
})
})
describe('#addAccounts', function() {
describe('with no arguments', function() {
it('creates a single wallet', function(done) {
keyring.addAccounts()
.then(() => {
assert.equal(keyring.wallets.length, 1)
done()
})
})
})
describe('with a numeric argument', function() {
it('creates that number of wallets', function(done) {
keyring.addAccounts(3)
.then(() => {
assert.equal(keyring.wallets.length, 3)
done()
})
})
})
})
describe('#getAccounts', function() {
it('calls getAddress on each wallet', function(done) {
// Push a mock wallet
const desiredOutput = 'foo'
keyring.wallets.push({
getAddress() {
return {
toString() {
return desiredOutput
}
}
}
})
const output = keyring.getAccounts()
.then((output) => {
assert.equal(output[0], desiredOutput)
assert.equal(output.length, 1)
done()
})
})
})
})

View File

@ -0,0 +1,91 @@
const assert = require('assert')
const extend = require('xtend')
const ethUtil = require('ethereumjs-util')
const SimpleKeyring = require('../../../app/scripts/keyrings/simple')
const TYPE_STR = 'Simple Key Pair'
// Sample account:
const privKeyHex = 'b8a9c05beeedb25df85f8d641538cbffedf67216048de9c678ee26260eb91952'
describe('simple-keyring', function() {
let keyring
beforeEach(function() {
keyring = new SimpleKeyring()
})
describe('Keyring.type', function() {
it('is a class property that returns the type string.', function() {
const type = SimpleKeyring.type
assert.equal(type, TYPE_STR)
})
})
describe('#type', function() {
it('returns the correct value', function() {
const type = keyring.type
assert.equal(type, TYPE_STR)
})
})
describe('#serialize empty wallets.', function() {
it('serializes an empty array', function(done) {
keyring.serialize()
.then((output) => {
assert.deepEqual(output, [])
done()
})
})
})
describe('#deserialize a private key', function() {
it('serializes what it deserializes', function() {
keyring.deserialize([privKeyHex])
.then(() => {
assert.equal(keyring.wallets.length, 1, 'has one wallet')
const serialized = keyring.serialize()
assert.equal(serialized[0], privKeyHex)
})
})
})
describe('#addAccounts', function() {
describe('with no arguments', function() {
it('creates a single wallet', function() {
keyring.addAccounts()
.then(() => {
assert.equal(keyring.wallets.length, 1)
})
})
})
describe('with a numeric argument', function() {
it('creates that number of wallets', function() {
keyring.addAccounts(3)
.then(() => {
assert.equal(keyring.wallets.length, 3)
})
})
})
})
describe('#getAccounts', function() {
it('calls getAddress on each wallet', function(done) {
// Push a mock wallet
const desiredOutput = '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761'
keyring.wallets.push({
getAddress() {
return ethUtil.toBuffer(desiredOutput)
}
})
keyring.getAccounts()
.then((output) => {
assert.equal(output[0], desiredOutput)
assert.equal(output.length, 1)
done()
})
})
})
})

View File

@ -9,7 +9,7 @@ describe('MetaMaskController', function() {
let controller = new MetaMaskController({
showUnconfirmedMessage: noop,
unlockAccountMessage: noop,
showUnconfirmedTx: noop,
showUnapprovedTx: noop,
setData,
loadData,
})
@ -25,24 +25,6 @@ describe('MetaMaskController', function() {
this.sinon.restore()
})
describe('#enforceTxValidations', function () {
it('returns null for positive values', function() {
var sample = {
value: '0x01'
}
var res = controller.enforceTxValidations(sample)
assert.equal(res, null, 'no error')
})
it('returns error for negative values', function() {
var sample = {
value: '-0x01'
}
var res = controller.enforceTxValidations(sample)
assert.ok(res, 'error')
})
})
})

22
test/unit/nodeify-test.js Normal file
View File

@ -0,0 +1,22 @@
const assert = require('assert')
const nodeify = require('../../app/scripts/lib/nodeify')
describe('nodeify', function() {
var obj = {
foo: 'bar',
promiseFunc: function (a) {
var solution = this.foo + a
return Promise.resolve(solution)
}
}
it('should retain original context', function(done) {
var nodified = nodeify(obj.promiseFunc).bind(obj)
nodified('baz', function (err, res) {
assert.equal(res, 'barbaz')
done()
})
})
})

View File

@ -5,13 +5,14 @@ const nock = require('nock')
const configManagerGen = require('../lib/mock-config-manager')
const NoticeController = require('../../app/scripts/notice-controller')
const STORAGE_KEY = 'metamask-persistance-key'
// Hacking localStorage support into JSDom
window.localStorage = {}
describe('notice-controller', function() {
var noticeController
beforeEach(function() {
// simple localStorage polyfill
window.localStorage = {}
if (window.localStorage.clear) window.localStorage.clear()
let configManager = configManagerGen()
noticeController = new NoticeController({
configManager: configManager,

View File

@ -0,0 +1,215 @@
const assert = require('assert')
const extend = require('xtend')
const EventEmitter = require('events')
const STORAGE_KEY = 'metamask-persistance-key'
const TransactionManager = require('../../app/scripts/transaction-manager')
describe('Transaction Manager', function() {
let txManager
const onTxDoneCb = () => true
beforeEach(function() {
txManager = new TransactionManager ({
txList: [],
setTxList: () => {},
provider: "testnet",
txHistoryLimit: 10,
blockTracker: new EventEmitter(),
getNetwork: function(){ return 'unit test' }
})
})
describe('#validateTxParams', function () {
it('returns null for positive values', function() {
var sample = {
value: '0x01'
}
var res = txManager.txProviderUtils.validateTxParams(sample, (err) => {
assert.equal(err, null, 'no error')
})
})
it('returns error for negative values', function() {
var sample = {
value: '-0x01'
}
var res = txManager.txProviderUtils.validateTxParams(sample, (err) => {
assert.ok(err, 'error')
})
})
})
describe('#getTxList', function() {
it('when new should return empty array', function() {
var result = txManager.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 0)
})
it('should also return transactions from local storage if any', function() {
})
})
describe('#_saveTxList', function() {
it('saves the submitted data to the tx list', function() {
var target = [{ foo: 'bar', metamaskNetworkId: 'unit test' }]
txManager._saveTxList(target)
var result = txManager.getTxList()
assert.equal(result[0].foo, 'bar')
})
})
describe('#addTx', function() {
it('adds a tx returned in getTxList', function() {
var tx = { id: 1, status: 'confirmed', metamaskNetworkId: 'unit test' }
txManager.addTx(tx, onTxDoneCb)
var result = txManager.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 1)
assert.equal(result[0].id, 1)
})
it('cuts off early txs beyond a limit', function() {
const limit = txManager.txHistoryLimit
for (let i = 0; i < limit + 1; i++) {
let tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: 'unit test' }
txManager.addTx(tx, onTxDoneCb)
}
var result = txManager.getTxList()
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
assert.equal(result[0].id, 1, 'early txs truncted')
})
it('cuts off early txs beyond a limit whether or not it is confirmed or rejected', function() {
const limit = txManager.txHistoryLimit
for (let i = 0; i < limit + 1; i++) {
let tx = { id: i, time: new Date(), status: 'rejected', metamaskNetworkId: 'unit test' }
txManager.addTx(tx, onTxDoneCb)
}
var result = txManager.getTxList()
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
assert.equal(result[0].id, 1, 'early txs truncted')
})
it('cuts off early txs beyond a limit but does not cut unapproved txs', function() {
var unconfirmedTx = { id: 0, time: new Date(), status: 'unapproved', metamaskNetworkId: 'unit test' }
txManager.addTx(unconfirmedTx, onTxDoneCb)
const limit = txManager.txHistoryLimit
for (let i = 1; i < limit + 1; i++) {
let tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: 'unit test' }
txManager.addTx(tx, onTxDoneCb)
}
var result = txManager.getTxList()
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
assert.equal(result[0].id, 0, 'first tx should still be there')
assert.equal(result[0].status, 'unapproved', 'first tx should be unapproved')
assert.equal(result[1].id, 2, 'early txs truncted')
})
})
describe('#setTxStatusSigned', function() {
it('sets the tx status to signed', function() {
var tx = { id: 1, status: 'unapproved', metamaskNetworkId: 'unit test' }
txManager.addTx(tx, onTxDoneCb)
txManager.setTxStatusSigned(1)
var result = txManager.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 1)
assert.equal(result[0].status, 'signed')
})
it('should emit a signed event to signal the exciton of callback', (done) => {
this.timeout(10000)
var tx = { id: 1, status: 'unapproved', metamaskNetworkId: 'unit test' }
let onTxDoneCb = function () {
assert(true, 'event listener has been triggered and onTxDoneCb executed')
done()
}
txManager.addTx(tx)
txManager.on('1:signed', onTxDoneCb)
txManager.setTxStatusSigned(1)
})
})
describe('#setTxStatusRejected', function() {
it('sets the tx status to rejected', function() {
var tx = { id: 1, status: 'unapproved', metamaskNetworkId: 'unit test' }
txManager.addTx(tx)
txManager.setTxStatusRejected(1)
var result = txManager.getTxList()
assert.ok(Array.isArray(result))
assert.equal(result.length, 1)
assert.equal(result[0].status, 'rejected')
})
it('should emit a rejected event to signal the exciton of callback', (done) => {
this.timeout(10000)
var tx = { id: 1, status: 'unapproved', metamaskNetworkId: 'unit test' }
txManager.addTx(tx)
let onTxDoneCb = function (err, txId) {
assert(true, 'event listener has been triggered and onTxDoneCb executed')
done()
}
txManager.on('1:rejected', onTxDoneCb)
txManager.setTxStatusRejected(1)
})
})
describe('#updateTx', function() {
it('replaces the tx with the same id', function() {
txManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: 'unit test' }, onTxDoneCb)
txManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: 'unit test' }, onTxDoneCb)
txManager.updateTx({ id: '1', status: 'blah', hash: 'foo', metamaskNetworkId: 'unit test' })
var result = txManager.getTx('1')
assert.equal(result.hash, 'foo')
})
})
describe('#getUnapprovedTxList', function() {
it('returns unapproved txs in a hash', function() {
txManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: 'unit test' }, onTxDoneCb)
txManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: 'unit test' }, onTxDoneCb)
let result = txManager.getUnapprovedTxList()
assert.equal(typeof result, 'object')
assert.equal(result['1'].status, 'unapproved')
assert.equal(result['2'], undefined)
})
})
describe('#getTx', function() {
it('returns a tx with the requested id', function() {
txManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: 'unit test' }, onTxDoneCb)
txManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: 'unit test' }, onTxDoneCb)
assert.equal(txManager.getTx('1').status, 'unapproved')
assert.equal(txManager.getTx('2').status, 'confirmed')
})
})
describe('#getFilteredTxList', function() {
it('returns a tx with the requested data', function() {
var foop = 0
var zoop = 0
for (let i = 0; i < 10; ++i ){
let everyOther = i % 2
txManager.addTx({ id: i,
status: everyOther ? 'unapproved' : 'confirmed',
metamaskNetworkId: 'unit test',
txParams: {
from: everyOther ? 'foop' : 'zoop',
to: everyOther ? 'zoop' : 'foop',
}
}, onTxDoneCb)
everyOther ? ++foop : ++zoop
}
assert.equal(txManager.getFilteredTxList({status: 'confirmed', from: 'zoop'}).length, zoop)
assert.equal(txManager.getFilteredTxList({status: 'confirmed', to: 'foop'}).length, zoop)
assert.equal(txManager.getFilteredTxList({status: 'confirmed', from: 'foop'}).length, 0)
assert.equal(txManager.getFilteredTxList({status: 'confirmed'}).length, zoop)
assert.equal(txManager.getFilteredTxList({from: 'foop'}).length, foop)
assert.equal(txManager.getFilteredTxList({from: 'zoop'}).length, zoop)
})
})
})

View File

@ -6,4 +6,5 @@ launch_in_ci:
- Firefox
framework:
- qunit
before_tests: "npm run buildCiUnits"
test_page: "test/integration/index.html"

View File

@ -25,7 +25,7 @@ const Selector = require('./development/selector')
// Query String
const qs = require('qs')
let queryString = qs.parse(window.location.href.split('#')[1])
let selectedView = queryString.view || 'account detail'
let selectedView = queryString.view || 'first time'
const firstState = states[selectedView]
updateQueryParams(selectedView)
@ -41,7 +41,7 @@ function updateQueryParams(newView) {
}
const actions = {
_setAccountManager(){},
_setBackgroundConnection(){},
update: function(stateName) {
selectedView = stateName
updateQueryParams(stateName)

View File

@ -26,12 +26,10 @@ function mapStateToProps (state) {
accounts: state.metamask.accounts,
address: state.metamask.selectedAccount,
accountDetail: state.appState.accountDetail,
transactions: state.metamask.transactions,
network: state.metamask.network,
unconfTxs: valuesFor(state.metamask.unconfTxs),
unconfMsgs: valuesFor(state.metamask.unconfMsgs),
isEthWarningConfirmed: state.metamask.isEthConfirmed,
shapeShiftTxList: state.metamask.shapeShiftTxList,
transactions: state.metamask.selectedAccountTxList || [],
}
}
@ -249,20 +247,10 @@ AccountDetailScreen.prototype.subview = function () {
}
AccountDetailScreen.prototype.transactionList = function () {
const { transactions, unconfTxs, unconfMsgs, address, network, shapeShiftTxList } = this.props
var txsToRender = transactions.concat(unconfTxs)
// only transactions that are from the current address
.filter(tx => tx.txParams.from === address)
// only transactions that are on the current network
.filter(tx => tx.txParams.metamaskNetworkId === network)
// sort by recency
.sort((a, b) => b.time - a.time)
const {transactions, unconfMsgs, address, network, shapeShiftTxList } = this.props
return h(TransactionList, {
txsToRender,
transactions: transactions.sort((a, b) => b.time - a.time),
network,
unconfTxs,
unconfMsgs,
address,
shapeShiftTxList,

View File

@ -15,19 +15,21 @@ function AccountListItem () {
}
AccountListItem.prototype.render = function () {
const identity = this.props.identity
var isSelected = this.props.selectedAddress === identity.address
var account = this.props.accounts[identity.address]
const { identity, selectedAccount, accounts, onShowDetail } = this.props
const isSelected = selectedAccount === identity.address
const account = accounts[identity.address]
const selectedClass = isSelected ? '.selected' : ''
return (
h(`.accounts-list-option.flex-row.flex-space-between.pointer.hover-white${selectedClass}`, {
key: `account-panel-${identity.address}`,
onClick: (event) => this.props.onShowDetail(identity.address, event),
onClick: (event) => onShowDetail(identity.address, event),
}, [
h('.identicon-wrapper.flex-column.flex-center.select-none', [
this.pendingOrNot(),
this.indicateIfLoose(),
h(Identicon, {
address: identity.address,
imageify: true,
@ -48,7 +50,7 @@ AccountListItem.prototype.render = function () {
},
}, ethUtil.toChecksumAddress(identity.address)),
h(EthBalance, {
value: account.balance,
value: account && account.balance,
style: {
lineHeight: '7px',
marginTop: '10px',
@ -70,6 +72,14 @@ AccountListItem.prototype.render = function () {
)
}
AccountListItem.prototype.indicateIfLoose = function () {
try { // Sometimes keyrings aren't loaded yet:
const type = this.props.keyring.type
const isLoose = type !== 'HD Key Tree'
return isLoose ? h('.keyring-label', 'LOOSE') : null
} catch (e) { return }
}
AccountListItem.prototype.pendingOrNot = function () {
const pending = this.props.pending
if (pending.length === 0) return null

View File

@ -19,9 +19,10 @@ function mapStateToProps (state) {
accounts: state.metamask.accounts,
identities: state.metamask.identities,
unconfTxs: state.metamask.unconfTxs,
selectedAddress: state.metamask.selectedAddress,
selectedAccount: state.metamask.selectedAccount,
scrollToBottom: state.appState.scrollToBottom,
pending,
keyrings: state.metamask.keyrings,
}
}
@ -31,14 +32,11 @@ function AccountsScreen () {
}
AccountsScreen.prototype.render = function () {
var state = this.props
var identityList = valuesFor(state.identities)
var unconfTxList = valuesFor(state.unconfTxs)
var actions = {
onSelect: this.onSelect.bind(this),
onShowDetail: this.onShowDetail.bind(this),
goHome: this.goHome.bind(this),
}
const props = this.props
const { keyrings } = props
const identityList = valuesFor(props.identities)
const unconfTxList = valuesFor(props.unconfTxs)
return (
h('.accounts-section.flex-grow', [
@ -46,7 +44,7 @@ AccountsScreen.prototype.render = function () {
// subtitle and nav
h('.section-title.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: actions.goHome,
onClick: this.goHome.bind(this),
}),
h('h2.page-subtitle', 'Select Account'),
]),
@ -73,13 +71,19 @@ AccountsScreen.prototype.render = function () {
}
})
const simpleAddress = identity.address.substring(2).toLowerCase()
const keyring = keyrings.find((kr) => {
return kr.accounts.includes(simpleAddress)
})
return h(AccountListItem, {
key: `acct-panel-${identity.address}`,
identity,
selectedAddress: this.props.selectedAddress,
selectedAccount: this.props.selectedAccount,
accounts: this.props.accounts,
onShowDetail: this.onShowDetail.bind(this),
pending,
keyring,
})
}),
@ -87,7 +91,7 @@ AccountsScreen.prototype.render = function () {
h('div.footer.hover-white.pointer', {
key: 'reveal-account-bar',
onClick: () => {
this.onRevealAccount()
this.addNewAccount()
},
style: {
display: 'flex',
@ -137,8 +141,8 @@ AccountsScreen.prototype.navigateToConfTx = function () {
AccountsScreen.prototype.onSelect = function (address, event) {
event.stopPropagation()
// if already selected, deselect
if (this.props.selectedAddress === address) address = null
this.props.dispatch(actions.setSelectedAddress(address))
if (this.props.selectedAccount === address) address = null
this.props.dispatch(actions.setSelectedAccount(address))
}
AccountsScreen.prototype.onShowDetail = function (address, event) {
@ -146,8 +150,8 @@ AccountsScreen.prototype.onShowDetail = function (address, event) {
this.props.dispatch(actions.showAccountDetail(address))
}
AccountsScreen.prototype.onRevealAccount = function () {
this.props.dispatch(actions.revealAccount())
AccountsScreen.prototype.addNewAccount = function () {
this.props.dispatch(actions.addNewAccount(0))
}
AccountsScreen.prototype.goHome = function () {

View File

@ -1,9 +1,15 @@
var actions = {
_setBackgroundConnection: _setBackgroundConnection,
GO_HOME: 'GO_HOME',
goHome: goHome,
// menu state
getNetworkStatus: 'getNetworkStatus',
// transition state
TRANSITION_FORWARD: 'TRANSITION_FORWARD',
TRANSITION_BACKWARD: 'TRANSITION_BACKWARD',
transitionForward,
transitionBackward,
// remote state
UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE',
updateMetamaskState: updateMetamaskState,
@ -14,26 +20,28 @@ var actions = {
showNotice: showNotice,
CLEAR_NOTICES: 'CLEAR_NOTICES',
clearNotices: clearNotices,
markAccountsFound,
// intialize screen
AGREE_TO_DISCLAIMER: 'AGREE_TO_DISCLAIMER',
agreeToDisclaimer: agreeToDisclaimer,
CREATE_NEW_VAULT_IN_PROGRESS: 'CREATE_NEW_VAULT_IN_PROGRESS',
SHOW_CREATE_VAULT: 'SHOW_CREATE_VAULT',
SHOW_RESTORE_VAULT: 'SHOW_RESTORE_VAULT',
FORGOT_PASSWORD: 'FORGOT_PASSWORD',
forgotPassword: forgotPassword,
SHOW_INIT_MENU: 'SHOW_INIT_MENU',
SHOW_NEW_VAULT_SEED: 'SHOW_NEW_VAULT_SEED',
SHOW_INFO_PAGE: 'SHOW_INFO_PAGE',
RECOVER_FROM_SEED: 'RECOVER_FROM_SEED',
CLEAR_SEED_WORD_CACHE: 'CLEAR_SEED_WORD_CACHE',
clearSeedWordCache: clearSeedWordCache,
recoverFromSeed: recoverFromSeed,
unlockMetamask: unlockMetamask,
unlockFailed: unlockFailed,
showCreateVault: showCreateVault,
showRestoreVault: showRestoreVault,
showInitializeMenu: showInitializeMenu,
createNewVault: createNewVault,
createNewVaultAndKeychain: createNewVaultAndKeychain,
createNewVaultAndRestore: createNewVaultAndRestore,
createNewVaultInProgress: createNewVaultInProgress,
addNewKeyring,
addNewAccount,
showNewVaultSeed: showNewVaultSeed,
showInfoPage: showInfoPage,
// seed recovery actions
@ -59,8 +67,6 @@ var actions = {
SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE',
SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE',
SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE',
REVEAL_ACCOUNT: 'REVEAL_ACCOUNT',
revealAccount: revealAccount,
SET_CURRENT_FIAT: 'SET_CURRENT_FIAT',
setCurrentFiat: setCurrentFiat,
// account detail screen
@ -74,16 +80,12 @@ var actions = {
showPrivateKey: showPrivateKey,
SAVE_ACCOUNT_LABEL: 'SAVE_ACCOUNT_LABEL',
saveAccountLabel: saveAccountLabel,
AGREE_TO_ETH_WARNING: 'AGREE_TO_ETH_WARNING',
agreeToEthWarning: agreeToEthWarning,
SHOW_ETH_WARNING: 'SHOW_ETH_WARNING',
showEthWarning: showEthWarning,
// tx conf screen
COMPLETED_TX: 'COMPLETED_TX',
TRANSACTION_ERROR: 'TRANSACTION_ERROR',
NEXT_TX: 'NEXT_TX',
PREVIOUS_TX: 'PREV_TX',
setSelectedAddress: setSelectedAddress,
setSelectedAccount: setSelectedAccount,
signMsg: signMsg,
cancelMsg: cancelMsg,
sendTx: sendTx,
@ -96,12 +98,12 @@ var actions = {
viewPendingTx: viewPendingTx,
VIEW_PENDING_TX: 'VIEW_PENDING_TX',
// app messages
confirmSeedWords: confirmSeedWords,
showAccountDetail: showAccountDetail,
BACK_TO_ACCOUNT_DETAIL: 'BACK_TO_ACCOUNT_DETAIL',
backToAccountDetail: backToAccountDetail,
showAccountsPage: showAccountsPage,
showConfTxPage: showConfTxPage,
confirmSeedWords: confirmSeedWords,
// config screen
SHOW_CONFIG_PAGE: 'SHOW_CONFIG_PAGE',
SET_RPC_TARGET: 'SET_RPC_TARGET',
@ -111,8 +113,6 @@ var actions = {
showConfigPage: showConfigPage,
setRpcTarget: setRpcTarget,
setProviderType: setProviderType,
// hacky - need a way to get a reference to account manager
_setAccountManager: _setAccountManager,
// loading overlay
SHOW_LOADING: 'SHOW_LOADING_INDICATION',
HIDE_LOADING: 'HIDE_LOADING_INDICATION',
@ -149,13 +149,18 @@ var actions = {
RECOVERY_IN_PROGRESS: 'RECOVERY_IN_PROGRESS',
BACK_TO_UNLOCK_VIEW: 'BACK_TO_UNLOCK_VIEW',
backToUnlockView: backToUnlockView,
// SHOWING KEYCHAIN
SHOW_NEW_KEYCHAIN: 'SHOW_NEW_KEYCHAIN',
showNewKeychain: showNewKeychain,
callBackgroundThenUpdate,
}
module.exports = actions
var _accountManager = null
function _setAccountManager (accountManager) {
_accountManager = accountManager
var background = null
function _setBackgroundConnection (backgroundConnection) {
background = backgroundConnection
}
function goHome () {
@ -170,29 +175,60 @@ function tryUnlockMetamask (password) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
dispatch(actions.unlockInProgress())
_accountManager.submitPassword(password, (err, selectedAccount) => {
background.submitPassword(password, (err, newState) => {
dispatch(actions.hideLoadingIndication())
if (err) {
dispatch(actions.unlockFailed())
dispatch(actions.unlockFailed(err.message))
} else {
dispatch(actions.unlockMetamask(selectedAccount))
dispatch(actions.transitionForward())
dispatch(actions.updateMetamaskState(newState))
}
})
}
}
function createNewVault (password, entropy) {
function transitionForward () {
return {
type: this.TRANSITION_FORWARD,
}
}
function transitionBackward () {
return {
type: this.TRANSITION_BACKWARD,
}
}
function confirmSeedWords () {
return (dispatch) => {
dispatch(actions.createNewVaultInProgress())
_accountManager.createNewVault(password, entropy, (err, result) => {
dispatch(actions.showLoadingIndication())
background.clearSeedWordCache((err, account) => {
dispatch(actions.hideLoadingIndication())
if (err) {
return dispatch(actions.displayWarning(err.message))
}
dispatch(actions.showNewVaultSeed(result))
console.log('Seed word cache cleared. ' + account)
dispatch(actions.showAccountDetail(account))
})
}
}
function createNewVaultAndRestore (password, seed) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
background.createNewVaultAndRestore(password, seed, (err) => {
dispatch(actions.hideLoadingIndication())
if (err) return dispatch(actions.displayWarning(err.message))
dispatch(actions.showAccountsPage())
})
}
}
function createNewVaultAndKeychain (password) {
return callBackgroundThenUpdate(background.createNewVaultAndKeychain, password)
}
function revealSeedConfirmation () {
return {
type: this.REVEAL_SEED_CONFIRMATION,
@ -202,29 +238,22 @@ function revealSeedConfirmation () {
function requestRevealSeed (password) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
_accountManager.tryPassword(password, (err, seed) => {
dispatch(actions.hideLoadingIndication())
background.submitPassword(password, (err) => {
if (err) return dispatch(actions.displayWarning(err.message))
_accountManager.recoverSeed((err, seed) => {
background.placeSeedWords((err) => {
if (err) return dispatch(actions.displayWarning(err.message))
dispatch(actions.showNewVaultSeed(seed))
dispatch(actions.hideLoadingIndication())
})
})
}
}
function recoverFromSeed (password, seed) {
return (dispatch) => {
// dispatch(actions.createNewVaultInProgress())
dispatch(actions.showLoadingIndication())
_accountManager.recoverFromSeed(password, seed, (err, metamaskState) => {
dispatch(actions.hideLoadingIndication())
if (err) return dispatch(actions.displayWarning(err.message))
function addNewKeyring (type, opts) {
return callBackgroundThenUpdate(background.addNewKeyring, type, opts)
}
var account = Object.keys(metamaskState.identities)[0]
dispatch(actions.unlockMetamask(account))
})
}
function addNewAccount (ringNumber = 0) {
return callBackgroundThenUpdate(background.addNewAccount, ringNumber)
}
function showInfoPage () {
@ -233,29 +262,14 @@ function showInfoPage () {
}
}
function setSelectedAddress (address) {
return (dispatch) => {
_accountManager.setSelectedAddress(address)
}
}
function revealAccount () {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
_accountManager.revealAccount((err) => {
dispatch(actions.hideLoadingIndication())
if (err) return dispatch(actions.displayWarning(err.message))
dispatch({
type: actions.REVEAL_ACCOUNT,
})
})
}
function setSelectedAccount (address) {
return callBackgroundThenUpdate(background.setSelectedAccount, address)
}
function setCurrentFiat (fiat) {
return (dispatch) => {
dispatch(this.showLoadingIndication())
_accountManager.setCurrentFiat(fiat, (data, err) => {
background.setCurrentFiat(fiat, (data, err) => {
dispatch(this.hideLoadingIndication())
dispatch({
type: this.SET_CURRENT_FIAT,
@ -273,7 +287,7 @@ function signMsg (msgData) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
_accountManager.signMessage(msgData, (err) => {
background.signMessage(msgData, (err) => {
dispatch(actions.hideLoadingIndication())
if (err) return dispatch(actions.displayWarning(err.message))
@ -284,7 +298,7 @@ function signMsg (msgData) {
function signTx (txData) {
return (dispatch) => {
_accountManager.setGasMultiplier(txData.gasMultiplier, (err) => {
background.setGasMultiplier(txData.gasMultiplier, (err) => {
if (err) return dispatch(actions.displayWarning(err.message))
web3.eth.sendTransaction(txData, (err, data) => {
dispatch(actions.hideLoadingIndication())
@ -299,7 +313,7 @@ function signTx (txData) {
function sendTx (txData) {
return (dispatch) => {
_accountManager.approveTransaction(txData.id, (err) => {
background.approveTransaction(txData.id, (err) => {
if (err) {
alert(err.message)
dispatch(actions.txError(err))
@ -325,12 +339,12 @@ function txError (err) {
}
function cancelMsg (msgData) {
_accountManager.cancelMessage(msgData.id)
background.cancelMessage(msgData.id)
return actions.completedTx(msgData.id)
}
function cancelTx (txData) {
_accountManager.cancelTransaction(txData.id)
background.cancelTransaction(txData.id)
return actions.completedTx(txData.id)
}
@ -350,6 +364,12 @@ function showRestoreVault () {
}
}
function forgotPassword () {
return {
type: actions.FORGOT_PASSWORD,
}
}
function showInitializeMenu () {
return {
type: actions.SHOW_INIT_MENU,
@ -359,7 +379,7 @@ function showInitializeMenu () {
function agreeToDisclaimer () {
return (dispatch) => {
dispatch(this.showLoadingIndication())
_accountManager.agreeToDisclaimer((err) => {
background.agreeToDisclaimer((err) => {
if (err) {
return dispatch(actions.displayWarning(err.message))
}
@ -391,6 +411,12 @@ function backToUnlockView () {
}
}
function showNewKeychain () {
return {
type: actions.SHOW_NEW_KEYCHAIN,
}
}
//
// unlock screen
//
@ -401,9 +427,10 @@ function unlockInProgress () {
}
}
function unlockFailed () {
function unlockFailed (message) {
return {
type: actions.UNLOCK_FAILED,
value: message,
}
}
@ -422,32 +449,22 @@ function updateMetamaskState (newState) {
}
function lockMetamask () {
return (dispatch) => {
_accountManager.setLocked((err) => {
dispatch(actions.hideLoadingIndication())
if (err) {
return dispatch(actions.displayWarning(err.message))
}
dispatch({
type: actions.LOCK_METAMASK,
})
})
}
return callBackgroundThenUpdate(background.setLocked)
}
function showAccountDetail (address) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
_accountManager.setSelectedAddress(address, (err, address) => {
background.setSelectedAccount(address, (err, newState) => {
dispatch(actions.hideLoadingIndication())
if (err) {
return dispatch(actions.displayWarning(err.message))
}
dispatch(actions.updateMetamaskState(newState))
dispatch({
type: actions.SHOW_ACCOUNT_DETAIL,
value: address,
value: newState.selectedAccount,
})
})
}
@ -459,27 +476,6 @@ function backToAccountDetail (address) {
value: address,
}
}
function clearSeedWordCache (account) {
return {
type: actions.CLEAR_SEED_WORD_CACHE,
value: account,
}
}
function confirmSeedWords () {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
_accountManager.clearSeedWordCache((err, account) => {
dispatch(actions.hideLoadingIndication())
if (err) {
return dispatch(actions.displayWarning(err.message))
}
console.log('Seed word cache cleared. ' + account)
dispatch(actions.showAccountDetail(account))
})
}
}
function showAccountsPage () {
return {
@ -533,10 +529,10 @@ function goBackToInitView () {
function markNoticeRead (notice) {
return (dispatch) => {
dispatch(this.showLoadingIndication())
_accountManager.markNoticeRead(notice, (err, notice) => {
background.markNoticeRead(notice, (err, notice) => {
dispatch(this.hideLoadingIndication())
if (err) {
return dispatch(actions.showWarning(err))
return dispatch(actions.displayWarning(err))
}
if (notice) {
return dispatch(actions.showNotice(notice))
@ -563,12 +559,16 @@ function clearNotices () {
}
}
function markAccountsFound() {
return callBackgroundThenUpdate(background.markAccountsFound)
}
//
// config
//
function setRpcTarget (newRpc) {
_accountManager.setRpcTarget(newRpc)
background.setRpcTarget(newRpc)
return {
type: actions.SET_RPC_TARGET,
value: newRpc,
@ -576,7 +576,7 @@ function setRpcTarget (newRpc) {
}
function setProviderType (type) {
_accountManager.setProviderType(type)
background.setProviderType(type)
return {
type: actions.SET_PROVIDER_TYPE,
value: type,
@ -584,7 +584,7 @@ function setProviderType (type) {
}
function useEtherscanProvider () {
_accountManager.useEtherscanProvider()
background.useEtherscanProvider()
return {
type: actions.USE_ETHERSCAN_PROVIDER,
}
@ -639,7 +639,7 @@ function exportAccount (address) {
return function (dispatch) {
dispatch(self.showLoadingIndication())
_accountManager.exportAccount(address, function (err, result) {
background.exportAccount(address, function (err, result) {
dispatch(self.hideLoadingIndication())
if (err) {
@ -662,7 +662,7 @@ function showPrivateKey (key) {
function saveAccountLabel (account, label) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
_accountManager.saveAccountLabel(account, label, (err) => {
background.saveAccountLabel(account, label, (err) => {
dispatch(actions.hideLoadingIndication())
if (err) {
return dispatch(actions.displayWarning(err.message))
@ -681,28 +681,9 @@ function showSendPage () {
}
}
function agreeToEthWarning () {
return (dispatch) => {
_accountManager.agreeToEthWarning((err) => {
if (err) {
return dispatch(actions.showEthWarning(err.message))
}
dispatch({
type: actions.AGREE_TO_ETH_WARNING,
})
})
}
}
function showEthWarning () {
return {
type: actions.SHOW_ETH_WARNING,
}
}
function buyEth (address, amount) {
return (dispatch) => {
_accountManager.buyEth(address, amount)
background.buyEth(address, amount)
dispatch({
type: actions.BUY_ETH,
})
@ -780,7 +761,7 @@ function coinShiftRquest (data, marketData) {
if (response.error) return dispatch(actions.displayWarning(response.error))
var message = `
Deposit your ${response.depositType} to the address bellow:`
_accountManager.createShapeShiftTx(response.deposit, response.depositType)
background.createShapeShiftTx(response.deposit, response.depositType)
dispatch(actions.showQrView(response.deposit, [message].concat(marketData)))
})
}
@ -836,3 +817,24 @@ function shapeShiftRequest (query, options, cb) {
return shapShiftReq.send()
}
}
// Call Background Then Update
//
// A function generator for a common pattern wherein:
// We show loading indication.
// We call a background method.
// We hide loading indication.
// If it errored, we show a warning.
// If it didn't, we update the state.
function callBackgroundThenUpdate (method, ...args) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
method.call(background, ...args, (err, newState) => {
dispatch(actions.hideLoadingIndication())
if (err) {
return dispatch(actions.displayWarning(err.message))
}
dispatch(actions.updateMetamaskState(newState))
})
}
}

View File

@ -7,9 +7,7 @@ const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
// init
const DisclaimerScreen = require('./first-time/disclaimer')
const InitializeMenuScreen = require('./first-time/init-menu')
const CreateVaultScreen = require('./first-time/create-vault')
const CreateVaultCompleteScreen = require('./first-time/create-vault-complete')
const RestoreVaultScreen = require('./first-time/restore-vault')
const NewKeyChainScreen = require('./new-keychain')
// unlock
const UnlockScreen = require('./unlock')
// accounts
@ -18,10 +16,10 @@ const AccountDetailScreen = require('./account-detail')
const SendTransactionScreen = require('./send')
const ConfirmTxScreen = require('./conf-tx')
// notice
const NoticeScreen = require('./notice')
const NoticeScreen = require('./components/notice')
const generateLostAccountsNotice = require('../lib/lost-accounts-notice')
// other views
const ConfigScreen = require('./config')
const RevealSeedConfirmation = require('./recover-seed/confirmation')
const InfoScreen = require('./info')
const LoadingIndicator = require('./components/loading')
const SandwichExpando = require('sandwich-expando')
@ -29,9 +27,12 @@ const MenuDroppo = require('menu-droppo')
const DropMenuItem = require('./components/drop-menu-item')
const NetworkIndicator = require('./components/network')
const Tooltip = require('./components/tooltip')
const EthStoreWarning = require('./eth-store-warning')
const BuyView = require('./components/buy-button-subview')
const QrView = require('./components/qr-code')
const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete')
const HDRestoreVaultScreen = require('./keychains/hd/restore-vault')
const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation')
module.exports = connect(mapStateToProps)(App)
inherits(App, Component)
@ -41,8 +42,7 @@ function mapStateToProps (state) {
return {
// state from plugin
isLoading: state.appState.isLoading,
isConfirmed: state.metamask.isConfirmed,
isEthConfirmed: state.metamask.isEthConfirmed,
isDisclaimerConfirmed: state.metamask.isDisclaimerConfirmed,
noActiveNotices: state.metamask.noActiveNotices,
isInitialized: state.metamask.isInitialized,
isUnlocked: state.metamask.isUnlocked,
@ -56,6 +56,8 @@ function mapStateToProps (state) {
network: state.metamask.network,
provider: state.metamask.provider,
forgottenPassword: state.appState.forgottenPassword,
lastUnreadNotice: state.metamask.lastUnreadNotice,
lostAccounts: state.metamask.lostAccounts,
}
}
@ -94,7 +96,6 @@ App.prototype.render = function () {
transitionLeaveTimeout: 300,
}, [
this.renderPrimary(),
this.renderBackToInitButton(),
]),
]),
])
@ -102,7 +103,6 @@ App.prototype.render = function () {
}
App.prototype.renderAppBar = function () {
if (window.METAMASK_UI_TYPE === 'notification') {
return null
}
@ -141,15 +141,21 @@ App.prototype.renderAppBar = function () {
src: '/images/icon-128.png',
}),
h(NetworkIndicator, {
network: this.props.network,
provider: this.props.provider,
onClick: (event) => {
event.preventDefault()
event.stopPropagation()
this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen })
h('#network-spacer.flex-center', {
style: {
marginRight: '-72px',
},
}),
}, [
h(NetworkIndicator, {
network: this.props.network,
provider: this.props.provider,
onClick: (event) => {
event.preventDefault()
event.stopPropagation()
this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen })
},
}),
]),
]),
// metamask name
@ -254,7 +260,15 @@ App.prototype.renderNetworkDropdown = function () {
activeNetworkRender: props.provider.rpcTarget,
}),
this.renderCustomOption(props.provider.rpcTarget),
this.renderCustomOption(props.provider),
h(DropMenuItem, {
label: 'Custom RPC',
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
action: () => this.props.dispatch(actions.showConfigPage()),
icon: h('i.fa.fa-question-circle.fa-lg'),
}),
])
}
@ -305,6 +319,7 @@ App.prototype.renderDropdown = function () {
}),
])
}
App.prototype.renderBackButton = function (style, justArrow = false) {
var props = this.props
return (
@ -322,85 +337,17 @@ App.prototype.renderBackButton = function (style, justArrow = false) {
}, 'BACK'),
])
)
}
App.prototype.renderBackToInitButton = function () {
var props = this.props
var button = null
if (!props.isConfirmed) return button
if (!props.isUnlocked) {
if (props.currentView.name === 'InitMenu') {
button = props.forgottenPassword ? h('.flex-row', {
key: 'rightArrow',
style: {
position: 'absolute',
bottom: '10px',
right: '15px',
fontSize: '21px',
fontFamily: 'Montserrat Light',
color: '#7F8082',
width: '77.578px',
alignItems: 'flex-end',
},
}, [
h('div.cursor-pointer', {
style: {
marginRight: '3px',
},
onClick: () => props.dispatch(actions.backToUnlockView()),
}, 'LOGIN'),
h('i.fa.fa-arrow-right.cursor-pointer'),
]) : null
} else if (props.isInitialized) {
var style
switch (props.currentView.name) {
case 'createVault':
style = {
position: 'absolute',
top: '41px',
left: '80px',
fontSize: '21px',
fontFamily: 'Montserrat Bold',
color: 'rgb(174, 174, 174)',
}
return this.renderBackButton(style, true)
case 'restoreVault':
style = {
position: 'absolute',
top: '41px',
left: '70px',
fontSize: '21px',
fontFamily: 'Montserrat Bold',
color: 'rgb(174, 174, 174)',
}
return this.renderBackButton(style, true)
default:
style = {
position: 'absolute',
bottom: '10px',
left: '15px',
fontSize: '21px',
fontFamily: 'Montserrat Light',
color: '#7F8082',
width: '71.969px',
alignItems: 'flex-end',
}
return this.renderBackButton(style)
}
}
}
return button
}
App.prototype.renderPrimary = function () {
var props = this.props
if (!props.isConfirmed) {
if (!props.isDisclaimerConfirmed) {
return h(DisclaimerScreen, {key: 'disclaimerScreen'})
}
if (props.seedWords) {
return h(CreateVaultCompleteScreen, {key: 'createVaultComplete'})
return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'})
}
// show initialize screen
@ -408,34 +355,43 @@ App.prototype.renderPrimary = function () {
// show current view
switch (props.currentView.name) {
case 'createVault':
return h(CreateVaultScreen, {key: 'createVault'})
case 'restoreVault':
return h(RestoreVaultScreen, {key: 'restoreVault'})
case 'createVaultComplete':
return h(CreateVaultCompleteScreen, {key: 'createVaultComplete'})
return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'})
default:
return h(InitializeMenuScreen, {key: 'menuScreenInit'})
}
}
// show unlock screen
if (!props.isUnlocked) {
return h(UnlockScreen, {key: 'locked'})
switch (props.currentView.name) {
case 'restoreVault':
return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'})
default:
return h(UnlockScreen, {key: 'locked'})
}
}
// notices
if (!props.noActiveNotices) {
return h(NoticeScreen, {key: 'NoticeScreen'})
return h(NoticeScreen, {
notice: props.lastUnreadNotice,
key: 'NoticeScreen',
onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)),
})
} else if (props.lostAccounts && props.lostAccounts.length > 0) {
return h(NoticeScreen, {
notice: generateLostAccountsNotice(props.lostAccounts),
key: 'LostAccountsNotice',
onConfirm: () => props.dispatch(actions.markAccountsFound()),
})
}
// show current view
switch (props.currentView.name) {
case 'EthStoreWarning':
return h(EthStoreWarning, {key: 'ethWarning'})
case 'accounts':
return h(AccountsScreen, {key: 'accounts'})
@ -446,6 +402,9 @@ App.prototype.renderPrimary = function () {
case 'sendTransaction':
return h(SendTransactionScreen, {key: 'send-transaction'})
case 'newKeychain':
return h(NewKeyChainScreen, {key: 'new-keychain'})
case 'confTx':
return h(ConfirmTxScreen, {key: 'confirm-tx'})
@ -458,10 +417,9 @@ App.prototype.renderPrimary = function () {
case 'info':
return h(InfoScreen, {key: 'info'})
case 'createVault':
return h(CreateVaultScreen, {key: 'createVault'})
case 'buyEth':
return h(BuyView, {key: 'buyEthView'})
case 'qr':
return h('div', {
style: {
@ -506,23 +464,14 @@ App.prototype.toggleMetamaskActive = function () {
}
}
App.prototype.renderCustomOption = function (rpcTarget) {
App.prototype.renderCustomOption = function (provider) {
const { rpcTarget, type } = provider
if (type !== 'rpc') return null
switch (rpcTarget) {
case undefined:
return h(DropMenuItem, {
label: 'Custom RPC',
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
action: () => this.props.dispatch(actions.showConfigPage()),
icon: h('i.fa.fa-question-circle.fa-lg'),
})
case 'http://localhost:8545':
return h(DropMenuItem, {
label: 'Custom RPC',
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
action: () => this.props.dispatch(actions.showConfigPage()),
icon: h('i.fa.fa-question-circle.fa-lg'),
})
return null
default:
return h(DropMenuItem, {

View File

@ -7,7 +7,7 @@ const actions = require('../actions')
const isValidAddress = require('../util').isValidAddress
module.exports = connect(mapStateToProps)(CoinbaseForm)
function mapStateToProps(state) {
function mapStateToProps (state) {
return {
selectedAccount: state.selectedAccount,
warning: state.appState.warning,
@ -16,7 +16,7 @@ function mapStateToProps(state) {
inherits(CoinbaseForm, Component)
function CoinbaseForm() {
function CoinbaseForm () {
Component.call(this)
}
@ -124,7 +124,6 @@ CoinbaseForm.prototype.toCoinbase = function () {
}
CoinbaseForm.prototype.renderLoading = function () {
return h('img', {
style: {
width: '27px',
@ -134,9 +133,8 @@ CoinbaseForm.prototype.renderLoading = function () {
})
}
function isValidAmountforCoinBase(amount) {
function isValidAmountforCoinBase (amount) {
amount = parseFloat(amount)
if (amount) {
if (amount <= 15 && amount > 0) {
return {

View File

@ -50,12 +50,10 @@ CopyButton.prototype.render = function () {
])
}
CopyButton.prototype.debounceRestore = function() {
CopyButton.prototype.debounceRestore = function () {
this.setState({ copied: true })
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.setState({ copied: false })
}, 850)
}

View File

@ -32,9 +32,9 @@ DropMenuItem.prototype.render = function () {
}
DropMenuItem.prototype.activeNetworkRender = function () {
let activeNetwork = this.props.activeNetworkRender
let { provider } = this.props
let providerType = provider ? provider.type : null
const activeNetwork = this.props.activeNetworkRender
const { provider } = this.props
const providerType = provider ? provider.type : null
if (activeNetwork === undefined) return
switch (this.props.label) {

View File

@ -15,9 +15,10 @@ function EthBalanceComponent () {
EthBalanceComponent.prototype.render = function () {
var props = this.props
let { value } = props
var style = props.style
var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true
const value = formatBalance(props.value, 6, needsParse)
value = value ? formatBalance(value, 6, needsParse) : '...'
var width = props.width
return (
@ -38,6 +39,7 @@ EthBalanceComponent.prototype.render = function () {
EthBalanceComponent.prototype.renderBalance = function (value) {
var props = this.props
if (value === 'None') return value
if (value === '...') return value
var balanceObj = generateBalanceObject(value, props.shorten ? 1 : 3)
var balance
var splitBalance = value.split(' ')

View File

@ -16,8 +16,8 @@ function IdenticonComponent () {
}
IdenticonComponent.prototype.render = function () {
var state = this.props
var diameter = state.diameter || this.defaultDiameter
var props = this.props
var diameter = props.diameter || this.defaultDiameter
return (
h('div', {
key: 'identicon-' + this.props.address,
@ -33,15 +33,31 @@ IdenticonComponent.prototype.render = function () {
}
IdenticonComponent.prototype.componentDidMount = function () {
var state = this.props
var address = state.address
var props = this.props
var address = props.address
if (!address) return
var container = findDOMNode(this)
var diameter = state.diameter || this.defaultDiameter
var imageify = state.imageify === undefined ? true : state.imageify
var img = iconFactory.iconForAddress(address, diameter, imageify)
var diameter = props.diameter || this.defaultDiameter
var img = iconFactory.iconForAddress(address, diameter, false)
container.appendChild(img)
}
IdenticonComponent.prototype.componentDidUpdate = function () {
var props = this.props
var address = props.address
if (!address) return
var container = findDOMNode(this)
var children = container.children
for (var i = 0; i < children.length; i++) {
container.removeChild(children[i])
}
var diameter = props.diameter || this.defaultDiameter
var img = iconFactory.iconForAddress(address, diameter, false)
container.appendChild(img)
}

View File

@ -22,7 +22,6 @@ Network.prototype.render = function () {
let iconName, hoverText
if (networkNumber === 'loading') {
return h('img.network-indicator', {
title: 'Attempting to connect to blockchain.',
onClick: (event) => this.props.onClick(event),
@ -32,7 +31,6 @@ Network.prototype.render = function () {
},
src: 'images/loading.svg',
})
} else if (providerName === 'mainnet') {
hoverText = 'Main Ethereum Network'
iconName = 'ethereum-network'
@ -48,11 +46,7 @@ Network.prototype.render = function () {
}
return (
h('#network_component.flex-center.pointer', {
style: {
marginRight: '-27px',
marginLeft: '-3px',
},
h('#network_component.pointer', {
title: hoverText,
onClick: (event) => this.props.onClick(event),
}, [

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