Merge branch 'master' into EdgeCompatibility

This commit is contained in:
Dan Finlay 2016-08-29 16:14:51 -07:00
commit e85418b11a
76 changed files with 3762 additions and 485 deletions

View File

@ -1 +1,2 @@
app/scripts/lib/extension-instance.js
ui/app/conversion-util.js

1
.gitignore vendored
View File

@ -13,3 +13,4 @@ builds/
notes.txt
app/.DS_Store
development/bundle.js
builds.zip

View File

@ -2,7 +2,43 @@
## Current Master
- Fix bug where provider menu did not allow switching to custom network from a custom network.
## 2.10.0 2016-08-29
- Changed transaction approval from notifications system to popup system.
- Add a back button to locked screen to allow restoring vault from seed words when password is forgotten.
- Forms now retain their values even when closing the popup and reopening it.
- Fixed a spelling error in provider menu.
## 2.9.2 2016-08-24
- Fixed shortcut bug from preventing installation.
## 2.9.1 2016-08-24
- Added static image as fallback for when WebGL isn't supported.
- Transaction history now has a hard limit.
- Added info link on account screen that visits Etherscan.
- Fixed bug where a message signing request would be lost if the vault was locked.
- Added shortcut to open MetaMask (Ctrl+Alt+M or Cmd+Opt/Alt+M)
- Prevent API calls in tests.
- Fixed bug where sign message confirmation would sometimes render blank.
## 2.9.0 2016-08-22
- Added ShapeShift to the transaction history
- Added affiliate key to Shapeshift requests
- Added feature to reflect current conversion rates of current vault balance.
- Modify balance display logic.
## 2.8.0 2016-08-15
- Integrate ShapeShift
- Add a form for Coinbase to specify amount to buy
- Fix various typos.
- Make dapp-metamask connection more reliable
- Remove Ethereum Classic from provider menu.
## 2.7.3 2016-07-29

View File

@ -0,0 +1,10 @@
{
"appName": {
"message": "MetaMask",
"description": "The name of the application"
},
"appDescription": {
"message": "Administración de identidad en Ethereum",
"description": "The description of the application"
}
}

View File

@ -0,0 +1,10 @@
{
"appName": {
"message": "MetaMask",
"description": "The name of the application"
},
"appDescription": {
"message": "Administración de identidad en Ethereum",
"description": "The description of the application"
}
}

View File

@ -0,0 +1,10 @@
{
"appName": {
"message": "MetaMask",
"description": "The name of the application"
},
"appDescription": {
"message": "以太坊身份管理",
"description": "The description of the application"
}
}

1
app/currencies.json Normal file

File diff suppressed because one or more lines are too long

BIN
app/images/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -1,10 +1,20 @@
{
"name": "MetaMask",
"short_name": "Metamask",
"version": "2.7.3",
"version": "2.10.0",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "Ethereum Browser Extension",
"commands": {
"_execute_browser_action": {
"suggested_key": {
"windows": "Alt+Shift+M",
"mac": "Alt+Shift+M",
"chromeos": "Search+M",
"linux": "Alt+Shift+M"
}
}
},
"icons": {
"16": "images/icon-16.png",
"128": "images/icon-128.png"

16
app/notification.html Normal file
View File

@ -0,0 +1,16 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>MetaMask Notification</title>
<style>
body {
overflow: hidden;
}
</style>
</head>
<body>
<div id="app-content"></div>
<script src="./scripts/popup.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>

View File

@ -3,9 +3,7 @@ const extend = require('xtend')
const Dnode = require('dnode')
const eos = require('end-of-stream')
const PortStream = require('./lib/port-stream.js')
const createUnlockRequestNotification = require('./lib/notifications.js').createUnlockRequestNotification
const createTxNotification = require('./lib/notifications.js').createTxNotification
const createMsgNotification = require('./lib/notifications.js').createMsgNotification
const notification = require('./lib/notifications.js')
const messageManager = require('./lib/message-manager')
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
const MetamaskController = require('./metamask-controller')
@ -13,6 +11,7 @@ const extension = require('./lib/extension')
const STORAGE_KEY = 'metamask-config'
const controller = new MetamaskController({
// User confirmation callbacks:
showUnconfirmedMessage,
@ -25,41 +24,15 @@ const controller = new MetamaskController({
const idStore = controller.idStore
function unlockAccountMessage () {
createUnlockRequestNotification({
title: 'Account Unlock Request',
})
notification.show()
}
function showUnconfirmedMessage (msgParams, msgId) {
var controllerState = controller.getState()
createMsgNotification({
imageifyIdenticons: false,
txData: {
msgParams: msgParams,
time: (new Date()).getTime(),
},
identities: controllerState.identities,
accounts: controllerState.accounts,
onConfirm: idStore.approveMessage.bind(idStore, msgId, noop),
onCancel: idStore.cancelMessage.bind(idStore, msgId),
})
notification.show()
}
function showUnconfirmedTx (txParams, txData, onTxDoneCb) {
var controllerState = controller.getState()
createTxNotification({
imageifyIdenticons: false,
txData: {
txParams: txParams,
time: (new Date()).getTime(),
},
identities: controllerState.identities,
accounts: controllerState.accounts,
onConfirm: idStore.approveTransaction.bind(idStore, txData.id, noop),
onCancel: idStore.cancelTransaction.bind(idStore, txData.id),
})
notification.show()
}
//
@ -68,7 +41,7 @@ function showUnconfirmedTx (txParams, txData, onTxDoneCb) {
extension.runtime.onConnect.addListener(connectRemote)
function connectRemote (remotePort) {
var isMetaMaskInternalProcess = (remotePort.name === 'popup')
var isMetaMaskInternalProcess = remotePort.name === 'popup' || remotePort.name === 'notification'
var portStream = new PortStream(remotePort)
if (isMetaMaskInternalProcess) {
// communication with popup
@ -108,7 +81,7 @@ function setupControllerConnection (stream) {
dnode.on('remote', (remote) => {
// push updates to popup
controller.ethStore.on('update', controller.sendUpdate.bind(controller))
controller.remote = remote
controller.listeners.push(remote)
idStore.on('update', controller.sendUpdate.bind(controller))
// teardown on disconnect
@ -188,4 +161,3 @@ function setData (data) {
window.localStorage[STORAGE_KEY] = JSON.stringify(data)
}
function noop () {}

View File

@ -1,14 +1,13 @@
const MAINET_RPC_URL = 'https://mainnet.infura.io/'
const TESTNET_RPC_URL = 'https://morden.infura.io/'
const DEFAULT_RPC_URL = TESTNET_RPC_URL
const CLASSIC_RPC_URL = 'https://mainnet-nf.infura.io/'
global.METAMASK_DEBUG = false
module.exports = {
network: {
default: DEFAULT_RPC_URL,
mainnet: MAINET_RPC_URL,
testnet: TESTNET_RPC_URL,
classic: CLASSIC_RPC_URL,
},
}

View File

@ -1,4 +1,4 @@
const LocalMessageDuplexStream = require('./lib/local-message-stream.js')
const LocalMessageDuplexStream = require('post-message-stream')
const PortStream = require('./lib/port-stream.js')
const ObjectMultiplex = require('./lib/obj-multiplex')
const extension = require('./lib/extension')

View File

@ -1,7 +1,7 @@
/*global Web3*/
cleanContextForImports()
require('web3/dist/web3.min.js')
const LocalMessageDuplexStream = require('./lib/local-message-stream.js')
const LocalMessageDuplexStream = require('post-message-stream')
const setupDappAutoReload = require('./lib/auto-reload.js')
const MetamaskInpageProvider = require('./lib/inpage-provider.js')
restoreContextAfterImports()
@ -54,7 +54,7 @@ var __define
function cleanContextForImports () {
__define = global.define
try {
delete global.define
global.define = undefined
} catch (_) {
console.warn('MetaMask - global.define could not be deleted.')
}

View File

@ -1,5 +1,5 @@
const once = require('once')
const ensnare = require('./ensnare.js')
const ensnare = require('ensnare')
module.exports = setupDappAutoReload

View File

@ -1,10 +1,11 @@
const Migrator = require('pojo-migrator')
const MetamaskConfig = require('../config.js')
const migrations = require('./migrations')
const rp = require('request-promise')
const TESTNET_RPC = MetamaskConfig.network.testnet
const MAINNET_RPC = MetamaskConfig.network.mainnet
const CLASSIC_RPC = MetamaskConfig.network.classic
const txLimit = 40
/* The config-manager is a convenience object
* wrapping a pojo-migrator.
@ -15,6 +16,8 @@ const CLASSIC_RPC = MetamaskConfig.network.classic
*/
module.exports = ConfigManager
function ConfigManager (opts) {
this.txLimit = txLimit
// ConfigManager is observable and will emit updates
this._subs = []
@ -145,9 +148,6 @@ ConfigManager.prototype.getCurrentRpcAddress = function () {
case 'testnet':
return TESTNET_RPC
case 'classic':
return CLASSIC_RPC
default:
return provider && provider.rpcTarget ? provider.rpcTarget : TESTNET_RPC
}
@ -184,6 +184,9 @@ ConfigManager.prototype._saveTxList = function (txList) {
ConfigManager.prototype.addTx = function (tx) {
var transactions = this.getTxList()
while (transactions.length > this.txLimit - 1) {
transactions.shift()
}
transactions.push(tx)
this._saveTxList(transactions)
}
@ -274,9 +277,61 @@ ConfigManager.prototype.getConfirmed = function () {
return ('isConfirmed' in data) && data.isConfirmed
}
ConfigManager.prototype.setShouldntShowWarning = function (confirmed) {
ConfigManager.prototype.setCurrentFiat = function (currency) {
var data = this.getData()
data.isEthConfirmed = confirmed
data.fiatCurrency = currency
this.setData(data)
}
ConfigManager.prototype.getCurrentFiat = function () {
var data = this.getData()
return ('fiatCurrency' in data) && data.fiatCurrency
}
ConfigManager.prototype.updateConversionRate = function () {
var data = this.getData()
return rp(`https://www.cryptonator.com/api/ticker/eth-${data.fiatCurrency}`)
.then((response) => {
const parsedResponse = JSON.parse(response)
this.setConversionPrice(parsedResponse.ticker.price)
this.setConversionDate(parsedResponse.timestamp)
}).catch((err) => {
console.error('Error in conversion.', err)
this.setConversionPrice(0)
this.setConversionDate('N/A')
})
}
ConfigManager.prototype.setConversionPrice = function (price) {
var data = this.getData()
data.conversionRate = Number(price)
this.setData(data)
}
ConfigManager.prototype.setConversionDate = function (datestring) {
var data = this.getData()
data.conversionDate = datestring
this.setData(data)
}
ConfigManager.prototype.getConversionRate = function () {
var data = this.getData()
return (('conversionRate' in data) && data.conversionRate) || 0
}
ConfigManager.prototype.getConversionDate = function () {
var data = this.getData()
return (('conversionDate' in data) && data.conversionDate) || 'N/A'
}
ConfigManager.prototype.setShouldntShowWarning = function () {
var data = this.getData()
if (data.isEthConfirmed) {
data.isEthConfirmed = !data.isEthConfirmed
} else {
data.isEthConfirmed = true
}
this.setData(data)
}
@ -284,3 +339,37 @@ ConfigManager.prototype.getShouldntShowWarning = function () {
var data = this.getData()
return ('isEthConfirmed' in data) && data.isEthConfirmed
}
ConfigManager.prototype.getShapeShiftTxList = function () {
var data = this.getData()
var shapeShiftTxList = data.shapeShiftTxList ? data.shapeShiftTxList : []
shapeShiftTxList.forEach((tx) => {
if (tx.response.status !== 'complete') {
var requestListner = function (request) {
tx.response = JSON.parse(this.responseText)
if (tx.response.status === 'complete') {
tx.time = new Date().getTime()
}
}
var shapShiftReq = new XMLHttpRequest()
shapShiftReq.addEventListener('load', requestListner)
shapShiftReq.open('GET', `https://shapeshift.io/txStat/${tx.depositAddress}`, true)
shapShiftReq.send()
}
})
this.setData(data)
return shapeShiftTxList
}
ConfigManager.prototype.createShapeShiftTx = function (depositAddress, depositType) {
var data = this.getData()
var shapeShiftTx = {depositAddress, depositType, key: 'shapeshift', time: new Date().getTime(), response: {}}
if (!data.shapeShiftTxList) {
data.shapeShiftTxList = [shapeShiftTx]
} else {
data.shapeShiftTxList.push(shapeShiftTx)
}
this.setData(data)
}

View File

@ -1,24 +0,0 @@
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(); obj[key] = val; return val },
})
return
}
})
return proxy
}

View File

@ -41,6 +41,12 @@ function Extension () {
}
} catch (e) {}
try {
if (browser[api]) {
_this[api] = browser[api]
}
} catch (e) {}
try {
_this.api = browser.extension[api]
} catch (e) {}

View File

@ -45,7 +45,11 @@ function IdentityStore (opts = {}) {
IdentityStore.prototype.createNewVault = function (password, entropy, cb) {
delete this._keyStore
var serializedKeystore = this.configManager.getWallet()
if (serializedKeystore) {
this.configManager.setData({})
}
this._createIdmgmt(password, null, entropy, (err) => {
if (err) return cb(err)
@ -100,6 +104,10 @@ IdentityStore.prototype.getState = function () {
unconfMsgs: messageManager.unconfirmedMsgs(),
messages: messageManager.getMsgList(),
selectedAddress: configManager.getSelectedAccount(),
shapeShiftTxList: configManager.getShapeShiftTxList(),
currentFiat: configManager.getCurrentFiat(),
conversionRate: configManager.getConversionRate(),
conversionDate: configManager.getConversionDate(),
}))
}
@ -153,8 +161,9 @@ IdentityStore.prototype.getNetwork = function (err) {
this._currentState.network = 'loading'
return this._didUpdate()
}
console.log('web3.getNetwork returned ' + network)
if (global.METAMASK_DEBUG) {
console.log('web3.getNetwork returned ' + network)
}
this._currentState.network = network
this._didUpdate()
})
@ -432,6 +441,7 @@ IdentityStore.prototype.tryPassword = function (password, cb) {
IdentityStore.prototype._createIdmgmt = function (password, seed, entropy, cb) {
const configManager = this.configManager
var keyStore = null
LightwalletKeyStore.deriveKeyFromPassword(password, (err, derivedKey) => {
if (err) return cb(err)
@ -475,7 +485,9 @@ IdentityStore.prototype._restoreFromSeed = function (password, seed, derivedKey)
keyStore.generateNewAddress(derivedKey, 3)
configManager.setWallet(keyStore.serialize())
console.log('restored from seed. saved to keystore')
if (global.METAMASK_DEBUG) {
console.log('restored from seed. saved to keystore')
}
return keyStore
}

View File

@ -1,9 +1,7 @@
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
@ -27,13 +25,6 @@ function MetamaskInpageProvider (connectionStream) {
})
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) {
@ -42,15 +33,23 @@ function MetamaskInpageProvider (connectionStream) {
})
asyncProvider.on('error', console.error.bind(console))
self.asyncProvider = asyncProvider
// overwrite own sendAsync method
self.sendAsync = asyncProvider.sendAsync.bind(asyncProvider)
// handle sendAsync requests via asyncProvider
self.sendAsync = function(payload, cb){
// rewrite request ids
var request = jsonrpcMessageTransform(payload, (message) => {
message.id = createRandomId()
return message
})
// forward to asyncProvider
asyncProvider.sendAsync(request, cb)
}
}
MetamaskInpageProvider.prototype.send = function (payload) {
const self = this
let selectedAddress
var result = null
let result = null
switch (payload.method) {
case 'eth_accounts':
@ -65,9 +64,10 @@ MetamaskInpageProvider.prototype.send = function (payload) {
result = selectedAddress || '0x0000000000000000000000000000000000000000'
break
// fallback to normal rpc
// throw not-supported Error
default:
return self.syncProvider.send(payload)
var message = 'The MetaMask Web3 object does not support synchronous methods. See https://github.com/MetaMask/faq/blob/master/DEVELOPERS.md#all-async---think-of-metamask-as-a-light-client for details.'
throw new Error(message)
}
@ -89,35 +89,6 @@ MetamaskInpageProvider.prototype.isConnected = function () {
// util
function createSyncProvider (providerConfig) {
providerConfig = providerConfig || {}
let syncProviderUrl
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
}
}
const provider = new HttpProvider(syncProviderUrl)
// Stubbing out the send method to throw on sync methods:
provider.send = function() {
var message = 'The MetaMask Web3 object does not support synchronous methods. See https://github.com/MetaMask/faq#all-async---think-of-metamask-as-a-light-client for details.'
throw new Error(message)
}
return provider
}
function remoteStoreWithLocalStorageCache (storageKey) {
// read local cache
var initState = JSON.parse(localStorage[storageKey] || '{}')
@ -129,3 +100,21 @@ function remoteStoreWithLocalStorageCache (storageKey) {
return store
}
function createRandomId(){
const extraDigits = 3
// 13 time digits
const datePart = new Date().getTime() * Math.pow(10, extraDigits)
// 3 random digits
const extraPart = Math.floor(Math.random() * Math.pow(10, extraDigits))
// 16 digits
return datePart + extraPart
}
function jsonrpcMessageTransform(payload, transformFn){
if (Array.isArray(payload)) {
return payload.map(transformFn)
} else {
return transformFn(payload)
}
}

View File

@ -0,0 +1,8 @@
module.exports = function isPopupOrNotification() {
const url = window.location.href
if (url.match(/popup.html$/)) {
return 'popup'
} else {
return 'notification'
}
}

View File

@ -1,56 +0,0 @@
const Duplex = require('readable-stream').Duplex
const inherits = require('util').inherits
module.exports = LocalMessageDuplexStream
inherits(LocalMessageDuplexStream, Duplex)
function LocalMessageDuplexStream (opts) {
Duplex.call(this, {
objectMode: true,
})
// this._origin = opts.origin
this._name = opts.name
this._target = opts.target
// console.log('LocalMessageDuplexStream ('+this._name+') - initialized...')
window.addEventListener('message', this._onMessage.bind(this), false)
}
// private
LocalMessageDuplexStream.prototype._onMessage = function (event) {
var msg = event.data
// console.log('LocalMessageDuplexStream ('+this._name+') - heard message...', event)
// validate message
if (event.origin !== location.origin) return // console.log('LocalMessageDuplexStream ('+this._name+') - rejected - (event.origin !== location.origin) ')
if (typeof msg !== 'object') return // console.log('LocalMessageDuplexStream ('+this._name+') - rejected - (typeof msg !== "object") ')
if (msg.target !== this._name) return // console.log('LocalMessageDuplexStream ('+this._name+') - rejected - (msg.target !== this._name) ', msg.target, this._name)
if (!msg.data) return // console.log('LocalMessageDuplexStream ('+this._name+') - rejected - (!msg.data) ')
// console.log('LocalMessageDuplexStream ('+this._name+') - accepted', msg.data)
// forward message
try {
this.push(msg.data)
} catch (err) {
this.emit('error', err)
}
}
// stream plumbing
LocalMessageDuplexStream.prototype._read = noop
LocalMessageDuplexStream.prototype._write = function (data, encoding, cb) {
// console.log('LocalMessageDuplexStream ('+this._name+') - sending message...')
var message = {
target: this._target,
data: data,
}
window.postMessage(message, location.origin)
cb()
}
// util
function noop () {}

View File

@ -1,159 +1,48 @@
const createId = require('hat')
const extend = require('xtend')
const unmountComponentAtNode = require('react-dom').unmountComponentAtNode
const findDOMNode = require('react-dom').findDOMNode
const render = require('react-dom').render
const h = require('react-hyperscript')
const PendingTxDetails = require('../../../ui/app/components/pending-tx-details')
const PendingMsgDetails = require('../../../ui/app/components/pending-msg-details')
const MetaMaskUiCss = require('../../../ui/css')
const extension = require('./extension')
var notificationHandlers = {}
const notifications = {
createUnlockRequestNotification: createUnlockRequestNotification,
createTxNotification: createTxNotification,
createMsgNotification: createMsgNotification,
show,
getPopup,
closePopup,
}
module.exports = notifications
window.METAMASK_NOTIFIER = notifications
setupListeners()
function setupListeners () {
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
if (!extension.notifications) return console.error('Chrome notifications API missing...')
// notification button press
extension.notifications.onButtonClicked.addListener(function (notificationId, buttonIndex) {
var handlers = notificationHandlers[notificationId]
if (buttonIndex === 0) {
handlers.confirm()
} else {
handlers.cancel()
function show () {
getPopup((popup) => {
if (popup) {
return extension.windows.update(popup.id, { focused: true })
}
extension.notifications.clear(notificationId)
})
// notification teardown
extension.notifications.onClosed.addListener(function (notificationId) {
delete notificationHandlers[notificationId]
extension.windows.create({
url: 'notification.html',
type: 'detached_panel',
focused: true,
width: 360,
height: 500,
})
})
}
// creation helper
function createUnlockRequestNotification (opts) {
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
if (!extension.notifications) return console.error('Chrome notifications API missing...')
var message = 'An Ethereum app has requested a signature. Please unlock your account.'
function getPopup(cb) {
var id = createId()
extension.notifications.create(id, {
type: 'basic',
iconUrl: '/images/icon-128.png',
title: opts.title,
message: message,
})
}
function createTxNotification (state) {
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
if (!extension.notifications) return console.error('Chrome notifications API missing...')
renderTxNotificationSVG(state, function (err, notificationSvgSource) {
if (err) throw err
showNotification(extend(state, {
title: 'New Unsigned Transaction',
imageUrl: toSvgUri(notificationSvgSource),
}))
})
}
function createMsgNotification (state) {
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
if (!extension.notifications) return console.error('Chrome notifications API missing...')
renderMsgNotificationSVG(state, function (err, notificationSvgSource) {
if (err) throw err
showNotification(extend(state, {
title: 'New Unsigned Message',
imageUrl: toSvgUri(notificationSvgSource),
}))
})
}
function showNotification (state) {
// guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236
if (!extension.notifications) return console.error('Chrome notifications API missing...')
var id = createId()
extension.notifications.create(id, {
type: 'image',
requireInteraction: true,
iconUrl: '/images/icon-128.png',
imageUrl: state.imageUrl,
title: state.title,
message: '',
buttons: [{
title: 'Approve',
}, {
title: 'Reject',
}],
})
notificationHandlers[id] = {
confirm: state.onConfirm,
cancel: state.onCancel,
// Ignore in test environment
if (!extension.windows) {
return cb(null)
}
}
function renderTxNotificationSVG (state, cb) {
var content = h(PendingTxDetails, state)
renderNotificationSVG(content, cb)
}
extension.windows.getAll({}, (windows) => {
let popup = windows.find((win) => {
return win.type === 'popup'
})
function renderMsgNotificationSVG (state, cb) {
var content = h(PendingMsgDetails, state)
renderNotificationSVG(content, cb)
}
function renderNotificationSVG (content, cb) {
var container = document.createElement('div')
var confirmView = h('div.app-primary', {
style: {
width: '360px',
height: '240px',
padding: '16px',
// background: '#F7F7F7',
background: 'white',
},
}, [
h('style', MetaMaskUiCss()),
content,
])
render(confirmView, container, function ready() {
var rootElement = findDOMNode(this)
var viewSource = rootElement.outerHTML
unmountComponentAtNode(container)
var svgSource = svgWrapper(viewSource)
// insert content into svg wrapper
cb(null, svgSource)
cb(popup)
})
}
function svgWrapper (content) {
var wrapperSource = `
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="240">
<foreignObject x="0" y="0" width="100%" height="100%">
<body xmlns="http://www.w3.org/1999/xhtml" height="100%">{{content}}</body>
</foreignObject>
</svg>
`
return wrapperSource.split('{{content}}').join(content)
}
function toSvgUri (content) {
return 'data:image/svg+xml;utf8,' + encodeURIComponent(content)
function closePopup() {
getPopup((popup) => {
if (!popup) return
extension.windows.remove(popup.id, console.error)
})
}

View File

@ -12,6 +12,7 @@ module.exports = class MetamaskController {
constructor (opts) {
this.opts = opts
this.listeners = []
this.configManager = new ConfigManager(opts)
this.idStore = new IdentityStore({
configManager: this.configManager,
@ -21,6 +22,9 @@ module.exports = class MetamaskController {
this.idStore.setStore(this.ethStore)
this.messageManager = messageManager
this.publicConfigStore = this.initPublicConfigStore()
this.configManager.setCurrentFiat('USD')
this.configManager.updateConversionRate()
this.scheduleConversionInterval()
}
getState () {
@ -40,7 +44,9 @@ module.exports = class MetamaskController {
setProviderType: this.setProviderType.bind(this),
useEtherscanProvider: this.useEtherscanProvider.bind(this),
agreeToDisclaimer: this.agreeToDisclaimer.bind(this),
setCurrentFiat: this.setCurrentFiat.bind(this),
agreeToEthWarning: this.agreeToEthWarning.bind(this),
// forward directly to idStore
createNewVault: idStore.createNewVault.bind(idStore),
recoverFromSeed: idStore.recoverFromSeed.bind(idStore),
@ -59,6 +65,8 @@ module.exports = class MetamaskController {
recoverSeed: idStore.recoverSeed.bind(idStore),
// coinbase
buyEth: this.buyEth.bind(this),
// shapeshift
createShapeShiftTx: this.createShapeShiftTx.bind(this),
}
}
@ -94,7 +102,9 @@ module.exports = class MetamaskController {
function logger (err, request, response) {
if (err) return console.error(err)
if (!request.isMetamaskInternal) {
console.log(`RPC (${originDomain}):`, request, '->', response)
if (global.METAMASK_DEBUG) {
console.log(`RPC (${originDomain}):`, request, '->', response)
}
if (response.error) {
console.error('Error in RPC response:\n', response.error)
}
@ -103,9 +113,9 @@ module.exports = class MetamaskController {
}
sendUpdate () {
if (this.remote) {
this.remote.sendUpdate(this.getState())
}
this.listeners.forEach((remote) => {
remote.sendUpdate(this.getState())
})
}
initializeProvider (opts) {
@ -121,10 +131,17 @@ module.exports = class MetamaskController {
},
// tx signing
approveTransaction: this.newUnsignedTransaction.bind(this),
signTransaction: idStore.signTransaction.bind(idStore),
signTransaction: (...args) => {
idStore.signTransaction(...args)
this.sendUpdate()
},
// msg signing
approveMessage: this.newUnsignedMessage.bind(this),
signMessage: idStore.signMessage.bind(idStore),
signMessage: (...args) => {
idStore.signMessage(...args)
this.sendUpdate()
},
}
var provider = MetaMaskProvider(providerOpts)
@ -184,6 +201,8 @@ module.exports = class MetamaskController {
// It's locked
if (!state.isUnlocked) {
// Allow the environment to define an unlock message.
this.opts.unlockAccountMessage()
idStore.addUnconfirmedTransaction(txParams, onTxDoneCb, noop)
@ -191,6 +210,7 @@ module.exports = class MetamaskController {
} else {
idStore.addUnconfirmedTransaction(txParams, onTxDoneCb, (err, txData) => {
if (err) return onTxDoneCb(err)
this.sendUpdate()
this.opts.showUnconfirmedTx(txParams, txData, onTxDoneCb)
})
}
@ -199,9 +219,11 @@ module.exports = class MetamaskController {
newUnsignedMessage (msgParams, cb) {
var state = this.idStore.getState()
if (!state.isUnlocked) {
this.idStore.addUnconfirmedMessage(msgParams, cb)
this.opts.unlockAccountMessage()
} else {
this.addUnconfirmedMessage(msgParams, cb)
this.sendUpdate()
}
}
@ -218,7 +240,9 @@ module.exports = class MetamaskController {
// Log blocks
processBlock (block) {
console.log(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`)
if (global.METAMASK_DEBUG) {
console.log(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`)
}
this.verifyNetwork()
}
@ -241,9 +265,34 @@ module.exports = class MetamaskController {
}
}
setCurrentFiat (fiat, cb) {
try {
this.configManager.setCurrentFiat(fiat)
this.configManager.updateConversionRate()
this.scheduleConversionInterval()
const data = {
conversionRate: this.configManager.getConversionRate(),
currentFiat: this.configManager.getCurrentFiat(),
conversionDate: this.configManager.getConversionDate(),
}
cb(data)
} catch (e) {
cb(null, e)
}
}
scheduleConversionInterval () {
if (this.conversionInterval) {
clearInterval(this.conversionInterval)
}
this.conversionInterval = setInterval(() => {
this.configManager.updateConversionRate()
}, 300000)
}
agreeToEthWarning (cb) {
try {
this.configManager.setShouldntShowWarning(true)
this.configManager.setShouldntShowWarning()
cb()
} catch (e) {
cb(e)
@ -283,6 +332,9 @@ module.exports = class MetamaskController {
})
}
createShapeShiftTx (depositAddress, depositType) {
this.configManager.createShapeShiftTx(depositAddress, depositType)
}
}
function noop () {}

View File

@ -9,7 +9,9 @@ const injectCss = require('inject-css')
const PortStream = require('./lib/port-stream.js')
const StreamProvider = require('web3-stream-provider')
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
const isPopupOrNotification = require('./lib/is-popup-or-notification')
const extension = require('./lib/extension')
const notification = require('./lib/notifications')
// setup app
var css = MetaMaskUiCss()
@ -22,7 +24,11 @@ async.parallel({
function connectToAccountManager (cb) {
// setup communication with background
var pluginPort = extension.runtime.connect({name: 'popup'})
var name = isPopupOrNotification()
closePopupIfOpen(name)
window.METAMASK_UI_TYPE = name
var pluginPort = extension.runtime.connect({ name })
var portStream = new PortStream(pluginPort)
// setup multiplexing
var mx = setupMultiplex(portStream)
@ -93,3 +99,9 @@ function setupApp (err, opts) {
networkVersion: opts.networkVersion,
})
}
function closePopupIfOpen(name) {
if (name !== 'notification') {
notification.closePopup()
}
}

View File

@ -4,3 +4,10 @@ machine:
dependencies:
pre:
- "npm i -g testem"
- "npm i -g mocha"
override:
- sudo apt-get install libxss1 libappindicator1 libindicator7 lsb-base
- curl -L -o google-chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
- sudo dpkg -i google-chrome.deb
- sudo sed -i 's|HERE/chrome\"|HERE/chrome\" --disable-setuid-sandbox|g' /opt/google/chrome/google-chrome
- rm google-chrome.deb

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,163 @@
{
"metamask": {
"currentFiat": "USD",
"conversionRate": 11.06608791,
"conversionDate": 1470421024,
"isInitialized": true,
"isUnlocked": true,
"currentDomain": "example.com",
"rpcTarget": "https://rawtestrpc.metamask.io/",
"shapeShiftTxList":[
{
"depositAddress": "1L8BJCR6KHkCiVceDqibt7zJscqPpH7pFw",
"depositType": "BTC",
"key": "shapeshift",
"time": 1471564825772,
"response": {
"status": "complete",
"outgoingCoin": "100.00",
"incomingCoin": "1.000",
"transaction": "0x3701e0ac344a12a1fc5417cf251109a7c41f3edab922310202630d9c012414c8"
}
},
{
"depositAddress": "1L8BJCR6KHkCiVceDqibt7zJscqPpH7pFw",
"depositType": "BTC",
"key": "shapeshift",
"time": 1471566579224,
"response": {
"status": "no_deposits",
"depositAddress": "1L8BJCR6KHkCiVceDqibt7zJscqPpH7pFw"
}
},
{
"depositAddress": "1L8BJCR6KHkCiVceDqibt7zJscqPpH7pFw",
"depositType": "BTC",
"key": "shapeshift",
"time": 1471566565378,
"response": {
"status": "received",
"depositAddress": "1L8BJCR6KHkCiVceDqibt7zJscqPpH7pFw"
}
}
],
"transactions": [ {
"id": 1467921503489592,
"txParams": {
"from": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"to": "0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761",
"value": "0x66c899104aa57038000",
"origin": "thelongestdomainnameintheworldandthensomeandthensomemoreandmore.com",
"metamaskId": 1467921503489592,
"metamaskNetworkId": "2"
},
"time": 1467921503489,
"status": "rejected",
"containsDelegateCall": false
},
{
"id": 1467923203344608,
"txParams": {
"from": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"to": "0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761",
"value": "0x0",
"origin": "thelongestdomainnameintheworldandthensomeandthensomemoreandmore.com",
"metamaskId": 1467923203344608,
"metamaskNetworkId": "2"
},
"time": 1467923203344,
"status": "confirmed",
"containsDelegateCall": false,
"hash": "0x957bbba51e2732a86c10c5e7e8a484a093795a06f2e2c38ad02da1b20aeca620"
},
{
"id": 1467921503489592,
"txParams": {
"from": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"to": "0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761",
"value": "0x66c899104aa57038000",
"origin": "thelongestdomainnameintheworldandthensomeandthensomemoreandmore.com",
"metamaskId": 1467921503489592,
"metamaskNetworkId": "2"
},
"time": 1467921503489,
"status": "confirmed",
"containsDelegateCall": false
}],
"identities": {
"0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": {
"name": "Wallet 1",
"address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"mayBeFauceting": false
},
"0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b": {
"name": "Wallet 2",
"address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b",
"mayBeFauceting": false
},
"0xeb9e64b93097bc15f01f13eae97015c57ab64823": {
"name": "Wallet 3",
"address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823",
"mayBeFauceting": false
},
"0x704107d04affddd9b66ab9de3dd7b095852e9b69": {
"name": "Wallet 4",
"address": "0x704107d04affddd9b66ab9de3dd7b095852e9b69",
"mayBeFauceting": false
}
},
"unconfTxs": {},
"accounts": {
"0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": {
"code": "0x",
"balance": "0x0",
"nonce": "0x0",
"address": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
},
"0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b": {
"code": "0x",
"nonce": "0x0",
"balance": "0x0",
"address": "0xec1adf982415d2ef5ec55899b9bfb8bc0f29251b"
},
"0xeb9e64b93097bc15f01f13eae97015c57ab64823": {
"code": "0x",
"nonce": "0x0",
"balance": "0x0",
"address": "0xeb9e64b93097bc15f01f13eae97015c57ab64823"
},
"0x704107d04affddd9b66ab9de3dd7b095852e9b69": {
"code": "0x",
"balance": "0x0",
"nonce": "0x0",
"address": "0x704107d04affddd9b66ab9de3dd7b095852e9b69"
}
},
"selectedAddress": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"network": "1",
"seedWords": null,
"isConfirmed": true,
"unconfMsgs": {},
"messages": [],
"provider": {
"type": "mainnet"
},
"selectedAccount": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
},
"appState": {
"menuOpen": false,
"currentView": {
"name": "accountDetail",
"detailView": null,
"context": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc"
},
"accountDetail": {
"subview": "transactions"
},
"currentDomain": "127.0.0.1:9966",
"transForward": true,
"isLoading": false,
"warning": null
},
"identities": {}
}

View File

@ -1,5 +1,8 @@
{
"metamask": {
"currentFiat": "USD",
"conversionRate": 11.06608791,
"conversionDate": 1470421024,
"isInitialized": true,
"isUnlocked": true,
"currentDomain": "example.com",
@ -72,7 +75,7 @@
"txParams": {
"from": "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc",
"to": "0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761",
"value": "0x99966c8104aa57038000",
"value": "0x0",
"origin": "thelongestdomainnameintheworldandthensomeandthensomemoreandmore.com",
"metamaskId": 1467923203344608,
"metamaskNetworkId": "2"

View File

@ -1,5 +1,8 @@
{
"metamask": {
"currentFiat": "USD",
"conversionRate": 11.06608791,
"conversionDate": 1470421024,
"isInitialized": true,
"isUnlocked": true,
"currentDomain": "example.com",
@ -81,4 +84,4 @@
"warning": null
},
"identities": {}
}
}

View File

@ -1,5 +1,8 @@
{
"metamask": {
"currentFiat": "USD",
"conversionRate": 11.06608791,
"conversionDate": 1470421024,
"isInitialized": true,
"isUnlocked": true,
"currentDomain": "example.com",
@ -68,7 +71,7 @@
"appState": {
"menuOpen": false,
"currentView": {
"name": "accounts"
"name": "config"
},
"accountDetail": {
"subview": "transactions",
@ -82,4 +85,4 @@
"scrollToBottom": true
},
"identities": {}
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,406 @@
{
"metamask": {
"isInitialized": true,
"isUnlocked": true,
"isEthConfirmed": true,
"currentDomain": "example.com",
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {
"name": "Wallet 1",
"address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"mayBeFauceting": false
},
"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": {
"name": "Wallet 2",
"address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb",
"mayBeFauceting": false
},
"0x2f8d4a878cfa04a6e60d46362f5644deab66572d": {
"name": "Wallet 3",
"address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d",
"mayBeFauceting": false
}
},
"unconfTxs": {},
"currentFiat": "USD",
"conversionRate": 11.02269525,
"conversionDate": 1472076963,
"accounts": {
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {
"balance": "0x056ace16d84b1c7e78",
"nonce": "0x17",
"code": "0x0",
"address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
},
"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": {
"balance": "0x00000000000000056bc75e2d63100000",
"nonce": "0x0",
"code": "0x0",
"address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb"
},
"0x2f8d4a878cfa04a6e60d46362f5644deab66572d": {
"balance": "0x00000000000000056bc75e2d63100000",
"nonce": "0x0",
"code": "0x0",
"address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d"
}
},
"transactions": [
{
"id": 1471975421223082,
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0x48ff0cbac0acefedf152281ee80e9a0a01d5da63",
"data": "0x90b98a11000000000000000000000000c5b8dbac4c1d3f152cdeb400e2313f309c410acb000000000000000000000000000000000000000000000000000000000000000a",
"origin": "localhost",
"metamaskId": 1471975421223082,
"metamaskNetworkId": "1471904489432"
},
"time": 1471975421223,
"status": "confirmed",
"containsDelegateCall": true,
"estimatedGas": "0x89ef",
"hash": "0xd0fe393e2586ebded866c9f13b90494e902bc49047fbf25ba2ac96c805a2f5d3"
},
{
"id": 1471975427199819,
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0x48ff0cbac0acefedf152281ee80e9a0a01d5da63",
"data": "0x90b98a11000000000000000000000000c5b8dbac4c1d3f152cdeb400e2313f309c410acb000000000000000000000000000000000000000000000000000000000000000a",
"origin": "localhost",
"metamaskId": 1471975427199819,
"metamaskNetworkId": "1471904489432"
},
"time": 1471975427199,
"status": "rejected",
"containsDelegateCall": true,
"estimatedGas": "0x89ef",
"hash": "0xb1f2f63f3e265f05d7c353ab38dd8b73fce8e7214489037311ee1f58a994dae3"
},
{
"id": 1471975806981442,
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0x48ff0cbac0acefedf152281ee80e9a0a01d5da63",
"data": "0x90b98a11000000000000000000000000c5b8dbac4c1d3f152cdeb400e2313f309c410acb000000000000000000000000000000000000000000000000000000000000000a",
"origin": "localhost",
"metamaskId": 1471975806981442,
"metamaskNetworkId": "1471904489432"
},
"time": 1471975806981,
"status": "confirmed",
"containsDelegateCall": true,
"estimatedGas": "0x89ef",
"hash": "0xeeb89e91aaeea14c4950016c45d60df8ee8874daa6f414de5cf267ea2c17bc6e"
},
{
"id": 1471975810133789,
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0x48ff0cbac0acefedf152281ee80e9a0a01d5da63",
"data": "0x90b98a11000000000000000000000000c5b8dbac4c1d3f152cdeb400e2313f309c410acb000000000000000000000000000000000000000000000000000000000000000a",
"origin": "localhost",
"metamaskId": 1471975810133789,
"metamaskNetworkId": "1471904489432"
},
"time": 1471975810133,
"status": "confirmed",
"containsDelegateCall": true,
"estimatedGas": "0x89ef",
"hash": "0xe54cab2e54b8495691b6d8315ca24a190cba546a9fcb056642479ce5770cec8b"
},
{
"id": 1471976546865348,
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0x48ff0cbac0acefedf152281ee80e9a0a01d5da63",
"data": "0x90b98a11000000000000000000000000c5b8dbac4c1d3f152cdeb400e2313f309c410acb000000000000000000000000000000000000000000000000000000000000000a",
"origin": "localhost",
"metamaskId": 1471976546865348,
"metamaskNetworkId": "1471904489432"
},
"time": 1471976546865,
"status": "confirmed",
"containsDelegateCall": true,
"estimatedGas": "0x89ef",
"hash": "0x54e10f77e17f28f4d12751749a2ca22f9b528592d1140ef53c6430a68e731542"
},
{
"id": 1471976930101889,
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0x48ff0cbac0acefedf152281ee80e9a0a01d5da63",
"data": "0x90b98a11000000000000000000000000c5b8dbac4c1d3f152cdeb400e2313f309c410acb0000000000000000000000000000000000000000000000000000000000000005",
"origin": "localhost",
"metamaskId": 1471976930101889,
"metamaskNetworkId": "1471904489432"
},
"time": 1471976930101,
"status": "confirmed",
"containsDelegateCall": true,
"estimatedGas": "0x89ef",
"hash": "0x60b5af26fad18c5549949064b67c8f965c9f20cd3e890c69512ca3acad10ed8b"
},
{
"id": 1471977268048169,
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0x48ff0cbac0acefedf152281ee80e9a0a01d5da63",
"data": "0x90b98a11000000000000000000000000c5b8dbac4c1d3f152cdeb400e2313f309c410acb0000000000000000000000000000000000000000000000000000000000000002",
"origin": "localhost",
"metamaskId": 1471977268048169,
"metamaskNetworkId": "1471904489432"
},
"time": 1471977268048,
"status": "confirmed",
"containsDelegateCall": true,
"estimatedGas": "0x89ef",
"hash": "0x1f96e29305ef11a9c993302c29e5419d87017e8222d4034daea0d86e155dc3aa"
},
{
"id": 1471977310778630,
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0x48ff0cbac0acefedf152281ee80e9a0a01d5da63",
"data": "0x90b98a11000000000000000000000000c5b8dbac4c1d3f152cdeb400e2313f309c410acb0000000000000000000000000000000000000000000000000000000000000005",
"origin": "localhost",
"metamaskId": 1471977310778630,
"metamaskNetworkId": "1471904489432"
},
"time": 1471977310778,
"status": "rejected",
"containsDelegateCall": true,
"estimatedGas": "0x89ef"
},
{
"id": 1471977316241561,
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0x48ff0cbac0acefedf152281ee80e9a0a01d5da63",
"data": "0x90b98a11000000000000000000000000c5b8dbac4c1d3f152cdeb400e2313f309c410acb0000000000000000000000000000000000000000000000000000000000000005",
"origin": "localhost",
"metamaskId": 1471977316241561,
"metamaskNetworkId": "1471904489432"
},
"time": 1471977316240,
"status": "confirmed",
"containsDelegateCall": true,
"estimatedGas": "0x89ef",
"hash": "0xdbd610c92d77a07c76b82e14e32674d382c45c4780dd2a550888b5cc40d54bcc"
},
{
"id": 1471977344018510,
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0x48ff0cbac0acefedf152281ee80e9a0a01d5da63",
"data": "0x90b98a11000000000000000000000000c5b8dbac4c1d3f152cdeb400e2313f309c410acb0000000000000000000000000000000000000000000000000000000000000004",
"origin": "localhost",
"metamaskId": 1471977344018510,
"metamaskNetworkId": "1471904489432"
},
"time": 1471977344018,
"status": "confirmed",
"containsDelegateCall": true,
"estimatedGas": "0x89ef",
"hash": "0x709d871d9ded0108de9f7718a7490b19d45e5e7562b1ba6c5bf6cce56e767d48"
},
{
"id": 1471977403830380,
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0x48ff0cbac0acefedf152281ee80e9a0a01d5da63",
"data": "0x90b98a11000000000000000000000000c5b8dbac4c1d3f152cdeb400e2313f309c410acb0000000000000000000000000000000000000000000000000000000000000001",
"origin": "localhost",
"metamaskId": 1471977403830380,
"metamaskNetworkId": "1471904489432"
},
"time": 1471977403830,
"status": "confirmed",
"containsDelegateCall": true,
"estimatedGas": "0x89ef",
"hash": "0x94f5088a127bba181b303d6427ae93cbfa9867997bea1326f30da311e36c6aca"
},
{
"id": 1471977431563703,
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0x48ff0cbac0acefedf152281ee80e9a0a01d5da63",
"data": "0x90b98a11000000000000000000000000c5b8dbac4c1d3f152cdeb400e2313f309c410acb0000000000000000000000000000000000000000000000000000000000000001",
"origin": "localhost",
"metamaskId": 1471977431563703,
"metamaskNetworkId": "1471904489432"
},
"time": 1471977431563,
"status": "rejected",
"containsDelegateCall": true,
"estimatedGas": "0x89ef"
},
{
"id": 1471977436074587,
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0x48ff0cbac0acefedf152281ee80e9a0a01d5da63",
"data": "0x90b98a11000000000000000000000000c5b8dbac4c1d3f152cdeb400e2313f309c410acb0000000000000000000000000000000000000000000000000000000000000001",
"origin": "localhost",
"metamaskId": 1471977436074587,
"metamaskNetworkId": "1471904489432"
},
"time": 1471977436074,
"status": "confirmed",
"containsDelegateCall": true,
"estimatedGas": "0x89ef",
"hash": "0x5f514dfe3bd48f6f301c809a7a75f73f0fc93bc3a0e469368b84dce032aff9ec"
},
{
"id": 1471991826717707,
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0x48ff0cbac0acefedf152281ee80e9a0a01d5da63",
"data": "0x90b98a11000000000000000000000000c5b8dbac4c1d3f152cdeb400e2313f309c410acb0000000000000000000000000000000000000000000000000000000000000004",
"origin": "localhost",
"metamaskId": 1471991826717707,
"metamaskNetworkId": "1471904489432"
},
"time": 1471991826717,
"status": "confirmed",
"containsDelegateCall": true,
"estimatedGas": "0x89ef",
"hash": "0xab1e706f8981680a6c921f9f57f8ce573392bbb4f0fe85cf45e5cbf858fa5f3e"
},
{
"id": 1471991851917592,
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0x48ff0cbac0acefedf152281ee80e9a0a01d5da63",
"data": "0x90b98a11000000000000000000000000c5b8dbac4c1d3f152cdeb400e2313f309c410acb0000000000000000000000000000000000000000000000000000000000000004",
"origin": "localhost",
"metamaskId": 1471991851917592,
"metamaskNetworkId": "1471904489432"
},
"time": 1471991851917,
"status": "confirmed",
"containsDelegateCall": true,
"estimatedGas": "0x89ef",
"hash": "0xdea144f7a6f06969739f676d8702a9a11efc689e032f1981fe67afc9261dd4de"
},
{
"id": 1471992032999543,
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0x48ff0cbac0acefedf152281ee80e9a0a01d5da63",
"data": "0x90b98a11000000000000000000000000c5b8dbac4c1d3f152cdeb400e2313f309c410acb0000000000000000000000000000000000000000000000000000000000000005",
"origin": "localhost",
"metamaskId": 1471992032999543,
"metamaskNetworkId": "1471904489432"
},
"time": 1471992032999,
"status": "confirmed",
"containsDelegateCall": true,
"estimatedGas": "0x89ef",
"hash": "0xdf31b8cc0fbd2ab6727e0b63536bd4eab51a147aa29e04691e68fae28b866fb3"
},
{
"id": 1471992043490878,
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"to": "0x48ff0cbac0acefedf152281ee80e9a0a01d5da63",
"data": "0x90b98a11000000000000000000000000c5b8dbac4c1d3f152cdeb400e2313f309c410acb0000000000000000000000000000000000000000000000000000000000000005",
"origin": "localhost",
"metamaskId": 1471992043490878,
"metamaskNetworkId": "1471904489432"
},
"time": 1471992043490,
"status": "rejected",
"containsDelegateCall": true,
"estimatedGas": "0x89ef"
},
{
"id": 1472068030402279,
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"value": "0x3782dace9d90000",
"gas": "0x493e0",
"to": "0x18a672e11d637fffadccc99b152f4895da069601",
"data": "0x5b7d47a900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001",
"origin": "www.rouleth.com",
"metamaskId": 1472068030402279,
"metamaskNetworkId": "1"
},
"time": 1472068030402,
"status": "rejected",
"containsDelegateCall": false,
"estimatedGas": "0x24704"
},
{
"id": 1472068061833258,
"txParams": {
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"value": "0x16345785d8a0000",
"gas": "0x493e0",
"to": "0x18a672e11d637fffadccc99b152f4895da069601",
"data": "0x5b7d47a900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001",
"origin": "www.rouleth.com",
"metamaskId": 1472068061833258,
"metamaskNetworkId": "1"
},
"time": 1472068061833,
"status": "confirmed",
"containsDelegateCall": false,
"estimatedGas": "0x24704",
"hash": "0xb6e6ff57e7b5f6bd7f2e6dc44c39f4e858a227c9509586634ca547179345a13e"
}
],
"selectedAddress": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"network": "1471904489432",
"seedWords": null,
"isConfirmed": true,
"unconfMsgs": {
"1472076978535283": {
"id": 1472076978535283,
"msgParams": {
"origin": "localhost",
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"data": "hello"
},
"time": 1472076978535,
"status": "unconfirmed"
}
},
"messages": [
{
"id": 1472076978535283,
"msgParams": {
"origin": "localhost",
"from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"data": "hello"
},
"time": 1472076978535,
"status": "unconfirmed"
}
],
"shapeShiftTxList": [],
"provider": {
"type": "rpc",
"rpcTarget": "http://localhost:8545"
},
"selectedAccount": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
},
"appState": {
"menuOpen": false,
"currentView": {
"name": "confTx",
"detailView": null,
"context": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
},
"accountDetail": {
"subview": "transactions"
},
"currentDomain": "ebjbdknjcgcbchkagneicjfpneaghdhb",
"transForward": true,
"isLoading": false,
"warning": null
},
"identities": {}
}

View File

@ -0,0 +1,39 @@
{
"metamask": {
"isInitialized": false,
"isUnlocked": false,
"isEthConfirmed": false,
"currentDomain": "example.com",
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {},
"unconfTxs": {},
"currentFiat": "USD",
"conversionRate": 0,
"conversionDate": "N/A",
"accounts": {},
"transactions": [],
"seedWords": null,
"isConfirmed": true,
"unconfMsgs": {},
"messages": [],
"shapeShiftTxList": [],
"provider": {
"type": "testnet"
},
"network": "2"
},
"appState": {
"menuOpen": false,
"currentView": {
"name": "restoreVault"
},
"accountDetail": {
"subview": "transactions"
},
"currentDomain": "extensions",
"transForward": true,
"isLoading": false,
"warning": null
},
"identities": {}
}

View File

@ -0,0 +1,76 @@
{
"metamask": {
"isInitialized": true,
"isUnlocked": true,
"isEthConfirmed": false,
"currentDomain": "example.com",
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {
"name": "Wallet 1",
"address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"mayBeFauceting": false
},
"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": {
"name": "Wallet 2",
"address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb",
"mayBeFauceting": false
},
"0x2f8d4a878cfa04a6e60d46362f5644deab66572d": {
"name": "Wallet 3",
"address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d",
"mayBeFauceting": false
}
},
"unconfTxs": {},
"currentFiat": "USD",
"conversionRate": 11.21283484,
"conversionDate": 1472158984,
"accounts": {
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {
"code": "0x",
"balance": "0x34693f54a1e25900",
"nonce": "0x100013",
"address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
},
"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": {
"code": "0x",
"nonce": "0x100000",
"balance": "0x18af912cee770000",
"address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb"
},
"0x2f8d4a878cfa04a6e60d46362f5644deab66572d": {
"code": "0x",
"nonce": "0x100000",
"balance": "0x2386f26fc10000",
"address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d"
}
},
"transactions": [],
"network": "2",
"seedWords": null,
"isConfirmed": true,
"unconfMsgs": {},
"messages": [],
"shapeShiftTxList": [],
"provider": {
"type": "testnet"
},
"selectedAccount": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
},
"appState": {
"menuOpen": false,
"currentView": {
"name": "sendTransaction"
},
"accountDetail": {
"subview": "transactions"
},
"currentDomain": "127.0.0.1:9966",
"transForward": true,
"isLoading": false,
"warning": null,
"detailView": {}
},
"identities": {}
}

View File

@ -0,0 +1,348 @@
{
"metamask": {
"isInitialized": true,
"isUnlocked": true,
"isEthConfirmed": true,
"currentDomain": "example.com",
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {
"name": "Wallet 1",
"address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"mayBeFauceting": false
},
"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": {
"name": "Wallet 2",
"address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb",
"mayBeFauceting": false
},
"0x2f8d4a878cfa04a6e60d46362f5644deab66572d": {
"name": "Wallet 3",
"address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d",
"mayBeFauceting": false
}
},
"unconfTxs": {},
"currentFiat": "USD",
"conversionRate": 11.21274318,
"conversionDate": 1472159644,
"accounts": {
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {
"code": "0x",
"nonce": "0x13",
"balance": "0x461d4a64e937d3d1",
"address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
},
"0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": {
"code": "0x",
"nonce": "0x0",
"balance": "0x0",
"address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb"
},
"0x2f8d4a878cfa04a6e60d46362f5644deab66572d": {
"code": "0x",
"balance": "0x0",
"nonce": "0x0",
"address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d"
}
},
"transactions": [],
"selectedAddress": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"network": "1",
"seedWords": null,
"isConfirmed": true,
"unconfMsgs": {},
"messages": [],
"shapeShiftTxList": [],
"provider": {
"type": "mainnet"
},
"selectedAccount": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
},
"appState": {
"menuOpen": false,
"currentView": {
"name": "buyEth",
"context": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
},
"accountDetail": {
"subview": "transactions"
},
"currentDomain": "127.0.0.1:9966",
"transForward": true,
"isLoading": false,
"detailView": {},
"buyView": {
"subview": "buyForm",
"formView": {
"coinbase": false,
"shapeshift": true,
"marketinfo": {
"pair": "btc_eth",
"rate": 51.14252949,
"minerFee": 0.01,
"limit": 2.60306578,
"minimum": 0.00038935,
"maxLimit": 8.67688592
},
"coinOptions": {
"BTC": {
"name": "Bitcoin",
"symbol": "BTC",
"image": "https://shapeshift.io/images/coins/bitcoin.png",
"status": "available"
},
"BCY": {
"name": "BitCrystals",
"symbol": "BCY",
"image": "https://shapeshift.io/images/coins/bitcrystals.png",
"status": "available"
},
"BLK": {
"name": "Blackcoin",
"symbol": "BLK",
"image": "https://shapeshift.io/images/coins/blackcoin.png",
"status": "available"
},
"BTS": {
"name": "Bitshares",
"symbol": "BTS",
"specialReturn": false,
"specialOutgoing": true,
"specialIncoming": true,
"fieldName": "destTag",
"fieldKey": "destTag",
"image": "https://shapeshift.io/images/coins/bitshares.png",
"status": "available"
},
"CLAM": {
"name": "Clams",
"symbol": "CLAM",
"image": "https://shapeshift.io/images/coins/clams.png",
"status": "available"
},
"DASH": {
"name": "Dash",
"symbol": "DASH",
"image": "https://shapeshift.io/images/coins/dash.png",
"status": "available"
},
"DGB": {
"name": "Digibyte",
"symbol": "DGB",
"image": "https://shapeshift.io/images/coins/digibyte.png",
"status": "available"
},
"DAO": {
"name": "TheDao",
"symbol": "DAO",
"image": "https://shapeshift.io/images/coins/thedao.png",
"status": "available"
},
"DGD": {
"name": "DigixDao",
"symbol": "DGD",
"image": "https://shapeshift.io/images/coins/digixdao.png",
"status": "available"
},
"DOGE": {
"name": "Dogecoin",
"symbol": "DOGE",
"image": "https://shapeshift.io/images/coins/dogecoin.png",
"status": "available"
},
"EMC": {
"name": "Emercoin",
"symbol": "EMC",
"image": "https://shapeshift.io/images/coins/emercoin.png",
"status": "available"
},
"ETH": {
"name": "Ether",
"symbol": "ETH",
"image": "https://shapeshift.io/images/coins/ether.png",
"status": "available"
},
"ETC": {
"name": "Ether Classic",
"symbol": "ETC",
"image": "https://shapeshift.io/images/coins/etherclassic.png",
"status": "available"
},
"FCT": {
"name": "Factoids",
"symbol": "FCT",
"image": "https://shapeshift.io/images/coins/factoids.png",
"status": "available"
},
"LBC": {
"name": "LBRY Credits",
"symbol": "LBC",
"image": "https://shapeshift.io/images/coins/lbry.png",
"status": "available"
},
"LSK": {
"name": "Lisk",
"symbol": "LSK",
"image": "https://shapeshift.io/images/coins/lisk.png",
"status": "available"
},
"LTC": {
"name": "Litecoin",
"symbol": "LTC",
"image": "https://shapeshift.io/images/coins/litecoin.png",
"status": "available"
},
"MAID": {
"name": "Maidsafe",
"symbol": "MAID",
"image": "https://shapeshift.io/images/coins/maidsafe.png",
"status": "available"
},
"MINT": {
"name": "Mintcoin",
"symbol": "MINT",
"image": "https://shapeshift.io/images/coins/mintcoin.png",
"status": "available"
},
"MONA": {
"name": "Monacoin",
"symbol": "MONA",
"image": "https://shapeshift.io/images/coins/monacoin.png",
"status": "available"
},
"MSC": {
"name": "Omni",
"symbol": "MSC",
"image": "https://shapeshift.io/images/coins/mastercoin.png",
"status": "available"
},
"NBT": {
"name": "Nubits",
"symbol": "NBT",
"image": "https://shapeshift.io/images/coins/nubits.png",
"status": "available"
},
"NMC": {
"name": "Namecoin",
"symbol": "NMC",
"image": "https://shapeshift.io/images/coins/namecoin.png",
"status": "available"
},
"NVC": {
"name": "Novacoin",
"symbol": "NVC",
"image": "https://shapeshift.io/images/coins/novacoin.png",
"status": "available"
},
"NXT": {
"name": "Nxt",
"symbol": "NXT",
"specialReturn": false,
"specialOutgoing": true,
"specialIncoming": true,
"specialIncomingStatus": false,
"fieldName": "Public Key (only for unfunded accounts!)",
"fieldKey": "rsAddress",
"image": "https://shapeshift.io/images/coins/nxt.png",
"status": "available"
},
"PPC": {
"name": "Peercoin",
"symbol": "PPC",
"image": "https://shapeshift.io/images/coins/peercoin.png",
"status": "available"
},
"RDD": {
"name": "Reddcoin",
"symbol": "RDD",
"image": "https://shapeshift.io/images/coins/reddcoin.png",
"status": "available"
},
"SDC": {
"name": "Shadowcash",
"symbol": "SDC",
"image": "https://shapeshift.io/images/coins/shadowcash.png",
"status": "available"
},
"SC": {
"name": "Siacoin",
"symbol": "SC",
"image": "https://shapeshift.io/images/coins/siacoin.png",
"status": "available"
},
"SJCX": {
"name": "StorjX",
"symbol": "SJCX",
"image": "https://shapeshift.io/images/coins/storjcoinx.png",
"status": "available"
},
"START": {
"name": "Startcoin",
"symbol": "START",
"image": "https://shapeshift.io/images/coins/startcoin.png",
"status": "available"
},
"STEEM": {
"name": "Steem",
"symbol": "STEEM",
"specialReturn": false,
"specialOutgoing": true,
"specialIncoming": true,
"fieldName": "destTag",
"fieldKey": "destTag",
"image": "https://shapeshift.io/images/coins/steem.png",
"status": "available"
},
"USDT": {
"name": "Tether",
"symbol": "USDT",
"image": "https://shapeshift.io/images/coins/tether.png",
"status": "available"
},
"VOX": {
"name": "Voxels",
"symbol": "VOX",
"image": "https://shapeshift.io/images/coins/voxels.png",
"status": "available"
},
"VRC": {
"name": "Vericoin",
"symbol": "VRC",
"image": "https://shapeshift.io/images/coins/vericoin.png",
"status": "available"
},
"VTC": {
"name": "Vertcoin",
"symbol": "VTC",
"image": "https://shapeshift.io/images/coins/vertcoin.png",
"status": "available"
},
"XCP": {
"name": "Counterparty",
"symbol": "XCP",
"image": "https://shapeshift.io/images/coins/counterparty.png",
"status": "available"
},
"XMR": {
"name": "Monero",
"symbol": "XMR",
"specialReturn": false,
"specialOutgoing": true,
"specialIncoming": true,
"fieldName": "Payment Id",
"qrName": "tx_payment_id",
"fieldKey": "paymentId",
"image": "https://shapeshift.io/images/coins/monero.png",
"status": "available"
}
}
},
"buyAddress": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
"amount": "5.00",
"warning": null
},
"isSubLoading": false
},
"identities": {}
}

View File

@ -0,0 +1,28 @@
# Form Persisting Architecture
Since:
- The popup is torn down completely on every click outside of it.
- We have forms with multiple fields (like passwords & seed phrases) that might encourage a user to leave our panel to refer to a password manager.
We cause user friction when we lose the contents of certain forms.
This calls for an architecture of a form component that can completely persist its values to LocalStorage on every relevant change, and restore those values on reopening.
To achieve this, we have defined a class, a subclass of `React.Component`, called `PersistentForm`, and it's stored at `ui/lib/persistent-form.js`.
To use this class, simply take your form component (the component that renders `input`, `select`, or `textarea` elements), and make it subclass from `PersistentForm` instead of `React.Component`.
You can see an example of this in use in `ui/app/first-time/restore-vault.js`.
Additionally, any field whose value should be persisted, should have a `persistentFormId` attribute, which needs to be assigned under a `dataset` key on the main `attributes` hash. For example:
```javascript
return h('textarea.twelve-word-phrase.letter-spacey', {
dataset: {
persistentFormId: 'wallet-seed',
},
})
```
That's it! This field should be persisted to `localStorage` on each `keyUp`, those values should be restored on view load, and the cached values should be cleared when navigating deliberately away from the form.

View File

@ -19,6 +19,7 @@ const Root = require('./ui/app/root')
const configureStore = require('./ui/app/store')
const actions = require('./ui/app/actions')
const states = require('./development/states')
const Selector = require('./development/selector')
const MetamaskController = require('./app/scripts/metamask-controller')
const extension = require('./development/mockExtension')
@ -139,6 +140,8 @@ render(
},
}, 'Reset State'),
h(Selector, { actions, selectedKey: selectedView, states, store }),
h('.mock-app-root', {
style: {
height: '500px',

View File

@ -5,6 +5,9 @@
"private": true,
"scripts": {
"start": "gulp dev",
"lint": "gulp lint",
"dev": "gulp dev",
"dist": "gulp dist",
"test": "npm run fastTest && npm run ci",
"fastTest": "mocha --require test/helper.js --compilers js:babel-register --recursive \"test/unit/**/*.js\"",
"watch": "mocha watch --compilers js:babel-register --recursive \"test/unit/**/*.js\"",
@ -35,6 +38,7 @@
"debounce": "^1.0.0",
"dnode": "^1.2.2",
"end-of-stream": "^1.1.0",
"ensnare": "^1.0.0",
"eth-bin-to-ops": "^1.0.0",
"eth-lightwallet": "^2.3.3",
"eth-query": "^1.0.3",
@ -47,12 +51,13 @@
"inject-css": "^0.1.1",
"jazzicon": "^1.1.3",
"menu-droppo": "^1.1.0",
"metamask-logo": "^1.1.5",
"metamask-logo": "^1.3.1",
"mississippi": "^1.2.0",
"multiplex": "^6.7.0",
"once": "^1.3.3",
"pojo-migrator": "^2.1.0",
"polyfill-crypto.getrandomvalues": "^1.0.0",
"post-message-stream": "^1.0.0",
"pumpify": "^1.3.4",
"react": "^15.0.2",
"react-addons-css-transition-group": "^15.0.2",
@ -65,14 +70,15 @@
"redux": "^3.0.5",
"redux-logger": "^2.3.1",
"redux-thunk": "^1.0.2",
"request-promise": "^4.1.1",
"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.8.3",
"web3-stream-provider": "^2.0.5",
"web3": "^0.17.0-alpha",
"web3-provider-engine": "^8.0.2",
"web3-stream-provider": "^2.0.6",
"xtend": "^4.0.1"
},
"devDependencies": {
@ -102,10 +108,12 @@
"mocha-eslint": "^2.1.1",
"mocha-jsdom": "^1.1.0",
"mocha-sinon": "^1.1.5",
"nock": "^8.0.0",
"qs": "^6.2.0",
"qunit": "^0.9.1",
"sinon": "^1.17.3",
"tape": "^4.5.1",
"testem": "^1.10.3",
"uglifyify": "^3.0.1",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",

View File

@ -0,0 +1,12 @@
var assert = require('assert')
var linkGen = require('../../ui/lib/account-link')
describe('account-link', function() {
it('adds testnet prefix to morden test network', function() {
var result = linkGen('account', '2')
assert.notEqual(result.indexOf('testnet'), -1, 'testnet injected')
assert.notEqual(result.indexOf('account'), -1, 'account included')
})
})

View File

@ -1,8 +1,10 @@
var assert = require('assert')
const assert = require('assert')
const extend = require('xtend')
const STORAGE_KEY = 'metamask-persistance-key'
var configManagerGen = require('../lib/mock-config-manager')
var configManager
const rp = require('request-promise')
const nock = require('nock')
describe('config-manager', function() {
@ -11,6 +13,91 @@ describe('config-manager', function() {
configManager = configManagerGen()
})
describe('currency conversions', function() {
describe('#getCurrentFiat', function() {
it('should return false if no previous key exists', function() {
var result = configManager.getCurrentFiat()
assert.ok(!result)
})
})
describe('#setCurrentFiat', function() {
it('should make getCurrentFiat return true once set', function() {
assert.equal(configManager.getCurrentFiat(), false)
configManager.setCurrentFiat('USD')
var result = configManager.getCurrentFiat()
assert.equal(result, 'USD')
})
it('should work with other currencies as well', function() {
assert.equal(configManager.getCurrentFiat(), false)
configManager.setCurrentFiat('JPY')
var result = configManager.getCurrentFiat()
assert.equal(result, 'JPY')
})
})
describe('#getConversionRate', function() {
it('should return false if non-existent', function() {
var result = configManager.getConversionRate()
assert.ok(!result)
})
})
describe('#updateConversionRate', function() {
it('should retrieve an update for ETH to USD and set it in memory', function(done) {
this.timeout(15000)
var usdMock = nock('https://www.cryptonator.com')
.get('/api/ticker/eth-USD')
.reply(200, '{"ticker":{"base":"ETH","target":"USD","price":"11.02456145","volume":"44948.91745289","change":"-0.01472534"},"timestamp":1472072136,"success":true,"error":""}')
assert.equal(configManager.getConversionRate(), false)
var promise = new Promise(
function (resolve, reject) {
configManager.setCurrentFiat('USD')
configManager.updateConversionRate().then(function() {
resolve()
})
})
promise.then(function() {
var result = configManager.getConversionRate()
assert.equal(typeof result, 'number')
done()
}).catch(function(err) {
console.log(err)
})
})
it('should work for JPY as well.', function() {
this.timeout(15000)
assert.equal(configManager.getConversionRate(), false)
var jpyMock = nock('https://www.cryptonator.com')
.get('/api/ticker/eth-JPY')
.reply(200, '{"ticker":{"base":"ETH","target":"JPY","price":"11.02456145","volume":"44948.91745289","change":"-0.01472534"},"timestamp":1472072136,"success":true,"error":""}')
var promise = new Promise(
function (resolve, reject) {
configManager.setCurrentFiat('JPY')
configManager.updateConversionRate().then(function() {
resolve()
})
})
promise.then(function() {
var result = configManager.getConversionRate()
assert.equal(typeof result, 'number')
}).catch(function(err) {
console.log(err)
})
})
})
})
describe('confirmation', function() {
describe('#getConfirmed', function() {
@ -157,6 +244,17 @@ describe('config-manager', function() {
assert.equal(result.length, 1)
assert.equal(result[0].id, 1)
})
it('cuts off early txs beyond a limit', function() {
const limit = configManager.txLimit
for (let i = 0; i < limit + 1; i++) {
let tx = { id: i }
configManager.addTx(tx)
}
var result = configManager.getTxList()
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
assert.equal(result[0].id, 1, 'early txs truncted')
})
})
describe('#confirmTx', function() {
@ -215,4 +313,3 @@ describe('config-manager', function() {
})
})
})

View File

@ -156,7 +156,12 @@ describe('util', function() {
var result = util.formatBalance(input)
assert.equal(result, '0.00032 ETH')
})
it('should not parse the balance and return value with 2 decimal points with ETH at the end', function() {
var value = '1.2456789'
var needsParse = false
var result = util.formatBalance(value, 2, needsParse)
assert.equal(result, '1.24 ETH')
})
})
describe('normalizing values', function() {

View File

@ -4,22 +4,24 @@ const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const CopyButton = require('./components/copyButton')
const AccountInfoLink = require('./components/account-info-link')
const actions = require('./actions')
const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
const valuesFor = require('./util').valuesFor
const Identicon = require('./components/identicon')
const EtherBalance = require('./components/eth-balance')
const AccountEtherBalance = require('./components/account-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')
const Tooltip = require('./components/tooltip')
const BuyButtonSubview = require('./components/buy-button-subview')
module.exports = connect(mapStateToProps)(AccountDetailScreen)
function mapStateToProps (state) {
return {
metamask: state.metamask,
identities: state.metamask.identities,
accounts: state.metamask.accounts,
address: state.metamask.selectedAccount,
@ -29,6 +31,7 @@ function mapStateToProps (state) {
unconfTxs: valuesFor(state.metamask.unconfTxs),
unconfMsgs: valuesFor(state.metamask.unconfMsgs),
isEthWarningConfirmed: state.metamask.isEthConfirmed,
shapeShiftTxList: state.metamask.shapeShiftTxList,
}
}
@ -42,6 +45,7 @@ AccountDetailScreen.prototype.render = function () {
var selected = props.address || Object.keys(props.accounts)[0]
var identity = props.identities[selected]
var account = props.accounts[selected]
const { network } = props
return (
@ -125,6 +129,9 @@ AccountDetailScreen.prototype.render = function () {
bottom: '15px',
},
}, [
h(AccountInfoLink, { selected, network }),
h(CopyButton, {
value: ethUtil.toChecksumAddress(selected),
}),
@ -134,16 +141,15 @@ AccountDetailScreen.prototype.render = function () {
}, [
h('div', {
style: {
margin: '5px',
display: 'flex',
alignItems: 'center',
},
}, [
h('img.cursor-pointer.color-orange', {
src: 'images/key-32.png',
onClick: () => this.requestAccountExport(selected),
style: {
margin: '0px 5px',
width: '20px',
height: '20px',
height: '19px',
},
}),
]),
@ -162,9 +168,8 @@ AccountDetailScreen.prototype.render = function () {
},
}, [
h(EtherBalance, {
h(AccountEtherBalance, {
value: account && account.balance,
mainBalance: true,
style: {
lineHeight: '7px',
marginTop: '10px',
@ -172,7 +177,7 @@ AccountDetailScreen.prototype.render = function () {
}),
h('button', {
onClick: () => props.dispatch(actions.buyEth(selected)),
onClick: () => props.dispatch(actions.buyEthView(selected)),
style: {
marginBottom: '20px',
marginRight: '8px',
@ -220,13 +225,15 @@ AccountDetailScreen.prototype.subview = function () {
case 'export':
var state = extend({key: 'export'}, this.props)
return h(ExportAccountView, state)
case 'buyForm':
return h(BuyButtonSubview, extend({key: 'buyForm'}, this.props))
default:
return this.transactionList()
}
}
AccountDetailScreen.prototype.transactionList = function () {
const { transactions, unconfTxs, unconfMsgs, address, network } = this.props
const { transactions, unconfTxs, unconfMsgs, address, network, shapeShiftTxList } = this.props
var txsToRender = transactions
// only transactions that are from the current address
@ -241,6 +248,8 @@ AccountDetailScreen.prototype.transactionList = function () {
network,
unconfTxs,
unconfMsgs,
address,
shapeShiftTxList,
viewPendingTx: (txId) => {
this.props.dispatch(actions.viewPendingTx(txId))
},
@ -251,3 +260,13 @@ AccountDetailScreen.prototype.requestAccountExport = function () {
this.props.dispatch(actions.requestExportAccount())
}
AccountDetailScreen.prototype.buyButtonDeligator = function () {
var props = this.props
if (this.props.accountDetail.subview === 'buyForm') {
props.dispatch(actions.backToAccountDetail(props.address))
} else {
props.dispatch(actions.buyEthView())
}
}

View File

@ -3,7 +3,7 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits
const ethUtil = require('ethereumjs-util')
const EtherBalance = require('../components/eth-balance')
const AccountEtherBalance = require('../components/account-eth-balance')
const CopyButton = require('../components/copyButton')
const Identicon = require('../components/identicon')
@ -50,8 +50,12 @@ NewComponent.prototype.render = function () {
textOverflow: 'ellipsis',
},
}, ethUtil.toChecksumAddress(identity.address)),
h(EtherBalance, {
h(AccountEtherBalance, {
value: account.balance,
style: {
lineHeight: '7px',
marginTop: '10px',
},
}),
]),

View File

@ -55,6 +55,8 @@ var actions = {
SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE',
REVEAL_ACCOUNT: 'REVEAL_ACCOUNT',
revealAccount: revealAccount,
SET_CURRENT_FIAT: 'SET_CURRENT_FIAT',
setCurrentFiat: setCurrentFiat,
// account detail screen
SHOW_SEND_PAGE: 'SHOW_SEND_PAGE',
showSendPage: showSendPage,
@ -113,6 +115,34 @@ var actions = {
// buy Eth with coinbase
BUY_ETH: 'BUY_ETH',
buyEth: buyEth,
buyEthView: buyEthView,
BUY_ETH_VIEW: 'BUY_ETH_VIEW',
UPDATE_COINBASE_AMOUNT: 'UPDATE_COIBASE_AMOUNT',
updateCoinBaseAmount: updateCoinBaseAmount,
UPDATE_BUY_ADDRESS: 'UPDATE_BUY_ADDRESS',
updateBuyAddress: updateBuyAddress,
COINBASE_SUBVIEW: 'COINBASE_SUBVIEW',
coinBaseSubview: coinBaseSubview,
SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW',
shapeShiftSubview: shapeShiftSubview,
PAIR_UPDATE: 'PAIR_UPDATE',
pairUpdate: pairUpdate,
coinShiftRquest: coinShiftRquest,
SHOW_SUB_LOADING_INDICATION: 'SHOW_SUB_LOADING_INDICATION',
showSubLoadingIndication: showSubLoadingIndication,
HIDE_SUB_LOADING_INDICATION: 'HIDE_SUB_LOADING_INDICATION',
hideSubLoadingIndication: hideSubLoadingIndication,
// QR STUFF:
SHOW_QR: 'SHOW_QR',
getQr: getQr,
reshowQrCode: reshowQrCode,
SHOW_QR_VIEW: 'SHOW_QR_VIEW',
// FORGOT PASSWORD:
BACK_TO_INIT_MENU: 'BACK_TO_INIT_MENU',
goBackToInitView: goBackToInitView,
RECOVERY_IN_PROGRESS: 'RECOVERY_IN_PROGRESS',
BACK_TO_UNLOCK_VIEW: 'BACK_TO_UNLOCK_VIEW',
backToUnlockView: backToUnlockView,
}
module.exports = actions
@ -214,6 +244,23 @@ function revealAccount () {
}
}
function setCurrentFiat (fiat) {
return (dispatch) => {
dispatch(this.showLoadingIndication())
_accountManager.setCurrentFiat(fiat, (data, err) => {
dispatch(this.hideLoadingIndication())
dispatch({
type: this.SET_CURRENT_FIAT,
value: {
currentFiat: data.currentFiat,
conversionRate: data.conversionRate,
conversionDate: data.conversionDate,
},
})
})
}
}
function signMsg (msgData) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
@ -329,6 +376,12 @@ function showNewVaultSeed (seed) {
}
}
function backToUnlockView () {
return {
type: actions.BACK_TO_UNLOCK_VIEW,
}
}
//
// unlock screen
//
@ -457,6 +510,12 @@ function showConfigPage (transitionForward = true) {
}
}
function goBackToInitView () {
return {
type: actions.BACK_TO_INIT_MENU,
}
}
//
// config
//
@ -496,6 +555,18 @@ function hideLoadingIndication () {
}
}
function showSubLoadingIndication () {
return {
type: actions.SHOW_SUB_LOADING_INDICATION,
}
}
function hideSubLoadingIndication () {
return {
type: actions.HIDE_SUB_LOADING_INDICATION,
}
}
function showWarning (text) {
return this.displayWarning(text)
}
@ -594,3 +665,159 @@ function buyEth (address, amount) {
})
}
}
function buyEthView (address) {
return {
type: actions.BUY_ETH_VIEW,
value: address,
}
}
function updateCoinBaseAmount (value) {
return {
type: actions.UPDATE_COINBASE_AMOUNT,
value,
}
}
function updateBuyAddress (value) {
return {
type: actions.UPDATE_BUY_ADDRESS,
value,
}
}
function coinBaseSubview () {
return {
type: actions.COINBASE_SUBVIEW,
}
}
function pairUpdate (coin) {
return (dispatch) => {
dispatch(actions.showSubLoadingIndication())
dispatch(actions.hideWarning())
shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => {
dispatch(actions.hideSubLoadingIndication())
dispatch({
type: actions.PAIR_UPDATE,
value: {
marketinfo: mktResponse,
},
})
})
}
}
function shapeShiftSubview (network) {
var pair = 'btc_eth'
return (dispatch) => {
dispatch(actions.showSubLoadingIndication())
shapeShiftRequest('marketinfo', {pair}, (mktResponse) => {
shapeShiftRequest('getcoins', {}, (response) => {
dispatch(actions.hideSubLoadingIndication())
if (mktResponse.error) return dispatch(actions.showWarning(mktResponse.error))
dispatch({
type: actions.SHAPESHIFT_SUBVIEW,
value: {
marketinfo: mktResponse,
coinOptions: response,
},
})
})
})
}
}
function coinShiftRquest (data, marketData) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
shapeShiftRequest('shift', { method: 'POST', data}, (response) => {
if (response.error) return dispatch(actions.showWarning(response.error))
var message = `
Deposit your ${response.depositType} to the address bellow:`
_accountManager.createShapeShiftTx(response.deposit, response.depositType)
dispatch(actions.getQr(response.deposit, '125x125', [message].concat(marketData)))
})
}
}
function getQr (data, size, message) {
return (dispatch) => {
qrRequest(data, size, (response) => {
dispatch(actions.hideLoadingIndication())
if (response.error) return dispatch(actions.showWarning(response.error))
dispatch({
type: actions.SHOW_QR,
value: {
qr: response,
message: message,
data: data,
},
})
})
}
}
function reshowQrCode (data, coin) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => {
if (mktResponse.error) return dispatch(actions.showWarning(mktResponse.error))
var message = [
`Deposit your ${coin} to the address bellow:`,
`Deposit Limit: ${mktResponse.limit}`,
`Deposit Minimum:${mktResponse.minimum}`,
]
qrRequest(data, '125x125', (response) => {
dispatch(actions.hideLoadingIndication())
dispatch({
type: actions.SHOW_QR_VIEW,
value: {
qr: response,
message: message,
data: data,
},
})
})
})
}
}
function shapeShiftRequest (query, options, cb) {
var queryResponse, method
!options ? options = {} : null
options.method ? method = options.method : method = 'GET'
var requestListner = function (request) {
queryResponse = JSON.parse(this.responseText)
cb ? cb(queryResponse) : null
return queryResponse
}
var shapShiftReq = new XMLHttpRequest()
shapShiftReq.addEventListener('load', requestListner)
shapShiftReq.open(method, `https://shapeshift.io/${query}/${options.pair ? options.pair : ''}`, true)
if (options.method === 'POST') {
var jsonObj = JSON.stringify(options.data)
shapShiftReq.setRequestHeader('Content-Type', 'application/json')
return shapShiftReq.send(jsonObj)
} else {
return shapShiftReq.send()
}
}
function qrRequest (data, size, cb) {
var requestListner = function (request) {
cb ? cb(this.responseText) : null
return this.responseText
}
var qrReq = new XMLHttpRequest()
qrReq.addEventListener('load', requestListner)
qrReq.open('GET', `https://api.qrserver.com/v1/create-qr-code/?size=${size}&format=svg&data=${data}`, true)
qrReq.send()
}

View File

@ -28,7 +28,8 @@ const DropMenuItem = require('./components/drop-menu-item')
const NetworkIndicator = require('./components/network')
const Tooltip = require('./components/tooltip')
const EthStoreWarning = require('./eth-store-warning')
const BuyView = require('./components/buy-button-subview')
const QrView = require('./components/qr-code')
module.exports = connect(mapStateToProps)(App)
inherits(App, Component)
@ -50,6 +51,7 @@ function mapStateToProps (state) {
menuOpen: state.appState.menuOpen,
network: state.metamask.network,
provider: state.metamask.provider,
forgottenPassword: state.appState.forgottenPassword,
}
}
@ -88,6 +90,7 @@ App.prototype.render = function () {
transitionLeaveTimeout: 300,
}, [
this.renderPrimary(),
this.renderBackToInitButton(),
]),
]),
])
@ -95,6 +98,11 @@ App.prototype.render = function () {
}
App.prototype.renderAppBar = function () {
if (window.METAMASK_UI_TYPE === 'notification') {
return null
}
const props = this.props
const state = this.state || {}
const isNetworkMenuOpen = state.isNetworkMenuOpen || false
@ -225,15 +233,6 @@ App.prototype.renderNetworkDropdown = function () {
provider: props.provider,
}),
h(DropMenuItem, {
label: 'Ethereum Classic Network',
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
action: () => props.dispatch(actions.setProviderType('classic')),
icon: h('.menu-icon.hollow-diamond'),
activeNetworkRender: props.network,
provider: props.provider,
}),
h(DropMenuItem, {
label: 'Morden Test Network',
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
@ -250,6 +249,13 @@ App.prototype.renderNetworkDropdown = function () {
activeNetworkRender: props.provider.rpcTarget,
}),
h(DropMenuItem, {
label: 'Custom RPC',
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
action: () => this.props.dispatch(actions.showConfigPage()),
icon: h('i.fa.fa-question-circle.fa-lg', { ariaHidden: true }),
}),
this.renderCustomOption(props.provider.rpcTarget),
])
}
@ -301,6 +307,92 @@ App.prototype.renderDropdown = function () {
}),
])
}
App.prototype.renderBackButton = function (style, justArrow = false) {
var props = this.props
return (
h('.flex-row', {
key: 'leftArrow',
transForward: false,
style: style,
onClick: () => props.dispatch(actions.goBackToInitView()),
}, [
h('i.fa.fa-arrow-left.cursor-pointer'),
justArrow ? null : h('div.cursor-pointer', {
style: {
marginLeft: '3px',
},
onClick: () => props.dispatch(actions.goBackToInitView()),
}, 'BACK'),
])
)
}
App.prototype.renderBackToInitButton = function () {
var props = this.props
var button = null
if (!props.isUnlocked) {
if (props.currentView.name === 'InitMenu') {
button = props.forgottenPassword ? h('.flex-row', {
key: 'rightArrow',
style: {
position: 'absolute',
bottom: '10px',
right: '15px',
fontSize: '21px',
fontFamily: 'Montserrat Light',
color: '#7F8082',
width: '77.578px',
alignItems: 'flex-end',
},
}, [
h('div.cursor-pointer', {
style: {
marginRight: '3px',
},
onClick: () => props.dispatch(actions.backToUnlockView()),
}, 'LOGIN'),
h('i.fa.fa-arrow-right.cursor-pointer'),
]) : null
} else if (props.isInitialized) {
var style
switch (props.currentView.name) {
case 'createVault':
style = {
position: 'absolute',
top: '41px',
left: '80px',
fontSize: '21px',
fontFamily: 'Montserrat Bold',
color: 'rgb(174, 174, 174)',
}
return this.renderBackButton(style, true)
case 'restoreVault':
style = {
position: 'absolute',
top: '41px',
left: '70px',
fontSize: '21px',
fontFamily: 'Montserrat Bold',
color: 'rgb(174, 174, 174)',
}
return this.renderBackButton(style, true)
default:
style = {
position: 'absolute',
bottom: '10px',
left: '15px',
fontSize: '21px',
fontFamily: 'Montserrat Light',
color: '#7F8082',
width: '71.969px',
alignItems: 'flex-end',
}
return this.renderBackButton(style)
}
}
}
return button
}
App.prototype.renderPrimary = function () {
var props = this.props
@ -314,7 +406,7 @@ App.prototype.renderPrimary = function () {
}
// show initialize screen
if (!props.isInitialized) {
if (!props.isInitialized || props.forgottenPassword) {
// show current view
switch (props.currentView.name) {
@ -366,6 +458,35 @@ App.prototype.renderPrimary = function () {
case 'createVault':
return h(CreateVaultScreen, {key: 'createVault'})
case 'buyEth':
return h(BuyView, {key: 'buyEthView'})
case 'qr':
return h('div', {
style: {
position: 'absolute',
height: '100%',
top: '0px',
left: '0px',
},
}, [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
onClick: () => props.dispatch(actions.backToAccountDetail(props.activeAddress)),
style: {
marginLeft: '10px',
marginTop: '50px',
},
}),
h('div', {
style: {
position: 'absolute',
bottom: '115px',
left: '44px',
width: '285px',
},
}, [
h(QrView, {key: 'qr'}),
]),
])
default:
return h(AccountDetailScreen, {key: 'account-detail'})
@ -387,12 +508,8 @@ App.prototype.toggleMetamaskActive = function () {
App.prototype.renderCustomOption = function (rpcTarget) {
switch (rpcTarget) {
case undefined:
return h(DropMenuItem, {
label: 'Custom RPC',
closeMenu: () => this.setState({ isNetworkMenuOpen: false }),
action: () => this.props.dispatch(actions.showConfigPage()),
icon: h('i.fa.fa-question-circle.fa-lg', { ariaHidden: true }),
})
return null
case 'http://localhost:8545':
return h(DropMenuItem, {
label: 'Custom RPC',

View File

@ -0,0 +1,140 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const formatBalance = require('../util').formatBalance
const generateBalanceObject = require('../util').generateBalanceObject
const Tooltip = require('./tooltip.js')
module.exports = connect(mapStateToProps)(EthBalanceComponent)
function mapStateToProps (state) {
return {
conversionRate: state.metamask.conversionRate,
conversionDate: state.metamask.conversionDate,
currentFiat: state.metamask.currentFiat,
}
}
inherits(EthBalanceComponent, Component)
function EthBalanceComponent () {
Component.call(this)
}
EthBalanceComponent.prototype.render = function () {
var state = this.props
var style = state.style
const value = formatBalance(state.value, 6)
var width = state.width
return (
h('.ether-balance', {
style: style,
}, [
h('.ether-balance-amount', {
style: {
display: 'inline',
width: width,
},
}, this.renderBalance(value, state)),
])
)
}
EthBalanceComponent.prototype.renderBalance = function (value, state) {
if (value === 'None') return value
var balanceObj = generateBalanceObject(value, state.shorten ? 1 : 3)
var balance, fiatDisplayNumber, fiatTooltipNumber
var splitBalance = value.split(' ')
var ethNumber = splitBalance[0]
var ethSuffix = splitBalance[1]
if (state.conversionRate !== 0) {
fiatTooltipNumber = Number(splitBalance[0]) * state.conversionRate
fiatDisplayNumber = fiatTooltipNumber.toFixed(2)
} else {
fiatDisplayNumber = 'N/A'
}
var fiatSuffix = state.currentFiat
if (state.shorten) {
balance = balanceObj.shortBalance
} else {
balance = balanceObj.balance
}
var label = balanceObj.label
return (
h('.flex-column', [
h(Tooltip, {
position: 'bottom',
title: `${ethNumber} ${ethSuffix}`,
}, [
h('.flex-row', {
style: {
alignItems: 'flex-end',
lineHeight: '13px',
fontFamily: 'Montserrat Light',
textRendering: 'geometricPrecision',
marginBottom: '5px',
},
}, [
h('div', {
style: {
width: '100%',
textAlign: 'right',
},
}, balance),
h('div', {
style: {
color: '#AEAEAE',
marginLeft: '5px',
},
}, label),
]),
]),
h(Tooltip, {
position: 'bottom',
title: `${fiatTooltipNumber} ${fiatSuffix}`,
}, [
fiatDisplay(fiatDisplayNumber, fiatSuffix),
]),
])
)
}
function fiatDisplay (fiatDisplayNumber, fiatSuffix) {
if (fiatDisplayNumber !== 'N/A') {
return h('.flex-row', {
style: {
alignItems: 'flex-end',
lineHeight: '13px',
fontFamily: 'Montserrat Light',
textRendering: 'geometricPrecision',
},
}, [
h('div', {
style: {
width: '100%',
textAlign: 'right',
fontSize: '12px',
color: '#333333',
},
}, fiatDisplayNumber),
h('div', {
style: {
color: '#AEAEAE',
marginLeft: '5px',
fontSize: '12px',
},
}, fiatSuffix),
])
} else {
return h('div')
}
}

View File

@ -0,0 +1,42 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const Tooltip = require('./tooltip')
const genAccountLink = require('../../lib/account-link')
const extension = require('../../../app/scripts/lib/extension')
module.exports = AccountInfoLink
inherits(AccountInfoLink, Component)
function AccountInfoLink () {
Component.call(this)
}
AccountInfoLink.prototype.render = function () {
const { selected, network } = this.props
const title = 'View account on etherscan'
const url = genAccountLink(selected, network)
if (!url) {
return null
}
return h('.account-info-link', {
style: {
display: 'flex',
alignItems: 'center',
},
}, [
h(Tooltip, {
title,
}, [
h('i.fa.fa-info-circle.cursor-pointer.color-orange', {
style: {
margin: '5px',
},
onClick () { extension.tabs.create({ url }) },
}),
]),
])
}

View File

@ -0,0 +1,123 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../actions')
const CoinbaseForm = require('./coinbase-form')
const ShapeshiftForm = require('./shapeshift-form')
const extension = require('../../../app/scripts/lib/extension')
module.exports = connect(mapStateToProps)(BuyButtonSubview)
function mapStateToProps (state) {
return {
selectedAccount: state.selectedAccount,
warning: state.appState.warning,
buyView: state.appState.buyView,
network: state.metamask.network,
provider: state.metamask.provider,
}
}
inherits(BuyButtonSubview, Component)
function BuyButtonSubview () {
Component.call(this)
}
BuyButtonSubview.prototype.render = function () {
const props = this.props
const currentForm = props.buyView.formView
return (
h('.buy-eth-section', [
// back button
h('.flex-row', {
style: {
alignItems: 'center',
justifyContent: 'center',
},
}, [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
onClick: () => props.dispatch(actions.backToAccountDetail(props.selectedAccount)),
style: {
position: 'absolute',
left: '10px',
},
}),
h('h2.page-subtitle', 'Buy Eth'),
]),
h('h3.flex-row.text-transform-uppercase', {
style: {
background: '#EBEBEB',
color: '#AEAEAE',
paddingTop: '4px',
justifyContent: 'space-around',
},
}, [
h(currentForm.coinbase ? '.activeForm' : '.inactiveForm.pointer', {
onClick: () => props.dispatch(actions.coinBaseSubview()),
}, 'Coinbase'),
h('a', {
onClick: (event) => this.navigateTo('https://github.com/MetaMask/faq/blob/master/COINBASE.md'),
}, [
h('i.fa.fa-question-circle', {
style: {
position: 'relative',
right: '33px',
},
}),
]),
h(currentForm.shapeshift ? '.activeForm' : '.inactiveForm.pointer', {
onClick: () => props.dispatch(actions.shapeShiftSubview(props.provider.type)),
}, 'Shapeshift'),
h('a', {
href: 'https://github.com/MetaMask/faq/blob/master/COINBASE.md',
onClick: (event) => this.navigateTo('https://info.shapeshift.io/about'),
}, [
h('i.fa.fa-question-circle', {
style: {
position: 'relative',
right: '28px',
},
}),
]),
]),
this.formVersionSubview(),
])
)
}
BuyButtonSubview.prototype.formVersionSubview = function () {
if (this.props.network === '1') {
if (this.props.buyView.formView.coinbase) {
return h(CoinbaseForm, this.props)
} else if (this.props.buyView.formView.shapeshift) {
return h(ShapeshiftForm, this.props)
}
} else {
return h('div.flex-column', {
style: {
alignItems: 'center',
margin: '50px',
},
}, [
h('h3.text-transform-uppercase', {
style: {
width: '225px',
},
}, 'In order to access this feature please switch too the Main Network'),
h('h3.text-transform-uppercase', 'or:'),
this.props.network === '2' ? h('button.text-transform-uppercase', {
onClick: () => this.props.dispatch(actions.buyEth()),
style: {
marginTop: '15px',
},
}, 'Go To Test Faucet') : null,
])
}
}
BuyButtonSubview.prototype.navigateTo = function (url) {
extension.tabs.create({ url })
}

View File

@ -0,0 +1,162 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../actions')
const isValidAddress = require('../util').isValidAddress
module.exports = connect(mapStateToProps)(CoinbaseForm)
function mapStateToProps(state) {
return {
selectedAccount: state.selectedAccount,
warning: state.appState.warning,
}
}
inherits(CoinbaseForm, Component)
function CoinbaseForm() {
Component.call(this)
}
CoinbaseForm.prototype.render = function () {
var props = this.props
var amount = props.buyView.amount
var address = props.buyView.buyAddress
return h('.flex-column', {
style: {
// margin: '10px',
padding: '25px',
},
}, [
h('.flex-column', {
style: {
alignItems: 'flex-start',
},
}, [
h('.flex-row', [
h('div', 'Address:'),
h('.ellip-address', address),
]),
h('.flex-row', [
h('div', 'Amount: $'),
h('.input-container', [
h('input.buy-inputs', {
style: {
width: '3em',
boxSizing: 'border-box',
},
defaultValue: amount,
onChange: this.handleAmount.bind(this),
}),
h('i.fa.fa-pencil-square-o.edit-text', {
style: {
fontSize: '12px',
color: '#F7861C',
position: 'relative',
bottom: '5px',
right: '11px',
},
}),
]),
]),
]),
h('.info-gray', {
style: {
fontSize: '10px',
fontFamily: 'Montserrat Light',
margin: '15px',
lineHeight: '13px',
},
},
`there is a USD$ 5 a day max and a USD$ 50
dollar limit per the life time of an account without a
coinbase account. A fee of 3.75% will be aplied to debit/credit cards.`),
!props.warning ? h('div', {
style: {
width: '340px',
height: '22px',
},
}) : props.warning && h('span.error.flex-center', props.warning),
h('.flex-row', {
style: {
justifyContent: 'space-around',
margin: '33px',
},
}, [
h('button', {
onClick: this.toCoinbase.bind(this),
}, 'Continue to Coinbase'),
h('button', {
onClick: () => props.dispatch(actions.backTobuyView(props.accounts.address)),
}, 'Cancel'),
]),
])
}
CoinbaseForm.prototype.handleAmount = function (event) {
this.props.dispatch(actions.updateCoinBaseAmount(event.target.value))
}
CoinbaseForm.prototype.handleAddress = function (event) {
this.props.dispatch(actions.updateBuyAddress(event.target.value))
}
CoinbaseForm.prototype.toCoinbase = function () {
var props = this.props
var amount = props.buyView.amount
var address = props.buyView.buyAddress
var message
if (isValidAddress(address) && isValidAmountforCoinBase(amount).valid) {
props.dispatch(actions.buyEth(address, props.buyView.amount))
} else if (!isValidAmountforCoinBase(amount).valid) {
message = isValidAmountforCoinBase(amount).message
return props.dispatch(actions.showWarning(message))
} else {
message = 'Receiving address is invalid.'
return props.dispatch(actions.showWarning(message))
}
}
CoinbaseForm.prototype.renderLoading = function () {
return h('img', {
style: {
width: '27px',
marginRight: '-27px',
},
src: 'images/loading.svg',
})
}
function isValidAmountforCoinBase(amount) {
amount = parseFloat(amount)
if (amount) {
if (amount <= 5 && amount > 0) {
return {
valid: true,
}
} else if (amount > 5) {
return {
valid: false,
message: 'The amount can not be greater then $5',
}
} else {
return {
valid: false,
message: 'Can not buy amounts less then $0',
}
}
} else {
return {
valid: false,
message: 'The amount entered is not a number',
}
}
}

View File

@ -41,9 +41,6 @@ DropMenuItem.prototype.activeNetworkRender = function () {
case 'Main Ethereum Network':
if (providerType === 'mainnet') return h('.check', '✓')
break
case 'Ethereum Classic Network':
if (providerType === 'classic') return h('.check', '✓')
break
case 'Morden Test Network':
if (activeNetwork === '2') return h('.check', '✓')
break

View File

@ -4,6 +4,7 @@ const inherits = require('util').inherits
const formatBalance = require('../util').formatBalance
const generateBalanceObject = require('../util').generateBalanceObject
const Tooltip = require('./tooltip.js')
module.exports = EthBalanceComponent
inherits(EthBalanceComponent, Component)
@ -14,29 +15,33 @@ function EthBalanceComponent () {
EthBalanceComponent.prototype.render = function () {
var state = this.props
var style = state.style
const value = formatBalance(state.value, 6)
var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true
const value = formatBalance(state.value, 6, needsParse)
var width = state.width
return (
h('.ether-balance', {
h('.ether-balance.ether-balance-amount', {
style: style,
}, [
h('.ether-balance-amount', {
h('div', {
style: {
display: 'inline',
width: width,
},
}, this.renderBalance(value, state)),
}, this.renderBalance(value)),
])
)
}
EthBalanceComponent.prototype.renderBalance = function (value, state) {
EthBalanceComponent.prototype.renderBalance = function (value) {
var state = this.props
if (value === 'None') return value
var balanceObj = generateBalanceObject(value, state.shorten ? 1 : 3)
var balance
var splitBalance = value.split(' ')
var ethNumber = splitBalance[0]
var ethSuffix = splitBalance[1]
if (state.shorten) {
balance = balanceObj.shortBalance
@ -49,7 +54,7 @@ EthBalanceComponent.prototype.renderBalance = function (value, state) {
return (
h(Tooltip, {
position: 'bottom',
title: value.split(' ')[0],
title: `${ethNumber} ${ethSuffix}`,
}, [
h('.flex-column', {
style: {
@ -64,7 +69,7 @@ EthBalanceComponent.prototype.renderBalance = function (value, state) {
width: '100%',
textAlign: 'right',
},
}, balance),
}, this.props.incoming ? `+${balance}` : balance),
h('div', {
style: {
color: ' #AEAEAE',

View File

@ -14,8 +14,9 @@ function Mascot () {
pxNotRatio: true,
width: 200,
height: 200,
staticImage: './images/icon-512.png',
})
if (!this.logo) return
if (!this.logo.webGLSupport) return
this.refollowMouse = debounce(this.logo.setFollowMouse.bind(this.logo, true), 1000)
this.unfollowMouse = this.logo.setFollowMouse.bind(this.logo, false)
}
@ -34,19 +35,24 @@ Mascot.prototype.render = function () {
}
Mascot.prototype.componentDidMount = function () {
if (!this.logo) return
var targetDivId = 'metamask-mascot-container'
var container = document.getElementById(targetDivId)
container.appendChild(this.logo.canvas)
if (!this.logo.webGLSupport) {
var staticLogo = this.logo.staticLogo
staticLogo.style.marginBottom = '40px'
container.appendChild(staticLogo)
} else {
container.appendChild(this.logo.canvas)
}
}
Mascot.prototype.componentWillUnmount = function () {
if (!this.logo) return
if (!this.logo.webGLSupport) return
this.logo.canvas.remove()
}
Mascot.prototype.handleAnimationEvents = function () {
if (!this.logo) return
if (!this.logo.webGLSupport) return
// only setup listeners once
if (this.animations) return
this.animations = this.props.animationEventEmitter
@ -55,7 +61,7 @@ Mascot.prototype.handleAnimationEvents = function () {
}
Mascot.prototype.lookAt = function (target) {
if (!this.logo) return
if (!this.logo.webGLSupport) return
this.unfollowMouse()
this.logo.lookAt(target)
this.refollowMouse()

View File

@ -23,7 +23,7 @@ Network.prototype.render = function () {
if (networkNumber === 'loading') {
return h('img', {
return h('img.network-indicator', {
title: 'Attempting to connect to blockchain.',
onClick: (event) => this.props.onClick(event),
style: {
@ -36,9 +36,6 @@ Network.prototype.render = function () {
} else if (providerName === 'mainnet') {
hoverText = 'Main Ethereum Network'
iconName = 'ethereum-network'
} else if (providerName === 'classic') {
hoverText = 'Ethereum Classic Network'
iconName = 'classic-network'
} else if (parseInt(networkNumber) === 2) {
hoverText = 'Morden Test Network'
iconName = 'morden-test-network'
@ -64,16 +61,7 @@ Network.prototype.render = function () {
style: {
color: '#039396',
}},
'Etherum Main Net'),
])
case 'classic-network':
return h('.network-indicator', [
h('.menu-icon.hollow-diamond'),
h('.network-name', {
style: {
color: '#039396',
}},
'Etherum Classic'),
'Ethereum Main Net'),
])
case 'morden-test-network':
return h('.network-indicator', [

View File

@ -3,7 +3,6 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits
const AccountPanel = require('./account-panel')
const readableDate = require('../util').readableDate
module.exports = PendingMsgDetails
@ -24,6 +23,9 @@ PendingMsgDetails.prototype.render = function () {
return (
h('div', {
key: msgData.id,
style: {
margin: '10px 20px',
},
}, [
// account that will sign
@ -36,11 +38,6 @@ PendingMsgDetails.prototype.render = function () {
// message data
h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-row.flex-space-between', [
h('label.font-small', 'DATE'),
h('span.font-small', readableDate(msgData.time)),
]),
h('.flex-row.flex-space-between', [
h('label.font-small', 'MESSAGE'),
h('span.font-small', msgParams.data),

View File

@ -0,0 +1,71 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const CopyButton = require('./copyButton')
module.exports = connect(mapStateToProps)(QrCodeView)
function mapStateToProps (state) {
return {
Qr: state.appState.Qr,
buyView: state.appState.buyView,
warning: state.appState.warning,
}
}
inherits(QrCodeView, Component)
function QrCodeView () {
Component.call(this)
}
QrCodeView.prototype.render = function () {
var props = this.props
var Qr = props.Qr
return h('.main-container.flex-column', {
key: 'qr',
style: {
justifyContent: 'center',
padding: '45px',
alignItems: 'center',
},
}, [
Array.isArray(Qr.message) ? h('.message-container', this.renderMultiMessage()) : h('h3', Qr.message),
this.props.warning ? this.props.warning && h('span.error.flex-center', {
style: {
textAlign: 'center',
width: '229px',
height: '82px',
},
},
this.props.warning) : null,
h('#qr-container.flex-column', {
style: {
marginTop: '25px',
marginBottom: '15px',
},
dangerouslySetInnerHTML: {
__html: Qr.image,
},
}),
h('.flex-row', [
h('h3.ellip-address', {
style: {
width: '247px',
},
}, Qr.data),
h(CopyButton, {
value: Qr.data,
}),
]),
])
}
QrCodeView.prototype.renderMultiMessage = function () {
var Qr = this.props.Qr
var multiMessage = Qr.message.map((message) => h('.qr-message', message))
return multiMessage
}

View File

@ -0,0 +1,322 @@
const PersistentForm = require('../../lib/persistent-form')
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
const actions = require('../actions')
const Qr = require('./qr-code')
const isValidAddress = require('../util').isValidAddress
module.exports = connect(mapStateToProps)(ShapeshiftForm)
function mapStateToProps(state) {
return {
selectedAccount: state.selectedAccount,
warning: state.appState.warning,
isSubLoading: state.appState.isSubLoading,
qrRequested: state.appState.qrRequested,
}
}
inherits(ShapeshiftForm, PersistentForm)
function ShapeshiftForm () {
PersistentForm.call(this)
this.persistentFormParentId = 'shapeshift-buy-form'
}
ShapeshiftForm.prototype.render = function () {
return h(ReactCSSTransitionGroup, {
className: 'css-transition-group',
transitionName: 'main',
transitionEnterTimeout: 300,
transitionLeaveTimeout: 300,
}, [
this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain(),
])
}
ShapeshiftForm.prototype.renderMain = function () {
const marketinfo = this.props.buyView.formView.marketinfo
const coinOptions = this.props.buyView.formView.coinOptions
var coin = marketinfo.pair.split('_')[0].toUpperCase()
return h('.flex-column', {
style: {
// marginTop: '10px',
padding: '25px',
width: '100%',
alignItems: 'center',
},
}, [
h('.flex-row', {
style: {
justifyContent: 'center',
alignItems: 'baseline',
},
}, [
h('img', {
src: coinOptions[coin].image,
width: '25px',
height: '25px',
style: {
marginRight: '5px',
},
}),
h('.input-container', [
h('input#fromCoin.buy-inputs.ex-coins', {
type: 'text',
list: 'coinList',
dataset: {
persistentFormId: 'input-coin',
},
style: {
boxSizing: 'border-box',
},
onChange: this.handleLiveInput.bind(this),
defaultValue: 'BTC',
}),
this.renderCoinList(),
h('i.fa.fa-pencil-square-o.edit-text', {
style: {
fontSize: '12px',
color: '#F7861C',
position: 'relative',
bottom: '48px',
left: '106px',
},
}),
]),
h('.icon-control', [
h('i.fa.fa-refresh.fa-4.orange', {
style: {
position: 'relative',
bottom: '5px',
left: '5px',
color: '#F7861C',
},
onClick: this.updateCoin.bind(this),
}),
h('i.fa.fa-chevron-right.fa-4.orange', {
style: {
position: 'relative',
bottom: '26px',
left: '10px',
color: '#F7861C',
},
onClick: this.updateCoin.bind(this),
}),
]),
h('#toCoin.ex-coins', marketinfo.pair.split('_')[1].toUpperCase()),
h('img', {
src: coinOptions[marketinfo.pair.split('_')[1].toUpperCase()].image,
width: '25px',
height: '25px',
style: {
marginLeft: '5px',
},
}),
]),
this.props.isSubLoading ? this.renderLoading() : null,
h('.flex-column', {
style: {
width: '235px',
alignItems: 'flex-start',
},
}, [
this.props.warning ? this.props.warning && h('span.error.flex-center', {
style: {
textAlign: 'center',
width: '229px',
height: '82px',
},
},
this.props.warning) : this.renderInfo(),
]),
h('.flex-row', {
style: {
padding: '10px',
paddingBottom: '2px',
width: '100%',
},
}, [
h('div', 'Receiving address:'),
h('.ellip-address', this.props.buyView.buyAddress),
]),
h(this.activeToggle('.input-container'), {
style: {
padding: '10px',
paddingTop: '0px',
width: '100%',
},
}, [
h('div', `${coin} Address:`),
h('input#fromCoinAddress.buy-inputs', {
type: 'text',
placeholder: `Your ${coin} Refund Address`,
dataset: {
persistentFormId: 'refund-address',
},
style: {
boxSizing: 'border-box',
width: '278px',
height: '20px',
padding: ' 5px ',
},
}),
h('i.fa.fa-pencil-square-o.edit-text', {
style: {
fontSize: '12px',
color: '#F7861C',
position: 'relative',
bottom: '5px',
right: '11px',
},
}),
h('.flex-row', {
style: {
justifyContent: 'flex-end',
},
}, [
h('button', {
onClick: this.shift.bind(this),
style: {
marginTop: '10px',
},
},
'Submit'),
]),
]),
])
}
ShapeshiftForm.prototype.shift = function () {
var props = this.props
var withdrawal = this.props.buyView.buyAddress
var returnAddress = document.getElementById('fromCoinAddress').value
var pair = this.props.buyView.formView.marketinfo.pair
var data = {
'withdrawal': withdrawal,
'pair': pair,
'returnAddress': returnAddress,
// Public api key
'apiKey': '803d1f5df2ed1b1476e4b9e6bcd089e34d8874595dda6a23b67d93c56ea9cc2445e98a6748b219b2b6ad654d9f075f1f1db139abfa93158c04e825db122c14b6',
}
var message = [
`Deposit Limit: ${props.buyView.formView.marketinfo.limit}`,
`Deposit Minimum:${props.buyView.formView.marketinfo.minimum}`,
]
if (isValidAddress(withdrawal)) {
this.props.dispatch(actions.coinShiftRquest(data, message))
}
}
ShapeshiftForm.prototype.renderCoinList = function () {
var list = Object.keys(this.props.buyView.formView.coinOptions).map((item) => {
return h('option', {
value: item,
}, item)
})
return h('datalist#coinList', {
onClick: (event) => {
event.preventDefault()
},
}, list)
}
ShapeshiftForm.prototype.updateCoin = function (event) {
event.preventDefault()
const props = this.props
var coinOptions = this.props.buyView.formView.coinOptions
var coin = document.getElementById('fromCoin').value
if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') {
var message = 'Not a valid coin'
return props.dispatch(actions.showWarning(message))
} else {
return props.dispatch(actions.pairUpdate(coin))
}
}
ShapeshiftForm.prototype.handleLiveInput = function () {
const props = this.props
var coinOptions = this.props.buyView.formView.coinOptions
var coin = document.getElementById('fromCoin').value
if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') {
return null
} else {
return props.dispatch(actions.pairUpdate(coin))
}
}
ShapeshiftForm.prototype.renderInfo = function () {
const marketinfo = this.props.buyView.formView.marketinfo
const coinOptions = this.props.buyView.formView.coinOptions
var coin = marketinfo.pair.split('_')[0].toUpperCase()
return h('span', {
style: {
marginTop: '15px',
marginBottom: '15px',
},
}, [
h('h3.flex-row.text-transform-uppercase', {
style: {
color: '#AEAEAE',
paddingTop: '4px',
justifyContent: 'space-around',
textAlign: 'center',
fontSize: '14px',
},
}, `Market Info for ${marketinfo.pair.replace('_', ' to ').toUpperCase()}:`),
h('.marketinfo', ['Status : ', `${coinOptions[coin].status}`]),
h('.marketinfo', ['Exchange Rate: ', `${marketinfo.rate}`]),
h('.marketinfo', ['Limit: ', `${marketinfo.limit}`]),
h('.marketinfo', ['Minimum : ', `${marketinfo.minimum}`]),
])
}
ShapeshiftForm.prototype.handleAddress = function (event) {
this.props.dispatch(actions.updateBuyAddress(event.target.value))
}
ShapeshiftForm.prototype.activeToggle = function (elementType) {
if (!this.props.buyView.formView.response || this.props.warning) return elementType
return `${elementType}.inactive`
}
ShapeshiftForm.prototype.renderLoading = function () {
return h('span', {
style: {
position: 'absolute',
left: '70px',
bottom: '194px',
background: 'transparent',
width: '229px',
height: '82px',
display: 'flex',
justifyContent: 'center',
},
}, [
h('img', {
style: {
width: '60px',
},
src: 'images/loading.svg',
}),
])
}

View File

@ -0,0 +1,202 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const vreme = new (require('vreme'))
const explorerLink = require('../../lib/explorer-link')
const extension = require('../../../app/scripts/lib/extension')
const actions = require('../actions')
const addressSummary = require('../util').addressSummary
const CopyButton = require('./copyButton')
const EtherBalance = require('./eth-balance')
const Tooltip = require('./tooltip')
module.exports = connect(mapStateToProps)(ShiftListItem)
function mapStateToProps (state) {
return {}
}
inherits(ShiftListItem, Component)
function ShiftListItem () {
Component.call(this)
}
ShiftListItem.prototype.render = function () {
return (
h('.transaction-list-item.flex-row', {
style: {
paddingTop: '20px',
paddingBottom: '20px',
justifyContent: 'space-around',
alignItems: 'center',
},
}, [
h('div', {
style: {
width: '0px',
position: 'relative',
bottom: '19px',
},
}, [
h('img', {
src: 'https://info.shapeshift.io/sites/default/files/logo.png',
style: {
height: '35px',
width: '132px',
position: 'absolute',
clip: 'rect(0px,23px,34px,0px)',
},
}),
]),
this.renderInfo(),
this.renderUtilComponents(),
])
)
}
function formatDate (date) {
return vreme.format(new Date(date), 'March 16 2014 14:30')
}
ShiftListItem.prototype.renderUtilComponents = function () {
var props = this.props
switch (props.response.status) {
case 'no_deposits':
return h('.flex-row', [
h(CopyButton, {
value: this.props.depositAddress,
}),
h(Tooltip, {
title: 'QR Code',
}, [
h('i.fa.fa-qrcode.pointer.pop-hover', {
onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType)),
style: {
margin: '5px',
marginLeft: '23px',
marginRight: '12px',
fontSize: '20px',
color: '#F7861C',
},
}),
]),
])
case 'received':
return h('.flex-row')
case 'complete':
return h('.flex-row', [
h(CopyButton, {
value: this.props.response.transaction,
}),
h(EtherBalance, {
value: `${props.response.outgoingCoin}`,
width: '55px',
shorten: true,
needsParse: false,
incoming: true,
style: {
fontSize: '15px',
color: '#01888C',
},
}),
])
case 'failed':
return ''
default:
return ''
}
}
ShiftListItem.prototype.renderInfo = function () {
var props = this.props
switch (props.response.status) {
case 'no_deposits':
return h('.flex-column', {
style: {
width: '200px',
overflow: 'hidden',
},
}, [
h('div', {
style: {
fontSize: 'x-small',
color: '#ABA9AA',
width: '100%',
},
}, `${props.depositType} to ETH via ShapeShift`),
h('div', 'No deposits received'),
h('div', {
style: {
fontSize: 'x-small',
color: '#ABA9AA',
width: '100%',
},
}, formatDate(props.time)),
])
case 'received':
return h('.flex-column', {
style: {
width: '200px',
overflow: 'hidden',
},
}, [
h('div', {
style: {
fontSize: 'x-small',
color: '#ABA9AA',
width: '100%',
},
}, `${props.depositType} to ETH via ShapeShift`),
h('div', 'Conversion in progress'),
h('div', {
style: {
fontSize: 'x-small',
color: '#ABA9AA',
width: '100%',
},
}, formatDate(props.time)),
])
case 'complete':
var url = explorerLink(props.response.transaction, parseInt('1'))
return h('.flex-column.pointer', {
style: {
width: '200px',
overflow: 'hidden',
},
onClick: () => extension.tabs.create({
url,
}),
}, [
h('div', {
style: {
fontSize: 'x-small',
color: '#ABA9AA',
width: '100%',
},
}, 'From ShapeShift'),
h('div', formatDate(props.time)),
h('div', {
style: {
fontSize: 'x-small',
color: '#ABA9AA',
width: '100%',
},
}, addressSummary(props.response.transaction)),
])
case 'failed':
return h('span.error', '(Failed)')
default:
return ''
}
}

View File

@ -10,7 +10,7 @@ const vreme = new (require('vreme'))
const extension = require('../../../app/scripts/lib/extension')
const TransactionIcon = require('./transaction-list-item-icon')
const ShiftListItem = require('./shift-list-item')
module.exports = TransactionListItem
inherits(TransactionListItem, Component)
@ -19,8 +19,10 @@ function TransactionListItem () {
}
TransactionListItem.prototype.render = function () {
const { transaction, i, network } = this.props
const { transaction, network } = this.props
if (transaction.key === 'shapeshift') {
if (network === '1') return h(ShiftListItem, transaction)
}
var date = formatDate(transaction.time)
let isLinkable = false
@ -42,7 +44,6 @@ TransactionListItem.prototype.render = function () {
return (
h(`.transaction-list-item.flex-row.flex-space-between${isClickable ? '.pointer' : ''}`, {
key: `tx-${transaction.id + i}`,
onClick: (event) => {
if (isPending) {
this.props.showTx(transaction.id)

View File

@ -14,7 +14,11 @@ function TransactionList () {
TransactionList.prototype.render = function () {
const { txsToRender, network, unconfMsgs } = this.props
const transactions = txsToRender.concat(unconfMsgs)
var shapeShiftTxList
if (network === '1') {
shapeShiftTxList = this.props.shapeShiftTxList
}
const transactions = !shapeShiftTxList ? txsToRender.concat(unconfMsgs) : txsToRender.concat(unconfMsgs, shapeShiftTxList)
.sort((a, b) => b.time - a.time)
return (
@ -39,33 +43,46 @@ TransactionList.prototype.render = function () {
paddingBottom: '4px',
},
}, [
'Transactions',
'History',
]),
h('.tx-list', {
style: {
overflowY: 'auto',
height: '305px',
height: '300px',
padding: '0 20px',
textAlign: 'center',
},
}, (
}, [
transactions.length
? transactions.map((transaction, i) => {
let key
switch (transaction.key) {
case 'shapeshift':
const { depositAddress, time } = transaction
key = `shift-tx-${depositAddress}-${time}-${i}`
break
default:
key = `tx-${transaction.id}-${i}`
}
return h(TransactionListItem, {
transaction, i, network,
transaction, i, network, key,
showTx: (txId) => {
this.props.viewPendingTx(txId)
},
})
})
: [h('.flex-center', {
: h('.flex-center', {
style: {
flexDirection: 'column',
height: '100%',
},
}, 'No transaction history...')]
)),
}, [
'No transaction history.',
]),
]),
])
)
}

View File

@ -5,6 +5,7 @@ const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('./actions')
const txHelper = require('../lib/tx-helper')
const isPopupOrNotification = require('../../app/scripts/lib/is-popup-or-notification')
const PendingTx = require('./components/pending-tx')
const PendingMsg = require('./components/pending-msg')
@ -35,7 +36,8 @@ ConfirmTxScreen.prototype.render = function () {
var unconfMsgs = state.unconfMsgs
var unconfTxList = txHelper(unconfTxs, unconfMsgs)
var index = state.index !== undefined ? state.index : 0
var txData = unconfTxList[index] || {}
var txData = unconfTxList[index] || unconfTxList[0] || {}
var isNotification = isPopupOrNotification() === 'notification'
return (
@ -43,9 +45,9 @@ ConfirmTxScreen.prototype.render = function () {
// subtitle and nav
h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
!isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: this.goHome.bind(this),
}),
}) : null,
h('h2.page-subtitle', 'Confirm Transaction'),
]),

View File

@ -3,7 +3,7 @@ const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('./actions')
const currencies = require('./conversion-util').availableCurrencies.rows
module.exports = connect(mapStateToProps)(ConfigScreen)
function mapStateToProps (state) {
@ -74,6 +74,8 @@ ConfigScreen.prototype.render = function () {
}, 'Save'),
]),
h('hr.horizontal-line'),
currentConversionInformation(metamaskState, state),
h('hr.horizontal-line'),
h('div', {
style: {
@ -97,6 +99,27 @@ ConfigScreen.prototype.render = function () {
)
}
function currentConversionInformation (metamaskState, state) {
var currentFiat = metamaskState.currentFiat
var conversionDate = metamaskState.conversionDate
return h('div', [
h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, 'Current Conversion'),
h('span', {style: { fontWeight: 'bold', paddingRight: '10px', fontSize: '13px'}}, `Updated ${Date(conversionDate)}`),
h('select#currentFiat', {
onChange (event) {
event.preventDefault()
var element = document.getElementById('currentFiat')
var newFiat = element.value
state.dispatch(actions.setCurrentFiat(newFiat))
},
defaultValue: currentFiat,
}, currencies.map((currency) => {
return h('option', {key: currency.code, value: currency.code}, `${currency.code} - ${currency.name}`)
})
),
])
}
function currentProviderDisplay (metamaskState) {
var provider = metamaskState.provider
var title, value

File diff suppressed because one or more lines are too long

View File

@ -471,3 +471,128 @@ input.large-input {
.eth-warning{
transition: opacity 400ms ease-in, transform 400ms ease-in;
}
.buy-subview{
transition: opacity 400ms ease-in, transform 400ms ease-in;
}
.input-container:hover .edit-text{
visibility: visible;
}
.buy-inputs{
font-family: 'Montserrat Light';
font-size: 13px;
height: 20px;
background: transparent;
box-sizing: border-box;
border: solid;
border-color: transparent;
border-width: 0.5px;
border-radius: 2px;
}
.input-container:hover .buy-inputs{
box-sizing: inherit;
border: solid;
border-color: #F7861C;
border-width: 0.5px;
border-radius: 2px;
}
.buy-inputs:focus{
border: solid;
border-color: #F7861C;
border-width: 0.5px;
border-radius: 2px;
}
.activeForm {
background: #F7F7F7;
border: none;
border-radius: 8px 8px 0px 0px;
width: 50%;
text-align: center;
padding-bottom: 4px;
}
.inactiveForm {
border: none;
border-radius: 8px 8px 0px 0px;
width: 50%;
text-align: center;
padding-bottom: 4px;
}
.ex-coins {
font-family: 'Montserrat Regular';
text-transform: uppercase;
text-align: center;
font-size: 33px;
width: 118px;
height: 42px;
padding: 1px;
color: #4D4D4D;
}
.marketinfo{
font-family: 'Montserrat light';
color: #AEAEAE;
font-size: 12px;
line-height: 14px;
}
#fromCoin::-webkit-calendar-picker-indicator {
display: none;
}
#coinList {
width: 400px;
height: 500px;
overflow: scroll;
}
.icon-control .fa-refresh{
visibility: hidden;
}
.icon-control:hover .fa-refresh{
visibility: visible;
}
.icon-control:hover .fa-chevron-right{
visibility: hidden;
}
.inactive {
color: #AEAEAE;
}
.inactive button{
background: #AEAEAE;
color: white;
}
.ellip-address {
overflow: hidden;
text-overflow: ellipsis;
width: 5em;
font-size: 14px;
font-family: "Montserrat Light";
margin-left: 5px;
}
.qr-message {
font-size: 12px;
color: #F7861C;
}
div.message-container > div:first-child {
font-size: 15px;
color: #4D4D4D;
}
.pop-hover:hover {
transform: scale(1.1);
}

View File

@ -35,9 +35,10 @@ EthStoreWarning.prototype.render = function () {
margin: '10px 10px 10px 10px',
},
},
`MetaMask is currently in beta -
exercise caution while handling
and storing your ether.
`The MetaMask team would like to
remind you that MetaMask is currently in beta - so
don't store large
amounts of ether in MetaMask.
`),
h('i.fa.fa-exclamation-triangle.fa-4', {

View File

@ -73,9 +73,7 @@ InitializeMenuScreen.prototype.renderMenu = function () {
margin: 12,
},
}, 'Restore Existing Vault'),
])
)
}

View File

@ -1,14 +1,14 @@
const inherits = require('util').inherits
const Component = require('react').Component
const PersistentForm = require('../../lib/persistent-form')
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const actions = require('../actions')
module.exports = connect(mapStateToProps)(RestoreVaultScreen)
inherits(RestoreVaultScreen, Component)
inherits(RestoreVaultScreen, PersistentForm)
function RestoreVaultScreen () {
Component.call(this)
PersistentForm.call(this)
}
function mapStateToProps (state) {
@ -19,6 +19,8 @@ function mapStateToProps (state) {
RestoreVaultScreen.prototype.render = function () {
var state = this.props
this.persistentFormParentId = 'restore-vault-form'
return (
h('.initialize-screen.flex-column.flex-center.flex-grow', [
@ -39,6 +41,9 @@ RestoreVaultScreen.prototype.render = function () {
// wallet seed entry
h('h3', 'Wallet Seed'),
h('textarea.twelve-word-phrase.letter-spacey', {
dataset: {
persistentFormId: 'wallet-seed',
},
placeholder: 'Enter your secret twelve word phrase here to restore your vault.',
}),
@ -47,6 +52,9 @@ RestoreVaultScreen.prototype.render = function () {
type: 'password',
id: 'password-box',
placeholder: 'New Password (min 8 chars)',
dataset: {
persistentFormId: 'password',
},
style: {
width: 260,
marginTop: 12,
@ -59,6 +67,9 @@ RestoreVaultScreen.prototype.render = function () {
id: 'password-box-confirm',
placeholder: 'Confirm Password',
onKeyPress: this.onMaybeCreate.bind(this),
dataset: {
persistentFormId: 'password-confirmation',
},
style: {
width: 260,
marginTop: 16,

View File

@ -1,6 +1,7 @@
const extend = require('xtend')
const actions = require('../actions')
const txHelper = require('../../lib/tx-helper')
const notification = require('../../../app/scripts/lib/notifications')
module.exports = reduceApp
@ -123,6 +124,7 @@ function reduceApp (state, action) {
case actions.UNLOCK_METAMASK:
return extend(appState, {
forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null,
detailView: {},
transForward: true,
isLoading: false,
@ -136,6 +138,25 @@ function reduceApp (state, action) {
warning: null,
})
case actions.BACK_TO_INIT_MENU:
return extend(appState, {
warning: null,
transForward: false,
forgottenPassword: true,
currentView: {
name: 'InitMenu',
},
})
case actions.BACK_TO_UNLOCK_VIEW:
return extend(appState, {
warning: null,
transForward: true,
forgottenPassword: !appState.forgottenPassword,
currentView: {
name: 'UnlockScreen',
},
})
// reveal seed words
case actions.REVEAL_SEED_CONFIRMATION:
@ -170,6 +191,7 @@ function reduceApp (state, action) {
case actions.SHOW_ACCOUNT_DETAIL:
return extend(appState, {
forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null,
currentView: {
name: 'accountDetail',
context: action.value,
@ -250,6 +272,9 @@ function reduceApp (state, action) {
warning: null,
})
} else {
notification.closePopup()
return extend(appState, {
transForward: false,
warning: null,
@ -317,6 +342,15 @@ function reduceApp (state, action) {
isLoading: false,
})
case actions.SHOW_SUB_LOADING_INDICATION:
return extend(appState, {
isSubLoading: true,
})
case actions.HIDE_SUB_LOADING_INDICATION:
return extend(appState, {
isSubLoading: false,
})
case actions.CLEAR_SEED_WORD_CACHE:
return extend(appState, {
transForward: true,
@ -369,15 +403,116 @@ function reduceApp (state, action) {
},
})
case actions.SHOW_ETH_WARNING:
case actions.BUY_ETH_VIEW:
return extend(appState, {
transForward: true,
currentView: {
name: 'accountDetail',
name: 'buyEth',
context: appState.currentView.context,
},
accountDetail: {
subview: 'buy-eth-warning',
buyView: {
subview: 'buyForm',
amount: '5.00',
buyAddress: action.value,
formView: {
coinbase: true,
shapeshift: false,
},
},
})
case actions.UPDATE_BUY_ADDRESS:
return extend(appState, {
buyView: {
subview: 'buyForm',
formView: {
coinbase: appState.buyView.formView.coinbase,
shapeshift: appState.buyView.formView.shapeshift,
},
buyAddress: action.value,
amount: appState.buyView.amount,
},
})
case actions.UPDATE_COINBASE_AMOUNT:
return extend(appState, {
buyView: {
subview: 'buyForm',
formView: {
coinbase: true,
shapeshift: false,
},
buyAddress: appState.buyView.buyAddress,
amount: action.value,
},
})
case actions.COINBASE_SUBVIEW:
return extend(appState, {
buyView: {
subview: 'buyForm',
formView: {
coinbase: true,
shapeshift: false,
},
buyAddress: appState.buyView.buyAddress,
amount: appState.buyView.amount,
},
})
case actions.SHAPESHIFT_SUBVIEW:
return extend(appState, {
buyView: {
subview: 'buyForm',
formView: {
coinbase: false,
shapeshift: true,
marketinfo: action.value.marketinfo,
coinOptions: action.value.coinOptions,
},
buyAddress: appState.buyView.buyAddress,
amount: appState.buyView.amount,
},
})
case actions.PAIR_UPDATE:
return extend(appState, {
buyView: {
subview: 'buyForm',
formView: {
coinbase: false,
shapeshift: true,
marketinfo: action.value.marketinfo,
coinOptions: appState.buyView.formView.coinOptions,
},
buyAddress: appState.buyView.buyAddress,
amount: appState.buyView.amount,
warning: null,
},
})
case actions.SHOW_QR:
return extend(appState, {
qrRequested: true,
transForward: true,
Qr: {
message: action.value.message,
image: action.value.qr,
data: action.value.data,
},
})
case actions.SHOW_QR_VIEW:
return extend(appState, {
currentView: {
name: 'qr',
context: appState.currentView.context,
},
transForward: true,
Qr: {
message: action.value.message,
image: action.value.qr,
data: action.value.data,
},
})
default:
@ -405,4 +540,3 @@ function indexForPending (state, txId) {
return idx
}

View File

@ -10,10 +10,14 @@ function reduceMetamask (state, action) {
var metamaskState = extend({
isInitialized: false,
isUnlocked: false,
isEthConfirmed: false,
currentDomain: 'example.com',
rpcTarget: 'https://rawtestrpc.metamask.io/',
identities: {},
unconfTxs: {},
currentFiat: 'USD',
conversionRate: 0,
conversionDate: 'N/A',
}, state.metamask)
switch (action.type) {
@ -33,7 +37,7 @@ function reduceMetamask (state, action) {
case actions.AGREE_TO_ETH_WARNING:
return extend(metamaskState, {
isEthConfirmed: true,
isEthConfirmed: !metamaskState.isEthConfirmed,
})
case actions.UNLOCK_METAMASK:
@ -114,6 +118,13 @@ function reduceMetamask (state, action) {
var identities = extend(metamaskState.identities, id)
return extend(metamaskState, { identities })
case actions.SET_CURRENT_FIAT:
return extend(metamaskState, {
currentFiat: action.value.currentFiat,
conversionRate: action.value.conversionRate,
conversionDate: action.value.conversionDate,
})
default:
return metamaskState

View File

@ -1,5 +1,5 @@
const inherits = require('util').inherits
const Component = require('react').Component
const PersistentForm = require('../lib/persistent-form')
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const Identicon = require('./components/identicon')
@ -29,12 +29,14 @@ function mapStateToProps (state) {
return result
}
inherits(SendTransactionScreen, Component)
inherits(SendTransactionScreen, PersistentForm)
function SendTransactionScreen () {
Component.call(this)
PersistentForm.call(this)
}
SendTransactionScreen.prototype.render = function () {
this.persistentFormParentId = 'send-tx-form'
var state = this.props
var address = state.address
var account = state.account
@ -137,6 +139,9 @@ SendTransactionScreen.prototype.render = function () {
h('input.large-input', {
name: 'address',
placeholder: 'Recipient Address',
dataset: {
persistentFormId: 'recipient-address',
},
}),
]),
@ -150,6 +155,9 @@ SendTransactionScreen.prototype.render = function () {
style: {
marginRight: 6,
},
dataset: {
persistentFormId: 'tx-amount',
},
}),
h('button.primary', {
@ -185,11 +193,12 @@ SendTransactionScreen.prototype.render = function () {
width: '100%',
resize: 'none',
},
dataset: {
persistentFormId: 'tx-data',
},
}),
]),
])
)
}

View File

@ -1,17 +1,20 @@
const createStore = require('redux').createStore
const applyMiddleware = require('redux').applyMiddleware
const thunkMiddleware = require('redux-thunk')
const createLogger = require('redux-logger')
const rootReducer = require('./reducers')
const createLogger = require('redux-logger')
global.METAMASK_DEBUG = false
module.exports = configureStore
const loggerMiddleware = createLogger()
const loggerMiddleware = createLogger({
predicate: () => global.METAMASK_DEBUG,
})
const createStoreWithMiddleware = applyMiddleware(
thunkMiddleware,
loggerMiddleware
)(createStore)
const middlewares = [thunkMiddleware, loggerMiddleware]
const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore)
function configureStore (initialState) {
return createStoreWithMiddleware(rootReducer, initialState)

View File

@ -3,10 +3,11 @@ const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('./actions')
const Mascot = require('./components/mascot')
const getCaretCoordinates = require('textarea-caret')
const EventEmitter = require('events').EventEmitter
const Mascot = require('./components/mascot')
module.exports = connect(mapStateToProps)(UnlockScreen)
inherits(UnlockScreen, Component)
@ -25,47 +26,46 @@ UnlockScreen.prototype.render = function () {
const state = this.props
const warning = state.warning
return (
h('.flex-column.hey-im-here', [
h('.unlock-screen.flex-column.flex-center.flex-grow', [
h('.unlock-screen.flex-column.flex-center.flex-grow', [
h(Mascot, {
animationEventEmitter: this.animationEventEmitter,
}),
h(Mascot, {
animationEventEmitter: this.animationEventEmitter,
}),
h('h1', {
style: {
fontSize: '1.4em',
textTransform: 'uppercase',
color: '#7F8082',
},
}, 'MetaMask'),
h('h1', {
style: {
fontSize: '1.4em',
textTransform: 'uppercase',
color: '#7F8082',
},
}, 'MetaMask'),
h('input.large-input', {
type: 'password',
id: 'password-box',
placeholder: 'enter password',
style: {
h('input.large-input', {
type: 'password',
id: 'password-box',
placeholder: 'enter password',
style: {
},
onKeyPress: this.onKeyPress.bind(this),
onInput: this.inputChanged.bind(this),
}),
},
onKeyPress: this.onKeyPress.bind(this),
onInput: this.inputChanged.bind(this),
}),
h('.error', {
style: {
display: warning ? 'block' : 'none',
},
}, warning),
h('button.primary.cursor-pointer', {
onClick: this.onSubmit.bind(this),
style: {
margin: 10,
},
}, 'Unlock'),
h('.error', {
style: {
display: warning ? 'block' : 'none',
},
}, warning),
h('button.primary.cursor-pointer', {
onClick: this.onSubmit.bind(this),
style: {
margin: 10,
},
}, 'Unlock'),
]),
])
)
}

View File

@ -92,8 +92,8 @@ function parseBalance (balance) {
// Takes wei hex, returns an object with three properties.
// Its "formatted" property is what we generally use to render values.
function formatBalance (balance, decimalsToKeep) {
var parsed = parseBalance(balance)
function formatBalance (balance, decimalsToKeep, needsParse = true) {
var parsed = needsParse ? parseBalance(balance) : balance.split('.')
var beforeDecimal = parsed[0]
var afterDecimal = parsed[1]
var formatted = 'None'
@ -141,14 +141,21 @@ function shortenBalance (balance, decimalsToKeep = 1) {
var convertedBalance = parseFloat(balance)
if (convertedBalance > 1000000) {
truncatedValue = (balance / 1000000).toFixed(decimalsToKeep)
return `>${truncatedValue}m`
return `${truncatedValue}m`
} else if (convertedBalance > 1000) {
truncatedValue = (balance / 1000).toFixed(decimalsToKeep)
return `>${truncatedValue}k`
return `${truncatedValue}k`
} else if (convertedBalance === 0) {
return '0'
} else if (convertedBalance < 0.001) {
return '<0.001'
} else if (convertedBalance < 1) {
var exponent = balance.match(/\.0*/)[0].length
truncatedValue = (convertedBalance * Math.pow(10, exponent)).toFixed(decimalsToKeep)
return `<${truncatedValue}e-${exponent}`
var stringBalance = convertedBalance.toString()
if (stringBalance.split('.')[1].length > 3) {
return convertedBalance.toFixed(3)
} else {
return stringBalance
}
} else {
return convertedBalance.toFixed(decimalsToKeep)
}

18
ui/lib/account-link.js Normal file
View File

@ -0,0 +1,18 @@
module.exports = function(address, network) {
const net = parseInt(network)
let link
switch (net) {
case 1: // main net
link = `http://etherscan.io/address/${address}`
break
case 2: // morden test net
link = `http://testnet.etherscan.io/address/${address}`
break
default:
link = ''
break
}
return link
}

57
ui/lib/persistent-form.js Normal file
View File

@ -0,0 +1,57 @@
const inherits = require('util').inherits
const Component = require('react').Component
const defaultKey = 'persistent-form-default'
const eventName = 'keyup'
module.exports = PersistentForm
function PersistentForm () {
Component.call(this)
}
inherits(PersistentForm, Component)
PersistentForm.prototype.componentDidMount = function () {
const fields = document.querySelectorAll('[data-persistent-formid]')
const store = this.getPersistentStore()
fields.forEach((field) => {
const key = field.getAttribute('data-persistent-formid')
const cached = store[key]
if (cached !== undefined) {
field.value = cached
}
field.addEventListener(eventName, this.persistentFieldDidUpdate.bind(this))
})
}
PersistentForm.prototype.getPersistentStore = function () {
let store = window.localStorage[this.persistentFormParentId || defaultKey]
if (store && store !== 'null') {
store = JSON.parse(store)
} else {
store = {}
}
return store
}
PersistentForm.prototype.setPersistentStore = function (newStore) {
window.localStorage[this.persistentFormParentId || defaultKey] = JSON.stringify(newStore)
}
PersistentForm.prototype.persistentFieldDidUpdate = function (event) {
const field = event.target
const store = this.getPersistentStore()
const key = field.getAttribute('data-persistent-formid')
const val = field.value
store[key] = val
this.setPersistentStore(store)
}
PersistentForm.prototype.componentWillUnmount = function () {
const fields = document.querySelectorAll('[data-persistent-formid]')
fields.forEach((field) => {
field.removeEventListener(eventName, this.persistentFieldDidUpdate.bind(this))
})
this.setPersistentStore({})
}