Merge pull request #204 from MetaMask/dev
Merge UI redesign into master
10
CHANGELOG.md
@ -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.
|
||||
|
BIN
app/fonts/Transat Black/transat_black-webfont.eot
Executable file
2592
app/fonts/Transat Black/transat_black-webfont.svg
Executable file
After Width: | Height: | Size: 160 KiB |
BIN
app/fonts/Transat Black/transat_black-webfont.ttf
Executable file
BIN
app/fonts/Transat Black/transat_black-webfont.woff
Executable file
BIN
app/fonts/Transat Black/transat_black-webfont.woff2
Executable file
BIN
app/fonts/Transat Light/transat_light-webfont.eot
Executable file
2399
app/fonts/Transat Light/transat_light-webfont.svg
Executable file
After Width: | Height: | Size: 151 KiB |
BIN
app/fonts/Transat Light/transat_light-webfont.ttf
Executable file
BIN
app/fonts/Transat Light/transat_light-webfont.woff
Executable file
BIN
app/fonts/Transat Light/transat_light-webfont.woff2
Executable file
BIN
app/fonts/Transat Medium/transat_medium-webfont.eot
Executable file
2813
app/fonts/Transat Medium/transat_medium-webfont.svg
Executable file
After Width: | Height: | Size: 169 KiB |
BIN
app/fonts/Transat Medium/transat_medium-webfont.ttf
Executable file
BIN
app/fonts/Transat Medium/transat_medium-webfont.woff
Executable file
BIN
app/fonts/Transat Medium/transat_medium-webfont.woff2
Executable file
BIN
app/fonts/Transat Standard/transat_standard-webfont.eot
Executable file
2827
app/fonts/Transat Standard/transat_standard-webfont.svg
Executable file
After Width: | Height: | Size: 171 KiB |
BIN
app/fonts/Transat Standard/transat_standard-webfont.ttf
Executable file
BIN
app/fonts/Transat Standard/transat_standard-webfont.woff
Executable file
BIN
app/fonts/Transat Standard/transat_standard-webfont.woff2
Executable 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": {
|
||||
|
@ -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
@ -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,
|
||||
},
|
||||
}
|
||||
|
@ -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' })
|
||||
})
|
@ -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...
|
||||
|
37
app/scripts/lib/auto-reload.js
Normal 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)
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
||||
|
24
app/scripts/lib/ensnare.js
Normal 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
|
||||
}
|
@ -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.
|
||||
|
123
app/scripts/lib/inpage-provider.js
Normal 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
|
||||
}
|
@ -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(){}
|
@ -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
|
||||
|
@ -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'))
|
||||
})
|
||||
|
18
package.json
@ -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": {
|
||||
|
@ -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' />
|
||||
|
@ -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() {
|
||||
|
36
test/unit/actions/save_account_label_test.js
Normal 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)
|
||||
});
|
||||
});
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -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'
|
||||
|
@ -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')
|
||||
|
@ -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){
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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,
|
||||
|
212
ui/app/app.js
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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')
|
||||
|
||||
|
31
ui/app/components/drop-menu-item.js
Normal 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,
|
||||
])
|
||||
}
|
52
ui/app/components/editable-label.js
Normal 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 })
|
||||
}
|
40
ui/app/components/eth-balance.js
Normal 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'),
|
||||
])
|
||||
|
||||
)
|
||||
}
|
55
ui/app/components/identicon.js
Normal 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
|
||||
}
|
@ -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),
|
||||
])
|
||||
}),
|
||||
|
@ -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)')
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}, [
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
// }
|
||||
|
@ -23,7 +23,8 @@ LoadingIndicator.prototype.render = function() {
|
||||
|
||||
return (
|
||||
h(ReactCSSTransitionGroup, {
|
||||
transitionName: "loader",
|
||||
className: 'css-transition-group',
|
||||
transitionName: 'loader',
|
||||
transitionEnterTimeout: 150,
|
||||
transitionLeaveTimeout: 150,
|
||||
}, [
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
211
ui/app/send.js
@ -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))
|
||||
|
@ -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'),
|
||||
|
||||
])
|
||||
|
@ -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)
|
||||
}
|
||||
|
BIN
ui/design/02a-metamask-AccDetails-OverTransaction.jpg
Normal file
After Width: | Height: | Size: 119 KiB |
BIN
ui/design/02b-metamask-AccDetails-Send.jpg
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
ui/design/05-metamask-Menu.jpg
Normal file
After Width: | Height: | Size: 127 KiB |