mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 18:00:18 +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
|
## Current Master
|
||||||
|
|
||||||
- Corrected text above account list. Selected account is visible to all sites, not just the current domain.
|
- 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
|
## 1.5.0 2016-04-13
|
||||||
|
|
||||||
|
21
README.md
21
README.md
@ -1,5 +1,7 @@
|
|||||||
# Metamask Plugin
|
# Metamask Plugin
|
||||||
|
|
||||||
|
[![Throughput Graph](https://graphs.waffle.io/MetaMask/metamask-plugin/throughput.svg)](https://waffle.io/MetaMask/metamask-plugin/metrics)
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -34,7 +36,7 @@ You now have the plugin, and can click 'inspect views: background plugin' to vie
|
|||||||
|
|
||||||
### Developing the UI
|
### 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.
|
1. Clone the dependency locally.
|
||||||
2. `npm install` in its folder.
|
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.
|
4. Run `npm link $DEP_NAME` in this project folder.
|
||||||
5. Next time you `gulp dev` it will watch the dependency for changes as well!
|
5. Next time you `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.
|
You must be authorized already on the Metamask plugin.
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const createId = require('hat')
|
const createId = require('hat')
|
||||||
const uiUtils = require('metamask-ui/app/util')
|
const uiUtils = require('../../../ui/app/util')
|
||||||
var notificationHandlers = {}
|
var notificationHandlers = {}
|
||||||
|
|
||||||
module.exports = createTxNotification
|
module.exports = createTxNotification
|
||||||
|
@ -4,8 +4,8 @@ const async = require('async')
|
|||||||
const Multiplex = require('multiplex')
|
const Multiplex = require('multiplex')
|
||||||
const Dnode = require('dnode')
|
const Dnode = require('dnode')
|
||||||
const Web3 = require('web3')
|
const Web3 = require('web3')
|
||||||
const MetaMaskUi = require('metamask-ui')
|
const MetaMaskUi = require('../../ui')
|
||||||
const MetaMaskUiCss = require('metamask-ui/css')
|
const MetaMaskUiCss = require('../../ui/css')
|
||||||
const injectCss = require('inject-css')
|
const injectCss = require('inject-css')
|
||||||
const PortStream = require('./lib/port-stream.js')
|
const PortStream = require('./lib/port-stream.js')
|
||||||
const StreamProvider = require('./lib/stream-provider.js')
|
const StreamProvider = require('./lib/stream-provider.js')
|
||||||
|
@ -93,7 +93,6 @@ function copyTask(opts){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function bundleTask(opts) {
|
function bundleTask(opts) {
|
||||||
var browserifyOpts = assign({}, watchify.args, {
|
var browserifyOpts = assign({}, watchify.args, {
|
||||||
entries: ['./app/scripts/'+opts.filename],
|
entries: ['./app/scripts/'+opts.filename],
|
||||||
@ -101,6 +100,7 @@ function bundleTask(opts) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
var bundler = browserify(browserifyOpts)
|
var bundler = browserify(browserifyOpts)
|
||||||
|
bundler.transform('brfs')
|
||||||
if (opts.watch) {
|
if (opts.watch) {
|
||||||
bundler = watchify(bundler)
|
bundler = watchify(bundler)
|
||||||
bundler.on('update', performBundle) // on any dep update, runs the bundler
|
bundler.on('update', performBundle) // on any dep update, runs the bundler
|
||||||
|
44
package.json
44
package.json
@ -6,34 +6,70 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "gulp dev",
|
"start": "gulp dev",
|
||||||
"test": "mocha --require test/helper.js --compilers js:babel-register --recursive",
|
"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": {
|
"dependencies": {
|
||||||
"async": "^1.5.2",
|
"async": "^1.5.2",
|
||||||
"clone": "^1.0.2",
|
"clone": "^1.0.2",
|
||||||
|
"copy-to-clipboard": "^2.0.0",
|
||||||
|
"debounce": "^1.0.0",
|
||||||
"dnode": "^1.2.2",
|
"dnode": "^1.2.2",
|
||||||
"end-of-stream": "^1.1.0",
|
"end-of-stream": "^1.1.0",
|
||||||
"eth-lightwallet": "^2.2.2",
|
"eth-lightwallet": "^2.2.2",
|
||||||
"eth-store": "^1.1.0",
|
"eth-store": "^1.1.0",
|
||||||
"ethereumjs-tx": "^1.0.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",
|
"faux-jax": "git+https://github.com/kumavis/faux-jax.git#c3648de04804f3895c5b4972750cae5b51ddb103",
|
||||||
"hat": "0.0.3",
|
"hat": "0.0.3",
|
||||||
"inject-css": "^0.1.1",
|
"inject-css": "^0.1.1",
|
||||||
"metamask-ui": "^1.5.0",
|
"metamask-logo": "^1.1.5",
|
||||||
"multiplex": "^6.7.0",
|
"multiplex": "^6.7.0",
|
||||||
"pojo-migrator": "^2.1.0",
|
"pojo-migrator": "^2.1.0",
|
||||||
"pumpify": "^1.3.4",
|
"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",
|
"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",
|
"through2": "^2.0.1",
|
||||||
"web3": "^0.15.1",
|
"web3": "^0.15.1",
|
||||||
"web3-provider-engine": "^7.2.1",
|
"web3-provider-engine": "^7.2.1",
|
||||||
"xtend": "^4.0.1"
|
"xtend": "^4.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"brfs": "^1.4.3",
|
||||||
"babel-preset-es2015": "^6.6.0",
|
"babel-preset-es2015": "^6.6.0",
|
||||||
|
"babelify": "^7.2.0",
|
||||||
"babel-register": "^6.7.2",
|
"babel-register": "^6.7.2",
|
||||||
|
"beefy": "^2.1.5",
|
||||||
"browserify": "^13.0.0",
|
"browserify": "^13.0.0",
|
||||||
|
"chai": "^3.5.0",
|
||||||
|
"deep-freeze-strict": "^1.1.1",
|
||||||
"del": "^2.2.0",
|
"del": "^2.2.0",
|
||||||
"gulp": "github:gulpjs/gulp#4.0",
|
"gulp": "github:gulpjs/gulp#4.0",
|
||||||
"gulp-livereload": "^3.8.1",
|
"gulp-livereload": "^3.8.1",
|
||||||
@ -45,8 +81,10 @@
|
|||||||
"jshint-stylish": "~0.1.5",
|
"jshint-stylish": "~0.1.5",
|
||||||
"lodash.assign": "^4.0.6",
|
"lodash.assign": "^4.0.6",
|
||||||
"mocha": "^2.4.5",
|
"mocha": "^2.4.5",
|
||||||
|
"mocha-jsdom": "^1.1.0",
|
||||||
"mocha-sinon": "^1.1.5",
|
"mocha-sinon": "^1.1.5",
|
||||||
"sinon": "^1.17.3",
|
"sinon": "^1.17.3",
|
||||||
|
"uglifyify": "^3.0.1",
|
||||||
"vinyl-buffer": "^1.0.0",
|
"vinyl-buffer": "^1.0.0",
|
||||||
"vinyl-source-stream": "^1.1.0",
|
"vinyl-source-stream": "^1.1.0",
|
||||||
"watchify": "^3.7.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