Merge pull request #204 from MetaMask/dev

Merge UI redesign into master
This commit is contained in:
Dan Finlay 2016-05-23 14:43:17 -07:00
commit 0c73e58372
68 changed files with 12355 additions and 813 deletions

View File

@ -2,6 +2,16 @@
## Current Master
## 2.0.0 2016-05-23
- UI Overhaul per Vlad Todirut's designs.
- Replaced identicons with jazzicons.
- Fixed glitchy transitions.
- Added support for capitalization-based address checksums.
- Send value is no longer limited by javascript number precision, and is always in ETH.
- Added ability to generate new accounts.
- Added ability to locally nickname accounts.
## 1.8.4 2016-05-13
- Point rpc servers to https endpoints.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,7 +1,7 @@
{
"name": "__MSG_appName__",
"short_name": "Metamask",
"version": "1.8.4",
"version": "2.0.0",
"manifest_version": 2,
"description": "__MSG_appDescription__",
"icons": {

View File

@ -1,11 +1,12 @@
const Dnode = require('dnode')
const ObjectMultiplex = require('./lib/obj-multiplex')
const eos = require('end-of-stream')
const combineStreams = require('pumpify')
const extend = require('xtend')
const EthStore = require('eth-store')
const PortStream = require('./lib/port-stream.js')
const MetaMaskProvider = require('web3-provider-engine/zero.js')
const handleRequestsFromStream = require('web3-stream-provider/handler')
const ObjectMultiplex = require('./lib/obj-multiplex')
const PortStream = require('./lib/port-stream.js')
const IdentityStore = require('./lib/idStore')
const createTxNotification = require('./lib/notifications.js').createTxNotification
const createMsgNotification = require('./lib/notifications.js').createMsgNotification
@ -132,25 +133,6 @@ function storeSetFromObj(store, obj){
}
// handle rpc requests
function onRpcRequest(remoteStream, payload){
// console.log('MetaMaskPlugin - incoming payload:', payload)
provider.sendAsync(payload, function onPayloadHandled(err, response){
// provider engine errors are included in response objects
if (!payload.isMetamaskInternal) {
console.log('MetaMaskPlugin - RPC complete:', payload, '->', response)
if (response.error) console.error('Error in RPC response:\n'+response.error.message)
}
try {
remoteStream.write(response)
} catch (err) {
console.error(err)
}
})
}
//
// remote features
//
@ -161,7 +143,15 @@ function setupPublicConfig(stream){
}
function setupProviderConnection(stream){
stream.on('data', onRpcRequest.bind(null, stream))
handleRequestsFromStream(stream, provider, logger)
function logger(err, request, response){
if (err) return console.error(err.stack)
if (!request.isMetamaskInternal) {
console.log('MetaMaskPlugin - RPC complete:', request, '->', response)
if (response.error) console.error('Error in RPC response:\n'+response.error.message)
}
}
}
function setupControllerConnection(stream){
@ -182,6 +172,8 @@ function setupControllerConnection(stream){
setLocked: idStore.setLocked.bind(idStore),
clearSeedWordCache: idStore.clearSeedWordCache.bind(idStore),
exportAccount: idStore.exportAccount.bind(idStore),
revealAccount: idStore.revealAccount.bind(idStore),
saveAccountLabel: idStore.saveAccountLabel.bind(idStore),
})
stream.pipe(dnode).pipe(stream)
dnode.on('remote', function(remote){

12
app/scripts/config.js Normal file
View File

@ -0,0 +1,12 @@
const MAINET_RPC_URL = 'https://mainnet.infura.io/'
const TESTNET_RPC_URL = 'https://morden.infura.io/'
const DEFAULT_RPC_URL = TESTNET_RPC_URL
module.exports = {
network: {
default: DEFAULT_RPC_URL,
mainnet: MAINET_RPC_URL,
testnet: TESTNET_RPC_URL,
},
}

View File

@ -25,14 +25,14 @@ pluginStream.on('error', console.error.bind(console))
// forward communication plugin->inpage
pageStream.pipe(pluginStream).pipe(pageStream)
// connect contentscript->inpage control stream
// connect contentscript->inpage reload stream
var mx = ObjectMultiplex()
mx.on('error', console.error.bind(console))
mx.pipe(pageStream)
var controlStream = mx.createStream('control')
controlStream.on('error', console.error.bind(console))
var reloadStream = mx.createStream('reload')
reloadStream.on('error', console.error.bind(console))
// if we lose connection with the plugin, trigger tab refresh
pluginStream.on('close', function(){
controlStream.write({ method: 'reset' })
reloadStream.write({ method: 'reset' })
})

View File

@ -1,18 +1,12 @@
cleanContextForImports()
const createPayload = require('web3-provider-engine/util/create-payload')
const StreamProvider = require('./lib/stream-provider.js')
const LocalMessageDuplexStream = require('./lib/local-message-stream.js')
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
const RemoteStore = require('./lib/remote-store.js').RemoteStore
const Web3 = require('web3')
const once = require('once')
const LocalMessageDuplexStream = require('./lib/local-message-stream.js')
const setupDappAutoReload = require('./lib/auto-reload.js')
const MetamaskInpageProvider = require('./lib/inpage-provider.js')
restoreContextAfterImports()
// rename on window
// remove from window
delete window.Web3
window.MetamaskWeb3 = Web3
const DEFAULT_RPC_URL = 'https://rpc.metamask.io/'
//
@ -20,148 +14,40 @@ const DEFAULT_RPC_URL = 'https://rpc.metamask.io/'
//
// setup background connection
var pluginStream = new LocalMessageDuplexStream({
var metamaskStream = new LocalMessageDuplexStream({
name: 'inpage',
target: 'contentscript',
})
var mx = setupMultiplex(pluginStream)
// connect to provider
var remoteProvider = new StreamProvider()
remoteProvider.pipe(mx.createStream('provider')).pipe(remoteProvider)
remoteProvider.on('error', console.error.bind(console))
// subscribe to metamask public config
var initState = JSON.parse(localStorage['MetaMask-Config'] || '{}')
var publicConfigStore = new RemoteStore(initState)
var storeStream = publicConfigStore.createStream()
storeStream.pipe(mx.createStream('publicConfig')).pipe(storeStream)
publicConfigStore.subscribe(function(state){
localStorage['MetaMask-Config'] = JSON.stringify(state)
})
// compose the inpage provider
var inpageProvider = new MetamaskInpageProvider(metamaskStream)
//
// setup web3
//
var web3 = new Web3(remoteProvider)
var web3 = new Web3(inpageProvider)
web3.setProvider = function(){
console.log('MetaMask - overrode web3.setProvider')
}
console.log('MetaMask - injected web3')
//
// automatic dapp reset
// export global web3 with auto dapp reload
//
// export web3 as a global, checking for usage
var pageIsUsingWeb3 = false
var resetWasRequested = false
window.web3 = ensnare(web3, once(function(){
// if web3 usage happened after a reset request, trigger reset late
if (resetWasRequested) return triggerReset()
// mark web3 as used
pageIsUsingWeb3 = true
// reset web3 reference
window.web3 = web3
}))
// listen for reset requests
mx.createStream('control').once('data', function(){
resetWasRequested = true
// ignore if web3 was not used
if (!pageIsUsingWeb3) return
// reload after short timeout
triggerReset()
})
function triggerReset(){
setTimeout(function(){
window.location.reload()
}, 500)
}
//
// handle synchronous requests
//
global.publicConfigStore = publicConfigStore
var reloadStream = inpageProvider.multiStream.createStream('reload')
setupDappAutoReload(web3, reloadStream)
// set web3 defaultAcount
publicConfigStore.subscribe(function(state){
inpageProvider.publicConfigStore.subscribe(function(state){
web3.eth.defaultAccount = state.selectedAddress
})
// setup sync http provider
var providerConfig = publicConfigStore.get('provider') || {}
var providerUrl = providerConfig.rpcTarget ? providerConfig.rpcTarget : DEFAULT_RPC_URL
var syncProvider = new Web3.providers.HttpProvider(providerUrl)
publicConfigStore.subscribe(function(state){
if (!state.provider) return
if (!state.provider.rpcTarget || state.provider.rpcTarget === providerUrl) return
providerUrl = state.provider.rpcTarget
syncProvider = new Web3.providers.HttpProvider(providerUrl)
})
// handle sync methods
remoteProvider.send = function(payload){
var result = null
switch (payload.method) {
case 'eth_accounts':
// read from localStorage
var selectedAddress = publicConfigStore.get('selectedAddress')
result = selectedAddress ? [selectedAddress] : []
break
case 'eth_coinbase':
// read from localStorage
var selectedAddress = publicConfigStore.get('selectedAddress')
result = selectedAddress || '0x0000000000000000000000000000000000000000'
break
// fallback to normal rpc
default:
return syncProvider.send(payload)
}
// return the result
return {
id: payload.id,
jsonrpc: payload.jsonrpc,
result: result,
}
}
//
// util
//
// creates a proxy object that calls cb everytime the obj's properties/fns are accessed
function ensnare(obj, cb){
var proxy = {}
Object.keys(obj).forEach(function(key){
var val = obj[key]
switch (typeof val) {
case 'function':
proxy[key] = function(){
cb()
val.apply(obj, arguments)
}
return
default:
Object.defineProperty(proxy, key, {
get: function(){ cb(); return obj[key] },
set: function(val){ cb(); return obj[key] = val },
})
return
}
})
return proxy
}
// need to make sure we aren't affected by overlapping namespaces
// and that we dont affect the app with our namespace
// mostly a fix for web3's BigNumber if AMD's "define" is defined...

View File

@ -0,0 +1,37 @@
const once = require('once')
const ensnare = require('./ensnare.js')
module.exports = setupDappAutoReload
function setupDappAutoReload(web3, controlStream){
// export web3 as a global, checking for usage
var pageIsUsingWeb3 = false
var resetWasRequested = false
global.web3 = ensnare(web3, once(function(){
// if web3 usage happened after a reset request, trigger reset late
if (resetWasRequested) return triggerReset()
// mark web3 as used
pageIsUsingWeb3 = true
// reset web3 reference
global.web3 = web3
}))
// listen for reset requests from metamask
controlStream.once('data', function(){
resetWasRequested = true
// ignore if web3 was not used
if (!pageIsUsingWeb3) return
// reload after short timeout
triggerReset()
})
// reload the page
function triggerReset(){
setTimeout(function(){
global.location.reload()
}, 500)
}
}

View File

@ -1,11 +1,12 @@
const Migrator = require('pojo-migrator')
const extend = require('xtend')
const MetamaskConfig = require('../config.js')
const migrations = require('./migrations')
const STORAGE_KEY = 'metamask-config'
const TESTNET_RPC = 'https://morden.infura.io'
const MAINNET_RPC = 'https://mainnet.infura.io/'
const TESTNET_RPC = MetamaskConfig.network.testnet
const MAINNET_RPC = MetamaskConfig.network.mainnet
const migrations = require('./migrations')
/* The config-manager is a convenience object
* wrapping a pojo-migrator.
@ -229,6 +230,26 @@ ConfigManager.prototype.updateTx = function(tx) {
this._saveTxList(transactions)
}
// wallet nickname methods
ConfigManager.prototype.getWalletNicknames = function() {
var data = this.getData()
let nicknames = ('walletNicknames' in data) ? data.walletNicknames : {}
return nicknames
}
ConfigManager.prototype.nicknameForWallet = function(account) {
let nicknames = this.getWalletNicknames()
return nicknames[account]
}
ConfigManager.prototype.setNicknameForWallet = function(account, nickname) {
let nicknames = this.getWalletNicknames()
nicknames[account] = nickname
var data = this.getData()
data.walletNicknames = nicknames
this.setData(data)
}
// observable

View File

@ -0,0 +1,24 @@
module.exports = ensnare
// creates a proxy object that calls cb everytime the obj's properties/fns are accessed
function ensnare(obj, cb){
var proxy = {}
Object.keys(obj).forEach(function(key){
var val = obj[key]
switch (typeof val) {
case 'function':
proxy[key] = function(){
cb()
val.apply(obj, arguments)
}
return
default:
Object.defineProperty(proxy, key, {
get: function(){ cb(); return obj[key] },
set: function(val){ cb(); return obj[key] = val },
})
return
}
})
return proxy
}

View File

@ -105,14 +105,29 @@ IdentityStore.prototype.getSelectedAddress = function(){
return configManager.getSelectedAccount()
}
IdentityStore.prototype.setSelectedAddress = function(address){
IdentityStore.prototype.setSelectedAddress = function(address, cb){
if (!address) {
var addresses = this._getAddresses()
address = addresses[0]
}
configManager.setSelectedAccount(address)
if (cb) return cb(null, address)
}
IdentityStore.prototype.revealAccount = function(cb) {
let addresses = this._getAddresses()
const derivedKey = this._idmgmt.derivedKey
const keyStore = this._keyStore
keyStore.setDefaultHdDerivationPath(this.hdPathString)
keyStore.generateNewAddress(derivedKey, 1)
configManager.setWallet(keyStore.serialize())
addresses = this._getAddresses()
this._loadIdentities()
this._didUpdate()
cb(null)
}
IdentityStore.prototype.getNetwork = function(tries) {
@ -310,9 +325,10 @@ IdentityStore.prototype._loadIdentities = function(){
// // add to ethStore
this._ethStore.addAccount(address)
// add to identities
const defaultLabel = 'Wallet ' + (i+1)
const nickname = configManager.nicknameForWallet(address)
var identity = {
name: 'Wallet ' + (i+1),
img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd',
name: nickname || defaultLabel,
address: address,
mayBeFauceting: this._mayBeFauceting(i),
}
@ -321,6 +337,13 @@ IdentityStore.prototype._loadIdentities = function(){
this._didUpdate()
}
IdentityStore.prototype.saveAccountLabel = function(account, label, cb) {
configManager.setNicknameForWallet(account, label)
this._loadIdentities()
cb(null, label)
this._didUpdate()
}
// mayBeFauceting
// If on testnet, index 0 may be fauceting.
// The UI will have to check the balance to know.

View File

@ -0,0 +1,123 @@
const HttpProvider = require('web3/lib/web3/httpprovider')
const Streams = require('mississippi')
const ObjectMultiplex = require('./obj-multiplex')
const StreamProvider = require('web3-stream-provider')
const RemoteStore = require('./remote-store.js').RemoteStore
const MetamaskConfig = require('../config.js')
module.exports = MetamaskInpageProvider
function MetamaskInpageProvider(connectionStream){
const self = this
// setup connectionStream multiplexing
var multiStream = ObjectMultiplex()
Streams.pipe(connectionStream, multiStream, connectionStream, function(err){
console.warn('MetamaskInpageProvider - lost connection to MetaMask')
if (err) throw err
})
self.multiStream = multiStream
// subscribe to metamask public config
var publicConfigStore = remoteStoreWithLocalStorageCache('MetaMask-Config')
var storeStream = publicConfigStore.createStream()
Streams.pipe(storeStream, multiStream.createStream('publicConfig'), storeStream, function(err){
console.warn('MetamaskInpageProvider - lost connection to MetaMask publicConfig')
if (err) throw err
})
self.publicConfigStore = publicConfigStore
// connect to sync provider
self.syncProvider = createSyncProvider(publicConfigStore.get('provider'))
// subscribe to publicConfig to update the syncProvider on change
publicConfigStore.subscribe(function(state){
self.syncProvider = createSyncProvider(state.provider)
})
// connect to async provider
var asyncProvider = new StreamProvider()
Streams.pipe(asyncProvider, multiStream.createStream('provider'), asyncProvider, function(err){
console.warn('MetamaskInpageProvider - lost connection to MetaMask provider')
if (err) throw err
})
asyncProvider.on('error', console.error.bind(console))
self.asyncProvider = asyncProvider
// overwrite own sendAsync method
self.sendAsync = asyncProvider.sendAsync.bind(asyncProvider)
}
MetamaskInpageProvider.prototype.send = function(payload){
const self = this
var result = null
switch (payload.method) {
case 'eth_accounts':
// read from localStorage
var selectedAddress = self.publicConfigStore.get('selectedAddress')
result = selectedAddress ? [selectedAddress] : []
break
case 'eth_coinbase':
// read from localStorage
var selectedAddress = self.publicConfigStore.get('selectedAddress')
result = selectedAddress || '0x0000000000000000000000000000000000000000'
break
// fallback to normal rpc
default:
return self.syncProvider.send(payload)
}
// return the result
return {
id: payload.id,
jsonrpc: payload.jsonrpc,
result: result,
}
}
MetamaskInpageProvider.prototype.sendAsync = function(){
throw new Error('MetamaskInpageProvider - sendAsync not overwritten')
}
MetamaskInpageProvider.prototype.isConnected = function(){
return true
}
// util
function createSyncProvider(providerConfig){
providerConfig = providerConfig || {}
var syncProviderUrl = undefined
if (providerConfig.rpcTarget) {
syncProviderUrl = providerConfig.rpcTarget
} else {
switch(providerConfig.type) {
case 'testnet':
syncProviderUrl = MetamaskConfig.network.testnet
break
case 'mainnet':
syncProviderUrl = MetamaskConfig.network.mainnet
break
default:
syncProviderUrl = MetamaskConfig.network.default
}
}
return new HttpProvider(syncProviderUrl)
}
function remoteStoreWithLocalStorageCache(storageKey){
// read local cache
var initState = JSON.parse(localStorage[storageKey] || '{}')
var store = new RemoteStore(initState)
// cache the latest state locally
store.subscribe(function(state){
localStorage[storageKey] = JSON.stringify(state)
})
return store
}

View File

@ -1,72 +0,0 @@
const Duplex = require('readable-stream').Duplex
const inherits = require('util').inherits
module.exports = StreamProvider
inherits(StreamProvider, Duplex)
function StreamProvider(){
Duplex.call(this, {
objectMode: true,
})
this._payloads = {}
}
// public
StreamProvider.prototype.send = function(payload){
throw new Error('StreamProvider - does not support synchronous RPC calls. called: "'+payload.method+'"')
}
StreamProvider.prototype.sendAsync = function(payload, callback){
// console.log('StreamProvider - sending payload', payload)
var id = payload.id
if (Array.isArray(payload)) {
id = 'batch'+payload[0].id
}
this._payloads[id] = [payload, callback]
// console.log('payload for plugin:', payload)
this.push(payload)
}
StreamProvider.prototype.isConnected = function(){
return true
}
// private
StreamProvider.prototype._onResponse = function(response){
// console.log('StreamProvider - got response', payload)
var id = response.id
if (Array.isArray(response)) {
id = 'batch'+response[0].id
}
var data = this._payloads[id]
if (!data) throw new Error('StreamProvider - Unknown response id')
delete this._payloads[id]
var payload = data[0]
var callback = data[1]
// logging
var res = Array.isArray(response) ? response : [response]
// ;(Array.isArray(payload) ? payload : [payload]).forEach(function(payload, index){
// console.log('plugin response:', payload.id, payload.method, payload.params, '->', res[index].result)
// })
callback(null, response)
}
// stream plumbing
StreamProvider.prototype._read = noop
StreamProvider.prototype._write = function(msg, encoding, cb){
this._onResponse(msg)
cb()
}
// util
function noop(){}

View File

@ -7,7 +7,7 @@ const MetaMaskUi = require('../../ui')
const MetaMaskUiCss = require('../../ui/css')
const injectCss = require('inject-css')
const PortStream = require('./lib/port-stream.js')
const StreamProvider = require('./lib/stream-provider.js')
const StreamProvider = require('web3-stream-provider')
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
// setup app

View File

@ -30,6 +30,10 @@ gulp.task('copy:images', copyTask({
source: './app/images/',
destination: './dist/images',
}))
gulp.task('copy:fonts', copyTask({
source: './app/fonts/',
destination: './dist/fonts',
}))
gulp.task('copy:reload', copyTask({
source: './app/scripts/',
destination: './dist/scripts',
@ -40,7 +44,7 @@ gulp.task('copy:root', copyTask({
destination: './dist',
pattern: '/*',
}))
gulp.task('copy', gulp.parallel('copy:locales','copy:images','copy:reload','copy:root'))
gulp.task('copy', gulp.parallel('copy:locales','copy:images','copy:fonts','copy:reload','copy:root'))
gulp.task('copy:watch', function(){
gulp.watch(['./app/{_locales,images}/', './app/scripts/chromereload.js', './app/*.{html,json}'], gulp.series('copy'))
})

View File

@ -23,6 +23,8 @@
},
"dependencies": {
"async": "^1.5.2",
"babel-preset-es2015": "^6.9.0",
"babel-register": "^6.9.0",
"browserify-derequire": "^0.9.4",
"clone": "^1.0.2",
"copy-to-clipboard": "^2.0.0",
@ -36,26 +38,32 @@
"hat": "0.0.3",
"identicon.js": "^1.2.1",
"inject-css": "^0.1.1",
"jazzicon": "^1.1.3",
"menu-droppo": "^1.1.0",
"metamask-logo": "^1.1.5",
"mississippi": "^1.2.0",
"multiplex": "^6.7.0",
"once": "^1.3.3",
"pojo-migrator": "^2.1.0",
"polyfill-crypto.getrandomvalues": "^1.0.0",
"pumpify": "^1.3.4",
"react": "^0.14.3",
"react-addons-css-transition-group": "^0.14.7",
"react-dom": "^0.14.3",
"react": "^15.0.2",
"react-addons-css-transition-group": "^15.0.2",
"react-dom": "^15.0.2",
"react-hyperscript": "^2.2.2",
"react-redux": "^4.0.3",
"react-redux": "^4.4.5",
"readable-stream": "^2.1.2",
"redux": "^3.0.5",
"redux-logger": "^2.3.1",
"redux-thunk": "^1.0.2",
"sandwich-expando": "^1.0.5",
"textarea-caret": "^3.0.1",
"three.js": "^0.73.2",
"through2": "^2.0.1",
"vreme": "^3.0.2",
"web3": "ethereum/web3.js#0.16.0",
"web3-provider-engine": "^7.6.3",
"web3-provider-engine": "^7.6.5",
"web3-stream-provider": "^2.0.1",
"xtend": "^4.0.1"
},
"devDependencies": {

View File

@ -12,6 +12,9 @@ Heres some utilities for preparing the data uri:
build a template using pure svg:
generate uri
'data:image/svg+xml;charset=utf-8,'+encodeURIComponent(svgSrc)
<svg xmlns='http://www.w3.org/2000/svg'
width='1000px' height='500px' viewBox='0 0 200 100'>
<rect x='0' y='0' width='100%' height='100%' fill='white' />

View File

@ -21,7 +21,13 @@ describe('#recoverFromSeed(password, seed)', function() {
// stub out account manager
actions._setAccountManager({
recoverFromSeed(pw, seed, cb) { cb(null, [{}, {}]) },
recoverFromSeed(pw, seed, cb) {
cb(null, {
identities: {
foo: 'bar'
}
})
},
})
it('sets metamask.isUnlocked to true', function() {

View File

@ -0,0 +1,36 @@
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, '..', '..', '..', 'ui', 'app', 'actions.js'))
var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js'))
describe('SAVE_ACCOUNT_LABEL', function() {
it('updates the state.metamask.identities[:i].name property of the state to the action.value.label', function() {
var initialState = {
metamask: {
identities: {
foo: {
name: 'bar'
}
},
}
}
freeze(initialState)
const action = {
type: actions.SAVE_ACCOUNT_LABEL,
value: {
account: 'foo',
label: 'baz'
},
}
freeze(action)
var resultingState = reducers(initialState, action)
assert.equal(resultingState.metamask.identities.foo.name, action.value.label)
});
});

View File

@ -26,3 +26,24 @@ describe('SET_SELECTED_ACCOUNT', function() {
assert.equal(resultingState.appState.activeAddress, action.value)
});
});
describe('SHOW_ACCOUNT_DETAIL', function() {
it('updates metamask state', function() {
var initialState = {
metamask: {
selectedAccount: 'foo'
}
}
freeze(initialState)
const action = {
type: actions.SHOW_ACCOUNT_DETAIL,
value: 'bar',
}
freeze(action)
var resultingState = reducers(initialState, action)
assert.equal(resultingState.metamask.selectedAccount, action.value)
assert.equal(resultingState.metamask.selectedAddress, action.value)
})
})

View File

@ -54,6 +54,27 @@ describe('config-manager', function() {
})
})
describe('wallet nicknames', function() {
it('should return null when no nicknames are saved', function() {
var nick = configManager.nicknameForWallet('0x0')
assert.equal(nick, null, 'no nickname returned')
})
it('should persist nicknames', function() {
var account = '0x0'
var nick1 = 'foo'
var nick2 = 'bar'
configManager.setNicknameForWallet(account, nick1)
var result1 = configManager.nicknameForWallet(account)
assert.equal(result1, nick1)
configManager.setNicknameForWallet(account, nick2)
var result2 = configManager.nicknameForWallet(account)
assert.equal(result2, nick2)
})
})
describe('rpc manipulations', function() {
it('changing rpc should return a different rpc', function() {
var firstRpc = 'first'

View File

@ -17,6 +17,53 @@ describe('util', function() {
this.sinon.restore()
})
describe('addressSummary', function() {
it('should add case-sensitive checksum', function() {
var address = '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825'
var result = util.addressSummary(address)
assert.equal(result, '0xFDEa65C8...b825')
})
})
describe('isValidAddress', function() {
it('should allow 40-char non-prefixed hex', function() {
var address = 'fdea65c8e26263f6d9a1b5de9555d2931a33b825'
var result = util.isValidAddress(address)
assert.ok(result)
})
it('should allow 42-char non-prefixed hex', function() {
var address = '0xfdea65c8e26263f6d9a1b5de9555d2931a33b825'
var result = util.isValidAddress(address)
assert.ok(result)
})
it('should not allow less non hex-prefixed', function() {
var address = 'fdea65c8e26263f6d9a1b5de9555d2931a33b85'
var result = util.isValidAddress(address)
assert.ok(!result)
})
it('should not allow less hex-prefixed', function() {
var address = '0xfdea65ce26263f6d9a1b5de9555d2931a33b85'
var result = util.isValidAddress(address)
assert.ok(!result)
})
it('should recognize correct capitalized checksum', function() {
var address = '0xFDEa65C8e26263F6d9A1B5de9555D2931A33b825'
var result = util.isValidAddress(address)
assert.ok(result)
})
it('should recognize incorrect capitalized checksum', function() {
var address = '0xFDea65C8e26263F6d9A1B5de9555D2931A33b825'
var result = util.isValidAddress(address)
assert.ok(!result)
})
})
describe('numericBalance', function() {
it('should return a BN 0 if given nothing', function() {
@ -112,8 +159,29 @@ describe('util', function() {
})
})
describe('normalizeEthStringToWei', function() {
it('should convert decimal eth to pure wei BN', function() {
var input = '1.23456789'
var output = util.normalizeEthStringToWei(input)
assert.equal(output.toString(10), '1234567890000000000')
})
it('should convert 1 to expected wei', function() {
var input = '1'
var output = util.normalizeEthStringToWei(input)
assert.equal(output.toString(10), ethInWei)
})
})
describe('#normalizeNumberToWei', function() {
it('should handle a simple use case', function() {
var input = 0.0002
var output = util.normalizeNumberToWei(input, 'ether')
var str = output.toString(10)
assert.equal(str, '200000000000000')
})
it('should convert a kwei number to the appropriate equivalent wei', function() {
var result = util.normalizeNumberToWei(1.111, 'kwei')
assert.equal(result.toString(10), '1111', 'accepts decimals')

View File

@ -5,11 +5,15 @@ const h = require('react-hyperscript')
const connect = require('react-redux').connect
const copyToClipboard = require('copy-to-clipboard')
const actions = require('./actions')
const addressSummary = require('./util').addressSummary
const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
const AccountPanel = require('./components/account-panel')
const Identicon = require('./components/identicon')
const EtherBalance = require('./components/eth-balance')
const transactionList = require('./components/transaction-list')
const ExportAccountView = require('./components/account-export')
const ethUtil = require('ethereumjs-util')
const EditableLabel = require('./components/editable-label')
module.exports = connect(mapStateToProps)(AccountDetailScreen)
@ -30,75 +34,131 @@ function AccountDetailScreen() {
}
AccountDetailScreen.prototype.render = function() {
var state = this.props
var selected = state.address || Object.keys(state.accounts)[0]
var identity = state.identities[selected]
var account = state.accounts[selected]
var accountDetail = state.accountDetail
var transactions = state.transactions
var props = this.props
var selected = props.address || Object.keys(props.accounts)[0]
var identity = props.identities[selected]
var account = props.accounts[selected]
var accountDetail = props.accountDetail
var transactions = props.transactions
return (
h('.account-detail-section.flex-column.flex-grow', {
style: {
width: '330px',
},
}, [
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,
key: 'accountPanel'
}),
h('div', {
// identicon, label, balance, etc
h('.account-data-subsection.flex-column.flex-grow', {
style: {
display: 'flex',
}
margin: '0 20px',
},
}, [
h('button', {
onClick: () => {
copyToClipboard(identity.address)
// header - identicon + nav
h('.flex-row.flex-space-between', {
style: {
marginTop: 28,
},
}, 'COPY ADDR'),
}, [
h('button', {
onClick: () => {
this.props.dispatch(actions.showSendPage())
},
}, 'SEND'),
// invisible placeholder for later
h('i.fa.fa-users.fa-lg.color-orange', {
style: {
visibility: 'hidden',
},
}),
h('button', {
onClick: () => {
this.requestAccountExport(identity.address)
// large identicon
h('.identicon-wrapper.flex-column.flex-center.select-none', [
h(Identicon, {
diameter: 62,
address: selected,
}),
]),
// small accounts nav
h('i.fa.fa-users.fa-lg.cursor-pointer.color-orange', {
onClick: this.navigateToAccounts.bind(this),
}),
]),
h('.flex-center', {
style: {
height: '62px',
paddingTop: '8px',
}
}, [
h(EditableLabel, {
textValue: identity ? identity.name : '',
state: {
isEditingLabel: false,
},
saveText: (text) => {
props.dispatch(actions.saveAccountLabel(selected, text))
},
}, [
// What is shown when not editing:
h('h2.font-medium.color-forest', identity && identity.name)
]),
]),
// address and getter actions
h('.flex-row.flex-space-between', {
style: {
marginBottom: 16,
},
}, 'EXPORT'),
}, [
h('div', {
style: {
lineHeight: '16px',
},
}, addressSummary(selected)),
h('i.fa.fa-download.fa-md.cursor-pointer.color-orange', {
onClick: () => this.requestAccountExport(selected),
}),
h('i.fa.fa-qrcode.fa-md.cursor-disabled.color-orange', {
onClick: () => console.warn('QRCode not implented...'),
}),
h('i.fa.fa-clipboard.fa-md.cursor-pointer.color-orange', {
onClick: () => copyToClipboard(ethUtil.toChecksumAddress(selected)),
}),
]),
// balance + send
h('.flex-row.flex-space-between', [
h(EtherBalance, {
value: account && account.balance,
style: {
lineHeight: '50px',
},
}),
h('button', {
onClick: () => this.props.dispatch(actions.showSendPage()),
style: {
margin: 10,
},
}, 'SEND ETH'),
]),
]),
// subview (tx history, pk export confirm)
h(ReactCSSTransitionGroup, {
transitionName: "main",
className: 'css-transition-group',
transitionName: 'main',
transitionEnterTimeout: 300,
transitionLeaveTimeout: 300,
}, [
this.subview(),
]),
// transaction table
/*
h('section.flex-column', [
h('span', 'your transaction history will go here.'),
]),
*/
])
)
}
@ -126,10 +186,15 @@ AccountDetailScreen.prototype.transactionList = function() {
var state = this.props
var transactions = state.transactions
return transactionList(transactions
.filter(tx => tx.txParams.from === state.address)
.filter(tx => tx.txParams.metamaskNetworkId === state.networkVersion)
.sort((a, b) => b.time - a.time), state.networkVersion)
var txsToRender = transactions
// only transactions that are from the current address
.filter(tx => tx.txParams.from === state.address)
// only transactions that are on the current network
.filter(tx => tx.txParams.metamaskNetworkId === state.networkVersion)
// sort by recency
.sort((a, b) => b.time - a.time)
return transactionList(txsToRender, state.networkVersion)
}
AccountDetailScreen.prototype.navigateToAccounts = function(event){

View File

@ -3,9 +3,13 @@ const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const extend = require('xtend')
const Identicon = require('./components/identicon')
const actions = require('./actions')
const AccountPanel = require('./components/account-panel')
const EtherBalance = require('./components/eth-balance')
const valuesFor = require('./util').valuesFor
const addressSummary = require('./util').addressSummary
const formatBalance = require('./util').formatBalance
const findDOMNode = require('react-dom').findDOMNode
module.exports = connect(mapStateToProps)(AccountsScreen)
@ -17,6 +21,7 @@ function mapStateToProps(state) {
unconfTxs: state.metamask.unconfTxs,
selectedAddress: state.metamask.selectedAddress,
currentDomain: state.appState.currentDomain,
scrollToBottom: state.appState.scrollToBottom,
}
}
@ -33,37 +38,52 @@ AccountsScreen.prototype.render = function() {
var actions = {
onSelect: this.onSelect.bind(this),
onShowDetail: this.onShowDetail.bind(this),
revealAccount: this.onRevealAccount.bind(this),
goHome: this.goHome.bind(this),
}
return (
h('.accounts-section.flex-column.flex-grow', [
h('.accounts-section.flex-grow', [
// subtitle and nav
h('.section-title.flex-column.flex-center', [
h('h2.page-subtitle', 'Accounts'),
h('.section-title.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: actions.goHome,
}),
h('h2.page-subtitle', 'Select Account'),
]),
// current domain
/* AUDIT
* Temporarily removed
* since accounts are currently injected
* regardless of the current domain.
*/
h('.current-domain-panel.flex-center.font-small', [
h('span', 'Selected address is visible to all sites you visit.'),
// h('span', state.currentDomain),
]),
h('hr.horizontal-line'),
// identity selection
h('section.identity-section.flex-column', {
style: {
maxHeight: '290px',
height: '418px',
overflowY: 'auto',
overflowX: 'hidden',
}
},
identityList.map(renderAccountPanel)
),
[
identityList.map(renderAccountPanel),
h('hr.horizontal-line', {key: 'horizontal-line1'}),
h('div.footer.hover-white.pointer', {
key: 'reveal-account-bar',
onClick:() => {
actions.revealAccount()
},
style: {
display: 'flex',
flex: '1 0 auto',
height: '40px',
paddint: '10px',
justifyContent: 'center',
alignItems: 'center',
}
}, [
h('i.fa.fa-chevron-down.fa-lg', {key: ''}),
]),
]),
unconfTxList.length ? (
@ -77,10 +97,7 @@ AccountsScreen.prototype.render = function() {
) : (
null
),
])
)
function renderAccountPanel(identity){
@ -94,7 +111,48 @@ AccountsScreen.prototype.render = function() {
isSelected: false,
isFauceting: isFauceting,
})
return h(AccountPanel, componentState)
const selectedClass = isSelected ? '.selected' : ''
return (
h(`.accounts-list-option.flex-row.flex-space-between.pointer.hover-white${selectedClass}`, {
key: `account-panel-${identity.address}`,
style: {
flex: '1 0 auto',
},
onClick: (event) => actions.onShowDetail(identity.address, event),
}, [
h('.identicon-wrapper.flex-column.flex-center.select-none', [
h(Identicon, {
address: identity.address
}),
]),
// account address, balance
h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [
h('span', identity.name),
h('span.font-small', addressSummary(identity.address)),
// h('span.font-small', formatBalance(account.balance)),
h(EtherBalance, {
value: account.balance,
}),
]),
])
)
}
}
// If a new account was revealed, scroll to the bottom
AccountsScreen.prototype.componentDidUpdate = function(){
const scrollToBottom = this.props.scrollToBottom
if (scrollToBottom) {
var container = findDOMNode(this)
var scrollable = container.querySelector('.identity-section')
scrollable.scrollTop = scrollable.scrollHeight
}
}
@ -114,3 +172,11 @@ AccountsScreen.prototype.onShowDetail = function(address, event){
event.stopPropagation()
this.props.dispatch(actions.showAccountDetail(address))
}
AccountsScreen.prototype.onRevealAccount = function() {
this.props.dispatch(actions.revealAccount())
}
AccountsScreen.prototype.goHome = function() {
this.props.dispatch(actions.goHome())
}

View File

@ -1,6 +1,11 @@
var actions = {
GO_HOME: 'GO_HOME',
goHome: goHome,
// menu state
TOGGLE_MENU: 'TOGGLE_MENU',
toggleMenu: toggleMenu,
SET_MENU_STATE: 'SET_MENU_STATE',
closeMenu: closeMenu,
// remote state
UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE',
updateMetamaskState: updateMetamaskState,
@ -43,6 +48,8 @@ var actions = {
SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE',
SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE',
SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE',
REVEAL_ACCOUNT: 'REVEAL_ACCOUNT',
revealAccount: revealAccount,
// account detail screen
SHOW_SEND_PAGE: 'SHOW_SEND_PAGE',
showSendPage: showSendPage,
@ -52,6 +59,8 @@ var actions = {
exportAccount: exportAccount,
SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY',
showPrivateKey: showPrivateKey,
SAVE_ACCOUNT_LABEL: 'SAVE_ACCOUNT_LABEL',
saveAccountLabel: saveAccountLabel,
// tx conf screen
COMPLETED_TX: 'COMPLETED_TX',
TRANSACTION_ERROR: 'TRANSACTION_ERROR',
@ -105,6 +114,21 @@ function goHome() {
}
}
// menu state
function toggleMenu() {
return {
type: this.TOGGLE_MENU,
}
}
function closeMenu() {
return {
type: this.SET_MENU_STATE,
value: false,
}
}
// async actions
function tryUnlockMetamask(password) {
@ -114,7 +138,7 @@ function tryUnlockMetamask(password) {
if (err) {
dispatch(this.unlockFailed())
} else {
dispatch(this.unlockMetamask())
dispatch(this.unlockMetamask(selectedAccount))
}
})
}
@ -133,12 +157,12 @@ function recoverFromSeed(password, seed) {
return (dispatch) => {
// dispatch(this.createNewVaultInProgress())
dispatch(this.showLoadingIndication())
_accountManager.recoverFromSeed(password, seed, (err, selectedAccount) => {
_accountManager.recoverFromSeed(password, seed, (err, metamaskState) => {
dispatch(this.hideLoadingIndication())
if (err) return dispatch(this.displayWarning(err.message))
dispatch(this.goHome())
dispatch(this.unlockMetamask())
var account = Object.keys(metamaskState.identities)[0]
dispatch(this.unlockMetamask(account))
})
}
}
@ -155,6 +179,19 @@ function setSelectedAddress(address) {
}
}
function revealAccount() {
return (dispatch) => {
dispatch(this.showLoadingIndication())
_accountManager.revealAccount((err) => {
dispatch(this.hideLoadingIndication())
if (err) return dispatch(this.displayWarning(err.message))
dispatch({
type: this.REVEAL_ACCOUNT,
})
})
}
}
function signMsg(msgData) {
return (dispatch) => {
dispatch(this.showLoadingIndication())
@ -271,9 +308,10 @@ function unlockFailed() {
}
}
function unlockMetamask() {
function unlockMetamask(account) {
return {
type: this.UNLOCK_METAMASK,
value: account,
}
}
@ -297,11 +335,13 @@ function lockMetamask() {
function showAccountDetail(address) {
return (dispatch) => {
_accountManager.setSelectedAddress(address)
dispatch({
type: this.SHOW_ACCOUNT_DETAIL,
value: address,
dispatch(this.showLoadingIndication())
_accountManager.setSelectedAddress(address, (err, address) => {
dispatch(this.hideLoadingIndication())
dispatch({
type: this.SHOW_ACCOUNT_DETAIL,
value: address,
})
})
}
}
@ -312,19 +352,19 @@ function backToAccountDetail(address) {
value: address,
}
}
function clearSeedWordCache() {
function clearSeedWordCache(account) {
return {
type: this.CLEAR_SEED_WORD_CACHE
type: this.CLEAR_SEED_WORD_CACHE,
value: account,
}
}
function confirmSeedWords() {
return (dispatch) => {
dispatch(this.showLoadingIndication())
_accountManager.clearSeedWordCache((err, accounts) => {
dispatch(this.clearSeedWordCache())
console.log('Seed word cache cleared.')
dispatch(this.showAccountDetail(accounts[0].address))
_accountManager.clearSeedWordCache((err, account) => {
console.log('Seed word cache cleared. ' + account)
dispatch(this.showAccountDetail(account))
})
}
}
@ -443,6 +483,22 @@ function showPrivateKey(key) {
}
}
function saveAccountLabel(account, label) {
return (dispatch) => {
dispatch(this.showLoadingIndication())
_accountManager.saveAccountLabel(account, label, (err) => {
dispatch(this.hideLoadingIndication())
if (err) {
return dispatch(this.showWarning(err.message))
}
dispatch({
type: this.SAVE_ACCOUNT_LABEL,
value: { account, label },
})
})
}
}
function showSendPage() {
return {
type: this.SHOW_SEND_PAGE,

View File

@ -24,6 +24,9 @@ const ConfigScreen = require('./config')
const InfoScreen = require('./info')
const LoadingIndicator = require('./loading')
const txHelper = require('../lib/tx-helper')
const SandwichExpando = require('sandwich-expando')
const MenuDroppo = require('menu-droppo')
const DropMenuItem = require('./components/drop-menu-item')
module.exports = connect(mapStateToProps)(App)
@ -42,6 +45,7 @@ function mapStateToProps(state) {
seedWords: state.metamask.seedWords,
unconfTxs: state.metamask.unconfTxs,
unconfMsgs: state.metamask.unconfMsgs,
menuOpen: state.appState.menuOpen,
}
}
@ -50,15 +54,6 @@ App.prototype.render = function() {
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 (
@ -67,16 +62,13 @@ App.prototype.render = function() {
// Windows was showing a vertical scroll bar:
overflow: 'hidden',
}
},
[
}, [
h(LoadingIndicator),
// top row
h('.app-header.flex-column.flex-center', {
}, [
h('h1', 'MetaMask'),
]),
// app bar
this.renderAppBar(),
this.renderDropdown(),
// panel content
h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), {
@ -86,7 +78,8 @@ App.prototype.render = function() {
}
}, [
h(ReactCSSTransitionGroup, {
transitionName: "main",
className: 'css-transition-group',
transitionName: 'main',
transitionEnterTimeout: 300,
transitionLeaveTimeout: 300,
}, [
@ -95,71 +88,148 @@ App.prototype.render = function() {
]),
// footer
h('.app-footer.flex-row.flex-space-around', {
// 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.renderAppBar = function(){
var state = this.props
return (
h('div', [
h('.app-header.flex-row.flex-space-between', {
style: {
display: shouldHaveFooter ? 'flex' : 'none',
alignItems: 'center',
height: '56px',
}
}, [
visibility: state.isUnlocked ? 'visible' : 'none',
background: state.isUnlocked ? 'white' : 'none',
height: '36px',
position: 'relative',
zIndex: 1,
},
}, state.isUnlocked && [
// 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())
},
// mini logo
h('img', {
height: 24,
width: 24,
src: '/images/icon-128.png',
}),
// toggle
onOffToggle({
toggleMetamaskActive: this.toggleMetamaskActive.bind(this),
isUnlocked: state.isUnlocked,
}),
// metamask name
h('h1', 'MetaMask'),
// help
h('i.fa.fa-question.fa-lg.cursor-pointer', {
style: {
opacity: state.isUnlocked ? '1.0' : '0.0',
// hamburger
h(SandwichExpando, {
width: 16,
barHeight: 2,
padding: 0,
isOpen: state.menuOpen,
color: 'rgb(247,146,30)',
onClick: (event) => {
event.preventDefault()
event.stopPropagation()
this.props.dispatch(actions.toggleMenu())
},
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.renderDropdown = function() {
const props = this.props
return h(MenuDroppo, {
isOpen: props.menuOpen,
onClickOutside: (event) => {
this.props.dispatch(actions.closeMenu())
},
style: {
position: 'fixed',
right: 0,
zIndex: 0,
},
innerStyle: {
background: 'white',
boxShadow: '1px 1px 2px rgba(0,0,0,0.1)',
},
}, [ // DROP MENU ITEMS
h('style', `
.drop-menu-item:hover { background:rgb(235, 235, 235); }
.drop-menu-item i { margin: 11px; }
`),
h(DropMenuItem, {
label: 'Settings',
closeMenu:() => this.props.dispatch(actions.closeMenu()),
action:() => this.props.dispatch(actions.showConfigPage()),
icon: h('i.fa.fa-gear.fa-lg', { ariaHidden: true }),
}),
h(DropMenuItem, {
label: 'Lock Account',
closeMenu:() => this.props.dispatch(actions.closeMenu()),
action:() => this.props.dispatch(actions.lockMetamask()),
icon: h('i.fa.fa-lock.fa-lg', { ariaHidden: true }),
}),
h(DropMenuItem, {
label: 'Help',
closeMenu:() => this.props.dispatch(actions.closeMenu()),
action:() => this.props.dispatch(actions.showInfoPage()),
icon: h('i.fa.fa-question.fa-lg', { ariaHidden: true }),
}),
])
}
App.prototype.renderPrimary = function(state){
var state = this.props
App.prototype.renderPrimary = function(){
var props = this.props
// If seed words haven't been dismissed yet, show them still.
/*
if (state.seedWords) {
if (props.seedWords) {
return h(CreateVaultCompleteScreen, {key: 'createVaultComplete'})
}
*/
// show initialize screen
if (!state.isInitialized) {
if (!props.isInitialized) {
// show current view
switch (state.currentView.name) {
switch (props.currentView.name) {
case 'createVault':
return h(CreateVaultScreen, {key: 'createVault'})
@ -167,6 +237,9 @@ App.prototype.renderPrimary = function(state){
case 'restoreVault':
return h(RestoreVaultScreen, {key: 'restoreVault'})
case 'createVaultComplete':
return h(CreateVaultCompleteScreen, {key: 'createVaultComplete'})
default:
return h(InitializeMenuScreen, {key: 'menuScreenInit'})
@ -174,15 +247,12 @@ App.prototype.renderPrimary = function(state){
}
// show unlock screen
if (!state.isUnlocked) {
if (!props.isUnlocked) {
return h(UnlockScreen, {key: 'locked'})
}
// show current view
switch (state.currentView.name) {
case 'createVaultComplete':
return h(CreateVaultCompleteScreen, {key: 'created-vault'})
switch (props.currentView.name) {
case 'accounts':
return h(AccountsScreen, {key: 'accounts'})
@ -214,6 +284,18 @@ App.prototype.renderPrimary = function(state){
}
}
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.hasPendingTxs = function() {
var state = this.props
var unconfTxs = state.unconfTxs

View File

@ -1,6 +1,7 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const copyToClipboard = require('copy-to-clipboard')
const actions = require('../actions')
module.exports = ExportAccountView
@ -31,19 +32,28 @@ ExportAccountView.prototype.render = function() {
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
submit.`
return h('div', { key: 'exporting' }, [
h('p.error', warning),
h('p', confirmation),
h('input#exportAccount', {
onKeyPress: this.onExportKeyPress.bind(this),
}),
h('button', {
onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }),
}, 'Submit'),
h('button', {
onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address))
}, 'Cancel'),
])
return (
h('div', {
key: 'exporting',
style: {
margin: '0 20px',
},
}, [
h('p.error', warning),
h('p', confirmation),
h('input#exportAccount', {
onKeyPress: this.onExportKeyPress.bind(this),
}),
h('button', {
onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }),
}, 'Submit'),
h('button', {
onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address))
}, 'Cancel'),
])
)
}
if (accountExported) {

View File

@ -4,7 +4,7 @@ const Component = require('react').Component
const h = require('react-hyperscript')
const addressSummary = require('../util').addressSummary
const formatBalance = require('../util').formatBalance
const Identicon = require('identicon.js')
const Identicon = require('./identicon')
const Panel = require('./panel')

View File

@ -0,0 +1,31 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
module.exports = DropMenuItem
inherits(DropMenuItem, Component)
function DropMenuItem() {
Component.call(this)
}
DropMenuItem.prototype.render = function() {
return h('li.drop-menu-item', {
onClick:() => {
this.props.closeMenu()
this.props.action()
},
style: {
listStyle: 'none',
padding: '6px 16px 6px 5px',
fontFamily: 'Transat Medium',
color: 'rgb(125, 128, 130)',
cursor: 'pointer',
},
}, [
this.props.icon,
this.props.label,
])
}

View File

@ -0,0 +1,52 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const findDOMNode = require('react-dom').findDOMNode
module.exports = EditableLabel
inherits(EditableLabel, Component)
function EditableLabel() {
Component.call(this)
}
EditableLabel.prototype.render = function() {
const props = this.props
let state = this.state
if (state && state.isEditingLabel) {
return h('div.editable-label', [
h('input', {
defaultValue: props.textValue,
onKeyPress:(event) => {
this.saveIfEnter(event)
},
}),
h('button', {
onClick:() => this.saveText(),
}, 'Save')
])
} else {
return h('div', {
onClick:(event) => {
this.setState({ isEditingLabel: true })
},
}, this.props.children)
}
}
EditableLabel.prototype.saveIfEnter = function(event) {
if (event.key === 'Enter') {
this.saveText()
}
}
EditableLabel.prototype.saveText = function() {
var container = findDOMNode(this)
var text = container.querySelector('.editable-label input').value
this.props.saveText(text)
this.setState({ isEditingLabel: false, textLabel: text })
}

View File

@ -0,0 +1,40 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const parseBalance = require('../util').parseBalance
module.exports = EthBalanceComponent
inherits(EthBalanceComponent, Component)
function EthBalanceComponent() {
Component.call(this)
}
EthBalanceComponent.prototype.render = function() {
var state = this.props
var parsedAmount = parseBalance(state.value)
var beforeDecimal = parsedAmount[0]
var afterDecimal = parsedAmount[1]
var value = beforeDecimal+(afterDecimal ? '.'+afterDecimal : '')
var style = state.style
return (
h('.ether-balance', {
style: style,
}, [
h('.ether-balance-amount', {
style: {
display: 'inline',
},
}, value),
h('.ether-balance-label', {
style: {
display: 'inline',
marginLeft: 6,
},
}, 'ETH'),
])
)
}

View File

@ -0,0 +1,55 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const jazzicon = require('jazzicon')
const findDOMNode = require('react-dom').findDOMNode
module.exports = IdenticonComponent
inherits(IdenticonComponent, Component)
function IdenticonComponent() {
Component.call(this)
this.defaultDiameter = 46
}
IdenticonComponent.prototype.render = function() {
var state = this.props
var diameter = state.diameter || this.defaultDiameter
return (
h('div', {
key: 'identicon-' + this.props.address,
style: {
display: 'inline-block',
height: diameter,
width: diameter,
borderRadius: diameter / 2,
overflow: 'hidden',
},
})
)
}
IdenticonComponent.prototype.componentDidMount = function(){
var state = this.props
var address = state.address
if (!address) return
var numericRepresentation = jsNumberForAddress(address)
var container = findDOMNode(this)
// jazzicon with hack to fix inline svg error
var diameter = state.diameter || this.defaultDiameter
var identicon = jazzicon(diameter, numericRepresentation)
var identiconSrc = identicon.innerHTML
var dataUri = 'data:image/svg+xml;charset=utf-8,'+encodeURIComponent(identiconSrc)
var img = document.createElement('img')
img.src = dataUri
container.appendChild(img)
}
function jsNumberForAddress(address) {
var addr = address.slice(2, 10)
var seed = parseInt(addr, 16)
return seed
}

View File

@ -2,7 +2,7 @@ const inherits = require('util').inherits
const ethUtil = require('ethereumjs-util')
const Component = require('react').Component
const h = require('react-hyperscript')
const Identicon = require('identicon.js')
const Identicon = require('./identicon')
module.exports = Panel
@ -18,26 +18,22 @@ Panel.prototype.render = function() {
var identity = state.identity || {}
var account = state.account || {}
var isFauceting = state.isFauceting
var style = {
flex: '1 0 auto',
}
var identicon = new Identicon(state.identiconKey, 46).toString()
var identiconSrc = `data:image/png;base64,${identicon}`
if (state.onClick) style.cursor = 'pointer'
return (
h('.identity-panel.flex-row.flex-space-between', {
style: {
flex: '1 0 auto',
},
style,
onClick: state.onClick,
}, [
// account identicon
h('.identicon-wrapper.flex-column.select-none', [
h('img.identicon', {
src: identiconSrc,
style: {
border: 'none',
borderRadius: '20px',
}
h(Identicon, {
address: state.identiconKey,
}),
h('span.font-small', state.identiconLabel),
]),
@ -49,7 +45,7 @@ Panel.prototype.render = function() {
return h('.flex-row.flex-space-between', {
key: '' + Math.round(Math.random() * 1000000),
}, [
h('label.font-small', attr.key),
h('label.font-small.no-select', attr.key),
h('span.font-small', attr.value),
])
}),

View File

@ -1,55 +1,159 @@
const h = require('react-hyperscript')
const vreme = new (require('vreme'))
const formatBalance = require('../util').formatBalance
const addressSummary = require('../util').addressSummary
const explorerLink = require('../../lib/explorer-link')
const Panel = require('./panel')
const Identicon = require('./identicon')
const EtherBalance = require('./eth-balance')
module.exports = function(transactions, network) {
return h('section', [
return (
h('.current-domain-panel.flex-center.font-small', [
h('span', 'Transactions'),
]),
h('section.transaction-list', [
h('.tx-list', {
h('style', `
.transaction-list .transaction-list-item:not(:last-of-type) {
border-bottom: 1px solid #D4D4D4;
}
.transaction-list .transaction-list-item .ether-balance-label {
display: block !important;
font-size: small;
}
`),
h('h3.flex-center.text-transform-uppercase', {
style: {
background: '#EBEBEB',
color: '#AEAEAE',
},
}, [
'Transactions',
]),
h('.tx-list', {
style: {
overflowY: 'auto',
height: '180px',
height: '204px',
padding: '0 20px',
textAlign: 'center',
},
},
}, (
[
transactions.map((transaction) => {
console.dir(transaction)
var panelOpts = {
key: `tx-${transaction.hash}`,
identiconKey: transaction.txParams.to,
transactions.length ?
transactions.map(renderTransaction)
:
[h('.flex-center', {
style: {
cursor: 'pointer',
height: '100%',
},
onClick: (event) => {
var url = explorerLink(transaction.hash, parseInt(network))
chrome.tabs.create({ url });
},
attributes: [
{
key: 'TO',
value: addressSummary(transaction.txParams.to),
},
{
key: 'VALUE',
value: formatBalance(transaction.txParams.value),
},
]
}
}, 'No transaction history...')]
))
])
)
function renderTransaction(transaction, i){
var txParams = transaction.txParams
var date = formatDate(transaction.time)
return (
h(`.transaction-list-item.flex-row.flex-space-between${transaction.hash ? '.pointer' : ''}`, {
key: `tx-${transaction.id + i}`,
onClick: (event) => {
if (!transaction.hash) return
var url = explorerLink(transaction.hash, parseInt(network))
chrome.tabs.create({ url })
},
style: {
padding: '20px 0',
},
}, [
// large identicon
h('.identicon-wrapper.flex-column.flex-center.select-none', [
identicon(txParams, transaction),
]),
h('.flex-column', [
h('div', date),
recipientField(txParams, transaction),
]),
h(EtherBalance, {
value: txParams.value,
}),
])
return h(Panel, panelOpts)
})
]
)
}
}
])
}
function recipientField(txParams, transaction) {
if (txParams.to) {
return h('div', {
style: {
fontSize: 'small',
color: '#ABA9AA',
},
}, [
addressSummary(txParams.to),
failIfFailed(transaction),
])
} else {
return h('div', {
style: {
fontSize: 'small',
color: '#ABA9AA',
},
},[
'Contract Published',
failIfFailed(transaction),
])
}
}
function formatDate(date){
return vreme.format(new Date(date), 'March 16 2014 14:30')
}
function identicon(txParams, transaction) {
if (transaction.status === 'rejected') {
return h('i.fa.fa-exclamation-triangle.fa-lg.error', {
style: {
width: '24px',
}
})
}
if (txParams.to) {
return h(Identicon, {
diameter: 24,
address: txParams.to || transaction.hash,
})
} else {
return h('i.fa.fa-file-text-o.fa-lg', {
style: {
width: '24px',
}
})
}
}
function failIfFailed(transaction) {
if (transaction.status === 'rejected') {
return h('span.error', ' (Failed)')
}
}

View File

@ -77,7 +77,8 @@ ConfirmTxScreen.prototype.render = function() {
warningIfExists(state.warning),
h(ReactCSSTransitionGroup, {
transitionName: "main",
className: 'css-transition-group',
transitionName: 'main',
transitionEnterTimeout: 300,
transitionLeaveTimeout: 300,
}, [

View File

@ -1,2 +1,46 @@
@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);
@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css);
@font-face {
font-family: 'Transat Standard';
src: url('/fonts/Transat Standard/transat_standard-webfont.eot');
src: url('/fonts/Transat Standard/transat_standard-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/Transat Standard/transat_standard-webfont.woff') format('woff'),
url('/fonts/Transat Standard/transat_standard-webfont.ttf') format('truetype'),
url('/fonts/Transat Standard/transat_standard-webfont.svg#ywftsvg') format('svg');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Transat Black';
src: url('/fonts/Transat Black/transat_black-webfont.eot');
src: url('/fonts/Transat Black/transat_black-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/Transat Black/transat_black-webfont.woff') format('woff'),
url('/fonts/Transat Black/transat_black-webfont.ttf') format('truetype'),
url('/fonts/Transat Black/transat_black-webfont.svg#ywftsvg') format('svg');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Transat Medium';
src: url('/fonts/Transat Medium/transat_medium-webfont.eot');
src: url('/fonts/Transat Medium/transat_medium-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/Transat Medium/transat_medium-webfont.woff') format('woff'),
url('/fonts/Transat Medium/transat_medium-webfont.ttf') format('truetype'),
url('/fonts/Transat Medium/transat_medium-webfont.svg#ywftsvg') format('svg');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Transat Light';
src: url('/fonts/Transat Light/transat_light-webfont.eot');
src: url('/fonts/Transat Light/transat_light-webfont.eot?#iefix') format('embedded-opentype'),
url('/fonts/Transat Light/transat_light-webfont.woff') format('woff'),
url('/fonts/Transat Light/transat_light-webfont.ttf') format('truetype'),
url('/fonts/Transat Light/transat_light-webfont.svg#ywftsvg') format('svg');
font-weight: normal;
font-style: normal;
}

View File

@ -14,11 +14,15 @@ application specific styles
}
html, body {
/*font-family: 'Open Sans', Arial, sans-serif;*/
font-family: 'Roboto', 'Noto', sans-serif;
font-family: 'Transat Standard', Arial;
color: #4D4D4D;
font-weight: 300;
line-height: 1.4em;
background: #F7F7F7;
}
input:focus, textarea:focus {
outline: none;
}
#app-content {
@ -29,18 +33,18 @@ html, body {
}
button {
font-family: 'Transat Black';
outline: none;
cursor: pointer;
margin: 10px;
padding: 6px;
/*margin: 10px;*/
padding: 8px 12px;
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);
}
@ -48,26 +52,6 @@ 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;
@ -85,6 +69,16 @@ app
color: #909090;
}
button.primary {
padding: 8px 12px;
background: #F7861C;
box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36);
color: white;
font-size: 1.1em;
font-family: 'Transat Standard';
text-transform: uppercase;
}
button.btn-thin {
border: 1px solid;
border-color: #4D4D4D;
@ -98,23 +92,25 @@ button.btn-thin {
}
.app-header {
padding-top: 20px;
padding: 6px 8px;
}
.app-header h1 {
font-size: 2em;
font-weight: 300;
height: 42px;
font-family: 'Transat Medium';
text-transform: uppercase;
color: #AEAEAE;
}
h2.page-subtitle {
font-family: 'Transat Light';
text-transform: uppercase;
color: #AEAEAE;
font-size: 1em;
font-weight: 500;
height: 24px;
color: #F3C83E;
margin: 12px;
}
.app-primary {
}
.app-footer {
@ -216,33 +212,70 @@ app sections
margin: -2px 8px 0px -8px;
}
.unlock-screen label {
color: #F3C83E;
font-weight: 500;
.unlock-screen #metamask-mascot-container {
margin-top: 24px;
}
.unlock-screen h1 {
margin-top: -28px;
margin-bottom: 42px;
}
.unlock-screen input[type=password] {
width: 60%;
height: 22px;
padding: 2px;
border-radius: 4px;
border: 2px solid #F3C83E;
background: #FAF6F0;
width: 260px;
/*height: 36px;
margin-bottom: 24px;
padding: 8px;*/
}
.unlock-screen input[type=password]:focus {
outline: none;
border: 3px solid #F3C83E;
/* Webkit */
.unlock-screen input::-webkit-input-placeholder {
text-align: center;
font-size: 1.2em;
}
/* Firefox 18- */
.unlock-screen input:-moz-placeholder {
text-align: center;
font-size: 1.2em;
}
/* Firefox 19+ */
.unlock-screen input::-moz-placeholder {
text-align: center;
font-size: 1.2em;
}
/* IE */
.unlock-screen input:-ms-input-placeholder {
text-align: center;
font-size: 1.2em;
}
input.large-input, textarea.large-input {
/*margin-bottom: 24px;*/
padding: 8px;
}
input.large-input {
height: 36px;
}
/* accounts */
.accounts-section {
margin: 0 20px;
margin: 0 0px;
}
.current-domain-panel {
border: 1px solid #B7B7B7;
.accounts-section .horizontal-line {
margin: 0px 18px;
}
.accounts-list-option {
height: 120px;
}
.accounts-list-option .identicon-wrapper {
width: 100px;
}
.unconftx-link {
@ -289,8 +322,7 @@ app sections
/* accounts screen */
.identity-section {
border: 2px solid #4D4D4D;
margin: 0;
}
.identity-section .identity-panel {
@ -298,9 +330,6 @@ app sections
border-bottom: 1px solid #B1B1B1;
cursor: pointer;
}
.identity-section .identity-panel:hover {
background: #F9F9F9;
}
.identity-section .identity-panel.selected {
background: white;
@ -311,10 +340,15 @@ app sections
border-color: orange;
}
.identity-section .accounts-list-option:hover,
.identity-section .accounts-list-option.selected {
background:white;
}
/* account detail screen */
.account-detail-section {
margin: 0 20px;
}
/* tx confirm */
@ -333,157 +367,28 @@ app sections
background: #FAF6F0;
}
/* Send Screen */
/*
react toggle
*/
.send-screen {
/* 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;
.send-screen section {
margin: 8px 16px;
}
.react-toggle-screenreader-only {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
.send-screen input {
width: 100%;
font-size: 12px;
letter-spacing: 0.1em;
}
.react-toggle--disabled {
opacity: 0.5;
-webkit-transition: opacity 0.25s;
transition: opacity 0.25s;
/* Ether Balance Widget */
.ether-balance-amount {
color: #F7861C;
}
.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;
.ether-balance-label {
color: #ABA9AA;
}
.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;
}

View File

@ -1,3 +1,13 @@
/* color */
.color-orange {
color: #F7861C;
}
.color-forest {
color: #0A5448;
}
/* lib */
.full-width {
@ -47,6 +57,10 @@
flex: none;
}
.flex-basis-auto {
flex-basis: auto;
}
.flex-grow {
flex: 1 1 auto;
}
@ -86,13 +100,16 @@
}
.select-none {
cursor: default;
cursor: inherit;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.pointer {
cursor: pointer;
}
.cursor-pointer {
cursor: pointer;
transform-origin: center center;
@ -105,6 +122,10 @@
transform: scale(0.95);
}
.cursor-disabled {
cursor: not-allowed;
}
.margin-bottom-sml {
margin-bottom: 20px;
}
@ -121,23 +142,27 @@
font-weight: bold;
}
.text-transform-uppercase {
text-transform: uppercase;
}
.font-small {
font-size: 12px;
}
/* Send Screen */
.send-screen {
margin: 0 20px;
.font-medium {
font-size: 1.2em;
}
.send-screen section {
margin: 7px;
display: flex;
flex-direction: row;
justify-content: center;
hr.horizontal-line {
display: block;
height: 1px;
border: 0;
border-top: 1px solid #ccc;
margin: 1em 0;
padding: 0;
}
.send-screen details {
width: 100%;
}
.send-screen section input {
width: 100%;
.hover-white:hover {
background: white;
}

View File

@ -1,48 +1,42 @@
/* initial positions */
.app-primary.from-right .main-enter {
transform: translateX(400px);
/* universal */
.app-primary .main-enter {
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;
.app-primary.from-right .main-enter-active,
.app-primary.from-left .main-enter-active {
overflow-x: hidden;
transform: translateX(0px);
transition: transform 300ms ease-in;
}
/* final positions */
/* exited positions */
.app-primary.from-left .main-leave-active {
transform: translateX(400px);
position: absolute;
width: 100%;
transition: transform 300ms ease-in-out;
transform: translateX(360px);
transition: transform 300ms ease-in;
}
.app-primary.from-right .main-leave-active {
transform: translateX(-400px);
position: absolute;
width: 100%;
transition: transform 300ms ease-in-out;
transform: translateX(-360px);
transition: transform 300ms ease-in;
}
/* loader transitions */
.loader-enter, .loader-leave-active {
opacity: 0.0;
transition: opacity 150 ease-in-out;
transition: opacity 150 ease-in;
}
.loader-enter-active, .loader-leave {
opacity: 1.0;
transition: opacity 150 ease-in-out;
transition: opacity 150 ease-in;
}
/* entering positions */
.app-primary.from-right .main-enter:not(.main-enter-active) {
transform: translateX(360px);
}
.app-primary.from-left .main-enter:not(.main-enter-active) {
transform: translateX(-360px);
}

View File

@ -29,15 +29,6 @@ InitializeMenuScreen.prototype.render = function() {
switch (state.currentView.name) {
case 'createVault':
return h(CreateVaultScreen)
case 'createVaultComplete':
return h(CreateVaultCompleteScreen)
case 'restoreVault':
return this.renderRestoreVault()
default:
return this.renderMenu()
@ -55,12 +46,12 @@ InitializeMenuScreen.prototype.renderMenu = function() {
h('.initialize-screen.flex-column.flex-center.flex-grow', [
h('h2.page-subtitle', 'Welcome!'),
h(Mascot, {
animationEventEmitter: this.animationEventEmitter,
}),
h('h2.page-subtitle', 'MetaMask'),
h('button.btn-thin', {
onClick: this.showCreateVault.bind(this),
}, 'Create New Vault'),
@ -80,31 +71,6 @@ InitializeMenuScreen.prototype.renderMenu = function() {
)
}
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())
// }

View File

@ -23,7 +23,8 @@ LoadingIndicator.prototype.render = function() {
return (
h(ReactCSSTransitionGroup, {
transitionName: "loader",
className: 'css-transition-group',
transitionName: 'loader',
transitionEnterTimeout: 150,
transitionLeaveTimeout: 150,
}, [

View File

@ -22,6 +22,7 @@ function reduceApp(state, action) {
var seedWords = state.metamask.seedWords
var appState = extend({
menuOpen: false,
currentView: seedWords ? seedConfView : defaultView,
accountDetail: {
subview: 'transactions',
@ -34,6 +35,16 @@ function reduceApp(state, action) {
switch (action.type) {
case actions.TOGGLE_MENU:
return extend(appState, {
menuOpen: !appState.menuOpen,
})
case actions.SET_MENU_STATE:
return extend(appState, {
menuOpen: action.value,
})
// intialize
case actions.SHOW_CREATE_VAULT:
@ -154,7 +165,7 @@ function reduceApp(state, action) {
accountExport: 'none',
privateKey: '',
},
transForward: true,
transForward: false,
})
case actions.BACK_TO_ACCOUNT_DETAIL:
@ -177,9 +188,15 @@ function reduceApp(state, action) {
currentView: {
name: seedWords ? 'createVaultComplete' : 'accounts',
},
transForward: appState.currentView.name == 'locked',
transForward: true,
isLoading: false,
warning: null,
scrollToBottom: false,
})
case actions.REVEAL_ACCOUNT:
return extend(appState, {
scrollToBottom: true,
})
case actions.SHOW_CONF_TX_PAGE:
@ -278,10 +295,13 @@ function reduceApp(state, action) {
case actions.CLEAR_SEED_WORD_CACHE:
return extend(appState, {
transForward: true,
currentView: {
name: 'accounts',
},
currentView: {},
isLoading: false,
accountDetail: {
subview: 'transactions',
accountExport: 'none',
privateKey: '',
},
})
case actions.DISPLAY_WARNING:

View File

@ -29,6 +29,7 @@ function reduceMetamask(state, action) {
return extend(metamaskState, {
isUnlocked: true,
isInitialized: true,
selectedAccount: action.value,
})
case actions.LOCK_METAMASK:
@ -69,18 +70,38 @@ function reduceMetamask(state, action) {
}
return newState
case actions.SHOW_NEW_VAULT_SEED:
return extend(metamaskState, {
isUnlocked: true,
isInitialized: false,
})
case actions.CLEAR_SEED_WORD_CACHE:
var newState = extend(metamaskState, {
isUnlocked: true,
isInitialized: true,
selectedAccount: action.value,
})
delete newState.seedWords
return newState
case actions.CREATE_NEW_VAULT_IN_PROGRESS:
return extend(metamaskState, {
case actions.SHOW_ACCOUNT_DETAIL:
const newState = extend(metamaskState, {
isUnlocked: true,
isInitialized: true,
selectedAccount: action.value,
selectedAddress: action.value,
})
delete newState.seedWords
return newState
case actions.SAVE_ACCOUNT_LABEL:
const account = action.value.account
const name = action.value.label
var id = {}
id[account] = extend(metamaskState.identities[account], { name })
var identities = extend(metamaskState.identities, id)
return extend(metamaskState, { identities })
default:
return metamaskState

View File

@ -2,10 +2,13 @@ const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const Identicon = require('./components/identicon')
const actions = require('./actions')
const util = require('./util')
const numericBalance = require('./util').numericBalance
const AccountPanel = require('./components/account-panel')
const formatBalance = require('./util').formatBalance
const addressSummary = require('./util').addressSummary
const EtherBalance = require('./components/eth-balance')
const ethUtil = require('ethereumjs-util')
module.exports = connect(mapStateToProps)(SendTransactionScreen)
@ -18,6 +21,8 @@ function mapStateToProps(state) {
warning: state.appState.warning,
}
result.error = result.warning && result.warning.split('.')[0]
result.account = result.accounts[result.address]
result.identity = result.identities[result.address]
result.balance = result.account ? numericBalance(result.account.balance) : null
@ -32,95 +37,190 @@ function SendTransactionScreen() {
SendTransactionScreen.prototype.render = function() {
var state = this.props
var address = state.address
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'),
//
// Sender Profile
//
h('.account-data-subsection.flex-column.flex-grow', {
style: {
margin: '0 20px',
},
}, [
// header - identicon + nav
h('.flex-row.flex-space-between', {
style: {
marginTop: 28,
},
}, [
// invisible placeholder for later
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
onClick: this.back.bind(this),
}),
// large identicon
h('.identicon-wrapper.flex-column.flex-center.select-none', [
h(Identicon, {
diameter: 62,
address: address,
}),
]),
// small accounts nav
h('i.fa.fa-users.fa-lg.cursor-pointer.color-orange', {
onClick: this.navigateToAccounts.bind(this),
}),
]),
// account label
h('h2.font-medium.color-forest.flex-center', {
style: {
paddingTop: 8,
marginBottom: 8,
},
}, identity && identity.name),
// address and getter actions
h('.flex-row.flex-center', {
style: {
marginBottom: 8,
},
}, [
h('div', {
style: {
lineHeight: '16px',
},
}, addressSummary(address)),
]),
// balance
h('.flex-row.flex-center', [
// h('div', formatBalance(account && account.balance)),
h(EtherBalance, {
value: account && account.balance,
})
]),
]),
h(AccountPanel, {
showFullAddress: true,
identity: identity,
account: account,
}),
//
// Required Fields
//
h('section.recipient', [
h('input.address', {
h('h3.flex-center.text-transform-uppercase', {
style: {
background: '#EBEBEB',
color: '#AEAEAE',
marginTop: 32,
marginBottom: 16,
},
}, [
'Send Transaction',
]),
// error message
state.error && h('span.error.flex-center', state.error),
// 'to' field
h('section.flex-row.flex-center', [
h('input.large-input', {
name: 'address',
placeholder: 'Recipient Address',
})
]),
h('section.ammount', [
h('input.ether', {
// 'amount' and send button
h('section.flex-row.flex-center', [
h('input.large-input', {
name: 'amount',
placeholder: 'Amount',
type: 'number',
style: { marginRight: '6px' }
style: {
marginRight: 6,
},
}),
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', {
h('button.primary', {
onClick: this.onSubmit.bind(this),
style: {
textTransform: 'uppercase',
},
}, 'Send')
]),
//
// Optional Fields
//
h('h3.flex-center.text-transform-uppercase', {
style: {
background: '#EBEBEB',
color: '#AEAEAE',
marginTop: 16,
marginBottom: 16,
},
}, [
'Tranasactional Data (optional)',
]),
// 'data' field
h('section.flex-row.flex-center', [
h('input.large-input', {
name: 'txData',
placeholder: '0x01234',
style: {
width: '100%',
resize: 'none',
}
}),
]),
state.warning ? h('span.error', state.warning.split('.')[0]) : null,
])
)
}
SendTransactionScreen.prototype.navigateToAccounts = function(event){
event.stopPropagation()
this.props.dispatch(actions.showAccountsPage())
}
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
SendTransactionScreen.prototype.onSubmit = function() {
var inputAmount = parseFloat(document.querySelector('input.ether').value)
var currency = document.querySelector('select.currency').value
var value = util.normalizeNumberToWei(inputAmount, currency)
var balance = this.props.balance
const recipient = document.querySelector('input[name="address"]').value
const input = document.querySelector('input[name="amount"]').value
const value = util.normalizeEthStringToWei(input)
const txData = document.querySelector('input[name="txData"]').value
const 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.'
if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) {
var message = 'Recipient address is invalid.'
return this.props.dispatch(actions.displayWarning(message))
}
@ -128,12 +228,11 @@ SendTransactionScreen.prototype.onSubmit = function(event) {
this.props.dispatch(actions.showLoadingIndication())
var txParams = {
to: recipient,
from: this.props.address,
value: '0x' + value.toString(16),
}
var txData = document.querySelector('textarea.txData').value
if (recipient) txParams.to = ethUtil.addHexPrefix(recipient)
if (txData) txParams.data = txData
this.props.dispatch(actions.signTx(txParams))

View File

@ -29,19 +29,25 @@ UnlockScreen.prototype.render = function() {
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('h1', {
style: {
fontSize: '1.4em',
textTransform: 'uppercase',
color: '#7F8082',
},
}, 'MetaMask'),
h('input', {
h('input.large-input', {
type: 'password',
id: 'password-box',
placeholder: 'enter password',
style: {
},
onKeyPress: this.onKeyPress.bind(this),
onInput: this.inputChanged.bind(this),
}),
@ -54,6 +60,9 @@ UnlockScreen.prototype.render = function() {
h('button.primary.cursor-pointer', {
onClick: this.onSubmit.bind(this),
style: {
margin: 10,
},
}, 'Unlock'),
])

View File

@ -21,13 +21,17 @@ for (var currency in valueTable) {
module.exports = {
valuesFor: valuesFor,
addressSummary: addressSummary,
isAllOneCase: isAllOneCase,
isValidAddress: isValidAddress,
numericBalance: numericBalance,
parseBalance: parseBalance,
formatBalance: formatBalance,
dataSize: dataSize,
readableDate: readableDate,
ethToWei: ethToWei,
weiToEth: weiToEth,
normalizeToWei: normalizeToWei,
normalizeEthStringToWei: normalizeEthStringToWei,
normalizeNumberToWei: normalizeNumberToWei,
valueTable: valueTable,
bnTable: bnTable,
@ -41,7 +45,21 @@ function valuesFor(obj) {
}
function addressSummary(address) {
return address ? address.slice(0,2+8)+'...'+address.slice(-4) : '...'
if (!address) return ''
var checked = ethUtil.toChecksumAddress(address)
return checked ? checked.slice(0,2+8)+'...'+checked.slice(-4) : '...'
}
function isValidAddress(address) {
var prefixed = ethUtil.addHexPrefix(address)
return isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed) || ethUtil.isValidChecksumAddress(prefixed)
}
function isAllOneCase(address) {
if (!address) return true
var lower = address.toLowerCase()
var upper = address.toUpperCase()
return address === lower || address === upper
}
// Takes wei Hex, returns wei BN, even if input is null
@ -65,16 +83,30 @@ function weiToEth(bn) {
return eth
}
var decimalsToKeep = 4
function formatBalance(balance) {
if (!balance || balance === '0x0') return 'None'
// Takes hex, returns [beforeDecimal, afterDecimal]
function parseBalance(balance, decimalsToKeep) {
if (decimalsToKeep === undefined) decimalsToKeep = 4
if (!balance || balance === '0x0') return ['0', '']
var wei = numericBalance(balance)
var padded = wei.toString(10)
var len = padded.length
var nonZeroIndex = padded.match(/[^0]/) && padded.match(/[^0]/).index
var match = padded.match(/[^0]/)
var nonZeroIndex = match && match.index
var beforeDecimal = padded.substr(nonZeroIndex ? nonZeroIndex : 0, len - 18) || '0'
var afterDecimal = padded.substr(len - 18, decimalsToKeep)
return `${beforeDecimal}.${afterDecimal} ETH`
return [beforeDecimal, afterDecimal]
}
// Takes wei hex, returns "None" or "${formattedAmount} ETH"
function formatBalance(balance) {
var parsed = parseBalance(balance)
var beforeDecimal = parsed[0]
var afterDecimal = parsed[1]
if (beforeDecimal === '0' && afterDecimal === '') return 'None'
var result = beforeDecimal
if (afterDecimal) result += '.'+afterDecimal
result += ' ETH'
return result
}
function dataSize(data) {
@ -91,9 +123,23 @@ function normalizeToWei(amount, currency) {
return amount
}
var multiple = new ethUtil.BN('1000', 10)
function normalizeEthStringToWei(str) {
const parts = str.split('.')
let eth = new ethUtil.BN(parts[0], 10).mul(bnTable.wei)
if (parts[1]) {
var decimal = parts[1]
while(decimal.length < 18) {
decimal += '0'
}
const decimalBN = new ethUtil.BN(decimal, 10)
eth = eth.add(decimalBN)
}
return eth
}
var multiple = new ethUtil.BN('10000', 10)
function normalizeNumberToWei(n, currency) {
var enlarged = n * 1000
var enlarged = n * 10000
var amount = new ethUtil.BN(String(enlarged), 10)
return normalizeToWei(amount, currency).div(multiple)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB