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

Merge pull request #105 from MetaMask/MergeWithUI

Merge metamask-ui into metamask plugin
This commit is contained in:
Dan Finlay 2016-04-13 16:20:37 -07:00
commit 54f13b31ec
56 changed files with 4367 additions and 14 deletions

View File

@ -3,6 +3,7 @@
## Current Master
- Corrected text above account list. Selected account is visible to all sites, not just the current domain.
- Merged the UI codebase into the main plugin codebase for simpler maintenance.
## 1.5.0 2016-04-13

View File

@ -1,5 +1,7 @@
# Metamask Plugin
[![Throughput Graph](https://graphs.waffle.io/MetaMask/metamask-plugin/throughput.svg)](https://waffle.io/MetaMask/metamask-plugin/metrics)
## Development
```bash
@ -34,7 +36,7 @@ You now have the plugin, and can click 'inspect views: background plugin' to vie
### Developing the UI
To enjoy the live-reloading that `gulp dev` offers while working on the `metamask-ui` or `web3-provider-engine` dependencies:
To enjoy the live-reloading that `gulp dev` offers while working on the `web3-provider-engine` or other dependencies:
1. Clone the dependency locally.
2. `npm install` in its folder.
@ -42,7 +44,22 @@ To enjoy the live-reloading that `gulp dev` offers while working on the `metamas
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!
### Deploying the UI
### Running Tests
Currently the tests are split between two suites (we recently merged the UI into the main plugin repository). There are two different test suites to be concerned with:
Plugin tests, `npm test`.
UI tests, `npm run testUi`.
You can also run both of these with continuously watching processes, via `npm run watch` and `npm run watchUi`.
#### UI Testing Particulars
Requires `mocha` installed. Run `npm install -g mocha`.
You can either run the test suite once with `npm testUi`, or you can reload on file changes, by running `mocha watch ui/test/**/**`.
### Deploying the UI
You must be authorized already on the Metamask plugin.

View File

@ -1,5 +1,5 @@
const createId = require('hat')
const uiUtils = require('metamask-ui/app/util')
const uiUtils = require('../../../ui/app/util')
var notificationHandlers = {}
module.exports = createTxNotification
@ -46,4 +46,4 @@ function createTxNotification(opts){
confirm: opts.confirm,
cancel: opts.cancel,
}
}
}

View File

@ -4,8 +4,8 @@ const async = require('async')
const Multiplex = require('multiplex')
const Dnode = require('dnode')
const Web3 = require('web3')
const MetaMaskUi = require('metamask-ui')
const MetaMaskUiCss = require('metamask-ui/css')
const MetaMaskUi = require('../../ui')
const MetaMaskUiCss = require('../../ui/css')
const injectCss = require('inject-css')
const PortStream = require('./lib/port-stream.js')
const StreamProvider = require('./lib/stream-provider.js')
@ -66,7 +66,7 @@ function linkDnode(stream, cb){
// setup push events
accountManager.on = eventEmitter.on.bind(eventEmitter)
cb(null, accountManager)
})
})
}
function getCurrentDomain(cb){
@ -96,4 +96,4 @@ function setupApp(err, opts){
currentDomain: opts.currentDomain,
})
}
}

View File

@ -31,7 +31,7 @@ gulp.task('copy:images', copyTask({
destination: './dist/images',
}))
gulp.task('copy:reload', copyTask({
source: './app/scripts/',
source: './app/scripts/',
destination: './dist/scripts',
pattern: '/chromereload.js',
}))
@ -93,7 +93,6 @@ function copyTask(opts){
}
}
function bundleTask(opts) {
var browserifyOpts = assign({}, watchify.args, {
entries: ['./app/scripts/'+opts.filename],
@ -101,6 +100,7 @@ function bundleTask(opts) {
})
var bundler = browserify(browserifyOpts)
bundler.transform('brfs')
if (opts.watch) {
bundler = watchify(bundler)
bundler.on('update', performBundle) // on any dep update, runs the bundler
@ -121,7 +121,7 @@ function bundleTask(opts) {
.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.
// Add transformation tasks to the pipeline here.
.pipe(sourcemaps.write('./')) // writes .map file
.pipe(gulp.dest('./dist/scripts'))
.pipe(livereload())

View File

@ -6,34 +6,70 @@
"scripts": {
"start": "gulp dev",
"test": "mocha --require test/helper.js --compilers js:babel-register --recursive",
"watch": "mocha watch --compilers js:babel-register --recursive"
"watch": "mocha watch --compilers js:babel-register --recursive",
"testUi": "mocha ui/test/**/*test.js",
"watchUi": "mocha watch ui/test/**/*test.js"
},
"browserify": {
"transform": [
[
"babelify",
{
"presets": [
"es2015"
]
}
],
"brfs"
]
},
"dependencies": {
"async": "^1.5.2",
"clone": "^1.0.2",
"copy-to-clipboard": "^2.0.0",
"debounce": "^1.0.0",
"dnode": "^1.2.2",
"end-of-stream": "^1.1.0",
"eth-lightwallet": "^2.2.2",
"eth-store": "^1.1.0",
"ethereumjs-tx": "^1.0.0",
"ethereumjs-util": "^1.3.5",
"ethereumjs-util": "^2.6.0",
"faux-jax": "git+https://github.com/kumavis/faux-jax.git#c3648de04804f3895c5b4972750cae5b51ddb103",
"hat": "0.0.3",
"inject-css": "^0.1.1",
"metamask-ui": "^1.5.0",
"metamask-logo": "^1.1.5",
"multiplex": "^6.7.0",
"pojo-migrator": "^2.1.0",
"pumpify": "^1.3.4",
"react": "^15.0.1",
"react-addons-css-transition-group": "^15.0.1",
"react-dom": "^15.0.1",
"react-hyperscript": "^2.4.0",
"readable-stream": "^2.0.5",
"react": "^0.14.3",
"react-addons-css-transition-group": "^0.14.7",
"react-dom": "^0.14.3",
"react-hyperscript": "^2.2.2",
"react-redux": "^4.0.3",
"redux": "^3.0.5",
"redux-logger": "^2.3.1",
"redux-thunk": "^1.0.2",
"textarea-caret": "^3.0.1",
"three.js": "^0.73.2",
"through2": "^2.0.1",
"web3": "^0.15.1",
"web3-provider-engine": "^7.2.1",
"xtend": "^4.0.1"
},
"devDependencies": {
"brfs": "^1.4.3",
"babel-preset-es2015": "^6.6.0",
"babelify": "^7.2.0",
"babel-register": "^6.7.2",
"beefy": "^2.1.5",
"browserify": "^13.0.0",
"chai": "^3.5.0",
"deep-freeze-strict": "^1.1.1",
"del": "^2.2.0",
"gulp": "github:gulpjs/gulp#4.0",
"gulp-livereload": "^3.8.1",
@ -45,8 +81,10 @@
"jshint-stylish": "~0.1.5",
"lodash.assign": "^4.0.6",
"mocha": "^2.4.5",
"mocha-jsdom": "^1.1.0",
"mocha-sinon": "^1.1.5",
"sinon": "^1.17.3",
"uglifyify": "^3.0.1",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"watchify": "^3.7.0"

66
ui/.gitignore vendored Normal file
View File

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

154
ui/app/account-detail.js Normal file
View File

@ -0,0 +1,154 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const copyToClipboard = require('copy-to-clipboard')
const actions = require('./actions')
const AccountPanel = require('./components/account-panel')
module.exports = connect(mapStateToProps)(AccountDetailScreen)
function mapStateToProps(state) {
var accountDetail = state.appState.accountDetail
return {
identities: state.metamask.identities,
accounts: state.metamask.accounts,
address: state.appState.currentView.context,
accountDetail: accountDetail,
}
}
inherits(AccountDetailScreen, Component)
function AccountDetailScreen() {
Component.call(this)
}
AccountDetailScreen.prototype.render = function() {
var state = this.props
var identity = state.identities[state.address]
var account = state.accounts[state.address]
var accountDetail = state.accountDetail
return (
h('.account-detail-section.flex-column.flex-grow', [
// subtitle and nav
h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: this.navigateToAccounts.bind(this),
}),
h('h2.page-subtitle', 'Account Detail'),
]),
// account summary, with embedded action buttons
h(AccountPanel, {
showFullAddress: true,
identity: identity,
account: account,
}, [
h('.flex-row.flex-space-around', [
// h('button', 'GET ETH'), DISABLED UNTIL WORKING
h('button', {
onClick: () => {
copyToClipboard(identity.address)
},
}, 'COPY ADDR'),
h('button', {
onClick: () => {
this.props.dispatch(actions.showSendPage())
},
}, 'SEND'),
h('button', {
onClick: () => {
this.requestAccountExport(identity.address)
},
}, 'EXPORT'),
]),
]),
this.exportedAccount(accountDetail),
// transaction table
/*
h('section.flex-column', [
h('span', 'your transaction history will go here.'),
]),
*/
])
)
}
AccountDetailScreen.prototype.navigateToAccounts = function(event){
event.stopPropagation()
this.props.dispatch(actions.showAccountsPage())
}
AccountDetailScreen.prototype.exportAccount = function(address) {
this.props.dispatch(actions.exportAccount(address))
}
AccountDetailScreen.prototype.requestAccountExport = function() {
this.props.dispatch(actions.requestExportAccount())
}
AccountDetailScreen.prototype.exportedAccount = function(accountDetail) {
if (!accountDetail) return
var accountExport = accountDetail.accountExport
var notExporting = accountExport === 'none'
var exportRequested = accountExport === 'requested'
var accountExported = accountExport === 'completed'
if (notExporting) return
if (exportRequested) {
var warning = `Exporting your private key is very dangerous,
and you should only do it if you know what you're doing.`
var confirmation = `If you're absolutely sure, type "I understand" below and
hit Enter.`
return h('div', {}, [
h('p.error', warning),
h('p', confirmation),
h('input#exportAccount', {
onKeyPress: this.onExportKeyPress.bind(this),
})
])
}
if (accountExported) {
return h('div.privateKey', {
}, [
h('label', 'Your private key (click to copy):'),
h('p.error.cursor-pointer', {
style: {
textOverflow: 'ellipsis',
overflow: 'hidden',
webkitUserSelect: 'text',
width: '100%',
},
onClick: function(event) {
copyToClipboard(accountDetail.privateKey)
}
}, accountDetail.privateKey),
])
}
}
AccountDetailScreen.prototype.onExportKeyPress = function(event) {
if (event.key !== 'Enter') return
event.preventDefault()
var input = document.getElementById('exportAccount')
if (input.value === 'I understand') {
this.props.dispatch(actions.exportAccount(this.props.address))
} else {
input.value = ''
input.placeholder = 'Please retype "I understand" exactly.'
}
}

116
ui/app/accounts.js Normal file
View File

@ -0,0 +1,116 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const extend = require('xtend')
const actions = require('./actions')
const AccountPanel = require('./components/account-panel')
const valuesFor = require('./util').valuesFor
module.exports = connect(mapStateToProps)(AccountsScreen)
function mapStateToProps(state) {
return {
accounts: state.metamask.accounts,
identities: state.metamask.identities,
unconfTxs: state.metamask.unconfTxs,
selectedAddress: state.metamask.selectedAddress,
currentDomain: state.appState.currentDomain,
}
}
inherits(AccountsScreen, Component)
function AccountsScreen() {
Component.call(this)
}
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),
}
return (
h('.accounts-section.flex-column.flex-grow', [
// subtitle and nav
h('.section-title.flex-column.flex-center', [
h('h2.page-subtitle', 'Accounts'),
]),
// current domain
/* AUDIT
* Temporarily removed
* since accounts are currently injected
* regardless of the current domain.
*/
h('.current-domain-panel.flex-center.font-small', [
h('spam', 'Selected address is visible to all sites you visit.'),
// h('span', state.currentDomain),
]),
// identity selection
h('section.identity-section.flex-column', {
style: {
maxHeight: '290px',
overflowY: 'auto',
overflowX: 'hidden',
}
},
identityList.map(renderAccountPanel)
),
unconfTxList.length ? (
h('.unconftx-link.flex-row.flex-center', {
onClick: this.navigateToConfTx.bind(this),
}, [
h('span', 'Unconfirmed Txs'),
h('i.fa.fa-arrow-right.fa-lg'),
])
) : (
null
),
])
)
function renderAccountPanel(identity){
var mayBeFauceting = identity.mayBeFauceting
var isSelected = state.selectedAddress === identity.address
var account = state.accounts[identity.address]
var isFauceting = mayBeFauceting && account.balance === '0x0'
var componentState = extend(actions, {
identity: identity,
account: account,
isSelected: isSelected,
isFauceting: isFauceting,
})
return h(AccountPanel, componentState)
}
}
AccountsScreen.prototype.navigateToConfTx = function(){
event.stopPropagation()
this.props.dispatch(actions.showConfTxPage())
}
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))
}
AccountsScreen.prototype.onShowDetail = function(address, event){
event.stopPropagation()
this.props.dispatch(actions.showAccountDetail(address))
}

418
ui/app/actions.js Normal file
View File

@ -0,0 +1,418 @@
var actions = {
// remote state
UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE',
updateMetamaskState: updateMetamaskState,
// intialize screen
CREATE_NEW_VAULT_IN_PROGRESS: 'CREATE_NEW_VAULT_IN_PROGRESS',
SHOW_CREATE_VAULT: 'SHOW_CREATE_VAULT',
SHOW_RESTORE_VAULT: 'SHOW_RESTORE_VAULT',
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,
createNewVaultInProgress: createNewVaultInProgress,
showNewVaultSeed: showNewVaultSeed,
showInfoPage: showInfoPage,
// unlock screen
UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS',
UNLOCK_FAILED: 'UNLOCK_FAILED',
UNLOCK_METAMASK: 'UNLOCK_METAMASK',
LOCK_METAMASK: 'LOCK_METAMASK',
tryUnlockMetamask: tryUnlockMetamask,
lockMetamask: lockMetamask,
unlockInProgress: unlockInProgress,
// error handling
displayWarning: displayWarning,
DISPLAY_WARNING: 'DISPLAY_WARNING',
HIDE_WARNING: 'HIDE_WARNING',
hideWarning: hideWarning,
// accounts screen
SET_SELECTED_ACCOUNT: 'SET_SELECTED_ACCOUNT',
SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL',
SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE',
SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE',
// account detail screen
SHOW_SEND_PAGE: 'SHOW_SEND_PAGE',
showSendPage: showSendPage,
REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT',
requestExportAccount: requestExportAccount,
EXPORT_ACCOUNT: 'EXPORT_ACCOUNT',
exportAccount: exportAccount,
SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY',
showPrivateKey: showPrivateKey,
// tx conf screen
COMPLETED_TX: 'COMPLETED_TX',
TRANSACTION_ERROR: 'TRANSACTION_ERROR',
NEXT_TX: 'NEXT_TX',
PREVIOUS_TX: 'PREV_TX',
setSelectedAddress: setSelectedAddress,
signTx: signTx,
sendTx: sendTx,
cancelTx: cancelTx,
completedTx: completedTx,
txError: txError,
nextTx: nextTx,
previousTx: previousTx,
// app messages
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',
USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER',
useEtherscanProvider: useEtherscanProvider,
showConfigPage: showConfigPage,
setRpcTarget: setRpcTarget,
// 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',
showLoadingIndication: showLoadingIndication,
hideLoadingIndication: hideLoadingIndication,
}
module.exports = actions
var _accountManager = null
function _setAccountManager(accountManager){
_accountManager = accountManager
}
// async actions
function tryUnlockMetamask(password) {
return (dispatch) => {
dispatch(this.unlockInProgress())
_accountManager.submitPassword(password, (err) => {
dispatch(this.hideLoadingIndication())
if (err) {
dispatch(this.unlockFailed())
} else {
dispatch(this.unlockMetamask())
dispatch(this.setSelectedAddress())
}
})
}
}
function createNewVault(password, entropy) {
return (dispatch) => {
dispatch(this.createNewVaultInProgress())
_accountManager.createNewVault(password, entropy, (err, result) => {
dispatch(this.showNewVaultSeed(result))
})
}
}
function recoverFromSeed(password, seed) {
return (dispatch) => {
// dispatch(this.createNewVaultInProgress())
dispatch(this.showLoadingIndication())
_accountManager.recoverFromSeed(password, seed, (err, result) => {
if (err) {
dispatch(this.hideLoadingIndication())
var message = err.message
return dispatch(this.displayWarning(err.message))
}
dispatch(this.unlockMetamask())
dispatch(this.setSelectedAddress())
dispatch(this.updateMetamaskState(result))
dispatch(this.hideLoadingIndication())
dispatch(this.showAccountsPage())
})
}
}
function showInfoPage() {
return {
type: this.SHOW_INFO_PAGE,
}
}
function setSelectedAddress(address) {
return (dispatch) => {
_accountManager.setSelectedAddress(address)
}
}
function signTx(txData) {
return (dispatch) => {
dispatch(this.showLoadingIndication())
web3.eth.sendTransaction(txData, (err, data) => {
dispatch(this.hideLoadingIndication())
if (err) return dispatch(this.displayWarning(err.message))
dispatch(this.hideWarning())
dispatch(this.showAccountsPage())
})
}
}
function sendTx(txData) {
return (dispatch) => {
_accountManager.approveTransaction(txData.id, (err) => {
if (err) {
alert(err.message)
dispatch(this.txError(err))
return console.error(err.message)
}
dispatch(this.completedTx(txData.id))
})
}
}
function completedTx(id) {
return {
type: this.COMPLETED_TX,
id,
}
}
function txError(err) {
return {
type: this.TRANSACTION_ERROR,
message: err.message,
}
}
function cancelTx(txData){
return (dispatch) => {
_accountManager.cancelTransaction(txData.id)
dispatch(this.showAccountsPage())
}
}
//
// initialize screen
//
function showCreateVault() {
return {
type: this.SHOW_CREATE_VAULT,
}
}
function showRestoreVault() {
return {
type: this.SHOW_RESTORE_VAULT,
}
}
function showInitializeMenu() {
return {
type: this.SHOW_INIT_MENU,
}
}
function createNewVaultInProgress() {
return {
type: this.CREATE_NEW_VAULT_IN_PROGRESS,
}
}
function showNewVaultSeed(seed) {
return {
type: this.SHOW_NEW_VAULT_SEED,
value: seed,
}
}
//
// unlock screen
//
function unlockInProgress() {
return {
type: this.UNLOCK_IN_PROGRESS,
}
}
function unlockFailed() {
return {
type: this.UNLOCK_FAILED,
}
}
function unlockMetamask() {
return {
type: this.UNLOCK_METAMASK,
}
}
function updateMetamaskState(newState) {
return {
type: this.UPDATE_METAMASK_STATE,
value: newState,
}
}
function lockMetamask() {
return (dispatch) => {
_accountManager.setLocked((err) => {
dispatch({
type: this.LOCK_METAMASK,
})
dispatch(this.hideLoadingIndication())
})
}
}
function showAccountDetail(address) {
return {
type: this.SHOW_ACCOUNT_DETAIL,
value: address,
}
}
function backToAccountDetail(address) {
return {
type: this.BACK_TO_ACCOUNT_DETAIL,
value: address,
}
}
function clearSeedWordCache() {
return {
type: this.CLEAR_SEED_WORD_CACHE
}
}
function confirmSeedWords() {
return (dispatch) => {
dispatch(this.showLoadingIndication())
_accountManager.clearSeedWordCache((err) => {
dispatch(this.clearSeedWordCache())
console.log('Seed word cache cleared.')
dispatch(this.setSelectedAddress())
})
}
}
function showAccountsPage() {
return {
type: this.SHOW_ACCOUNTS_PAGE,
}
}
function showConfTxPage() {
return {
type: this.SHOW_CONF_TX_PAGE,
}
}
function nextTx() {
return {
type: this.NEXT_TX,
}
}
function previousTx() {
return {
type: this.PREVIOUS_TX,
}
}
function showConfigPage() {
return {
type: this.SHOW_CONFIG_PAGE,
}
}
//
// config
//
function setRpcTarget(newRpc) {
_accountManager.setRpcTarget(newRpc)
return {
type: this.SET_RPC_TARGET,
value: newRpc,
}
}
function useEtherscanProvider() {
_accountManager.useEtherscanProvider()
return {
type: this.USE_ETHERSCAN_PROVIDER,
}
}
function showLoadingIndication() {
return {
type: this.SHOW_LOADING,
}
}
function hideLoadingIndication() {
return {
type: this.HIDE_LOADING,
}
}
function displayWarning(text) {
return {
type: this.DISPLAY_WARNING,
value: text,
}
}
function hideWarning() {
return {
type: this.HIDE_WARNING,
}
}
function requestExportAccount() {
return {
type: this.REQUEST_ACCOUNT_EXPORT,
}
}
function exportAccount(address) {
var self = this
return function(dispatch) {
dispatch(self.showLoadingIndication())
_accountManager.exportAccount(address, function(err, result) {
dispatch(self.hideLoadingIndication())
if (err) {
console.error(err)
return dispatch(self.displayWarning('Had a problem exporting the account.'))
}
dispatch(self.showPrivateKey(result))
})
}
}
function showPrivateKey(key) {
return {
type: this.SHOW_PRIVATE_KEY,
value: key,
}
}
function showSendPage() {
return {
type: this.SHOW_SEND_PAGE,
}
}

242
ui/app/app.js Normal file
View File

@ -0,0 +1,242 @@
const inherits = require('util').inherits
const React = require('react')
const Component = require('react').Component
const PropTypes = require('react').PropTypes
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const extend = require('xtend')
const actions = require('./actions')
const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
// init
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')
// unlock
const UnlockScreen = require('./unlock')
// accounts
const AccountsScreen = require('./accounts')
const AccountDetailScreen = require('./account-detail')
const SendTransactionScreen = require('./send')
const ConfirmTxScreen = require('./conf-tx')
// other views
const ConfigScreen = require('./config')
const InfoScreen = require('./info')
const LoadingIndicator = require('./loading')
module.exports = connect(mapStateToProps)(App)
inherits(App, Component)
function App() { Component.call(this) }
function mapStateToProps(state) {
return {
// state from plugin
isInitialized: state.metamask.isInitialized,
isUnlocked: state.metamask.isUnlocked,
currentView: state.appState.currentView,
activeAddress: state.appState.activeAddress,
transForward: state.appState.transForward,
seedWords: state.metamask.seedWords,
}
}
App.prototype.render = function() {
// const { selectedReddit, posts, isFetching, lastUpdated } = this.props
var state = this.props
var view = state.currentView.name
var transForward = state.transForward
var shouldHaveFooter = true
switch (view) {
case 'restoreVault':
shouldHaveFooter = false;
case 'createVault':
shouldHaveFooter = false;
case 'createVaultComplete':
shouldHaveFooter = false;
}
return (
h('.flex-column.flex-grow.full-height', {
style: {
// Windows was showing a vertical scroll bar:
overflowY: 'hidden',
}
},
[
h(LoadingIndicator),
// top row
h('.app-header.flex-column.flex-center', {
}, [
h('h1', 'MetaMask'),
]),
// panel content
h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), {
style: {
height: '380px',
}
}, [
h(ReactCSSTransitionGroup, {
transitionName: "main",
transitionEnterTimeout: 300,
transitionLeaveTimeout: 300,
}, [
this.renderPrimary(),
]),
]),
// footer
h('.app-footer.flex-row.flex-space-around', {
style: {
display: shouldHaveFooter ? 'flex' : 'none',
alignItems: 'center',
height: '56px',
}
}, [
// settings icon
h('i.fa.fa-cog.fa-lg' + (view === 'config' ? '.active' : '.cursor-pointer'), {
style: {
opacity: state.isUnlocked ? '1.0' : '0.0',
transition: 'opacity 200ms ease-in',
//transform: `translateX(${state.isUnlocked ? '0px' : '-100px'})`,
},
onClick: function(ev) {
state.dispatch(actions.showConfigPage())
},
}),
// toggle
onOffToggle({
toggleMetamaskActive: this.toggleMetamaskActive.bind(this),
isUnlocked: state.isUnlocked,
}),
// help
h('i.fa.fa-question.fa-lg.cursor-pointer', {
style: {
opacity: state.isUnlocked ? '1.0' : '0.0',
},
onClick() { state.dispatch(actions.showInfoPage()) }
}),
]),
])
)
}
App.prototype.toggleMetamaskActive = function(){
if (!this.props.isUnlocked) {
// currently inactive: redirect to password box
var passwordBox = document.querySelector('input[type=password]')
if (!passwordBox) return
passwordBox.focus()
} else {
// currently active: deactivate
this.props.dispatch(actions.lockMetamask(false))
}
}
App.prototype.renderPrimary = function(state){
var state = this.props
// If seed words haven't been dismissed yet, show them still.
/*
if (state.seedWords) {
return h(CreateVaultCompleteScreen, {key: 'createVaultComplete'})
}
*/
// show initialize screen
if (!state.isInitialized) {
// show current view
switch (state.currentView.name) {
case 'createVault':
return h(CreateVaultScreen, {key: 'createVault'})
case 'restoreVault':
return h(RestoreVaultScreen, {key: 'restoreVault'})
default:
return h(InitializeMenuScreen, {key: 'menuScreenInit'})
}
}
// show unlock screen
if (!state.isUnlocked) {
return h(UnlockScreen, {key: 'locked'})
}
// show current view
switch (state.currentView.name) {
case 'createVaultComplete':
return h(CreateVaultCompleteScreen, {key: 'created-vault'})
case 'accounts':
return h(AccountsScreen, {key: 'accounts'})
case 'accountDetail':
return h(AccountDetailScreen, {key: 'account-detail'})
case 'sendTransaction':
return h(SendTransactionScreen, {key: 'send-transaction'})
case 'confTx':
return h(ConfirmTxScreen, {key: 'confirm-tx'})
case 'config':
return h(ConfigScreen, {key: 'config'})
case 'info':
return h(InfoScreen, {key: 'info'})
case 'createVault':
return h(CreateVaultScreen, {key: 'createVault'})
default:
return h(AccountsScreen, {key: 'accounts'})
}
}
function onOffToggle(state){
var buttonSize = '50px';
var lockWidth = '20px';
return (
h('.app-toggle.flex-row.flex-center.lock' + (state.isUnlocked ? '.unlocked' : '.locked'), {
width: buttonSize,
height: buttonSize,
}, [
h('div', {
onClick: state.toggleMetamaskActive,
style: {
width: lockWidth,
height: '' + parseInt(lockWidth) * 1.5 + 'px',
position: 'relative',
}
}, [
h('img.lock-top', {
src: 'images/lock-top.png',
style: {
width: lockWidth,
position: 'absolute',
}
}),
h('img', {
src: 'images/lock-base.png',
style: {
width: lockWidth,
position: 'absolute',
}
}),
])
])
)
}

View File

@ -0,0 +1,93 @@
const inherits = require('util').inherits
const ethUtil = require('ethereumjs-util')
const Component = require('react').Component
const h = require('react-hyperscript')
const addressSummary = require('../util').addressSummary
const formatBalance = require('../util').formatBalance
module.exports = AccountPanel
inherits(AccountPanel, Component)
function AccountPanel() {
Component.call(this)
}
AccountPanel.prototype.render = function() {
var state = this.props
var identity = state.identity || {}
var account = state.account || {}
var isFauceting = state.isFauceting
return (
h('.identity-panel.flex-row.flex-space-between'+(state.isSelected?'.selected':''), {
style: {
flex: '1 0 auto',
},
onClick: state.onSelect && state.onSelect.bind(null, identity.address),
}, [
// account identicon
h('.identicon-wrapper.flex-column.select-none', [
h('.identicon', {
style: { backgroundImage: 'url("https://ipfs.io/ipfs/'+identity.img+'")' }
}),
h('span.font-small', identity.name),
]),
// account address, balance
h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-row.flex-space-between', [
h('label.font-small', 'ADDRESS'),
h('span.font-small', addressSummary(identity.address)),
]),
balanceOrFaucetingIndication(account, isFauceting),
// outlet for inserting additional stuff
state.children,
]),
// navigate to account detail
!state.onShowDetail ? null :
h('.arrow-right.cursor-pointer', {
onClick: state.onShowDetail && state.onShowDetail.bind(null, identity.address),
}, [
h('i.fa.fa-chevron-right.fa-lg'),
]),
])
)
}
function balanceOrFaucetingIndication(account, isFauceting) {
// Temporarily deactivating isFauceting indication
// because it shows fauceting for empty restored accounts.
if (/*isFauceting*/ false) {
return h('.flex-row.flex-space-between', [
h('span.font-small', {
}, [
'Account is auto-funding,',
h('br'),
'please wait.'
]),
])
} else {
return h('.flex-row.flex-space-between', [
h('label.font-small', 'BALANCE'),
h('span.font-small', {
style: {
overflowX: 'hidden',
maxWidth: '136px',
}
}, formatBalance(account.balance)),
])
}
}

View File

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

140
ui/app/conf-tx.js Normal file
View File

@ -0,0 +1,140 @@
const inherits = require('util').inherits
const Component = require('react').Component
const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const copyToClipboard = require('copy-to-clipboard')
const actions = require('./actions')
const AccountPanel = require('./components/account-panel')
const valuesFor = require('./util').valuesFor
const addressSummary = require('./util').addressSummary
const readableDate = require('./util').readableDate
const formatBalance = require('./util').formatBalance
const dataSize = require('./util').dataSize
module.exports = connect(mapStateToProps)(ConfirmTxScreen)
function mapStateToProps(state) {
return {
identities: state.metamask.identities,
accounts: state.metamask.accounts,
selectedAddress: state.metamask.selectedAddress,
unconfTxs: state.metamask.unconfTxs,
index: state.appState.currentView.context,
}
}
inherits(ConfirmTxScreen, Component)
function ConfirmTxScreen() {
Component.call(this)
}
ConfirmTxScreen.prototype.render = function() {
var state = this.props
var unconfTxList = valuesFor(state.unconfTxs).sort(tx => tx.time)
var txData = unconfTxList[state.index] || {}
var txParams = txData.txParams || {}
var address = txParams.from || state.selectedAddress
var identity = state.identities[address] || { address: address }
var account = state.accounts[address] || { address: address }
return (
h('.unconftx-section.flex-column.flex-grow', [
// subtitle and nav
h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: this.navigateToAccounts.bind(this),
}),
h('h2.page-subtitle', 'Confirm Transaction'),
]),
h('h3', {
style: {
alignSelf: 'center',
display: unconfTxList.length > 1 ? 'block' : 'none',
},
}, [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
style: {
display: state.index === 0 ? 'none' : 'inline-block',
},
onClick: () => state.dispatch(actions.previousTx()),
}),
` Transaction ${state.index + 1} of ${unconfTxList.length} `,
h('i.fa.fa-arrow-right.fa-lg.cursor-pointer', {
style: {
display: state.index + 1 === unconfTxList.length ? 'none' : 'inline-block',
},
onClick: () => state.dispatch(actions.nextTx()),
}),
]),
h(ReactCSSTransitionGroup, {
transitionName: "main",
transitionEnterTimeout: 300,
transitionLeaveTimeout: 300,
}, [
h('.transaction', {
key: txData.id,
}, [
// account that will sign
h(AccountPanel, {
showFullAddress: true,
identity: identity,
account: account,
}),
// tx data
h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-row.flex-space-between', [
h('label.font-small', 'TO ADDRESS'),
h('span.font-small', addressSummary(txParams.to)),
]),
h('.flex-row.flex-space-between', [
h('label.font-small', 'DATE'),
h('span.font-small', readableDate(txData.time)),
]),
h('.flex-row.flex-space-between', [
h('label.font-small', 'AMOUNT'),
h('span.font-small', formatBalance(txParams.value)),
]),
]),
// send + cancel
h('.flex-row.flex-space-around', [
h('button', {
onClick: this.cancelTransaction.bind(this, txData),
}, 'Cancel'),
h('button', {
onClick: this.sendTransaction.bind(this, txData),
}, 'Send'),
]),
]),
]),
]) // No comma or semicolon can go here
)
}
ConfirmTxScreen.prototype.sendTransaction = function(txData, event){
event.stopPropagation()
this.props.dispatch(actions.sendTx(txData))
}
ConfirmTxScreen.prototype.cancelTransaction = function(txData, event){
event.stopPropagation()
this.props.dispatch(actions.cancelTx(txData))
}
ConfirmTxScreen.prototype.navigateToAccounts = function(event){
event.stopPropagation()
this.props.dispatch(actions.showAccountsPage())
}

103
ui/app/config.js Normal file
View File

@ -0,0 +1,103 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('./actions')
module.exports = connect(mapStateToProps)(ConfigScreen)
function mapStateToProps(state) {
return {
rpc: state.metamask.rpcTarget,
metamask: state.metamask,
}
}
inherits(ConfigScreen, Component)
function ConfigScreen() {
Component.call(this)
}
ConfigScreen.prototype.render = function() {
var state = this.props
var rpc = state.rpc
var metamaskState = state.metamask
return (
h('.flex-column.flex-grow', [
// subtitle and nav
h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: (event) => {
state.dispatch(actions.showAccountsPage())
}
}),
h('h2.page-subtitle', 'Configuration'),
]),
// conf view
h('.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-space-around', {
style: {
padding: '20px',
}
}, [
currentProviderDisplay(metamaskState),
h('div', [
h('input', {
placeholder: 'New RPC URL',
style: {
width: '100%',
},
onKeyPress(event) {
if (event.key === 'Enter') {
var element = event.target
var newRpc = element.value
state.dispatch(actions.setRpcTarget(newRpc))
}
}
}),
]),
h('div', [
h('button', {
style: {
alignSelf: 'center',
},
onClick(event) {
event.preventDefault()
state.dispatch(actions.setRpcTarget('https://rpc.metamask.io/'))
}
}, 'Use Main Network')
]),
h('div', [
h('button', {
style: {
alignSelf: 'center',
},
onClick(event) {
event.preventDefault()
state.dispatch(actions.setRpcTarget('https://testrpc.metamask.io/'))
}
}, 'Use Morden Test Network')
]),
]),
]),
])
)
}
function currentProviderDisplay(metamaskState) {
var rpc = metamaskState.rpcTarget
return h('div', [
h('h3', {style: { fontWeight: 'bold' }}, 'Currently using RPC'),
h('p', rpc)
])
}

21
ui/app/css/debug.css Normal file
View File

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

2
ui/app/css/fonts.css Normal file
View File

@ -0,0 +1,2 @@
@import url(https://fonts.googleapis.com/css?family=Roboto:300,500);
@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css);

489
ui/app/css/index.css Normal file
View File

@ -0,0 +1,489 @@
/*
faint orange (textfield shades) #FAF6F0
light orange (button shades): #F5C26D
dark orange (text): #F5A623
borders/font/any gray: #4A4A4A
*/
/*
application specific styles
*/
* {
box-sizing: border-box;
}
html, body {
/*font-family: 'Open Sans', Arial, sans-serif;*/
font-family: 'Roboto', 'Noto', sans-serif;
color: #4D4D4D;
font-weight: 300;
line-height: 1.4em;
}
#app-content {
overflow-x: hidden;
min-width: 357px;
width: 360px;
height: 500px;
}
button {
outline: none;
cursor: pointer;
margin: 10px;
padding: 6px;
border: none;
border-radius: 3px;
background: #F7861C;
font-weight: 500;
color: white;
transform-origin: center center;
transition: transform 50ms ease-in;
}
button:hover {
transform: scale(1.1);
}
button:active {
transform: scale(0.95);
}
button.primary {
margin: 10px;
padding: 6px;
border: none;
border-radius: 3px;
background: #F7861C;
font-weight: 500;
color: white;
}
input, textarea {
width: 300px;
padding: 6px;
border-radius: 6px;
border-style: solid;
outline: none;
border: 1px solid #F5A623;
background: #FAF6F0;
}
a {
text-decoration: none;
color: inherit;
}
a:hover{
color: #df6b0e;
}
/*
app
*/
.active {
color: #909090;
}
button.btn-thin {
border: 1px solid;
border-color: #4D4D4D;
color: #4D4D4D;
background: rgb(255, 174, 41);
border-radius: 4px;
min-width: 200px;
margin: 12px 0;
padding: 6px;
font-size: 13px;
}
.app-header {
padding-top: 20px;
}
.app-header h1 {
font-size: 2em;
font-weight: 300;
height: 42px;
}
h2.page-subtitle {
font-size: 1em;
font-weight: 500;
height: 24px;
color: #F3C83E;
}
.app-primary {
}
.app-footer {
padding-bottom: 10px;
align-items: center;
}
.identicon {
height: 46px;
width: 46px;
background-size: cover;
border-radius: 100%;
border: 3px solid gray;
}
textarea.twelve-word-phrase {
margin-top: 20px;
width: 300px;
height: 180px;
font-size: 16px;
background: #FAF6F0;
resize: none;
}
/*
app sections
*/
/* initialize */
.initialize-screen hr {
width: 60px;
margin: 12px;
border-color: #F3C83E;
border-style: solid;
}
.initialize-screen input[type="password"], .initialize-screen textarea {
width: 300px;
padding: 6px;
border-radius: 6px;
border-style: solid;
outline: none;
border: 1px solid #F5A623;
background: #FAF6F0;
}
.initialize-screen label {
margin-top: 20px;
}
.initialize-screen button.create-vault {
margin-top: 40px;
}
.initialize-screen .warning {
font-size: 14px;
margin: 0 16px;
}
/* unlock */
.error {
color: #E20202;
}
.lock {
width: 50px;
height: 50px;
}
.lock.locked {
transform: scale(1.5);
opacity: 0.0;
transition: opacity 400ms ease-in, transform 400ms ease-in;
}
.lock.unlocked {
transform: scale(1);
opacity: 1;
transition: opacity 500ms ease-out, transform 500ms ease-out, background 200ms ease-in;
}
.lock.locked .lock-top {
transform: scaleX(1) translateX(0);
transition: transform 250ms ease-in;
}
.lock.unlocked .lock-top {
transform: scaleX(-1) translateX(-12px);
transition: transform 250ms ease-in;
}
.lock.unlocked:hover {
border-radius: 4px;
background: #e5e5e5;
border: 1px solid #b1b1b1;
}
.lock.unlocked:active {
background: #c3c3c3;
}
.section-title .fa-arrow-left {
margin: -2px 8px 0px -8px;
}
.unlock-screen label {
color: #F3C83E;
font-weight: 500;
}
.unlock-screen input[type=password] {
width: 60%;
height: 22px;
padding: 2px;
border-radius: 4px;
border: 2px solid #F3C83E;
background: #FAF6F0;
}
.unlock-screen input[type=password]:focus {
outline: none;
border: 3px solid #F3C83E;
}
/* accounts */
.accounts-section {
margin: 0 20px;
}
.current-domain-panel {
border: 1px solid #B7B7B7;
}
.unconftx-link {
margin-top: 24px;
cursor: pointer;
}
.unconftx-link .fa-arrow-right {
margin: 0px -8px 0px 8px;
}
/* identity panel */
.identity-panel {
font-weight: 500;
}
.identity-panel .identicon-wrapper {
margin: 4px;
margin-top: 8px;
}
.identity-panel .identicon-wrapper span {
margin: 0 auto;
}
.identity-panel .identity-data {
margin: 8px 8px 8px 18px;
}
.identity-panel i {
margin-top: 32px;
margin-right: 6px;
color: #B9B9B9;
}
.identity-panel .arrow-right {
padding-left: 18px;
width: 42px;
min-width: 18px;
height: 100%;
}
/* accounts screen */
.identity-section {
border: 2px solid #4D4D4D;
margin: 0;
}
.identity-section .identity-panel {
background: #E9E9E9;
border-bottom: 1px solid #B1B1B1;
cursor: pointer;
}
.identity-section .identity-panel:hover {
background: #F9F9F9;
}
.identity-section .identity-panel.selected {
background: white;
color: #F3C83E;
}
.identity-section .identity-panel.selected .identicon {
border-color: orange;
}
/* account detail screen */
.account-detail-section {
margin: 0 20px;
}
/* tx confirm */
.unconftx-section {
margin: 0 20px;
}
.unconftx-section input[type=password] {
height: 22px;
padding: 2px;
margin: 12px;
margin-bottom: 24px;
border-radius: 4px;
border: 2px solid #F3C83E;
background: #FAF6F0;
}
/*
react toggle
*/
/* overrides */
.react-toggle-track-check {
display: none;
}
.react-toggle-track-x {
display: none;
}
/* modified original */
.react-toggle {
display: inline-block;
position: relative;
cursor: pointer;
background-color: transparent;
border: 0;
padding: 0;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
-webkit-tap-highlight-color: transparent;
}
.react-toggle-screenreader-only {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
.react-toggle--disabled {
opacity: 0.5;
-webkit-transition: opacity 0.25s;
transition: opacity 0.25s;
}
.react-toggle-track {
width: 50px;
height: 24px;
padding: 0;
border-radius: 30px;
background-color: #4D4D4D;
-webkit-transition: all 0.2s ease;
-moz-transition: all 0.2s ease;
transition: all 0.2s ease;
}
.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track {
background-color: #000000;
}
.react-toggle--checked .react-toggle-track {
background-color: rgb(255, 174, 41);
}
.react-toggle.react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track {
background-color: rgb(243, 151, 0);
}
.react-toggle-track-check {
position: absolute;
width: 14px;
height: 10px;
top: 0px;
bottom: 0px;
margin-top: auto;
margin-bottom: auto;
line-height: 0;
left: 8px;
opacity: 0;
-webkit-transition: opacity 0.25s ease;
-moz-transition: opacity 0.25s ease;
transition: opacity 0.25s ease;
}
.react-toggle--checked .react-toggle-track-check {
opacity: 1;
-webkit-transition: opacity 0.25s ease;
-moz-transition: opacity 0.25s ease;
transition: opacity 0.25s ease;
}
.react-toggle-track-x {
position: absolute;
width: 10px;
height: 10px;
top: 0px;
bottom: 0px;
margin-top: auto;
margin-bottom: auto;
line-height: 0;
right: 10px;
opacity: 1;
-webkit-transition: opacity 0.25s ease;
-moz-transition: opacity 0.25s ease;
transition: opacity 0.25s ease;
}
.react-toggle--checked .react-toggle-track-x {
opacity: 0;
}
.react-toggle-thumb {
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms;
position: absolute;
top: 1px;
left: 1px;
width: 22px;
height: 22px;
border: 1px solid #4D4D4D;
border-radius: 50%;
background-color: #FAFAFA;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-webkit-transition: all 0.25s ease;
-moz-transition: all 0.25s ease;
transition: all 0.25s ease;
}
.react-toggle--checked .react-toggle-thumb {
left: 27px;
border-color: #828282;
}
/*
.react-toggle--focus .react-toggle-thumb {
-webkit-box-shadow: 0px 0px 3px 2px #0099E0;
-moz-box-shadow: 0px 0px 3px 2px #0099E0;
box-shadow: 0px 0px 2px 3px #0099E0;
}
.react-toggle:active .react-toggle-thumb {
-webkit-box-shadow: 0px 0px 5px 5px #0099E0;
-moz-box-shadow: 0px 0px 5px 5px #0099E0;
box-shadow: 0px 0px 5px 5px #0099E0;
}

143
ui/app/css/lib.css Normal file
View File

@ -0,0 +1,143 @@
/* lib */
.full-width {
width: 100%;
}
.full-height {
height: 100%;
}
.flex-column {
display: flex;
flex-direction: column;
}
.flex-column-bottom {
display: flex;
flex-direction: column-reverse;
}
.flex-row {
display: flex;
flex-direction: row;
}
.flex-space-between {
justify-content: space-between;
}
.flex-space-around {
justify-content: space-around;
}
.flex-right {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.flex-left {
display: flex;
flex-direction: row;
justify-content: flex-start;
}
.flex-fixed {
flex: none;
}
.flex-grow {
flex: 1 1 auto;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.flex-justify-center {
justify-content: center;
}
.flex-align-center {
align-items: center;
}
.flex-self-end {
align-self: flex-end;
}
.flex-self-stretch {
align-self: stretch;
}
.flex-vertical {
flex-direction: column;
}
.z-bump {
z-index: 1;
}
.select-none {
cursor: default;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.cursor-pointer {
cursor: pointer;
transform-origin: center center;
transition: transform 50ms ease-in-out;
}
.cursor-pointer:hover {
transform: scale(1.1);
}
.cursor-pointer:active {
transform: scale(0.95);
}
.margin-bottom-sml {
margin-bottom: 20px;
}
.margin-bottom-med {
margin-bottom: 40px;
}
.margin-right-left {
margin: 0 20px;
}
.bold {
font-weight: bold;
}
.font-small {
font-size: 12px;
}
/* Send Screen */
.send-screen {
margin: 0 20px;
}
.send-screen section {
margin: 7px;
display: flex;
flex-direction: row;
justify-content: center;
}
.send-screen details {
width: 100%;
}
.send-screen section input {
width: 100%;
}

48
ui/app/css/reset.css Normal file
View File

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

View File

@ -0,0 +1,47 @@
/* initial positions */
.app-primary.from-right .main-enter {
transform: translateX(400px);
position: absolute;
width: 100%;
transition: transform 300ms ease-in-out;
}
.app-primary.from-left .main-enter {
transform: translateX(-400px);
position: absolute;
width: 100%;
transition: transform 300ms ease-in-out;
}
/* center position */
.app-primary .main-enter.main-enter-active,
.app-primary .main-leave {
transform: translateX(0px);
position: absolute;
width: 100%;
transition: transform 300ms ease-in-out;
}
/* final positions */
.app-primary.from-left .main-leave-active {
transform: translateX(400px);
position: absolute;
width: 100%;
transition: transform 300ms ease-in-out;
}
.app-primary.from-right .main-leave-active {
transform: translateX(-400px);
position: absolute;
width: 100%;
transition: transform 300ms ease-in-out;
}
/* loader transitions */
.loader-enter, .loader-leave-active {
opacity: 0.0;
transition: opacity 150 ease-in-out;
}
.loader-enter-active, .loader-leave {
opacity: 1.0;
transition: opacity 150 ease-in-out;
}

View File

@ -0,0 +1,57 @@
const inherits = require('util').inherits
const Component = require('react').Component
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const actions = require('../actions')
module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen)
inherits(CreateVaultCompleteScreen, Component)
function CreateVaultCompleteScreen() {
Component.call(this)
}
function mapStateToProps(state) {
return {
seed: state.appState.currentView.context,
cachedSeed: state.metamask.seedWords,
}
}
CreateVaultCompleteScreen.prototype.render = function() {
var state = this.props
var seed = state.seed || state.cachedSeed
return (
h('.initialize-screen.flex-column.flex-center.flex-grow', [
// subtitle and nav
h('.section-title.flex-row.flex-center', [
h('h2.page-subtitle', 'Vault Created'),
]),
h('span.error', { // Error for the right red
style: {
padding: '12px 20px 0px 20px',
textAlign: 'center',
}
}, 'These 12 words can restore all of your MetaMask accounts for this vault.\nSave them somewhere safe and secret.'),
h('textarea.twelve-word-phrase', {
readOnly: true,
value: seed,
}),
h('button.btn-thin', {
onClick: () => this.confirmSeedWords(),
}, 'I\'ve copied it somewhere safe.'),
])
)
}
CreateVaultCompleteScreen.prototype.confirmSeedWords = function() {
this.props.dispatch(actions.confirmSeedWords())
}

View File

@ -0,0 +1,123 @@
const inherits = require('util').inherits
const Component = require('react').Component
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const actions = require('../actions')
module.exports = connect(mapStateToProps)(CreateVaultScreen)
inherits(CreateVaultScreen, Component)
function CreateVaultScreen() {
Component.call(this)
}
function mapStateToProps(state) {
return {
warning: state.appState.warning,
}
}
CreateVaultScreen.prototype.render = function() {
var state = this.props
return (
h('.initialize-screen.flex-column.flex-center.flex-grow', [
// subtitle and nav
h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: this.showInitializeMenu.bind(this),
}),
h('h2.page-subtitle', 'Create Vault'),
]),
// password
h('label', {
htmlFor: 'password-box',
}, 'Enter Password (min 8 chars):'),
h('input', {
type: 'password',
id: 'password-box',
}),
// confirm password
h('label', {
htmlFor: 'password-box-confirm',
}, 'Confirm Password:'),
h('input', {
type: 'password',
id: 'password-box-confirm',
onKeyPress: this.createVaultOnEnter.bind(this),
}),
/* ENTROPY TEXT INPUT CURRENTLY DISABLED
// entropy
h('label', {
htmlFor: 'entropy-text-entry',
}, 'Enter random text (optional)'),
h('textarea', {
id: 'entropy-text-entry',
style: { resize: 'none' },
onKeyPress: this.createVaultOnEnter.bind(this),
}),
*/
// submit
h('button.create-vault.btn-thin', {
onClick: this.createNewVault.bind(this),
}, 'OK'),
(!state.inProgress && state.warning) && (
h('span.in-progress-notification', state.warning)
),
state.inProgress && (
h('span.in-progress-notification', 'Generating Seed...')
),
])
)
}
CreateVaultScreen.prototype.componentDidMount = function(){
document.getElementById('password-box').focus()
}
CreateVaultScreen.prototype.showInitializeMenu = function() {
this.props.dispatch(actions.showInitializeMenu())
}
// create vault
CreateVaultScreen.prototype.createVaultOnEnter = function(event) {
if (event.key === 'Enter') {
event.preventDefault()
this.createNewVault()
}
}
CreateVaultScreen.prototype.createNewVault = function(){
var passwordBox = document.getElementById('password-box')
var password = passwordBox.value
var passwordConfirmBox = document.getElementById('password-box-confirm')
var passwordConfirm = passwordConfirmBox.value
// var entropy = document.getElementById('entropy-text-entry').value
if (password.length < 8) {
this.warning = 'password not long enough'
this.props.dispatch(actions.displayWarning(this.warning))
return
}
if (password !== passwordConfirm) {
this.warning = 'passwords dont match'
this.props.dispatch(actions.displayWarning(this.warning))
return
}
this.props.dispatch(actions.createNewVault(password, ''/*entropy*/))
}

View File

@ -0,0 +1,123 @@
const inherits = require('util').inherits
const EventEmitter = require('events').EventEmitter
const Component = require('react').Component
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const getCaretCoordinates = require('textarea-caret')
const Mascot = require('../components/mascot')
const actions = require('../actions')
const CreateVaultScreen = require('./create-vault')
const CreateVaultCompleteScreen = require('./create-vault-complete')
module.exports = connect(mapStateToProps)(InitializeMenuScreen)
inherits(InitializeMenuScreen, Component)
function InitializeMenuScreen() {
Component.call(this)
this.animationEventEmitter = new EventEmitter()
}
function mapStateToProps(state) {
return {
// state from plugin
currentView: state.appState.currentView,
}
}
InitializeMenuScreen.prototype.render = function() {
var state = this.props
switch (state.currentView.name) {
case 'createVault':
return h(CreateVaultScreen)
case 'createVaultComplete':
return h(CreateVaultCompleteScreen)
case 'restoreVault':
return this.renderRestoreVault()
default:
return this.renderMenu()
}
}
// InitializeMenuScreen.prototype.componentDidMount = function(){
// document.getElementById('password-box').focus()
// }
InitializeMenuScreen.prototype.renderMenu = function() {
var state = this.props
return (
h('.initialize-screen.flex-column.flex-center.flex-grow', [
h('h2.page-subtitle', 'Welcome!'),
h(Mascot, {
animationEventEmitter: this.animationEventEmitter,
}),
h('button.btn-thin', {
onClick: this.showCreateVault.bind(this),
}, 'Create New Vault'),
h('.flex-row.flex-center.flex-grow', [
h('hr'),
h('div', 'OR'),
h('hr'),
]),
h('button.btn-thin', {
onClick: this.showRestoreVault.bind(this),
}, 'Restore Existing Vault'),
])
)
}
InitializeMenuScreen.prototype.renderRestoreVault = function() {
var state = this.props
return (
h('.initialize-screen.flex-column.flex-center.flex-grow', [
// subtitle and nav
h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: this.showInitializeMenu.bind(this),
}),
h('h2.page-subtitle', 'Restore Vault'),
]),
h('h3', 'Coming soon....'),
// h('textarea.twelve-word-phrase', {
// value: 'hey ho what the actual hello rubber duck bumbersnatch crumplezone frankenfurter',
// }),
])
)
}
// InitializeMenuScreen.prototype.splitWor = function() {
// this.props.dispatch(actions.showInitializeMenu())
// }
InitializeMenuScreen.prototype.showInitializeMenu = function() {
this.props.dispatch(actions.showInitializeMenu())
}
InitializeMenuScreen.prototype.showCreateVault = function() {
this.props.dispatch(actions.showCreateVault())
}
InitializeMenuScreen.prototype.showRestoreVault = function() {
this.props.dispatch(actions.showRestoreVault())
}

View File

@ -0,0 +1,116 @@
const inherits = require('util').inherits
const Component = require('react').Component
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const actions = require('../actions')
module.exports = connect(mapStateToProps)(RestoreVaultScreen)
inherits(RestoreVaultScreen, Component)
function RestoreVaultScreen() {
Component.call(this)
}
function mapStateToProps(state) {
return {
warning: state.appState.warning,
}
}
RestoreVaultScreen.prototype.render = function() {
var state = this.props
return (
h('.initialize-screen.flex-column.flex-center.flex-grow', [
// subtitle and nav
h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: this.showInitializeMenu.bind(this),
}),
h('h2.page-subtitle', 'Restore Vault'),
]),
// wallet seed entry
h('h3', 'Wallet Seed'),
h('textarea.twelve-word-phrase', {
placeholder: 'Enter your secret twelve word phrase here to restore your vault.'
}),
// password
h('label', {
htmlFor: 'password-box',
}, 'New Password (min 8 chars):'),
h('input', {
type: 'password',
id: 'password-box',
}),
// confirm password
h('label', {
htmlFor: 'password-box-confirm',
}, 'Confirm Password:'),
h('input', {
type: 'password',
id: 'password-box-confirm',
onKeyPress: this.onMaybeCreate.bind(this),
}),
(state.warning) && (
h('span.error.in-progress-notification', state.warning)
),
// submit
h('button.btn-thin', {
onClick: this.restoreVault.bind(this),
}, 'I\'ve double checked the 12 word phrase.'),
])
)
}
RestoreVaultScreen.prototype.showInitializeMenu = function() {
this.props.dispatch(actions.showInitializeMenu())
}
RestoreVaultScreen.prototype.onMaybeCreate = function(event) {
if (event.key === 'Enter') {
this.restoreVault()
}
}
RestoreVaultScreen.prototype.restoreVault = function(){
// check password
var passwordBox = document.getElementById('password-box')
var password = passwordBox.value
var passwordConfirmBox = document.getElementById('password-box-confirm')
var passwordConfirm = passwordConfirmBox.value
if (password.length < 8) {
this.warning = 'Password not long enough'
this.props.dispatch(actions.displayWarning(this.warning))
return
}
if (password !== passwordConfirm) {
this.warning = 'Passwords don\'t match'
this.props.dispatch(actions.displayWarning(this.warning))
return
}
// check seed
var seedBox = document.querySelector('textarea.twelve-word-phrase')
var seed = seedBox.value.trim()
if (seed.split(' ').length !== 12) {
this.warning = 'seed phrases are 12 words long'
this.props.dispatch(actions.displayWarning(this.warning))
return
}
// submit
this.warning = null
this.props.dispatch(actions.displayWarning(this.warning))
this.props.dispatch(actions.recoverFromSeed(password, seed))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

90
ui/app/info.js Normal file
View File

@ -0,0 +1,90 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('./actions')
module.exports = connect(mapStateToProps)(InfoScreen)
function mapStateToProps(state) {
return {}
}
inherits(InfoScreen, Component)
function InfoScreen() {
Component.call(this)
}
InfoScreen.prototype.render = function() {
var state = this.props
var rpc = state.rpc
return (
h('.flex-column.flex-grow', [
// subtitle and nav
h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: (event) => {
state.dispatch(actions.showAccountsPage())
}
}),
h('h2.page-subtitle', 'Info'),
]),
// main view
h('.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-space-around', {
style: {
padding: '20px',
}
}, [
h('div', [
h('a', {
href: 'https://consensys.slack.com/archives/team-metamask',
target: '_blank',
onClick(event) { this.navigateTo(event.target.href) },
}, 'Join the conversation on Slack'),
]),
h('div', [
h('a', {
href: 'https://metamask.io/',
target: '_blank',
onClick(event) { this.navigateTo(event.target.href) },
}, 'Visit our web site'),
]),
h('div', [
h('a', {
href: 'https://twitter.com/metamask_io',
target: '_blank',
onClick(event) { this.navigateTo(event.target.href) },
}, 'Follow us on Twitter'),
]),
h('div', [
h('a', {
href: 'mailto:hello@metamask.io?subject=Feedback',
target: '_blank',
}, 'Email us any questions or comments!'),
]),
h('div', [
h('a', {
href: 'https://github.com/metamask/talk/issues',
target: '_blank',
onClick(event) { this.navigateTo(event.target.href) },
}, 'Start a thread on Github'),
]),
]),
]),
])
)
}
InfoScreen.prototype.navigateTo = function(url) {
chrome.tabs.create({ url });
}

51
ui/app/loading.js Normal file
View File

@ -0,0 +1,51 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('./actions')
const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
module.exports = connect(mapStateToProps)(LoadingIndicator)
function mapStateToProps(state) {
return {
isLoading: state.appState.isLoading,
}
}
inherits(LoadingIndicator, Component)
function LoadingIndicator() {
Component.call(this)
}
LoadingIndicator.prototype.render = function() {
console.dir(this.props)
var isLoading = this.props.isLoading
return (
h(ReactCSSTransitionGroup, {
transitionName: "loader",
transitionEnterTimeout: 150,
transitionLeaveTimeout: 150,
}, [
isLoading ? h('div', {
style: {
position: 'absolute',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
width: '100%',
background: 'rgba(255, 255, 255, 0.5)',
}
}, [
h('img', {
src: 'images/loading.svg',
}),
]) : null,
])
)
}

41
ui/app/reducers.js Normal file
View File

@ -0,0 +1,41 @@
const combineReducers = require('redux').combineReducers
const actions = require('./actions')
const extend = require('xtend')
//
// Sub-Reducers take in the complete state and return their sub-state
//
const reduceIdentities = require('./reducers/identities')
const reduceMetamask = require('./reducers/metamask')
const reduceApp = require('./reducers/app')
module.exports = rootReducer
function rootReducer(state, action) {
// clone
state = extend(state)
//
// Identities
//
state.identities = reduceIdentities(state, action)
//
// MetaMask
//
state.metamask = reduceMetamask(state, action)
//
// AppState
//
state.appState = reduceApp(state, action)
return state
}

281
ui/app/reducers/app.js Normal file
View File

@ -0,0 +1,281 @@
const extend = require('xtend')
const actions = require('../actions')
module.exports = reduceApp
function reduceApp(state, action) {
// clone and defaults
var defaultView = {
name: 'accounts',
detailView: null,
}
// confirm seed words
var seedConfView = {
name: 'createVaultComplete',
}
var seedWords = state.metamask.seedWords
var appState = extend({
currentView: seedWords ? seedConfView : defaultView,
currentDomain: 'example.com',
transForward: true, // Used to render transition direction
isLoading: false, // Used to display loading indicator
warning: null, // Used to display error text
}, state.appState)
switch (action.type) {
// intialize
case actions.SHOW_CREATE_VAULT:
return extend(appState, {
currentView: {
name: 'createVault',
},
transForward: true,
warning: null,
})
case actions.SHOW_RESTORE_VAULT:
return extend(appState, {
currentView: {
name: 'restoreVault',
},
transForward: true,
})
case actions.SHOW_INIT_MENU:
return extend(appState, {
currentView: defaultView,
transForward: false,
})
case actions.SHOW_CONFIG_PAGE:
return extend(appState, {
currentView: {
name: 'config',
},
transForward: true,
})
case actions.SHOW_INFO_PAGE:
return extend(appState, {
currentView: {
name: 'info',
},
transForward: true,
})
case actions.CREATE_NEW_VAULT_IN_PROGRESS:
return extend(appState, {
currentView: {
name: 'createVault',
inProgress: true,
},
transForward: true,
isLoading: true,
})
case actions.SHOW_NEW_VAULT_SEED:
return extend(appState, {
currentView: {
name: 'createVaultComplete',
context: action.value,
},
transForward: true,
isLoading: false,
})
case actions.SHOW_SEND_PAGE:
return extend(appState, {
currentView: {
name: 'sendTransaction',
context: appState.currentView.context,
},
transForward: true,
warning: null,
})
// unlock
case actions.UNLOCK_METAMASK:
return extend(appState, {
transForward: true,
warning: null,
})
case actions.LOCK_METAMASK:
return extend(appState, {
currentView: defaultView,
transForward: false,
warning: null,
})
// accounts
case actions.SET_SELECTED_ACCOUNT:
return extend(appState, {
activeAddress: action.value,
})
case actions.SHOW_ACCOUNT_DETAIL:
return extend(appState, {
currentView: {
name: 'accountDetail',
context: action.value,
},
accountDetail: {
accountExport: 'none',
privateKey: '',
},
transForward: true,
})
case actions.BACK_TO_ACCOUNT_DETAIL:
return extend(appState, {
currentView: {
name: 'accountDetail',
context: action.value,
},
accountDetail: {
accountExport: 'none',
privateKey: '',
},
transForward: false,
})
case actions.SHOW_ACCOUNTS_PAGE:
var seedWords = state.metamask.seedWords
return extend(appState, {
currentView: {
name: seedWords ? 'createVaultComplete' : 'accounts',
},
transForward: appState.currentView.name == 'locked',
isLoading: false,
warning: null,
})
case actions.SHOW_CONF_TX_PAGE:
return extend(appState, {
currentView: {
name: 'confTx',
context: 0,
},
transForward: true,
warning: null,
})
case actions.COMPLETED_TX:
var unconfTxs = Object.keys(state.metamask.unconfTxs).filter(tx => tx !== tx.id)
if (unconfTxs && unconfTxs.length > 0) {
return extend(appState, {
transForward: false,
currentView: {
name: 'confTx',
context: 0,
},
warning: null,
})
} else {
return extend(appState, {
transForward: false,
currentView: {
name: 'accounts',
context: 0,
},
transForward: false,
warning: null,
})
}
case actions.NEXT_TX:
return extend(appState, {
transForward: true,
currentView: {
name: 'confTx',
context: ++appState.currentView.context,
warning: null,
}
})
case actions.PREVIOUS_TX:
return extend(appState, {
transForward: false,
currentView: {
name: 'confTx',
context: --appState.currentView.context,
warning: null,
}
})
case actions.TRANSACTION_ERROR:
return extend(appState, {
currentView: {
name: 'confTx',
errorMessage: 'There was a problem submitting this transaction.',
},
})
case actions.UNLOCK_FAILED:
return extend(appState, {
warning: 'Incorrect password. Try again.'
})
case actions.SHOW_LOADING:
return extend(appState, {
isLoading: true,
})
case actions.HIDE_LOADING:
return extend(appState, {
isLoading: false,
})
case actions.CLEAR_SEED_WORD_CACHE:
return extend(appState, {
transForward: true,
currentView: {
name: 'accounts',
},
isLoading: false,
})
case actions.DISPLAY_WARNING:
return extend(appState, {
warning: action.value,
})
case actions.HIDE_WARNING:
return extend(appState, {
warning: undefined,
})
case actions.REQUEST_ACCOUNT_EXPORT:
return extend(appState, {
accountDetail: {
accountExport: 'requested',
},
})
case actions.EXPORT_ACCOUNT:
return extend(appState, {
accountDetail: {
accountExport: 'completed',
},
})
case actions.SHOW_PRIVATE_KEY:
return extend(appState, {
accountDetail: {
accountExport: 'completed',
privateKey: action.value,
},
})
default:
return appState
}
}

View File

@ -0,0 +1,18 @@
const extend = require('xtend')
const actions = require('../actions')
module.exports = reduceIdentities
function reduceIdentities(state, action) {
// clone + defaults
var idState = extend({
}, state.identities)
switch (action.type) {
default:
return idState
}
}

View File

@ -0,0 +1,73 @@
const extend = require('xtend')
const actions = require('../actions')
module.exports = reduceMetamask
function reduceMetamask(state, action) {
// clone + defaults
var metamaskState = extend({
isInitialized: false,
isUnlocked: false,
currentDomain: 'example.com',
rpcTarget: 'https://rawtestrpc.metamask.io/',
identities: {},
unconfTxs: {},
}, state.metamask)
switch (action.type) {
case actions.SHOW_ACCOUNTS_PAGE:
var state = extend(metamaskState)
delete state.seedWords
return state
case actions.UPDATE_METAMASK_STATE:
return extend(metamaskState, action.value)
case actions.UNLOCK_METAMASK:
return extend(metamaskState, {
isUnlocked: true,
isInitialized: true,
})
case actions.LOCK_METAMASK:
return extend(metamaskState, {
isUnlocked: false,
})
case actions.SET_RPC_TARGET:
return extend(metamaskState, {
rpcTarget: action.value,
})
case actions.COMPLETED_TX:
var stringId = String(action.id)
var newState = extend(metamaskState, {
unconfTxs: {}
})
for (var id in metamaskState.unconfTxs) {
if (id !== stringId) {
newState.unconfTxs[id] = metamaskState.unconfTxs[id]
}
}
return newState
case actions.CLEAR_SEED_WORD_CACHE:
var newState = extend(metamaskState, {
isInitialized: true,
})
delete newState.seedWords
return newState
case actions.CREATE_NEW_VAULT_IN_PROGRESS:
return extend(metamaskState, {
isUnlocked: true,
isInitialized: true,
})
default:
return metamaskState
}
}

24
ui/app/root.js Normal file
View File

@ -0,0 +1,24 @@
const inherits = require('util').inherits
const React = require('react')
const Component = require('react').Component
const Provider = require('react-redux').Provider
const h = require('react-hyperscript')
const App = require('./app')
module.exports = Root
inherits(Root, Component)
function Root() { Component.call(this) }
Root.prototype.render = function() {
return (
h(Provider, {
store: this.props.store,
}, [
h(App)
])
)
}

139
ui/app/send.js Normal file
View File

@ -0,0 +1,139 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('./actions')
const util = require('./util')
const numericBalance = require('./util').numericBalance
const AccountPanel = require('./components/account-panel')
const ethUtil = require('ethereumjs-util')
module.exports = connect(mapStateToProps)(SendTransactionScreen)
function mapStateToProps(state) {
var result = {
address: state.appState.currentView.context,
accounts: state.metamask.accounts,
identities: state.metamask.identities,
warning: state.appState.warning,
}
result.account = result.accounts[result.address]
result.identity = result.identities[result.address]
result.balance = result.account ? numericBalance(result.account.balance) : null
return result
}
inherits(SendTransactionScreen, Component)
function SendTransactionScreen() {
Component.call(this)
}
SendTransactionScreen.prototype.render = function() {
var state = this.props
var account = state.account
var identity = state.identity
return (
h('.send-screen.flex-column.flex-grow', [
// subtitle and nav
h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: this.back.bind(this),
}),
h('h2.page-subtitle', 'Send Transaction'),
]),
h(AccountPanel, {
showFullAddress: true,
identity: identity,
account: account,
}),
h('section.recipient', [
h('input.address', {
placeholder: 'Recipient Address',
})
]),
h('section.ammount', [
h('input.ether', {
placeholder: 'Amount',
type: 'number',
style: { marginRight: '6px' }
}),
h('select.currency', {
name: 'currency',
}, [
h('option', { value: 'ether' }, 'Ether (1e18 wei)'),
h('option', { value: 'wei' }, 'Wei'),
]),
]),
h('section.data', [
h('details', [
h('summary', {
style: {cursor: 'pointer'},
}, 'Advanced'),
h('textarea.txData', {
type: 'textarea',
placeholder: 'Transaction data (optional)',
style: {
height: '100px',
width: '100%',
resize: 'none',
}
})
])
]),
h('section', {
}, [
h('button', {
onClick: this.onSubmit.bind(this),
}, 'Send')
]),
state.warning ? h('span.error', state.warning) : null,
])
)
}
SendTransactionScreen.prototype.back = function() {
var address = this.props.address
this.props.dispatch(actions.backToAccountDetail(address))
}
SendTransactionScreen.prototype.onSubmit = function(event) {
var recipient = document.querySelector('input.address').value
var amount = new ethUtil.BN(document.querySelector('input.ether').value, 10)
var currency = document.querySelector('select.currency').value
var txData = document.querySelector('textarea.txData').value
var value = util.normalizeToWei(amount, currency)
var balance = this.props.balance
if (value.gt(balance)) {
var message = 'Insufficient funds.'
return this.props.dispatch(actions.displayWarning(message))
}
if (recipient.length !== 42) {
var message = 'Recipient address is the incorrect length.'
return this.props.dispatch(actions.displayWarning(message))
}
this.props.dispatch(actions.hideWarning())
this.props.dispatch(actions.showLoadingIndication())
var txParams = {
to: recipient,
from: this.props.address,
value: '0x' + value.toString(16),
}
if (txData) txParams.data = txData
this.props.dispatch(actions.signTx(txParams))
}

69
ui/app/settings.js Normal file
View File

@ -0,0 +1,69 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const copyToClipboard = require('copy-to-clipboard')
const actions = require('./actions')
const AccountPanel = require('./components/account-panel')
module.exports = connect(mapStateToProps)(AppSettingsPage)
function mapStateToProps(state) {
return {
identities: state.metamask.identities,
address: state.appState.currentView.context,
}
}
inherits(AppSettingsPage, Component)
function AppSettingsPage() {
Component.call(this)
}
AppSettingsPage.prototype.render = function() {
var state = this.props
var identity = state.identities[state.address]
return (
h('.account-detail-section.flex-column.flex-grow', [
// subtitle and nav
h('.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: this.navigateToAccounts.bind(this),
}),
h('h2.page-subtitle', 'Settings'),
]),
h('label', {
htmlFor: 'settings-rpc-endpoint',
}, 'RPC Endpoint:'),
h('input', {
// value: '//testrpc.metamask.io',
type: 'url',
id: 'settings-rpc-endpoint',
onKeyPress: this.onKeyPress.bind(this),
}),
])
)
}
AppSettingsPage.prototype.componentDidMount = function(){
document.querySelector('input').focus()
}
AppSettingsPage.prototype.onKeyPress = function(event) {
// get submit event
if (event.key === 'Enter') {
// this.submitPassword(event)
}
}
AppSettingsPage.prototype.navigateToAccounts = function(event){
event.stopPropagation()
this.props.dispatch(actions.showAccountsPage())
}

19
ui/app/store.js Normal file
View File

@ -0,0 +1,19 @@
const createStore = require('redux').createStore
const applyMiddleware = require('redux').applyMiddleware
const thunkMiddleware = require('redux-thunk')
const createLogger = require('redux-logger')
const rootReducer = require('./reducers')
module.exports = configureStore
const loggerMiddleware = createLogger()
const createStoreWithMiddleware = applyMiddleware(
thunkMiddleware,
loggerMiddleware
)(createStore)
function configureStore(initialState) {
return createStoreWithMiddleware(rootReducer, initialState)
}

31
ui/app/template.js Normal file
View File

@ -0,0 +1,31 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('./actions')
module.exports = connect(mapStateToProps)(COMPONENTNAME)
function mapStateToProps(state) {
return {}
}
inherits(COMPONENTNAME, Component)
function COMPONENTNAME() {
Component.call(this)
}
COMPONENTNAME.prototype.render = function() {
var state = this.props
var rpc = state.rpc
return (
h('div', {
style: {
display: 'none',
}
}, [
])
)
}

101
ui/app/unlock.js Normal file
View File

@ -0,0 +1,101 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('./actions')
const Mascot = require('./components/mascot')
const getCaretCoordinates = require('textarea-caret')
const EventEmitter = require('events').EventEmitter
module.exports = connect(mapStateToProps)(UnlockScreen)
inherits(UnlockScreen, Component)
function UnlockScreen() {
Component.call(this)
this.animationEventEmitter = new EventEmitter()
}
function mapStateToProps(state) {
return {
warning: state.appState.warning,
}
}
UnlockScreen.prototype.render = function() {
const state = this.props
const warning = state.warning
return (
h('.unlock-screen.flex-column.flex-center.flex-grow', [
h('h2.page-subtitle', 'Welcome!'),
h(Mascot, {
animationEventEmitter: this.animationEventEmitter,
}),
h('label', {
htmlFor: 'password-box',
}, 'Enter Password:'),
h('input', {
type: 'password',
id: 'password-box',
onKeyPress: this.onKeyPress.bind(this),
onInput: this.inputChanged.bind(this),
}),
h('.error', {
style: {
display: warning ? 'block' : 'none',
}
}, warning),
h('button.primary.cursor-pointer', {
onClick: this.onSubmit.bind(this),
}, 'Unlock'),
])
)
}
UnlockScreen.prototype.componentDidMount = function(){
document.getElementById('password-box').focus()
}
UnlockScreen.prototype.onSubmit = function(event) {
const input = document.getElementById('password-box')
const password = input.value
this.props.dispatch(actions.tryUnlockMetamask(password))
}
UnlockScreen.prototype.onKeyPress = function(event) {
if (event.key === 'Enter') {
this.submitPassword(event)
}
}
UnlockScreen.prototype.submitPassword = function(event){
var element = event.target
var password = element.value
// reset input
element.value = ''
this.props.dispatch(actions.tryUnlockMetamask(password))
}
UnlockScreen.prototype.inputChanged = function(event){
// tell mascot to look at page action
var element = event.target
var boundingRect = element.getBoundingClientRect()
var coordinates = getCaretCoordinates(element, element.selectionEnd)
this.animationEventEmitter.emit('point', {
x: boundingRect.left + coordinates.left - element.scrollLeft,
y: boundingRect.top + coordinates.top - element.scrollTop,
})
}
UnlockScreen.prototype.emitAnim = function(name, a, b, c){
this.animationEventEmitter.emit(name, a, b, c)
}

102
ui/app/util.js Normal file
View File

@ -0,0 +1,102 @@
const ethUtil = require('ethereumjs-util')
var valueTable = {
wei: '1000000000000000000',
kwei: '1000000000000000',
mwei: '1000000000000',
gwei: '1000000000',
szabo: '1000000',
finney:'1000',
ether: '1',
kether:'0.001',
mether:'0.000001',
gether:'0.000000001',
tether:'0.000000000001',
}
var bnTable = {}
for (var currency in valueTable) {
bnTable[currency] = new ethUtil.BN(valueTable[currency], 10)
}
module.exports = {
valuesFor: valuesFor,
addressSummary: addressSummary,
numericBalance: numericBalance,
formatBalance: formatBalance,
dataSize: dataSize,
readableDate: readableDate,
ethToWei: ethToWei,
weiToEth: weiToEth,
normalizeToWei: normalizeToWei,
valueTable: valueTable,
bnTable: bnTable,
}
function valuesFor(obj) {
if (!obj) return []
return Object.keys(obj)
.map(function(key){ return obj[key] })
}
function addressSummary(address) {
return address ? address.slice(0,2+8)+'...'+address.slice(-4) : '...'
}
// Takes wei Hex, returns wei BN, even if input is null
function numericBalance(balance) {
if (!balance) return new ethUtil.BN(0, 16)
var stripped = ethUtil.stripHexPrefix(balance)
return new ethUtil.BN(stripped, 16)
}
// Takes eth BN, returns BN wei
function ethToWei(bn) {
var eth = new ethUtil.BN('1000000000000000000')
var wei = bn.mul(eth)
return wei
}
// Takes BN in Wei, returns BN in eth
function weiToEth(bn) {
var diff = new ethUtil.BN('1000000000000000000')
var eth = bn.div(diff)
return eth
}
function formatBalance(balance) {
if (!balance) return 'None'
var wei = numericBalance(balance)
var eth = weiToEth(wei)
return eth.toString(10) + ' ETH'
}
function dataSize(data) {
var size = data ? ethUtil.stripHexPrefix(data).length : 0
return size+' bytes'
}
// Takes a BN and an ethereum currency name,
// returns a BN in wei
function normalizeToWei(amount, currency) {
try {
var ether = amount.div(bnTable[currency])
var wei = ether.mul(bnTable.wei)
return wei
} catch (e) {}
return amount
}
function readableDate(ms) {
var date = new Date(ms)
var month = date.getMonth()
var day = date.getDate()
var year = date.getFullYear()
var hours = date.getHours()
var minutes = "0" + date.getMinutes()
var seconds = "0" + date.getSeconds()
var date = `${month}/${day}/${year}`
var time = `${hours}:${minutes.substr(-2)}:${seconds.substr(-2)}`
return `${date} ${time}`
}

26
ui/css.js Normal file
View File

@ -0,0 +1,26 @@
const fs = require('fs')
module.exports = bundleCss
var cssFiles = {
'fonts.css': fs.readFileSync(__dirname+'/app/css/fonts.css', 'utf8'),
'reset.css': fs.readFileSync(__dirname+'/app/css/reset.css', 'utf8'),
'lib.css': fs.readFileSync(__dirname+'/app/css/lib.css', 'utf8'),
'index.css': fs.readFileSync(__dirname+'/app/css/index.css', 'utf8'),
'transitions.css': fs.readFileSync(__dirname+'/app/css/transitions.css', 'utf8'),
}
function bundleCss() {
var cssBundle = Object.keys(cssFiles).reduce(function(bundle, fileName){
var fileContent = cssFiles[fileName]
var output = String()
output += '/*========== '+fileName+' ==========*/\n\n'
output += fileContent
output += '\n\n'
return bundle+output
}, String())
return cssBundle
}

BIN
ui/design/1st_time_use.png Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

123
ui/example.js Normal file
View File

@ -0,0 +1,123 @@
const injectCss = require('inject-css')
const MetaMaskUi = require('./index.js')
const MetaMaskUiCss = require('./css.js')
const EventEmitter = require('events').EventEmitter
// account management
var identities = {
'0x1113462427bcc9133bb46e88bcbe39cd7ef0e111': {
name: 'Walrus',
img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd',
address: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111',
balance: 220,
txCount: 4,
},
'0x222462427bcc9133bb46e88bcbe39cd7ef0e7222': {
name: 'Tardus',
img: 'QmQYaRdrf2EhRhJWaHnts8Meu1mZiXrNib5W1P6cYmXWRL',
address: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222',
balance: 10.005,
txCount: 16,
},
'0x333462427bcc9133bb46e88bcbe39cd7ef0e7333': {
name: 'Gambler',
img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd',
address: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333',
balance: 0.000001,
txCount: 1,
}
}
var unconfTxs = {}
addUnconfTx({
from: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222',
to: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111',
value: '0x123',
})
addUnconfTx({
from: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111',
to: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333',
value: '0x0000',
data: '0x000462427bcc9133bb46e88bcbe39cd7ef0e7000',
})
function addUnconfTx(txParams){
var time = (new Date()).getTime()
var id = createRandomId()
unconfTxs[id] = {
id: id,
txParams: txParams,
time: time,
}
}
var isUnlocked = false
var selectedAddress = null
function getState(){
return {
isUnlocked: isUnlocked,
identities: isUnlocked ? identities : {},
unconfTxs: isUnlocked ? unconfTxs : {},
selectedAddress: selectedAddress,
}
}
var accountManager = new EventEmitter()
accountManager.getState = function(cb){
cb(null, getState())
}
accountManager.setLocked = function(){
isUnlocked = false
this._didUpdate()
}
accountManager.submitPassword = function(password, cb){
if (password === 'test') {
isUnlocked = true
cb(null, getState())
this._didUpdate()
} else {
cb(new Error('Bad password -- try "test"'))
}
}
accountManager.setSelectedAddress = function(address, cb){
selectedAddress = address
cb(null, getState())
this._didUpdate()
}
accountManager.signTransaction = function(txParams, cb){
alert('signing tx....')
}
accountManager._didUpdate = function(){
this.emit('update', getState())
}
// start app
var container = document.getElementById('app-content')
var css = MetaMaskUiCss()
injectCss(css)
var app = MetaMaskUi({
container: container,
accountManager: accountManager
})
// util
function createRandomId(){
// 13 time digits
var datePart = new Date().getTime()*Math.pow(10, 3)
// 3 random digits
var extraPart = Math.floor(Math.random()*Math.pow(10, 3))
// 16 digits
return datePart+extraPart
}

38
ui/index.html Normal file
View File

@ -0,0 +1,38 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>MetaMask</title>
</head>
<body>
<!-- app content -->
<div id="app-content"></div>
<script src="./bundle.js" type="text/javascript" charset="utf-8"></script>
<!-- design reference -->
<link rel="stylesheet" type="text/css" href="./app/css/debug.css">
<div id="design-container">
<img id="design-img" src="./design/metamask_wfs_jan_13.png">
<!-- persist scroll position on refresh -->
<script type="text/javascript">
var scrollElement = document.getElementById('design-container')
function getScrollPosition () {
var scrollTop = scrollElement.scrollTop, scrollLeft = scrollElement.scrollLeft
window.location.hash = 'scrollTop='+scrollTop+'&scrollLeft='+scrollLeft
}
window.onload = function () {
setInterval(getScrollPosition, 1000)
var hashLocation = window.location.hash.split('#')[1]
if (!hashLocation) return
var sections = hashLocation.split('&')
var scrollTop = sections[0].split('=')[1]
var scrollLeft = sections[1].split('=')[1]
scrollElement.scrollTop = scrollTop
scrollElement.scrollLeft = scrollLeft
}
</script>
</div>
</body>
</html>

55
ui/index.js Normal file
View File

@ -0,0 +1,55 @@
const React = require('react')
const render = require('react-dom').render
const h = require('react-hyperscript')
const extend = require('xtend')
const Root = require('./app/root')
const actions = require('./app/actions')
const configureStore = require('./app/store')
module.exports = launchApp
function launchApp(opts) {
var accountManager = opts.accountManager
actions._setAccountManager(accountManager)
// check if we are unlocked first
accountManager.getState(function(err, metamaskState){
if (err) throw err
startApp(metamaskState, accountManager, opts)
})
}
function startApp(metamaskState, accountManager, opts){
// parse opts
var store = configureStore({
// metamaskState represents the cross-tab state
metamask: metamaskState,
// appState represents the current tab's popup state
appState: {
currentDomain: opts.currentDomain,
}
})
// if unconfirmed txs, start on txConf page
if (Object.keys(metamaskState.unconfTxs || {}).length) {
store.dispatch(actions.showConfTxPage())
}
accountManager.on('update', function(metamaskState){
store.dispatch(actions.updateMetamaskState(metamaskState))
})
// start app
render(
h(Root, {
// inject initial state
store: store,
}
), opts.container)
}

8
ui/test/setup.js Normal file
View File

@ -0,0 +1,8 @@
if (typeof process === 'object') {
// Initialize node environment
global.expect = require('chai').expect
require('mocha-jsdom')()
} else {
window.expect = window.chai.expect
window.require = function () { /* noop */ }
}

View File

@ -0,0 +1,43 @@
var jsdom = require('mocha-jsdom')
var assert = require('assert')
var freeze = require('deep-freeze-strict')
var path = require('path')
var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js'))
var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js'))
describe ('config view actions', function() {
var initialState = {
metamask: {
rpcTarget: 'foo',
},
appState: {
currentView: {
name: 'accounts',
}
}
}
freeze(initialState)
describe('SHOW_CONFIG_PAGE', function() {
it('should set appState.currentView.name to config', function() {
var result = reducers(initialState, actions.showConfigPage())
assert.equal(result.appState.currentView.name, 'config')
})
})
describe('SET_RPC_TARGET', function() {
it('sets the state.metamask.rpcTarget property of the state to the action.value', function() {
const action = {
type: actions.SET_RPC_TARGET,
value: 'bar',
}
var result = reducers(initialState, action)
assert.equal(result.metamask.rpcTarget, action.value)
})
})
})

View File

@ -0,0 +1,54 @@
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, '..', '..', '..', 'app', 'actions.js'))
var reducers = require(path.join(__dirname, '..', '..', '..', '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() },
})
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

@ -0,0 +1,28 @@
var jsdom = require('mocha-jsdom')
var assert = require('assert')
var freeze = require('deep-freeze-strict')
var path = require('path')
var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js'))
var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js'))
describe('SET_SELECTED_ACCOUNT', function() {
it('sets the state.appState.activeAddress property of the state to the action.value', function() {
var initialState = {
appState: {
activeAddress: 'foo',
}
}
freeze(initialState)
const action = {
type: actions.SET_SELECTED_ACCOUNT,
value: 'bar',
}
freeze(action)
var resultingState = reducers(initialState, action)
assert.equal(resultingState.appState.activeAddress, action.value)
});
});

View File

@ -0,0 +1,168 @@
var jsdom = require('mocha-jsdom')
var assert = require('assert')
var freeze = require('deep-freeze-strict')
var path = require('path')
var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js'))
var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js'))
describe('tx confirmation screen', function() {
var initialState, result
describe('when there is only one tx', function() {
var firstTxId = 1457634084250832
beforeEach(function() {
initialState = {
appState: {
currentView: {
name: 'confTx',
},
},
metamask: {
unconfTxs: {
'1457634084250832': {
id: 1457634084250832,
status: "unconfirmed",
time: 1457634084250,
}
},
}
}
freeze(initialState)
})
describe('cancelTx', function() {
before(function(done) {
actions._setAccountManager({
approveTransaction(txId, cb) { cb('An error!') },
cancelTransaction(txId) { /* noop */ },
clearSeedWordCache(cb) { cb() },
})
actions.cancelTx({id: firstTxId})(function(action) {
result = reducers(initialState, action)
done()
})
})
it('should transition to the accounts list', function() {
assert.equal(result.appState.currentView.name, 'accounts')
})
it('should have no unconfirmed txs remaining', function() {
var count = getUnconfirmedTxCount(result)
assert.equal(count, 0)
})
})
describe('sendTx', function() {
var result
describe('when there is an error', function() {
before(function(done) {
alert = () => {/* noop */}
actions._setAccountManager({
approveTransaction(txId, cb) { cb('An error!') },
})
actions.sendTx({id: firstTxId})(function(action) {
result = reducers(initialState, action)
done()
})
})
it('should stay on the page', function() {
assert.equal(result.appState.currentView.name, 'confTx')
})
it('should set errorMessage on the currentView', function() {
assert(result.appState.currentView.errorMessage)
})
})
describe('when there is success', function() {
before(function(done) {
actions._setAccountManager({
approveTransaction(txId, cb) { cb() },
})
actions.sendTx({id: firstTxId})(function(action) {
result = reducers(initialState, action)
done()
})
})
it('should navigate away from the tx page', function() {
assert.equal(result.appState.currentView.name, 'accounts')
})
it('should clear the tx from the unconfirmed transactions', function() {
assert(!(firstTxId in result.metamask.unconfTxs), 'tx is cleared')
})
})
})
describe('when there are two pending txs', function() {
var firstTxId = 1457634084250832
var result, initialState
before(function(done) {
initialState = {
appState: {
currentView: {
name: 'confTx',
},
},
metamask: {
unconfTxs: {
'1457634084250832': {
id: 1457634084250832,
status: "unconfirmed",
time: 1457634084250,
},
'1457634084250833': {
id: 1457634084250833,
status: "unconfirmed",
time: 1457634084255,
},
},
}
}
freeze(initialState)
actions._setAccountManager({
approveTransaction(txId, cb) { cb() },
})
actions.sendTx({id: firstTxId})(function(action) {
result = reducers(initialState, action)
done()
})
})
it('should stay on the confTx view', function() {
assert.equal(result.appState.currentView.name, 'confTx')
})
it('should transition to the first tx', function() {
assert.equal(result.appState.currentView.context, 0)
})
it('should only have one unconfirmed tx remaining', function() {
var count = getUnconfirmedTxCount(result)
assert.equal(count, 1)
})
})
})
});
function getUnconfirmedTxCount(state) {
var txs = state.metamask.unconfTxs
var count = Object.keys(txs).length
return count
}

View File

@ -0,0 +1,23 @@
var jsdom = require('mocha-jsdom')
var assert = require('assert')
var freeze = require('deep-freeze-strict')
var path = require('path')
var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js'))
var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js'))
describe('SHOW_INFO_PAGE', function() {
it('sets the state.appState.currentView.name property to info', function() {
var initialState = {
appState: {
activeAddress: 'foo',
}
}
freeze(initialState)
const action = actions.showInfoPage()
var resultingState = reducers(initialState, action)
assert.equal(resultingState.appState.currentView.name, 'info')
});
});

View File

@ -0,0 +1,24 @@
var jsdom = require('mocha-jsdom')
var assert = require('assert')
var freeze = require('deep-freeze-strict')
var path = require('path')
var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js'))
var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js'))
describe('action DISPLAY_WARNING', function() {
it('sets appState.warning to provided value', function() {
var initialState = {
appState: {},
}
freeze(initialState)
const warningText = 'This is a sample warning message'
const action = actions.displayWarning(warningText)
const resultingState = reducers(initialState, action)
assert.equal(resultingState.appState.warning, warningText, 'warning text set')
});
});

102
ui/test/unit/util_test.js Normal file
View File

@ -0,0 +1,102 @@
var assert = require('assert')
var sinon = require('sinon')
const ethUtil = require('ethereumjs-util')
var path = require('path')
var util = require(path.join(__dirname, '..', '..', 'app', 'util.js'))
describe('util', function() {
var ethInWei = '1'
for (var i = 0; i < 18; i++ ) { ethInWei += '0' }
beforeEach(function() {
this.sinon = sinon.sandbox.create()
})
afterEach(function() {
this.sinon.restore()
})
describe('numericBalance', function() {
it('should return a BN 0 if given nothing', function() {
var result = util.numericBalance()
assert.equal(result.toString(10), 0)
})
it('should work with hex prefix', function() {
var result = util.numericBalance('0x012')
assert.equal(result.toString(10), '18')
})
it('should work with no hex prefix', function() {
var result = util.numericBalance('012')
assert.equal(result.toString(10), '18')
})
})
describe('#ethToWei', function() {
it('should take an eth BN, returns wei BN', function() {
var input = new ethUtil.BN(1, 10)
var result = util.ethToWei(input)
assert.equal(result, ethInWei, '18 zeroes')
})
})
describe('#weiToEth', function() {
it('should take a wei BN and return an eth BN', function() {
var result = util.weiToEth(new ethUtil.BN(ethInWei))
assert.equal(result, '1', 'equals 1 eth')
})
})
describe('#formatBalance', function() {
it('when given nothing', function() {
var result = util.formatBalance()
assert.equal(result, 'None', 'should return "None"')
})
it('should return eth as string followed by ETH', function() {
var input = new ethUtil.BN(ethInWei).toJSON()
var result = util.formatBalance(input)
assert.equal(result, '1 ETH')
})
})
describe('#normalizeToWei', function() {
it('should convert an eth to the appropriate equivalent values', function() {
var valueTable = {
wei: '1000000000000000000',
kwei: '1000000000000000',
mwei: '1000000000000',
gwei: '1000000000',
szabo: '1000000',
finney:'1000',
ether: '1',
kether:'0.001',
mether:'0.000001',
// AUDIT: We're getting BN numbers on these ones.
// I think they're big enough to ignore for now.
// gether:'0.000000001',
// tether:'0.000000000001',
}
var oneEthBn = new ethUtil.BN(ethInWei, 10)
for(var currency in valueTable) {
var value = new ethUtil.BN(valueTable[currency], 10)
var output = util.normalizeToWei(value, currency)
assert.equal(output.toString(10), valueTable.wei, `value of ${output.toString(10)} ${currency} should convert to ${oneEthBn}`)
}
})
})
})