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:
commit
54f13b31ec
@ -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
|
||||
|
||||
|
21
README.md
21
README.md
@ -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.
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
|
44
package.json
44
package.json
@ -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
66
ui/.gitignore
vendored
Normal 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
154
ui/app/account-detail.js
Normal 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
116
ui/app/accounts.js
Normal 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
418
ui/app/actions.js
Normal 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
242
ui/app/app.js
Normal 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',
|
||||
}
|
||||
}),
|
||||
])
|
||||
])
|
||||
)
|
||||
}
|
93
ui/app/components/account-panel.js
Normal file
93
ui/app/components/account-panel.js
Normal 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)),
|
||||
])
|
||||
|
||||
}
|
||||
}
|
65
ui/app/components/mascot.js
Normal file
65
ui/app/components/mascot.js
Normal 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
140
ui/app/conf-tx.js
Normal 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
103
ui/app/config.js
Normal 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
21
ui/app/css/debug.css
Normal 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
2
ui/app/css/fonts.css
Normal 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
489
ui/app/css/index.css
Normal 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
143
ui/app/css/lib.css
Normal 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
48
ui/app/css/reset.css
Normal 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;
|
||||
}
|
47
ui/app/css/transitions.css
Normal file
47
ui/app/css/transitions.css
Normal 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;
|
||||
}
|
||||
|
57
ui/app/first-time/create-vault-complete.js
Normal file
57
ui/app/first-time/create-vault-complete.js
Normal 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())
|
||||
}
|
||||
|
123
ui/app/first-time/create-vault.js
Normal file
123
ui/app/first-time/create-vault.js
Normal 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*/))
|
||||
}
|
123
ui/app/first-time/init-menu.js
Normal file
123
ui/app/first-time/init-menu.js
Normal 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())
|
||||
}
|
||||
|
116
ui/app/first-time/restore-vault.js
Normal file
116
ui/app/first-time/restore-vault.js
Normal 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))
|
||||
}
|
BIN
ui/app/img/identicon-tardigrade.png
Normal file
BIN
ui/app/img/identicon-tardigrade.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 138 KiB |
BIN
ui/app/img/identicon-walrus.png
Normal file
BIN
ui/app/img/identicon-walrus.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 380 KiB |
90
ui/app/info.js
Normal file
90
ui/app/info.js
Normal 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
51
ui/app/loading.js
Normal 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
41
ui/app/reducers.js
Normal 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
281
ui/app/reducers/app.js
Normal 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
|
||||
|
||||
}
|
||||
}
|
18
ui/app/reducers/identities.js
Normal file
18
ui/app/reducers/identities.js
Normal 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
|
||||
}
|
||||
|
||||
}
|
73
ui/app/reducers/metamask.js
Normal file
73
ui/app/reducers/metamask.js
Normal 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
24
ui/app/root.js
Normal 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
139
ui/app/send.js
Normal 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
69
ui/app/settings.js
Normal 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
19
ui/app/store.js
Normal 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
31
ui/app/template.js
Normal 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
101
ui/app/unlock.js
Normal 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
102
ui/app/util.js
Normal 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
26
ui/css.js
Normal 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
BIN
ui/design/1st_time_use.png
Normal file
Binary file not shown.
BIN
ui/design/metamask_wfs_jan_13.pdf
Normal file
BIN
ui/design/metamask_wfs_jan_13.pdf
Normal file
Binary file not shown.
BIN
ui/design/metamask_wfs_jan_13.png
Normal file
BIN
ui/design/metamask_wfs_jan_13.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 409 KiB |
BIN
ui/design/metamask_wfs_jan_18.pdf
Normal file
BIN
ui/design/metamask_wfs_jan_18.pdf
Normal file
Binary file not shown.
123
ui/example.js
Normal file
123
ui/example.js
Normal 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
38
ui/index.html
Normal 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
55
ui/index.js
Normal 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
8
ui/test/setup.js
Normal 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 */ }
|
||||
}
|
43
ui/test/unit/actions/config_test.js
Normal file
43
ui/test/unit/actions/config_test.js
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
54
ui/test/unit/actions/restore_vault_test.js
Normal file
54
ui/test/unit/actions/restore_vault_test.js
Normal 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')
|
||||
});
|
||||
});
|
28
ui/test/unit/actions/set_selected_account_test.js
Normal file
28
ui/test/unit/actions/set_selected_account_test.js
Normal 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)
|
||||
});
|
||||
});
|
168
ui/test/unit/actions/tx_test.js
Normal file
168
ui/test/unit/actions/tx_test.js
Normal 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
|
||||
}
|
23
ui/test/unit/actions/view_info_test.js
Normal file
23
ui/test/unit/actions/view_info_test.js
Normal 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')
|
||||
});
|
||||
});
|
24
ui/test/unit/actions/warning_test.js
Normal file
24
ui/test/unit/actions/warning_test.js
Normal 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
102
ui/test/unit/util_test.js
Normal 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}`)
|
||||
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
})
|
Loading…
Reference in New Issue
Block a user