parent
9f1438b85b
commit
d814a45dff
@ -0,0 +1,66 @@ |
||||
|
||||
# Created by https://www.gitignore.io/api/osx,node |
||||
|
||||
### OSX ### |
||||
.DS_Store |
||||
.AppleDouble |
||||
.LSOverride |
||||
|
||||
# Icon must end with two \r |
||||
Icon
|
||||
|
||||
# Thumbnails |
||||
._* |
||||
|
||||
# Files that might appear in the root of a volume |
||||
.DocumentRevisions-V100 |
||||
.fseventsd |
||||
.Spotlight-V100 |
||||
.TemporaryItems |
||||
.Trashes |
||||
.VolumeIcon.icns |
||||
|
||||
# Directories potentially created on remote AFP share |
||||
.AppleDB |
||||
.AppleDesktop |
||||
Network Trash Folder |
||||
Temporary Items |
||||
.apdisk |
||||
|
||||
|
||||
### Node ### |
||||
# Logs |
||||
logs |
||||
*.log |
||||
npm-debug.log* |
||||
|
||||
# Runtime data |
||||
pids |
||||
*.pid |
||||
*.seed |
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover |
||||
lib-cov |
||||
|
||||
# Coverage directory used by tools like istanbul |
||||
coverage |
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) |
||||
.grunt |
||||
|
||||
# node-waf configuration |
||||
.lock-wscript |
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html) |
||||
build/Release |
||||
|
||||
# Dependency directory |
||||
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git |
||||
node_modules |
||||
|
||||
# Optional npm cache directory |
||||
.npm |
||||
|
||||
# Optional REPL history |
||||
.node_repl_history |
||||
|
@ -0,0 +1,14 @@ |
||||
## Installation |
||||
|
||||
``` |
||||
git clone git@github.com:MetaMask/metamask-ui.git |
||||
cd metamask-ui |
||||
npm install |
||||
grunt dev |
||||
``` |
||||
|
||||
## Testing |
||||
|
||||
Requires `mocha` installed. Run `npm install -g mocha`. |
||||
|
||||
You can either run the test suite once with `npm test`, or you can reload on file changes, by running `mocha watch test/**/**`. |
@ -0,0 +1,154 @@ |
||||
const inherits = require('util').inherits |
||||
const Component = require('react').Component |
||||
const h = require('react-hyperscript') |
||||
const connect = require('react-redux').connect |
||||
const copyToClipboard = require('copy-to-clipboard') |
||||
const actions = require('./actions') |
||||
const AccountPanel = require('./components/account-panel') |
||||
|
||||
module.exports = connect(mapStateToProps)(AccountDetailScreen) |
||||
|
||||
function mapStateToProps(state) { |
||||
var accountDetail = state.appState.accountDetail |
||||
return { |
||||
identities: state.metamask.identities, |
||||
accounts: state.metamask.accounts, |
||||
address: state.appState.currentView.context, |
||||
accountDetail: accountDetail, |
||||
} |
||||
} |
||||
|
||||
inherits(AccountDetailScreen, Component) |
||||
function AccountDetailScreen() { |
||||
Component.call(this) |
||||
} |
||||
|
||||
|
||||
AccountDetailScreen.prototype.render = function() { |
||||
var state = this.props |
||||
var identity = state.identities[state.address] |
||||
var account = state.accounts[state.address] |
||||
var accountDetail = state.accountDetail |
||||
|
||||
return ( |
||||
|
||||
h('.account-detail-section.flex-column.flex-grow', [ |
||||
|
||||
// subtitle and nav
|
||||
h('.section-title.flex-row.flex-center', [ |
||||
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { |
||||
onClick: this.navigateToAccounts.bind(this), |
||||
}), |
||||
h('h2.page-subtitle', 'Account Detail'), |
||||
]), |
||||
|
||||
// account summary, with embedded action buttons
|
||||
h(AccountPanel, { |
||||
showFullAddress: true, |
||||
identity: identity, |
||||
account: account, |
||||
}, [ |
||||
h('.flex-row.flex-space-around', [ |
||||
// h('button', 'GET ETH'), DISABLED UNTIL WORKING
|
||||
|
||||
h('button', { |
||||
onClick: () => { |
||||
copyToClipboard(identity.address) |
||||
}, |
||||
}, 'COPY ADDR'), |
||||
|
||||
h('button', { |
||||
onClick: () => { |
||||
this.props.dispatch(actions.showSendPage()) |
||||
}, |
||||
}, 'SEND'), |
||||
|
||||
h('button', { |
||||
onClick: () => { |
||||
this.requestAccountExport(identity.address) |
||||
}, |
||||
}, 'EXPORT'), |
||||
]), |
||||
]), |
||||
|
||||
this.exportedAccount(accountDetail), |
||||
|
||||
// transaction table
|
||||
/* |
||||
h('section.flex-column', [ |
||||
h('span', 'your transaction history will go here.'), |
||||
]), |
||||
*/ |
||||
]) |
||||
) |
||||
} |
||||
|
||||
AccountDetailScreen.prototype.navigateToAccounts = function(event){ |
||||
event.stopPropagation() |
||||
this.props.dispatch(actions.showAccountsPage()) |
||||
} |
||||
|
||||
AccountDetailScreen.prototype.exportAccount = function(address) { |
||||
this.props.dispatch(actions.exportAccount(address)) |
||||
} |
||||
|
||||
AccountDetailScreen.prototype.requestAccountExport = function() { |
||||
this.props.dispatch(actions.requestExportAccount()) |
||||
} |
||||
|
||||
AccountDetailScreen.prototype.exportedAccount = function(accountDetail) { |
||||
if (!accountDetail) return |
||||
var accountExport = accountDetail.accountExport |
||||
|
||||
var notExporting = accountExport === 'none' |
||||
var exportRequested = accountExport === 'requested' |
||||
var accountExported = accountExport === 'completed' |
||||
|
||||
if (notExporting) return |
||||
|
||||
if (exportRequested) { |
||||
var warning = `Exporting your private key is very dangerous,
|
||||
and you should only do it if you know what you're doing.` |
||||
var confirmation = `If you're absolutely sure, type "I understand" below and
|
||||
hit Enter.` |
||||
return h('div', {}, [ |
||||
h('p.error', warning), |
||||
h('p', confirmation), |
||||
h('input#exportAccount', { |
||||
onKeyPress: this.onExportKeyPress.bind(this), |
||||
}) |
||||
]) |
||||
} |
||||
|
||||
if (accountExported) { |
||||
return h('div.privateKey', { |
||||
|
||||
}, [ |
||||
h('label', 'Your private key (click to copy):'), |
||||
h('p.error.cursor-pointer', { |
||||
style: { |
||||
textOverflow: 'ellipsis', |
||||
overflow: 'hidden', |
||||
webkitUserSelect: 'text', |
||||
width: '100%', |
||||
}, |
||||
onClick: function(event) { |
||||
copyToClipboard(accountDetail.privateKey) |
||||
} |
||||
}, accountDetail.privateKey), |
||||
]) |
||||
} |
||||
} |
||||
|
||||
AccountDetailScreen.prototype.onExportKeyPress = function(event) { |
||||
if (event.key !== 'Enter') return |
||||
event.preventDefault() |
||||
|
||||
var input = document.getElementById('exportAccount') |
||||
if (input.value === 'I understand') { |
||||
this.props.dispatch(actions.exportAccount(this.props.address)) |
||||
} else { |
||||
input.value = '' |
||||
input.placeholder = 'Please retype "I understand" exactly.' |
||||
} |
||||
} |
@ -0,0 +1,116 @@ |
||||
const inherits = require('util').inherits |
||||
const Component = require('react').Component |
||||
const h = require('react-hyperscript') |
||||
const connect = require('react-redux').connect |
||||
const extend = require('xtend') |
||||
const actions = require('./actions') |
||||
const AccountPanel = require('./components/account-panel') |
||||
const valuesFor = require('./util').valuesFor |
||||
|
||||
module.exports = connect(mapStateToProps)(AccountsScreen) |
||||
|
||||
|
||||
function mapStateToProps(state) { |
||||
return { |
||||
accounts: state.metamask.accounts, |
||||
identities: state.metamask.identities, |
||||
unconfTxs: state.metamask.unconfTxs, |
||||
selectedAddress: state.metamask.selectedAddress, |
||||
currentDomain: state.appState.currentDomain, |
||||
} |
||||
} |
||||
|
||||
inherits(AccountsScreen, Component) |
||||
function AccountsScreen() { |
||||
Component.call(this) |
||||
} |
||||
|
||||
|
||||
AccountsScreen.prototype.render = function() { |
||||
var state = this.props |
||||
var identityList = valuesFor(state.identities) |
||||
var unconfTxList = valuesFor(state.unconfTxs) |
||||
var actions = { |
||||
onSelect: this.onSelect.bind(this), |
||||
onShowDetail: this.onShowDetail.bind(this), |
||||
} |
||||
return ( |
||||
|
||||
h('.accounts-section.flex-column.flex-grow', [ |
||||
|
||||
// subtitle and nav
|
||||
h('.section-title.flex-column.flex-center', [ |
||||
h('h2.page-subtitle', 'Accounts'), |
||||
]), |
||||
|
||||
// current domain
|
||||
/* AUDIT |
||||
* Temporarily removed |
||||
* since accounts are currently injected |
||||
* regardless of the current domain. |
||||
*/ |
||||
h('.current-domain-panel.flex-center.font-small', [ |
||||
h('spam', 'Selected address is visible to all sites you visit.'), |
||||
// h('span', state.currentDomain),
|
||||
]), |
||||
|
||||
// identity selection
|
||||
h('section.identity-section.flex-column', { |
||||
style: { |
||||
maxHeight: '290px', |
||||
overflowY: 'auto', |
||||
overflowX: 'hidden', |
||||
} |
||||
}, |
||||
identityList.map(renderAccountPanel) |
||||
), |
||||
|
||||
unconfTxList.length ? ( |
||||
|
||||
h('.unconftx-link.flex-row.flex-center', { |
||||
onClick: this.navigateToConfTx.bind(this), |
||||
}, [ |
||||
h('span', 'Unconfirmed Txs'), |
||||
h('i.fa.fa-arrow-right.fa-lg'), |
||||
]) |
||||
|
||||
) : ( |
||||
null |
||||
), |
||||
|
||||
|
||||
]) |
||||
|
||||
) |
||||
|
||||
function renderAccountPanel(identity){ |
||||
var mayBeFauceting = identity.mayBeFauceting |
||||
var isSelected = state.selectedAddress === identity.address |
||||
var account = state.accounts[identity.address] |
||||
var isFauceting = mayBeFauceting && account.balance === '0x0' |
||||
var componentState = extend(actions, { |
||||
identity: identity, |
||||
account: account, |
||||
isSelected: isSelected, |
||||
isFauceting: isFauceting, |
||||
}) |
||||
return h(AccountPanel, componentState) |
||||
} |
||||
} |
||||
|
||||
AccountsScreen.prototype.navigateToConfTx = function(){ |
||||
event.stopPropagation() |
||||
this.props.dispatch(actions.showConfTxPage()) |
||||
} |
||||
|
||||
AccountsScreen.prototype.onSelect = function(address, event){ |
||||
event.stopPropagation() |
||||
// if already selected, deselect
|
||||
if (this.props.selectedAddress === address) address = null |
||||
this.props.dispatch(actions.setSelectedAddress(address)) |
||||
} |
||||
|
||||
AccountsScreen.prototype.onShowDetail = function(address, event){ |
||||
event.stopPropagation() |
||||
this.props.dispatch(actions.showAccountDetail(address)) |
||||
} |
@ -0,0 +1,418 @@ |
||||
var actions = { |
||||
// remote state
|
||||
UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE', |
||||
updateMetamaskState: updateMetamaskState, |
||||
// intialize screen
|
||||
CREATE_NEW_VAULT_IN_PROGRESS: 'CREATE_NEW_VAULT_IN_PROGRESS', |
||||
SHOW_CREATE_VAULT: 'SHOW_CREATE_VAULT', |
||||
SHOW_RESTORE_VAULT: 'SHOW_RESTORE_VAULT', |
||||
SHOW_INIT_MENU: 'SHOW_INIT_MENU', |
||||
SHOW_NEW_VAULT_SEED: 'SHOW_NEW_VAULT_SEED', |
||||
SHOW_INFO_PAGE: 'SHOW_INFO_PAGE', |
||||
RECOVER_FROM_SEED: 'RECOVER_FROM_SEED', |
||||
CLEAR_SEED_WORD_CACHE: 'CLEAR_SEED_WORD_CACHE', |
||||
clearSeedWordCache: clearSeedWordCache, |
||||
recoverFromSeed: recoverFromSeed, |
||||
unlockMetamask: unlockMetamask, |
||||
unlockFailed: unlockFailed, |
||||
showCreateVault: showCreateVault, |
||||
showRestoreVault: showRestoreVault, |
||||
showInitializeMenu: showInitializeMenu, |
||||
createNewVault: createNewVault, |
||||
createNewVaultInProgress: createNewVaultInProgress, |
||||
showNewVaultSeed: showNewVaultSeed, |
||||
showInfoPage: showInfoPage, |
||||
// unlock screen
|
||||
UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS', |
||||
UNLOCK_FAILED: 'UNLOCK_FAILED', |
||||
UNLOCK_METAMASK: 'UNLOCK_METAMASK', |
||||
LOCK_METAMASK: 'LOCK_METAMASK', |
||||
tryUnlockMetamask: tryUnlockMetamask, |
||||
lockMetamask: lockMetamask, |
||||
unlockInProgress: unlockInProgress, |
||||
// error handling
|
||||
displayWarning: displayWarning, |
||||
DISPLAY_WARNING: 'DISPLAY_WARNING', |
||||
HIDE_WARNING: 'HIDE_WARNING', |
||||
hideWarning: hideWarning, |
||||
// accounts screen
|
||||
SET_SELECTED_ACCOUNT: 'SET_SELECTED_ACCOUNT', |
||||
SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL', |
||||
SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE', |
||||
SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE', |
||||
// account detail screen
|
||||
SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', |
||||
showSendPage: showSendPage, |
||||
REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT', |
||||
requestExportAccount: requestExportAccount, |
||||
EXPORT_ACCOUNT: 'EXPORT_ACCOUNT', |
||||
exportAccount: exportAccount, |
||||
SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY', |
||||
showPrivateKey: showPrivateKey, |
||||
// tx conf screen
|
||||
COMPLETED_TX: 'COMPLETED_TX', |
||||
TRANSACTION_ERROR: 'TRANSACTION_ERROR', |
||||
NEXT_TX: 'NEXT_TX', |
||||
PREVIOUS_TX: 'PREV_TX', |
||||
setSelectedAddress: setSelectedAddress, |
||||
signTx: signTx, |
||||
sendTx: sendTx, |
||||
cancelTx: cancelTx, |
||||
completedTx: completedTx, |
||||
txError: txError, |
||||
nextTx: nextTx, |
||||
previousTx: previousTx, |
||||
// app messages
|
||||
showAccountDetail: showAccountDetail, |
||||
BACK_TO_ACCOUNT_DETAIL: 'BACK_TO_ACCOUNT_DETAIL', |
||||
backToAccountDetail: backToAccountDetail, |
||||
showAccountsPage: showAccountsPage, |
||||
showConfTxPage: showConfTxPage, |
||||
confirmSeedWords: confirmSeedWords, |
||||
// config screen
|
||||
SHOW_CONFIG_PAGE: 'SHOW_CONFIG_PAGE', |
||||
SET_RPC_TARGET: 'SET_RPC_TARGET', |
||||
USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER', |
||||
useEtherscanProvider: useEtherscanProvider, |
||||
showConfigPage: showConfigPage, |
||||
setRpcTarget: setRpcTarget, |
||||
// hacky - need a way to get a reference to account manager
|
||||
_setAccountManager: _setAccountManager, |
||||
// loading overlay
|
||||
SHOW_LOADING: 'SHOW_LOADING_INDICATION', |
||||
HIDE_LOADING: 'HIDE_LOADING_INDICATION', |
||||
showLoadingIndication: showLoadingIndication, |
||||
hideLoadingIndication: hideLoadingIndication, |
||||
} |
||||
|
||||
module.exports = actions |
||||
|
||||
|
||||
var _accountManager = null |
||||
function _setAccountManager(accountManager){ |
||||
_accountManager = accountManager |
||||
} |
||||
|
||||
// async actions
|
||||
|
||||
function tryUnlockMetamask(password) { |
||||
return (dispatch) => { |
||||
dispatch(this.unlockInProgress()) |
||||
_accountManager.submitPassword(password, (err) => { |
||||
dispatch(this.hideLoadingIndication()) |
||||
if (err) { |
||||
dispatch(this.unlockFailed()) |
||||
} else { |
||||
dispatch(this.unlockMetamask()) |
||||
dispatch(this.setSelectedAddress()) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
function createNewVault(password, entropy) { |
||||
return (dispatch) => { |
||||
dispatch(this.createNewVaultInProgress()) |
||||
_accountManager.createNewVault(password, entropy, (err, result) => { |
||||
dispatch(this.showNewVaultSeed(result)) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
function recoverFromSeed(password, seed) { |
||||
return (dispatch) => { |
||||
// dispatch(this.createNewVaultInProgress())
|
||||
dispatch(this.showLoadingIndication()) |
||||
_accountManager.recoverFromSeed(password, seed, (err, result) => { |
||||
if (err) { |
||||
dispatch(this.hideLoadingIndication()) |
||||
var message = err.message |
||||
return dispatch(this.displayWarning(err.message)) |
||||
} |
||||
|
||||
dispatch(this.unlockMetamask()) |
||||
dispatch(this.setSelectedAddress()) |
||||
dispatch(this.updateMetamaskState(result)) |
||||
dispatch(this.hideLoadingIndication()) |
||||
dispatch(this.showAccountsPage()) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
function showInfoPage() { |
||||
return { |
||||
type: this.SHOW_INFO_PAGE, |
||||
} |
||||
} |
||||
|
||||
function setSelectedAddress(address) { |
||||
return (dispatch) => { |
||||
_accountManager.setSelectedAddress(address) |
||||
} |
||||
} |
||||
|
||||
function signTx(txData) { |
||||
return (dispatch) => { |
||||
dispatch(this.showLoadingIndication()) |
||||
|
||||
web3.eth.sendTransaction(txData, (err, data) => { |
||||
dispatch(this.hideLoadingIndication()) |
||||
|
||||
if (err) return dispatch(this.displayWarning(err.message)) |
||||
dispatch(this.hideWarning()) |
||||
dispatch(this.showAccountsPage()) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
function sendTx(txData) { |
||||
return (dispatch) => { |
||||
_accountManager.approveTransaction(txData.id, (err) => { |
||||
if (err) { |
||||
alert(err.message) |
||||
dispatch(this.txError(err)) |
||||
return console.error(err.message) |
||||
} |
||||
dispatch(this.completedTx(txData.id)) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
function completedTx(id) { |
||||
return { |
||||
type: this.COMPLETED_TX, |
||||
id, |
||||
} |
||||
} |
||||
|
||||
function txError(err) { |
||||
return { |
||||
type: this.TRANSACTION_ERROR, |
||||
message: err.message, |
||||
} |
||||
} |
||||
|
||||
function cancelTx(txData){ |
||||
return (dispatch) => { |
||||
_accountManager.cancelTransaction(txData.id) |
||||
dispatch(this.showAccountsPage()) |
||||
} |
||||
} |
||||
|
||||
//
|
||||
// initialize screen
|
||||
//
|
||||
|
||||
|
||||
function showCreateVault() { |
||||
return { |
||||
type: this.SHOW_CREATE_VAULT, |
||||
} |
||||
} |
||||
|
||||
function showRestoreVault() { |
||||
return { |
||||
type: this.SHOW_RESTORE_VAULT, |
||||
} |
||||
} |
||||
|
||||
function showInitializeMenu() { |
||||
return { |
||||
type: this.SHOW_INIT_MENU, |
||||
} |
||||
} |
||||
|
||||
function createNewVaultInProgress() { |
||||
return { |
||||
type: this.CREATE_NEW_VAULT_IN_PROGRESS, |
||||
} |
||||
} |
||||
|
||||
function showNewVaultSeed(seed) { |
||||
return { |
||||
type: this.SHOW_NEW_VAULT_SEED, |
||||
value: seed, |
||||
} |
||||
} |
||||
|
||||
//
|
||||
// unlock screen
|
||||
//
|
||||
|
||||
function unlockInProgress() { |
||||
return { |
||||
type: this.UNLOCK_IN_PROGRESS, |
||||
} |
||||
} |
||||
|
||||
function unlockFailed() { |
||||
return { |
||||
type: this.UNLOCK_FAILED, |
||||
} |
||||
} |
||||
|
||||
function unlockMetamask() { |
||||
return { |
||||
type: this.UNLOCK_METAMASK, |
||||
} |
||||
} |
||||
|
||||
function updateMetamaskState(newState) { |
||||
return { |
||||
type: this.UPDATE_METAMASK_STATE, |
||||
value: newState, |
||||
} |
||||
} |
||||
|
||||
function lockMetamask() { |
||||
return (dispatch) => { |
||||
_accountManager.setLocked((err) => { |
||||
dispatch({ |
||||
type: this.LOCK_METAMASK, |
||||
}) |
||||
dispatch(this.hideLoadingIndication()) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
function showAccountDetail(address) { |
||||
return { |
||||
type: this.SHOW_ACCOUNT_DETAIL, |
||||
value: address, |
||||
} |
||||
} |
||||
|
||||
function backToAccountDetail(address) { |
||||
return { |
||||
type: this.BACK_TO_ACCOUNT_DETAIL, |
||||
value: address, |
||||
} |
||||
} |
||||
function clearSeedWordCache() { |
||||
return { |
||||
type: this.CLEAR_SEED_WORD_CACHE |
||||
} |
||||
} |
||||
|
||||
function confirmSeedWords() { |
||||
return (dispatch) => { |
||||
dispatch(this.showLoadingIndication()) |
||||
_accountManager.clearSeedWordCache((err) => { |
||||
dispatch(this.clearSeedWordCache()) |
||||
console.log('Seed word cache cleared.') |
||||
dispatch(this.setSelectedAddress()) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
function showAccountsPage() { |
||||
return { |
||||
type: this.SHOW_ACCOUNTS_PAGE, |
||||
} |
||||
} |
||||
|
||||
function showConfTxPage() { |
||||
return { |
||||
type: this.SHOW_CONF_TX_PAGE, |
||||
} |
||||
} |
||||
|
||||
function nextTx() { |
||||
return { |
||||
type: this.NEXT_TX, |
||||
} |
||||
} |
||||
|
||||
function previousTx() { |
||||
return { |
||||
type: this.PREVIOUS_TX, |
||||
} |
||||
} |
||||
|
||||
function showConfigPage() { |
||||
return { |
||||
type: this.SHOW_CONFIG_PAGE, |
||||
} |
||||
} |
||||
|
||||
//
|
||||
// config
|
||||
//
|
||||
|
||||
function setRpcTarget(newRpc) { |
||||
_accountManager.setRpcTarget(newRpc) |
||||
return { |
||||
type: this.SET_RPC_TARGET, |
||||
value: newRpc, |
||||
} |
||||
} |
||||
|
||||
function useEtherscanProvider() { |
||||
_accountManager.useEtherscanProvider() |
||||
return { |
||||
type: this.USE_ETHERSCAN_PROVIDER, |
||||
} |
||||
} |
||||
|
||||
function showLoadingIndication() { |
||||
return { |
||||
type: this.SHOW_LOADING, |
||||
} |
||||
} |
||||
|
||||
function hideLoadingIndication() { |
||||
return { |
||||
type: this.HIDE_LOADING, |
||||
} |
||||
} |
||||
|
||||
function displayWarning(text) { |
||||
return { |
||||
type: this.DISPLAY_WARNING, |
||||
value: text, |
||||
} |
||||
} |
||||
|
||||
function hideWarning() { |
||||
return { |
||||
type: this.HIDE_WARNING, |
||||
} |
||||
} |
||||
|
||||
function requestExportAccount() { |
||||
return { |
||||
type: this.REQUEST_ACCOUNT_EXPORT, |
||||
} |
||||
} |
||||
|
||||
function exportAccount(address) { |
||||
var self = this |
||||
|
||||
return function(dispatch) { |
||||
dispatch(self.showLoadingIndication()) |
||||
|
||||
_accountManager.exportAccount(address, function(err, result) { |
||||
dispatch(self.hideLoadingIndication()) |
||||
|
||||
if (err) { |
||||
console.error(err) |
||||
return dispatch(self.displayWarning('Had a problem exporting the account.')) |
||||
} |
||||
|
||||
dispatch(self.showPrivateKey(result)) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
function showPrivateKey(key) { |
||||
return { |
||||
type: this.SHOW_PRIVATE_KEY, |
||||
value: key, |
||||
} |
||||
} |
||||
|
||||
function showSendPage() { |
||||
return { |
||||
type: this.SHOW_SEND_PAGE, |
||||
} |
||||
} |
@ -0,0 +1,242 @@ |
||||
const inherits = require('util').inherits |
||||
const React = require('react') |
||||
const Component = require('react').Component |
||||
const PropTypes = require('react').PropTypes |
||||
const connect = require('react-redux').connect |
||||
const h = require('react-hyperscript') |
||||
const extend = require('xtend') |
||||
const actions = require('./actions') |
||||
const ReactCSSTransitionGroup = require('react-addons-css-transition-group') |
||||
// init
|
||||
const InitializeMenuScreen = require('./first-time/init-menu') |
||||
const CreateVaultScreen = require('./first-time/create-vault') |
||||
const CreateVaultCompleteScreen = require('./first-time/create-vault-complete') |
||||
const RestoreVaultScreen = require('./first-time/restore-vault') |
||||
// unlock
|
||||
const UnlockScreen = require('./unlock') |
||||
// accounts
|
||||
const AccountsScreen = require('./accounts') |
||||
const AccountDetailScreen = require('./account-detail') |
||||
const SendTransactionScreen = require('./send') |
||||
const ConfirmTxScreen = require('./conf-tx') |
||||
// other views
|
||||
const ConfigScreen = require('./config') |
||||
const InfoScreen = require('./info') |
||||
const LoadingIndicator = require('./loading') |
||||
|
||||
module.exports = connect(mapStateToProps)(App) |
||||
|
||||
|
||||
inherits(App, Component) |
||||
function App() { Component.call(this) } |
||||
|
||||
function mapStateToProps(state) { |
||||
return { |
||||
// state from plugin
|
||||
isInitialized: state.metamask.isInitialized, |
||||
isUnlocked: state.metamask.isUnlocked, |
||||
currentView: state.appState.currentView, |
||||
activeAddress: state.appState.activeAddress, |
||||
transForward: state.appState.transForward, |
||||
seedWords: state.metamask.seedWords, |
||||
} |
||||
} |
||||
|
||||
App.prototype.render = function() { |
||||
// const { selectedReddit, posts, isFetching, lastUpdated } = this.props
|
||||
var state = this.props |
||||
var view = state.currentView.name |
||||
var transForward = state.transForward |
||||
var shouldHaveFooter = true |
||||
switch (view) { |
||||
case 'restoreVault': |
||||
shouldHaveFooter = false; |
||||
case 'createVault': |
||||
shouldHaveFooter = false; |
||||
case 'createVaultComplete': |
||||
shouldHaveFooter = false; |
||||
} |
||||
|
||||
return ( |
||||
|
||||
h('.flex-column.flex-grow.full-height', { |
||||
style: { |
||||
// Windows was showing a vertical scroll bar:
|
||||
overflowY: 'hidden', |
||||
} |
||||
}, |
||||
[ |
||||
|
||||
h(LoadingIndicator), |
||||
|
||||
// top row
|
||||
h('.app-header.flex-column.flex-center', { |
||||
}, [ |
||||
h('h1', 'MetaMask'), |
||||
]), |
||||
|
||||
// panel content
|
||||
h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), { |
||||
style: { |
||||
height: '380px', |
||||
} |
||||
}, [ |
||||
h(ReactCSSTransitionGroup, { |
||||
transitionName: "main", |
||||
transitionEnterTimeout: 300, |
||||
transitionLeaveTimeout: 300, |
||||
}, [ |
||||
this.renderPrimary(), |
||||
]), |
||||
]), |
||||
|
||||
// footer
|
||||
h('.app-footer.flex-row.flex-space-around', { |
||||
style: { |
||||
display: shouldHaveFooter ? 'flex' : 'none', |
||||
alignItems: 'center', |
||||
height: '56px', |
||||
} |
||||
}, [ |
||||
|
||||
// settings icon
|
||||
h('i.fa.fa-cog.fa-lg' + (view === 'config' ? '.active' : '.cursor-pointer'), { |
||||
style: { |
||||
opacity: state.isUnlocked ? '1.0' : '0.0', |
||||
transition: 'opacity 200ms ease-in', |
||||
//transform: `translateX(${state.isUnlocked ? '0px' : '-100px'})`,
|
||||
}, |
||||
onClick: function(ev) { |
||||
state.dispatch(actions.showConfigPage()) |
||||
}, |
||||
}), |
||||
|
||||
// toggle
|
||||
onOffToggle({ |
||||
toggleMetamaskActive: this.toggleMetamaskActive.bind(this), |
||||
isUnlocked: state.isUnlocked, |
||||
}), |
||||
|
||||
// help
|
||||
h('i.fa.fa-question.fa-lg.cursor-pointer', { |
||||
style: { |
||||
opacity: state.isUnlocked ? '1.0' : '0.0', |
||||
}, |
||||
onClick() { state.dispatch(actions.showInfoPage()) } |
||||
}), |
||||
]), |
||||
]) |
||||
) |
||||
} |
||||
|
||||
App.prototype.toggleMetamaskActive = function(){ |
||||
if (!this.props.isUnlocked) { |
||||
// currently inactive: redirect to password box
|
||||
var passwordBox = document.querySelector('input[type=password]') |
||||
if (!passwordBox) return |
||||
passwordBox.focus() |
||||
} else { |
||||
// currently active: deactivate
|
||||
this.props.dispatch(actions.lockMetamask(false)) |
||||
} |
||||
} |
||||
|
||||
App.prototype.renderPrimary = function(state){ |
||||
var state = this.props |
||||
|
||||
// If seed words haven't been dismissed yet, show them still.
|
||||
/* |
||||
if (state.seedWords) { |
||||
return h(CreateVaultCompleteScreen, {key: 'createVaultComplete'}) |
||||
} |
||||
*/ |
||||
|
||||
// show initialize screen
|
||||
if (!state.isInitialized) { |
||||
|
||||
// show current view
|
||||
switch (state.currentView.name) { |
||||
|
||||
case 'createVault': |
||||
return h(CreateVaultScreen, {key: 'createVault'}) |
||||
|
||||
case 'restoreVault': |
||||
return h(RestoreVaultScreen, {key: 'restoreVault'}) |
||||
|
||||
default: |
||||
return h(InitializeMenuScreen, {key: 'menuScreenInit'}) |
||||
|
||||
} |
||||
} |
||||
|
||||
// show unlock screen
|
||||
if (!state.isUnlocked) { |
||||
return h(UnlockScreen, {key: 'locked'}) |
||||
} |
||||
|
||||
// show current view
|
||||
switch (state.currentView.name) { |
||||
|
||||
case 'createVaultComplete': |
||||
return h(CreateVaultCompleteScreen, {key: 'created-vault'}) |
||||
|
||||
case 'accounts': |
||||
return h(AccountsScreen, {key: 'accounts'}) |
||||
|
||||
case 'accountDetail': |
||||
return h(AccountDetailScreen, {key: 'account-detail'}) |
||||
|
||||
case 'sendTransaction': |
||||
return h(SendTransactionScreen, {key: 'send-transaction'}) |
||||
|
||||
case 'confTx': |
||||
return h(ConfirmTxScreen, {key: 'confirm-tx'}) |
||||
|
||||
case 'config': |
||||
return h(ConfigScreen, {key: 'config'}) |
||||
|
||||
case 'info': |
||||
return h(InfoScreen, {key: 'info'}) |
||||
|
||||
case 'createVault': |
||||
return h(CreateVaultScreen, {key: 'createVault'}) |
||||
|
||||
default: |
||||
return h(AccountsScreen, {key: 'accounts'}) |
||||
} |
||||
} |
||||
|
||||
function onOffToggle(state){ |
||||
var buttonSize = '50px'; |
||||
var lockWidth = '20px'; |
||||
return ( |
||||
h('.app-toggle.flex-row.flex-center.lock' + (state.isUnlocked ? '.unlocked' : '.locked'), { |
||||
width: buttonSize, |
||||
height: buttonSize, |
||||
}, [ |
||||
h('div', { |
||||
onClick: state.toggleMetamaskActive, |
||||
style: { |
||||
width: lockWidth, |
||||
height: '' + parseInt(lockWidth) * 1.5 + 'px', |
||||
position: 'relative', |
||||
} |
||||
}, [ |
||||
h('img.lock-top', { |
||||
src: 'images/lock-top.png', |
||||
style: { |
||||
width: lockWidth, |
||||
position: 'absolute', |
||||
} |
||||
}), |
||||
h('img', { |
||||
src: 'images/lock-base.png', |
||||
style: { |
||||
width: lockWidth, |
||||
position: 'absolute', |
||||
} |
||||
}), |
||||
]) |
||||
]) |
||||
) |
||||
} |
@ -0,0 +1,93 @@ |
||||
const inherits = require('util').inherits |
||||
const ethUtil = require('ethereumjs-util') |
||||
const Component = require('react').Component |
||||
const h = require('react-hyperscript') |
||||
const addressSummary = require('../util').addressSummary |
||||
const formatBalance = require('../util').formatBalance |
||||
|
||||
module.exports = AccountPanel |
||||
|
||||
|
||||
inherits(AccountPanel, Component) |
||||
function AccountPanel() { |
||||
Component.call(this) |
||||
} |
||||
|
||||
AccountPanel.prototype.render = function() { |
||||
var state = this.props |
||||
var identity = state.identity || {} |
||||
var account = state.account || {} |
||||
var isFauceting = state.isFauceting |
||||
|
||||
return ( |
||||
|
||||
h('.identity-panel.flex-row.flex-space-between'+(state.isSelected?'.selected':''), { |
||||
style: { |
||||
flex: '1 0 auto', |
||||
}, |
||||
onClick: state.onSelect && state.onSelect.bind(null, identity.address), |
||||
}, [ |
||||
|
||||
// account identicon
|
||||
h('.identicon-wrapper.flex-column.select-none', [ |
||||
h('.identicon', { |
||||
style: { backgroundImage: 'url("https://ipfs.io/ipfs/'+identity.img+'")' } |
||||
}), |
||||
h('span.font-small', identity.name), |
||||
]), |
||||
|
||||
// account address, balance
|
||||
h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [ |
||||
|
||||
h('.flex-row.flex-space-between', [ |
||||
h('label.font-small', 'ADDRESS'), |
||||
h('span.font-small', addressSummary(identity.address)), |
||||
]), |
||||
|
||||
balanceOrFaucetingIndication(account, isFauceting), |
||||
|
||||
// outlet for inserting additional stuff
|
||||
state.children, |
||||
|
||||
]), |
||||
|
||||
// navigate to account detail
|
||||
!state.onShowDetail ? null : |
||||
h('.arrow-right.cursor-pointer', { |
||||
onClick: state.onShowDetail && state.onShowDetail.bind(null, identity.address), |
||||
}, [ |
||||
h('i.fa.fa-chevron-right.fa-lg'), |
||||
]), |
||||
]) |
||||
) |
||||
} |
||||
|
||||
function balanceOrFaucetingIndication(account, isFauceting) { |
||||
|
||||
// Temporarily deactivating isFauceting indication
|
||||
// because it shows fauceting for empty restored accounts.
|
||||
if (/*isFauceting*/ false) { |
||||
|
||||
return h('.flex-row.flex-space-between', [ |
||||
h('span.font-small', { |
||||
}, [ |
||||
'Account is auto-funding,', |
||||
h('br'), |
||||
'please wait.' |
||||
]), |
||||
]) |
||||
|
||||
} else { |
||||
|
||||
return h('.flex-row.flex-space-between', [ |
||||
h('label.font-small', 'BALANCE'), |
||||
h('span.font-small', { |
||||
style: { |
||||
overflowX: 'hidden', |
||||
maxWidth: '136px', |
||||
} |
||||
}, formatBalance(account.balance)), |
||||
]) |
||||
|
||||
} |
||||
} |
@ -0,0 +1,65 @@ |
||||
const inherits = require('util').inherits |
||||
const Component = require('react').Component |
||||
const h = require('react-hyperscript') |
||||
const metamaskLogo = require('metamask-logo') |
||||
const getCaretCoordinates = require('textarea-caret') |
||||
const debounce = require('debounce') |
||||
|
||||
module.exports = Mascot |
||||
|
||||
|
||||
inherits(Mascot, Component) |
||||
function Mascot() { |
||||
Component.call(this) |
||||
this.logo = metamaskLogo({ |
||||
followMouse: true, |
||||
pxNotRatio: true, |
||||
width: 200, |
||||
height: 200, |
||||
}) |
||||
if (!this.logo) return |
||||
this.refollowMouse = debounce(this.logo.setFollowMouse.bind(this.logo, true), 1000) |
||||
this.unfollowMouse = this.logo.setFollowMouse.bind(this.logo, false) |
||||
} |
||||
|
||||
|
||||
Mascot.prototype.render = function() { |
||||
// this is a bit hacky
|
||||
// the event emitter is on `this.props`
|
||||
// and we dont get that until render
|
||||
this.handleAnimationEvents() |
||||
|
||||
return ( |
||||
|
||||
h('#metamask-mascot-container') |
||||
|
||||
) |
||||
} |
||||
|
||||
Mascot.prototype.componentDidMount = function() { |
||||
if (!this.logo) return |
||||
var targetDivId = 'metamask-mascot-container' |
||||
var container = document.getElementById(targetDivId) |
||||
container.appendChild(this.logo.canvas) |
||||
} |
||||
|
||||
Mascot.prototype.componentWillUnmount = function() { |
||||
if (!this.logo) return |
||||
this.logo.canvas.remove() |
||||
} |
||||
|
||||
Mascot.prototype.handleAnimationEvents = function(){ |
||||
if (!this.logo) return |
||||
// only setup listeners once
|
||||
if (this.animations) return |
||||
this.animations = this.props.animationEventEmitter |
||||
this.animations.on('point', this.lookAt.bind(this)) |
||||
this.animations.on('setFollowMouse', this.logo.setFollowMouse.bind(this.logo)) |
||||
} |
||||
|
||||
Mascot.prototype.lookAt = function(target){ |
||||
if (!this.logo) return |
||||
this.unfollowMouse() |
||||
this.logo.lookAt(target) |
||||
this.refollowMouse() |
||||
} |
@ -0,0 +1,140 @@ |
||||
const inherits = require('util').inherits |
||||
const Component = require('react').Component |
||||
const ReactCSSTransitionGroup = require('react-addons-css-transition-group') |
||||
const h = require('react-hyperscript') |
||||
const connect = require('react-redux').connect |
||||
const copyToClipboard = require('copy-to-clipboard') |
||||
const actions = require('./actions') |
||||
const AccountPanel = require('./components/account-panel') |
||||
const valuesFor = require('./util').valuesFor |
||||
const addressSummary = require('./util').addressSummary |
||||
const readableDate = require('./util').readableDate |
||||
const formatBalance = require('./util').formatBalance |
||||
const dataSize = require('./util').dataSize |
||||
|
||||
module.exports = connect(mapStateToProps)(ConfirmTxScreen) |
||||
|
||||
function mapStateToProps(state) { |
||||
return { |
||||
identities: state.metamask.identities, |
||||
accounts: state.metamask.accounts, |
||||
selectedAddress: state.metamask.selectedAddress, |
||||
unconfTxs: state.metamask.unconfTxs, |
||||
index: state.appState.currentView.context, |
||||
} |
||||
} |
||||
|
||||
inherits(ConfirmTxScreen, Component) |
||||
function ConfirmTxScreen() { |
||||
Component.call(this) |
||||
} |
||||
|
||||
|
||||
ConfirmTxScreen.prototype.render = function() { |
||||
var state = this.props |
||||
var unconfTxList = valuesFor(state.unconfTxs).sort(tx => tx.time) |
||||
var txData = unconfTxList[state.index] || {} |
||||
var txParams = txData.txParams || {} |
||||
var address = txParams.from || state.selectedAddress |
||||
var identity = state.identities[address] || { address: address } |
||||
var account = state.accounts[address] || { address: address } |
||||
|
||||
return ( |
||||
|
||||
h('.unconftx-section.flex-column.flex-grow', [ |
||||
|
||||
// subtitle and nav
|
||||
h('.section-title.flex-row.flex-center', [ |
||||
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { |
||||
onClick: this.navigateToAccounts.bind(this), |
||||
}), |
||||
h('h2.page-subtitle', 'Confirm Transaction'), |
||||
]), |
||||
|
||||
h('h3', { |
||||
style: { |
||||
alignSelf: 'center', |
||||
display: unconfTxList.length > 1 ? 'block' : 'none', |
||||
}, |
||||
}, [ |
||||
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { |
||||
style: { |
||||
display: state.index === 0 ? 'none' : 'inline-block', |
||||
}, |
||||
onClick: () => state.dispatch(actions.previousTx()), |
||||
}), |
||||
` Transaction ${state.index + 1} of ${unconfTxList.length} `, |
||||
h('i.fa.fa-arrow-right.fa-lg.cursor-pointer', { |
||||
style: { |
||||
display: state.index + 1 === unconfTxList.length ? 'none' : 'inline-block', |
||||
}, |
||||
onClick: () => state.dispatch(actions.nextTx()), |
||||
}), |
||||
]), |
||||
|
||||
h(ReactCSSTransitionGroup, { |
||||
transitionName: "main", |
||||
transitionEnterTimeout: 300, |
||||
transitionLeaveTimeout: 300, |
||||
}, [ |
||||
|
||||
h('.transaction', { |
||||
key: txData.id, |
||||
}, [ |
||||
|
||||
// account that will sign
|
||||
h(AccountPanel, { |
||||
showFullAddress: true, |
||||
identity: identity, |
||||
account: account, |
||||
}), |
||||
|
||||
// tx data
|
||||
h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [ |
||||
|
||||
h('.flex-row.flex-space-between', [ |
||||
h('label.font-small', 'TO ADDRESS'), |
||||
h('span.font-small', addressSummary(txParams.to)), |
||||
]), |
||||
|
||||
h('.flex-row.flex-space-between', [ |
||||
h('label.font-small', 'DATE'), |
||||
h('span.font-small', readableDate(txData.time)), |
||||
]), |
||||
|
||||
h('.flex-row.flex-space-between', [ |
||||
h('label.font-small', 'AMOUNT'), |
||||
h('span.font-small', formatBalance(txParams.value)), |
||||
]), |
||||
|
||||
]), |
||||
|
||||
// send + cancel
|
||||
h('.flex-row.flex-space-around', [ |
||||
h('button', { |
||||
onClick: this.cancelTransaction.bind(this, txData), |
||||
}, 'Cancel'), |
||||
h('button', { |
||||
onClick: this.sendTransaction.bind(this, txData), |
||||
}, 'Send'), |
||||
]), |
||||
]), |
||||
]), |
||||
]) // No comma or semicolon can go here
|
||||
) |
||||
} |
||||
|
||||
ConfirmTxScreen.prototype.sendTransaction = function(txData, event){ |
||||
event.stopPropagation() |
||||
this.props.dispatch(actions.sendTx(txData)) |
||||
} |
||||
|
||||
ConfirmTxScreen.prototype.cancelTransaction = function(txData, event){ |
||||
event.stopPropagation() |
||||
this.props.dispatch(actions.cancelTx(txData)) |
||||
} |
||||
|
||||
ConfirmTxScreen.prototype.navigateToAccounts = function(event){ |
||||
event.stopPropagation() |
||||
this.props.dispatch(actions.showAccountsPage()) |
||||
} |
@ -0,0 +1,103 @@ |
||||
const inherits = require('util').inherits |
||||
const Component = require('react').Component |
||||
const h = require('react-hyperscript') |
||||
const connect = require('react-redux').connect |
||||
const actions = require('./actions') |
||||
|
||||
module.exports = connect(mapStateToProps)(ConfigScreen) |
||||
|
||||
function mapStateToProps(state) { |
||||
return { |
||||
rpc: state.metamask.rpcTarget, |
||||
metamask: state.metamask, |
||||
} |
||||
} |
||||
|
||||
inherits(ConfigScreen, Component) |
||||
function ConfigScreen() { |
||||
Component.call(this) |
||||
} |
||||
|
||||
|
||||
ConfigScreen.prototype.render = function() { |
||||
var state = this.props |
||||
var rpc = state.rpc |
||||
var metamaskState = state.metamask |
||||
|
||||
return ( |
||||
h('.flex-column.flex-grow', [ |
||||
|
||||
// subtitle and nav
|
||||
h('.section-title.flex-row.flex-center', [ |
||||
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { |
||||
onClick: (event) => { |
||||
state.dispatch(actions.showAccountsPage()) |
||||
} |
||||
}), |
||||
h('h2.page-subtitle', 'Configuration'), |
||||
]), |
||||
|
||||
// conf view
|
||||
h('.flex-column.flex-justify-center.flex-grow.select-none', [ |
||||
h('.flex-space-around', { |
||||
style: { |
||||
padding: '20px', |
||||
} |
||||
}, [ |
||||
|
||||
currentProviderDisplay(metamaskState), |
||||
|
||||
|
||||
h('div', [ |
||||
h('input', { |
||||
placeholder: 'New RPC URL', |
||||
style: { |
||||
width: '100%', |
||||
}, |
||||
onKeyPress(event) { |
||||
if (event.key === 'Enter') { |
||||
var element = event.target |
||||
var newRpc = element.value |
||||
state.dispatch(actions.setRpcTarget(newRpc)) |
||||
} |
||||
} |
||||
}), |
||||
]), |
||||
|
||||
h('div', [ |
||||
h('button', { |
||||
style: { |
||||
alignSelf: 'center', |
||||
}, |
||||
onClick(event) { |
||||
event.preventDefault() |
||||
state.dispatch(actions.setRpcTarget('https://rpc.metamask.io/')) |
||||
} |
||||
}, 'Use Main Network') |
||||
]), |
||||
|
||||
h('div', [ |
||||
h('button', { |
||||
style: { |
||||
alignSelf: 'center', |
||||
}, |
||||
onClick(event) { |
||||
event.preventDefault() |
||||
state.dispatch(actions.setRpcTarget('https://testrpc.metamask.io/')) |
||||
} |
||||
}, 'Use Morden Test Network') |
||||
]), |
||||
|
||||
]), |
||||
]), |
||||
]) |
||||
) |
||||
} |
||||
|
||||
function currentProviderDisplay(metamaskState) { |
||||
var rpc = metamaskState.rpcTarget |
||||
return h('div', [ |
||||
h('h3', {style: { fontWeight: 'bold' }}, 'Currently using RPC'), |
||||
h('p', rpc) |
||||
]) |
||||
} |
@ -0,0 +1,21 @@ |
||||
/* |
||||
debug / dev |
||||
*/ |
||||
|
||||
#app-content { |
||||
border: 2px solid green; |
||||
} |
||||
|
||||
#design-container { |
||||
position: absolute; |
||||
left: 360px; |
||||
top: -42px; |
||||
width: calc(100vw - 360px); |
||||
height: 100vh; |
||||
overflow: scroll; |
||||
} |
||||
|
||||
#design-container img { |
||||
width: 2000px; |
||||
margin-right: 600px; |
||||
} |
@ -0,0 +1,2 @@ |
||||
@import url(https://fonts.googleapis.com/css?family=Roboto:300,500); |
||||
@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css); |
@ -0,0 +1,489 @@ |
||||
/* |
||||
faint orange (textfield shades) #FAF6F0 |
||||
light orange (button shades): #F5C26D |
||||
dark orange (text): #F5A623 |
||||
borders/font/any gray: #4A4A4A |
||||
*/ |
||||
|
||||
/* |
||||
application specific styles |
||||
*/ |
||||
|
||||
* { |
||||
box-sizing: border-box; |
||||
} |
||||
|
||||
html, body { |
||||
/*font-family: 'Open Sans', Arial, sans-serif;*/ |
||||
font-family: 'Roboto', 'Noto', sans-serif; |
||||
color: #4D4D4D; |
||||
font-weight: 300; |
||||
line-height: 1.4em; |
||||
} |
||||
|
||||
#app-content { |
||||
overflow-x: hidden; |
||||
min-width: 357px; |
||||
width: 360px; |
||||
height: 500px; |
||||
} |
||||
|
||||
button { |
||||
outline: none; |
||||
cursor: pointer; |
||||
margin: 10px; |
||||
padding: 6px; |
||||
border: none; |
||||
border-radius: 3px; |
||||
background: #F7861C; |
||||
font-weight: 500; |
||||
color: white; |
||||
transform-origin: center center; |
||||
transition: transform 50ms ease-in; |
||||
} |
||||
button:hover { |
||||
transform: scale(1.1); |
||||
} |
||||
button:active { |
||||
transform: scale(0.95); |
||||
} |
||||
|
||||
button.primary { |
||||
margin: 10px; |
||||
padding: 6px; |
||||
border: none; |
||||
border-radius: 3px; |
||||
background: #F7861C; |
||||
font-weight: 500; |
||||
color: white; |
||||
} |
||||
|
||||
input, textarea { |
||||
width: 300px; |
||||
padding: 6px; |
||||
border-radius: 6px; |
||||
border-style: solid; |
||||
outline: none; |
||||
border: 1px solid #F5A623; |
||||
background: #FAF6F0; |
||||
} |
||||
|
||||
a { |
||||
text-decoration: none; |
||||
color: inherit; |
||||
} |
||||
|
||||
a:hover{ |
||||
color: #df6b0e; |
||||
} |
||||
|
||||
/* |
||||
app |
||||
*/ |
||||
|
||||
.active { |
||||
color: #909090; |
||||
} |
||||
|
||||
button.btn-thin { |
||||
border: 1px solid; |
||||
border-color: #4D4D4D; |
||||
color: #4D4D4D; |
||||
background: rgb(255, 174, 41); |
||||
border-radius: 4px; |
||||
min-width: 200px; |
||||
margin: 12px 0; |
||||
padding: 6px; |
||||
font-size: 13px; |
||||
} |
||||
|
||||
.app-header { |
||||
padding-top: 20px; |
||||
} |
||||
|
||||
.app-header h1 { |
||||
font-size: 2em; |
||||
font-weight: 300; |
||||
height: 42px; |
||||
} |
||||
|
||||
h2.page-subtitle { |
||||
font-size: 1em; |
||||
font-weight: 500; |
||||
height: 24px; |
||||
color: #F3C83E; |
||||
} |
||||
|
||||
.app-primary { |
||||
} |
||||
|
||||
.app-footer { |
||||
padding-bottom: 10px; |
||||
align-items: center; |
||||
} |
||||
|
||||
.identicon { |
||||
height: 46px; |
||||
width: 46px; |
||||
background-size: cover; |
||||
border-radius: 100%; |
||||
border: 3px solid gray; |
||||
} |
||||
|
||||
textarea.twelve-word-phrase { |
||||
margin-top: 20px; |
||||
width: 300px; |
||||
height: 180px; |
||||
font-size: 16px; |
||||
background: #FAF6F0; |
||||
resize: none; |
||||
} |
||||
|
||||
/* |
||||
app sections |
||||
*/ |
||||
|
||||
/* initialize */ |
||||
|
||||
.initialize-screen hr { |
||||
width: 60px; |
||||
margin: 12px; |
||||
border-color: #F3C83E; |
||||
border-style: solid; |
||||
} |
||||
|
||||
.initialize-screen input[type="password"], .initialize-screen textarea { |
||||
width: 300px; |
||||
padding: 6px; |
||||
border-radius: 6px; |
||||
border-style: solid; |
||||
outline: none; |
||||
border: 1px solid #F5A623; |
||||
background: #FAF6F0; |
||||
} |
||||
|
||||
.initialize-screen label { |
||||
margin-top: 20px; |
||||
} |
||||
|
||||
.initialize-screen button.create-vault { |
||||
margin-top: 40px; |
||||
} |
||||
|
||||
.initialize-screen .warning { |
||||
font-size: 14px; |
||||
margin: 0 16px; |
||||
} |
||||
|
||||
/* unlock */ |
||||
.error { |
||||
color: #E20202; |
||||
} |
||||
.lock { |
||||
width: 50px; |
||||
height: 50px; |
||||
} |
||||
|
||||
.lock.locked { |
||||
transform: scale(1.5); |
||||
opacity: 0.0; |
||||
transition: opacity 400ms ease-in, transform 400ms ease-in; |
||||
} |
||||
.lock.unlocked { |
||||
transform: scale(1); |
||||
opacity: 1; |
||||
transition: opacity 500ms ease-out, transform 500ms ease-out, background 200ms ease-in; |
||||
} |
||||
|
||||
.lock.locked .lock-top { |
||||
transform: scaleX(1) translateX(0); |
||||
transition: transform 250ms ease-in; |
||||
} |
||||
.lock.unlocked .lock-top { |
||||
transform: scaleX(-1) translateX(-12px); |
||||
transition: transform 250ms ease-in; |
||||
} |
||||
.lock.unlocked:hover { |
||||
border-radius: 4px; |
||||
background: #e5e5e5; |
||||
border: 1px solid #b1b1b1; |
||||
} |
||||
.lock.unlocked:active { |
||||
background: #c3c3c3; |
||||
} |
||||
|
||||
.section-title .fa-arrow-left { |
||||
margin: -2px 8px 0px -8px; |
||||
} |
||||
|
||||
.unlock-screen label { |
||||
color: #F3C83E; |
||||
font-weight: 500; |
||||
} |
||||
|
||||
.unlock-screen input[type=password] { |
||||
width: 60%; |
||||
height: 22px; |
||||
padding: 2px; |
||||
border-radius: 4px; |
||||
border: 2px solid #F3C83E; |
||||
background: #FAF6F0; |
||||
} |
||||
|
||||
.unlock-screen input[type=password]:focus { |
||||
outline: none; |
||||
border: 3px solid #F3C83E; |
||||
} |
||||
|
||||
/* accounts */ |
||||
|
||||
.accounts-section { |
||||
margin: 0 20px; |
||||
} |
||||
|
||||
.current-domain-panel { |
||||
border: 1px solid #B7B7B7; |
||||
} |
||||
|
||||
.unconftx-link { |
||||
margin-top: 24px; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
.unconftx-link .fa-arrow-right { |
||||
margin: 0px -8px 0px 8px; |
||||
} |
||||
|
||||
/* identity panel */ |
||||
|
||||
.identity-panel { |
||||
font-weight: 500; |
||||
} |
||||
|
||||
.identity-panel .identicon-wrapper { |
||||
margin: 4px; |
||||
margin-top: 8px; |
||||
} |
||||
|
||||
.identity-panel .identicon-wrapper span { |
||||
margin: 0 auto; |
||||
} |
||||
|
||||
.identity-panel .identity-data { |
||||
margin: 8px 8px 8px 18px; |
||||
} |
||||
|
||||
.identity-panel i { |
||||
margin-top: 32px; |
||||
margin-right: 6px; |
||||
color: #B9B9B9; |
||||
} |
||||
|
||||
.identity-panel .arrow-right { |
||||
padding-left: 18px; |
||||
width: 42px; |
||||
min-width: 18px; |
||||
height: 100%; |
||||
} |
||||
|
||||
/* accounts screen */ |
||||
|
||||
.identity-section { |
||||
border: 2px solid #4D4D4D; |
||||
margin: 0; |
||||
} |
||||
|
||||
.identity-section .identity-panel { |
||||
background: #E9E9E9; |
||||
border-bottom: 1px solid #B1B1B1; |
||||
cursor: pointer; |
||||
} |
||||
.identity-section .identity-panel:hover { |
||||
background: #F9F9F9; |
||||
} |
||||
|
||||
.identity-section .identity-panel.selected { |
||||
background: white; |
||||
color: #F3C83E; |
||||
} |
||||
|
||||
.identity-section .identity-panel.selected .identicon { |
||||
border-color: orange; |
||||
} |
||||
|
||||
/* account detail screen */ |
||||
|
||||
.account-detail-section { |
||||
margin: 0 20px; |
||||
} |
||||
|
||||
/* tx confirm */ |
||||
|
||||
.unconftx-section { |
||||
margin: 0 20px; |
||||
} |
||||
|
||||
.unconftx-section input[type=password] { |
||||
height: 22px; |
||||
padding: 2px; |
||||
margin: 12px; |
||||
margin-bottom: 24px; |
||||
border-radius: 4px; |
||||
border: 2px solid #F3C83E; |
||||
background: #FAF6F0; |
||||
} |
||||
|
||||
|
||||
/* |
||||
react toggle |
||||
*/ |
||||
|
||||
/* overrides */ |
||||
|
||||
.react-toggle-track-check { |
||||
display: none; |
||||
} |
||||
.react-toggle-track-x { |
||||
display: none; |
||||
} |
||||
|
||||
/* modified original */ |
||||
|
||||
.react-toggle { |
||||
display: inline-block; |
||||
position: relative; |
||||
cursor: pointer; |
||||
background-color: transparent; |
||||
border: 0; |
||||
padding: 0; |
||||
|
||||
-webkit-touch-callout: none; |
||||
-webkit-user-select: none; |
||||
-khtml-user-select: none; |
||||
-moz-user-select: none; |
||||
-ms-user-select: none; |
||||
user-select: none; |
||||
|
||||
-webkit-tap-highlight-color: rgba(0,0,0,0); |
||||
-webkit-tap-highlight-color: transparent; |
||||
} |
||||
|
||||
.react-toggle-screenreader-only { |
||||
border: 0; |
||||
clip: rect(0 0 0 0); |
||||
height: 1px; |
||||
margin: -1px; |
||||
overflow: hidden; |
||||
padding: 0; |
||||
position: absolute; |
||||
width: 1px; |
||||
} |
||||
|
||||
.react-toggle--disabled { |
||||
opacity: 0.5; |
||||
-webkit-transition: opacity 0.25s; |
||||
transition: opacity 0.25s; |
||||
} |
||||
|
||||
.react-toggle-track { |
||||
width: 50px; |
||||
height: 24px; |
||||
padding: 0; |
||||
border-radius: 30px; |
||||
background-color: #4D4D4D; |
||||
-webkit-transition: all 0.2s ease; |
||||
-moz-transition: all 0.2s ease; |
||||
transition: all 0.2s ease; |
||||
} |
||||
|
||||
.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track { |
||||
background-color: #000000; |
||||
} |
||||
|
||||
.react-toggle--checked .react-toggle-track { |
||||
background-color: rgb(255, 174, 41); |
||||
} |
||||
|
||||
.react-toggle.react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track { |
||||
background-color: rgb(243, 151, 0); |
||||
} |
||||
|
||||
.react-toggle-track-check { |
||||
position: absolute; |
||||
width: 14px; |
||||
height: 10px; |
||||
top: 0px; |
||||
bottom: 0px; |
||||
margin-top: auto; |
||||
margin-bottom: auto; |
||||
line-height: 0; |
||||
left: 8px; |
||||
opacity: 0; |
||||
-webkit-transition: opacity 0.25s ease; |
||||
-moz-transition: opacity 0.25s ease; |
||||
transition: opacity 0.25s ease; |
||||
} |
||||
|
||||
.react-toggle--checked .react-toggle-track-check { |
||||
opacity: 1; |
||||
-webkit-transition: opacity 0.25s ease; |
||||
-moz-transition: opacity 0.25s ease; |
||||
transition: opacity 0.25s ease; |
||||
} |
||||
|
||||
.react-toggle-track-x { |
||||
position: absolute; |
||||
width: 10px; |
||||
height: 10px; |
||||
top: 0px; |
||||
bottom: 0px; |
||||
margin-top: auto; |
||||
margin-bottom: auto; |
||||
line-height: 0; |
||||
right: 10px; |
||||
opacity: 1; |
||||
-webkit-transition: opacity 0.25s ease; |
||||
-moz-transition: opacity 0.25s ease; |
||||
transition: opacity 0.25s ease; |
||||
} |
||||
|
||||
.react-toggle--checked .react-toggle-track-x { |
||||
opacity: 0; |
||||
} |
||||
|
||||
.react-toggle-thumb { |
||||
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms; |
||||
position: absolute; |
||||
top: 1px; |
||||
left: 1px; |
||||
width: 22px; |
||||
height: 22px; |
||||
border: 1px solid #4D4D4D; |
||||
border-radius: 50%; |
||||
background-color: #FAFAFA; |
||||
|
||||
-webkit-box-sizing: border-box; |
||||
-moz-box-sizing: border-box; |
||||
box-sizing: border-box; |
||||
|
||||
-webkit-transition: all 0.25s ease; |
||||
-moz-transition: all 0.25s ease; |
||||
transition: all 0.25s ease; |
||||
} |
||||
|
||||
.react-toggle--checked .react-toggle-thumb { |
||||
left: 27px; |
||||
border-color: #828282; |
||||
} |
||||
/* |
||||
.react-toggle--focus .react-toggle-thumb { |
||||
-webkit-box-shadow: 0px 0px 3px 2px #0099E0; |
||||
-moz-box-shadow: 0px 0px 3px 2px #0099E0; |
||||
box-shadow: 0px 0px 2px 3px #0099E0; |
||||
} |
||||
|
||||
.react-toggle:active .react-toggle-thumb { |
||||
-webkit-box-shadow: 0px 0px 5px 5px #0099E0; |
||||
-moz-box-shadow: 0px 0px 5px 5px #0099E0; |
||||
box-shadow: 0px 0px 5px 5px #0099E0; |
||||
} |
@ -0,0 +1,143 @@ |
||||
/* lib */ |
||||
|
||||
.full-width { |
||||
width: 100%; |
||||
} |
||||
|
||||
.full-height { |
||||
height: 100%; |
||||
} |
||||
|
||||
.flex-column { |
||||
display: flex; |
||||
flex-direction: column; |
||||
} |
||||
|
||||
.flex-column-bottom { |
||||
display: flex; |
||||
flex-direction: column-reverse; |
||||
} |
||||
|
||||
.flex-row { |
||||
display: flex; |
||||
flex-direction: row; |
||||
} |
||||
|
||||
.flex-space-between { |
||||
justify-content: space-between; |
||||
} |
||||
|
||||
.flex-space-around { |
||||
justify-content: space-around; |
||||
} |
||||
|
||||
.flex-right { |
||||
display: flex; |
||||
flex-direction: row; |
||||
justify-content: flex-end; |
||||
} |
||||
|
||||
.flex-left { |
||||
display: flex; |
||||
flex-direction: row; |
||||
justify-content: flex-start; |
||||
} |
||||
|
||||
.flex-fixed { |
||||
flex: none; |
||||
} |
||||
|
||||
.flex-grow { |
||||
flex: 1 1 auto; |
||||
} |
||||
|
||||
.flex-wrap { |
||||
flex-wrap: wrap; |
||||
} |
||||
|
||||
.flex-center { |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
} |
||||
|
||||
.flex-justify-center { |
||||
justify-content: center; |
||||
} |
||||
|
||||
.flex-align-center { |
||||
align-items: center; |
||||
} |
||||
|
||||
.flex-self-end { |
||||
align-self: flex-end; |
||||
} |
||||
|
||||
.flex-self-stretch { |
||||
align-self: stretch; |
||||
} |
||||
|
||||
.flex-vertical { |
||||
flex-direction: column; |
||||
} |
||||
|
||||
.z-bump { |
||||
z-index: 1; |
||||
} |
||||
|
||||
.select-none { |
||||
cursor: default; |
||||
-moz-user-select: none; |
||||
-webkit-user-select: none; |
||||
-ms-user-select: none; |
||||
user-select: none; |
||||
} |
||||
|
||||
.cursor-pointer { |
||||
cursor: pointer; |
||||
transform-origin: center center; |
||||
transition: transform 50ms ease-in-out; |
||||
} |
||||
.cursor-pointer:hover { |
||||
transform: scale(1.1); |
||||
} |
||||
.cursor-pointer:active { |
||||
transform: scale(0.95); |
||||
} |
||||
|
||||
.margin-bottom-sml { |
||||
margin-bottom: 20px; |
||||
} |
||||
|
||||
.margin-bottom-med { |
||||
margin-bottom: 40px; |
||||
} |
||||
|
||||
.margin-right-left { |
||||
margin: 0 20px; |
||||
} |
||||
|
||||
.bold { |
||||
font-weight: bold; |
||||
} |
||||
|
||||
.font-small { |
||||
font-size: 12px; |
||||
} |
||||
|
||||
/* Send Screen */ |
||||
.send-screen { |
||||
margin: 0 20px; |
||||
} |
||||
.send-screen section { |
||||
margin: 7px; |
||||
display: flex; |
||||
flex-direction: row; |
||||
justify-content: center; |
||||
} |
||||
.send-screen details { |
||||
width: 100%; |
||||
} |
||||
.send-screen section input { |
||||
width: 100%; |
||||
} |
@ -0,0 +1,48 @@ |
||||
/* http://meyerweb.com/eric/tools/css/reset/ |
||||
v2.0 | 20110126 |
||||
License: none (public domain) |
||||
*/ |
||||
|
||||
html, body, div, span, applet, object, iframe, |
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre, |
||||
a, abbr, acronym, address, big, cite, code, |
||||
del, dfn, em, img, ins, kbd, q, s, samp, |
||||
small, strike, strong, sub, sup, tt, var, |
||||
b, u, i, center, |
||||
dl, dt, dd, ol, ul, li, |
||||
fieldset, form, label, legend, |
||||
table, caption, tbody, tfoot, thead, tr, th, td, |
||||
article, aside, canvas, details, embed, |
||||
figure, figcaption, footer, header, hgroup, |
||||
menu, nav, output, ruby, section, summary, |
||||
time, mark, audio, video { |
||||
margin: 0; |
||||
padding: 0; |
||||
border: 0; |
||||
font-size: 100%; |
||||
font: inherit; |
||||
vertical-align: baseline; |
||||
} |
||||
/* HTML5 display-role reset for older browsers */ |
||||
article, aside, details, figcaption, figure, |
||||
footer, header, hgroup, menu, nav, section { |
||||
display: block; |
||||
} |
||||
body { |
||||
line-height: 1; |
||||
} |
||||
ol, ul { |
||||
list-style: none; |
||||
} |
||||
blockquote, q { |
||||
quotes: none; |
||||
} |
||||
blockquote:before, blockquote:after, |
||||
q:before, q:after { |
||||
content: ''; |
||||
content: none; |
||||
} |
||||
table { |
||||
border-collapse: collapse; |
||||
border-spacing: 0; |
||||
} |
@ -0,0 +1,47 @@ |
||||
/* initial positions */ |
||||
.app-primary.from-right .main-enter { |
||||
transform: translateX(400px); |
||||
position: absolute; |
||||
width: 100%; |
||||
transition: transform 300ms ease-in-out; |
||||
} |
||||
.app-primary.from-left .main-enter { |
||||
transform: translateX(-400px); |
||||
position: absolute; |
||||
width: 100%; |
||||
transition: transform 300ms ease-in-out; |
||||
} |
||||
|
||||
/* center position */ |
||||
.app-primary .main-enter.main-enter-active, |
||||
.app-primary .main-leave { |
||||
transform: translateX(0px); |
||||
position: absolute; |
||||
width: 100%; |
||||
transition: transform 300ms ease-in-out; |
||||
} |
||||
|
||||
/* final positions */ |
||||
.app-primary.from-left .main-leave-active { |
||||
transform: translateX(400px); |
||||
position: absolute; |
||||
width: 100%; |
||||
transition: transform 300ms ease-in-out; |
||||
} |
||||
.app-primary.from-right .main-leave-active { |
||||
transform: translateX(-400px); |
||||
position: absolute; |
||||
width: 100%; |
||||
transition: transform 300ms ease-in-out; |
||||
} |
||||
|
||||
/* loader transitions */ |
||||
.loader-enter, .loader-leave-active { |
||||
opacity: 0.0; |
||||
transition: opacity 150 ease-in-out; |
||||
} |
||||
.loader-enter-active, .loader-leave { |
||||
opacity: 1.0; |
||||
transition: opacity 150 ease-in-out; |
||||
} |
||||
|
@ -0,0 +1,57 @@ |
||||
const inherits = require('util').inherits |
||||
const Component = require('react').Component |
||||
const connect = require('react-redux').connect |
||||
const h = require('react-hyperscript') |
||||
const actions = require('../actions') |
||||
|
||||
module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen) |
||||
|
||||
|
||||
inherits(CreateVaultCompleteScreen, Component) |
||||
function CreateVaultCompleteScreen() { |
||||
Component.call(this) |
||||
} |
||||
|
||||
function mapStateToProps(state) { |
||||
return { |
||||
seed: state.appState.currentView.context, |
||||
cachedSeed: state.metamask.seedWords, |
||||
} |
||||
} |
||||
|
||||
CreateVaultCompleteScreen.prototype.render = function() { |
||||
var state = this.props |
||||
var seed = state.seed || state.cachedSeed |
||||
|
||||
return ( |
||||
|
||||
h('.initialize-screen.flex-column.flex-center.flex-grow', [ |
||||
|
||||
// subtitle and nav
|
||||
h('.section-title.flex-row.flex-center', [ |
||||
h('h2.page-subtitle', 'Vault Created'), |
||||
]), |
||||
|
||||
h('span.error', { // Error for the right red
|
||||
style: { |
||||
padding: '12px 20px 0px 20px', |
||||
textAlign: 'center', |
||||
} |
||||
}, 'These 12 words can restore all of your MetaMask accounts for this vault.\nSave them somewhere safe and secret.'), |
||||
|
||||
h('textarea.twelve-word-phrase', { |
||||
readOnly: true, |
||||
value: seed, |
||||
}), |
||||
|
||||
h('button.btn-thin', { |
||||
onClick: () => this.confirmSeedWords(), |
||||
}, 'I\'ve copied it somewhere safe.'), |
||||
]) |
||||
) |
||||
} |
||||
|
||||
CreateVaultCompleteScreen.prototype.confirmSeedWords = function() { |
||||
this.props.dispatch(actions.confirmSeedWords()) |
||||
} |
||||
|
@ -0,0 +1,123 @@ |
||||
const inherits = require('util').inherits |
||||
|
||||
const Component = require('react').Component |
||||
const connect = require('react-redux').connect |
||||
const h = require('react-hyperscript') |
||||
const actions = require('../actions') |
||||
|
||||
module.exports = connect(mapStateToProps)(CreateVaultScreen) |
||||
|
||||
|
||||
inherits(CreateVaultScreen, Component) |
||||
function CreateVaultScreen() { |
||||
Component.call(this) |
||||
} |
||||
|
||||
function mapStateToProps(state) { |
||||
return { |
||||
warning: state.appState.warning, |
||||
} |
||||
} |
||||
|
||||
CreateVaultScreen.prototype.render = function() { |
||||
var state = this.props |
||||
return ( |
||||
|
||||
h('.initialize-screen.flex-column.flex-center.flex-grow', [ |
||||
|
||||
// subtitle and nav
|
||||
h('.section-title.flex-row.flex-center', [ |
||||
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { |
||||
onClick: this.showInitializeMenu.bind(this), |
||||
}), |
||||
h('h2.page-subtitle', 'Create Vault'), |
||||
]), |
||||
|
||||
// password
|
||||
h('label', { |
||||
htmlFor: 'password-box', |
||||
}, 'Enter Password (min 8 chars):'), |
||||
|
||||
h('input', { |
||||
type: 'password', |
||||
id: 'password-box', |
||||
}), |
||||
|
||||
// confirm password
|
||||
h('label', { |
||||
htmlFor: 'password-box-confirm', |
||||
}, 'Confirm Password:'), |
||||
|
||||
h('input', { |
||||
type: 'password', |
||||
id: 'password-box-confirm', |
||||
onKeyPress: this.createVaultOnEnter.bind(this), |
||||
}), |
||||
|
||||
/* ENTROPY TEXT INPUT CURRENTLY DISABLED |
||||
// entropy
|
||||
h('label', { |
||||
htmlFor: 'entropy-text-entry', |
||||
}, 'Enter random text (optional)'), |
||||
|
||||
h('textarea', { |
||||
id: 'entropy-text-entry', |
||||
style: { resize: 'none' }, |
||||
onKeyPress: this.createVaultOnEnter.bind(this), |
||||
}), |
||||
*/ |
||||
|
||||
// submit
|
||||
h('button.create-vault.btn-thin', { |
||||
onClick: this.createNewVault.bind(this), |
||||
}, 'OK'), |
||||
|
||||
(!state.inProgress && state.warning) && ( |
||||
h('span.in-progress-notification', state.warning) |
||||
|
||||
), |
||||
|
||||
state.inProgress && ( |
||||
h('span.in-progress-notification', 'Generating Seed...') |
||||
), |
||||
]) |
||||
) |
||||
} |
||||
|
||||
CreateVaultScreen.prototype.componentDidMount = function(){ |
||||
document.getElementById('password-box').focus() |
||||
} |
||||
|
||||
CreateVaultScreen.prototype.showInitializeMenu = function() { |
||||
this.props.dispatch(actions.showInitializeMenu()) |
||||
} |
||||
|
||||
// create vault
|
||||
|
||||
CreateVaultScreen.prototype.createVaultOnEnter = function(event) { |
||||
if (event.key === 'Enter') { |
||||
event.preventDefault() |
||||
this.createNewVault() |
||||
} |
||||
} |
||||
|
||||
CreateVaultScreen.prototype.createNewVault = function(){ |
||||
var passwordBox = document.getElementById('password-box') |
||||
var password = passwordBox.value |
||||
var passwordConfirmBox = document.getElementById('password-box-confirm') |
||||
var passwordConfirm = passwordConfirmBox.value |
||||
// var entropy = document.getElementById('entropy-text-entry').value
|
||||
|
||||
if (password.length < 8) { |
||||
this.warning = 'password not long enough' |
||||
this.props.dispatch(actions.displayWarning(this.warning)) |
||||
return |
||||
} |
||||
if (password !== passwordConfirm) { |
||||
this.warning = 'passwords dont match' |
||||
this.props.dispatch(actions.displayWarning(this.warning)) |
||||
return |
||||
} |
||||
|
||||
this.props.dispatch(actions.createNewVault(password, ''/*entropy*/)) |
||||
} |
@ -0,0 +1,123 @@ |
||||
const inherits = require('util').inherits |
||||
const EventEmitter = require('events').EventEmitter |
||||
const Component = require('react').Component |
||||
const connect = require('react-redux').connect |
||||
const h = require('react-hyperscript') |
||||
const getCaretCoordinates = require('textarea-caret') |
||||
const Mascot = require('../components/mascot') |
||||
const actions = require('../actions') |
||||
const CreateVaultScreen = require('./create-vault') |
||||
const CreateVaultCompleteScreen = require('./create-vault-complete') |
||||
|
||||
module.exports = connect(mapStateToProps)(InitializeMenuScreen) |
||||
|
||||
inherits(InitializeMenuScreen, Component) |
||||
function InitializeMenuScreen() { |
||||
Component.call(this) |
||||
this.animationEventEmitter = new EventEmitter() |
||||
} |
||||
|
||||
function mapStateToProps(state) { |
||||
return { |
||||
// state from plugin
|
||||
currentView: state.appState.currentView, |
||||
} |
||||
} |
||||
|
||||
InitializeMenuScreen.prototype.render = function() { |
||||
var state = this.props |
||||
|
||||
switch (state.currentView.name) { |
||||
|
||||
case 'createVault': |
||||
return h(CreateVaultScreen) |
||||
|
||||
case 'createVaultComplete': |
||||
return h(CreateVaultCompleteScreen) |
||||
|
||||
case 'restoreVault': |
||||
return this.renderRestoreVault() |
||||
|
||||
default: |
||||
return this.renderMenu() |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
// InitializeMenuScreen.prototype.componentDidMount = function(){
|
||||
// document.getElementById('password-box').focus()
|
||||
// }
|
||||
|
||||
InitializeMenuScreen.prototype.renderMenu = function() { |
||||
var state = this.props |
||||
return ( |
||||
|
||||
h('.initialize-screen.flex-column.flex-center.flex-grow', [ |
||||
|
||||
h('h2.page-subtitle', 'Welcome!'), |
||||
|
||||
h(Mascot, { |
||||
animationEventEmitter: this.animationEventEmitter, |
||||
}), |
||||
|
||||
h('button.btn-thin', { |
||||
onClick: this.showCreateVault.bind(this), |
||||
}, 'Create New Vault'), |
||||
|
||||
h('.flex-row.flex-center.flex-grow', [ |
||||
h('hr'), |
||||
h('div', 'OR'), |
||||
h('hr'), |
||||
]), |
||||
|
||||
h('button.btn-thin', { |
||||
onClick: this.showRestoreVault.bind(this), |
||||
}, 'Restore Existing Vault'), |
||||
|
||||
]) |
||||
|
||||
) |
||||
} |
||||
|
||||
InitializeMenuScreen.prototype.renderRestoreVault = function() { |
||||
var state = this.props |
||||
return ( |
||||
|
||||
h('.initialize-screen.flex-column.flex-center.flex-grow', [ |
||||
|
||||
// subtitle and nav
|
||||
h('.section-title.flex-row.flex-center', [ |
||||
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { |
||||
onClick: this.showInitializeMenu.bind(this), |
||||
}), |
||||
h('h2.page-subtitle', 'Restore Vault'), |
||||
]), |
||||
|
||||
|
||||
h('h3', 'Coming soon....'), |
||||
// h('textarea.twelve-word-phrase', {
|
||||
// value: 'hey ho what the actual hello rubber duck bumbersnatch crumplezone frankenfurter',
|
||||
// }),
|
||||
|
||||
]) |
||||
|
||||
) |
||||
} |
||||
|
||||
// InitializeMenuScreen.prototype.splitWor = function() {
|
||||
// this.props.dispatch(actions.showInitializeMenu())
|
||||
// }
|
||||
|
||||
InitializeMenuScreen.prototype.showInitializeMenu = function() { |
||||
this.props.dispatch(actions.showInitializeMenu()) |
||||
} |
||||
|
||||
InitializeMenuScreen.prototype.showCreateVault = function() { |
||||
this.props.dispatch(actions.showCreateVault()) |
||||
} |
||||
|
||||
InitializeMenuScreen.prototype.showRestoreVault = function() { |
||||
this.props.dispatch(actions.showRestoreVault()) |
||||
} |
||||
|
@ -0,0 +1,116 @@ |
||||
const inherits = require('util').inherits |
||||
const Component = require('react').Component |
||||
const connect = require('react-redux').connect |
||||
const h = require('react-hyperscript') |
||||
const actions = require('../actions') |
||||
|
||||
module.exports = connect(mapStateToProps)(RestoreVaultScreen) |
||||
|
||||
|
||||
inherits(RestoreVaultScreen, Component) |
||||
function RestoreVaultScreen() { |
||||
Component.call(this) |
||||
} |
||||
|
||||
function mapStateToProps(state) { |
||||
return { |
||||
warning: state.appState.warning, |
||||
} |
||||
} |
||||
|
||||
|
||||
RestoreVaultScreen.prototype.render = function() { |
||||
var state = this.props |
||||
return ( |
||||
|
||||
h('.initialize-screen.flex-column.flex-center.flex-grow', [ |
||||
|
||||
// subtitle and nav
|
||||
h('.section-title.flex-row.flex-center', [ |
||||
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { |
||||
onClick: this.showInitializeMenu.bind(this), |
||||
}), |
||||
h('h2.page-subtitle', 'Restore Vault'), |
||||
]), |
||||
|
||||
// wallet seed entry
|
||||
h('h3', 'Wallet Seed'), |
||||
h('textarea.twelve-word-phrase', { |
||||
placeholder: 'Enter your secret twelve word phrase here to restore your vault.' |
||||
}), |
||||
|
||||
// password
|
||||
h('label', { |
||||
htmlFor: 'password-box', |
||||
}, 'New Password (min 8 chars):'), |
||||
|
||||
h('input', { |
||||
type: 'password', |
||||
id: 'password-box', |
||||
}), |
||||
|
||||
// confirm password
|
||||
h('label', { |
||||
htmlFor: 'password-box-confirm', |
||||
}, 'Confirm Password:'), |
||||
|
||||
h('input', { |
||||
type: 'password', |
||||
id: 'password-box-confirm', |
||||
onKeyPress: this.onMaybeCreate.bind(this), |
||||
}), |
||||
|
||||
(state.warning) && ( |
||||
h('span.error.in-progress-notification', state.warning) |
||||
), |
||||
|
||||
// submit
|
||||
h('button.btn-thin', { |
||||
onClick: this.restoreVault.bind(this), |
||||
}, 'I\'ve double checked the 12 word phrase.'), |
||||
|
||||
]) |
||||
|
||||
) |
||||
} |
||||
|
||||
RestoreVaultScreen.prototype.showInitializeMenu = function() { |
||||
this.props.dispatch(actions.showInitializeMenu()) |
||||
} |
||||
|
||||
RestoreVaultScreen.prototype.onMaybeCreate = function(event) { |
||||
if (event.key === 'Enter') { |
||||
this.restoreVault() |
||||
} |
||||
} |
||||
|
||||
RestoreVaultScreen.prototype.restoreVault = function(){ |
||||
// check password
|
||||
var passwordBox = document.getElementById('password-box') |
||||
var password = passwordBox.value |
||||
var passwordConfirmBox = document.getElementById('password-box-confirm') |
||||
var passwordConfirm = passwordConfirmBox.value |
||||
if (password.length < 8) { |
||||
this.warning = 'Password not long enough' |
||||
|
||||
this.props.dispatch(actions.displayWarning(this.warning)) |
||||
return |
||||
} |
||||
if (password !== passwordConfirm) { |
||||
this.warning = 'Passwords don\'t match' |
||||
this.props.dispatch(actions.displayWarning(this.warning)) |
||||
return |
||||
} |
||||
// check seed
|
||||
var seedBox = document.querySelector('textarea.twelve-word-phrase') |
||||
var seed = seedBox.value.trim() |
||||
if (seed.split(' ').length !== 12) { |
||||
this.warning = 'seed phrases are 12 words long' |
||||
this.props.dispatch(actions.displayWarning(this.warning)) |
||||
return |
||||
} |
||||
// submit
|
||||
this.warning = null |
||||
this.props.dispatch(actions.displayWarning(this.warning)) |
||||
this.props.dispatch(actions.recoverFromSeed(password, seed)) |
||||
} |
After Width: | Height: | Size: 138 KiB |
After Width: | Height: | Size: 380 KiB |
@ -0,0 +1,90 @@ |
||||
const inherits = require('util').inherits |
||||
const Component = require('react').Component |
||||
const h = require('react-hyperscript') |
||||
const connect = require('react-redux').connect |
||||
const actions = require('./actions') |
||||
|
||||
module.exports = connect(mapStateToProps)(InfoScreen) |
||||
|
||||
function mapStateToProps(state) { |
||||
return {} |
||||
} |
||||
|
||||
inherits(InfoScreen, Component) |
||||
function InfoScreen() { |
||||
Component.call(this) |
||||
} |
||||
|
||||
InfoScreen.prototype.render = function() { |
||||
var state = this.props |
||||
var rpc = state.rpc |
||||
|
||||
return ( |
||||
h('.flex-column.flex-grow', [ |
||||
|
||||
// subtitle and nav
|
||||
h('.section-title.flex-row.flex-center', [ |
||||
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { |
||||
onClick: (event) => { |
||||
state.dispatch(actions.showAccountsPage()) |
||||
} |
||||
}), |
||||
h('h2.page-subtitle', 'Info'), |
||||
]), |
||||
|
||||
// main view
|
||||
h('.flex-column.flex-justify-center.flex-grow.select-none', [ |
||||
h('.flex-space-around', { |
||||
style: { |
||||
padding: '20px', |
||||
} |
||||
}, [ |
||||
|
||||
h('div', [ |
||||
h('a', { |
||||
href: 'https://consensys.slack.com/archives/team-metamask', |
||||
target: '_blank', |
||||
onClick(event) { this.navigateTo(event.target.href) }, |
||||
}, 'Join the conversation on Slack'), |
||||
]), |
||||
|
||||
h('div', [ |
||||
h('a', { |
||||
href: 'https://metamask.io/', |
||||
target: '_blank', |
||||
onClick(event) { this.navigateTo(event.target.href) }, |
||||
}, 'Visit our web site'), |
||||
]), |
||||
|
||||
h('div', [ |
||||
h('a', { |
||||
href: 'https://twitter.com/metamask_io', |
||||
target: '_blank', |
||||
onClick(event) { this.navigateTo(event.target.href) }, |
||||
}, 'Follow us on Twitter'), |
||||
]), |
||||
|
||||
h('div', [ |
||||
h('a', { |
||||
href: 'mailto:hello@metamask.io?subject=Feedback', |
||||
target: '_blank', |
||||
}, 'Email us any questions or comments!'), |
||||
]), |
||||
|
||||
h('div', [ |
||||
h('a', { |
||||
href: 'https://github.com/metamask/talk/issues', |
||||
target: '_blank', |
||||
onClick(event) { this.navigateTo(event.target.href) }, |
||||
}, 'Start a thread on Github'), |
||||
]), |
||||
|
||||
]), |
||||
]), |
||||
]) |
||||
) |
||||
} |
||||
|
||||
InfoScreen.prototype.navigateTo = function(url) { |
||||
chrome.tabs.create({ url }); |
||||
} |
@ -0,0 +1,51 @@ |
||||
const inherits = require('util').inherits |
||||
const Component = require('react').Component |
||||
const h = require('react-hyperscript') |
||||
const connect = require('react-redux').connect |
||||
const actions = require('./actions') |
||||
const ReactCSSTransitionGroup = require('react-addons-css-transition-group') |
||||
|
||||
module.exports = connect(mapStateToProps)(LoadingIndicator) |
||||
|
||||
function mapStateToProps(state) { |
||||
return { |
||||
isLoading: state.appState.isLoading, |
||||
} |
||||
} |
||||
|
||||
inherits(LoadingIndicator, Component) |
||||
function LoadingIndicator() { |
||||
Component.call(this) |
||||
} |
||||
|
||||
LoadingIndicator.prototype.render = function() { |
||||
console.dir(this.props) |
||||
var isLoading = this.props.isLoading |
||||
|
||||
return ( |
||||
h(ReactCSSTransitionGroup, { |
||||
transitionName: "loader", |
||||
transitionEnterTimeout: 150, |
||||
transitionLeaveTimeout: 150, |
||||
}, [ |
||||
|
||||
isLoading ? h('div', { |
||||
style: { |
||||
position: 'absolute', |
||||
display: 'flex', |
||||
justifyContent: 'center', |
||||
alignItems: 'center', |
||||
height: '100%', |
||||
width: '100%', |
||||
background: 'rgba(255, 255, 255, 0.5)', |
||||
} |
||||
}, [ |
||||
h('img', { |
||||
src: 'images/loading.svg', |
||||
}), |
||||
]) : null, |
||||
|
||||
]) |
||||
) |
||||
} |
||||
|
@ -0,0 +1,41 @@ |
||||
const combineReducers = require('redux').combineReducers |
||||
const actions = require('./actions') |
||||
const extend = require('xtend') |
||||
|
||||
//
|
||||
// Sub-Reducers take in the complete state and return their sub-state
|
||||
//
|
||||
const reduceIdentities = require('./reducers/identities') |
||||
const reduceMetamask = require('./reducers/metamask') |
||||
const reduceApp = require('./reducers/app') |
||||
|
||||
module.exports = rootReducer |
||||
|
||||
function rootReducer(state, action) { |
||||
|
||||
// clone
|
||||
state = extend(state) |
||||
|
||||
//
|
||||
// Identities
|
||||
//
|
||||
|
||||
state.identities = reduceIdentities(state, action) |
||||
|
||||
//
|
||||
// MetaMask
|
||||
//
|
||||
|
||||
state.metamask = reduceMetamask(state, action) |
||||
|
||||
//
|
||||
// AppState
|
||||
//
|
||||
|
||||
state.appState = reduceApp(state, action) |
||||
|
||||
|
||||
return state |
||||
|
||||
} |
||||
|
@ -0,0 +1,281 @@ |
||||
const extend = require('xtend') |
||||
const actions = require('../actions') |
||||
|
||||
module.exports = reduceApp |
||||
|
||||
function reduceApp(state, action) { |
||||
|
||||
// clone and defaults
|
||||
var defaultView = { |
||||
name: 'accounts', |
||||
detailView: null, |
||||
} |
||||
|
||||
// confirm seed words
|
||||
var seedConfView = { |
||||
name: 'createVaultComplete', |
||||
} |
||||
var seedWords = state.metamask.seedWords |
||||
|
||||
var appState = extend({ |
||||
currentView: seedWords ? seedConfView : defaultView, |
||||
currentDomain: 'example.com', |
||||
transForward: true, // Used to render transition direction
|
||||
isLoading: false, // Used to display loading indicator
|
||||
warning: null, // Used to display error text
|
||||
}, state.appState) |
||||
|
||||
switch (action.type) { |
||||
|
||||
// intialize
|
||||
|
||||
case actions.SHOW_CREATE_VAULT: |
||||
return extend(appState, { |
||||
currentView: { |
||||
name: 'createVault', |
||||
}, |
||||
transForward: true, |
||||
warning: null, |
||||
}) |
||||
|
||||
case actions.SHOW_RESTORE_VAULT: |
||||
return extend(appState, { |
||||
currentView: { |
||||
name: 'restoreVault', |
||||
}, |
||||
transForward: true, |
||||
}) |
||||
|
||||
case actions.SHOW_INIT_MENU: |
||||
return extend(appState, { |
||||
currentView: defaultView, |
||||
transForward: false, |
||||
}) |
||||
|
||||
case actions.SHOW_CONFIG_PAGE: |
||||
return extend(appState, { |
||||
currentView: { |
||||
name: 'config', |
||||
}, |
||||
transForward: true, |
||||
}) |
||||
|
||||
case actions.SHOW_INFO_PAGE: |
||||
return extend(appState, { |
||||
currentView: { |
||||
name: 'info', |
||||
}, |
||||
transForward: true, |
||||
}) |
||||
|
||||
case actions.CREATE_NEW_VAULT_IN_PROGRESS: |
||||
return extend(appState, { |
||||
currentView: { |
||||
name: 'createVault', |
||||
inProgress: true, |
||||
}, |
||||
transForward: true, |
||||
isLoading: true, |
||||
}) |
||||
|
||||
case actions.SHOW_NEW_VAULT_SEED: |
||||
return extend(appState, { |
||||
currentView: { |
||||
name: 'createVaultComplete', |
||||
context: action.value, |
||||
}, |
||||
transForward: true, |
||||
isLoading: false, |
||||
}) |
||||
|
||||
case actions.SHOW_SEND_PAGE: |
||||
return extend(appState, { |
||||
currentView: { |
||||
name: 'sendTransaction', |
||||
context: appState.currentView.context, |
||||
}, |
||||
transForward: true, |
||||
warning: null, |
||||
}) |
||||
|
||||
// unlock
|
||||
|
||||
case actions.UNLOCK_METAMASK: |
||||
return extend(appState, { |
||||
transForward: true, |
||||
warning: null, |
||||
}) |
||||
|
||||
case actions.LOCK_METAMASK: |
||||
return extend(appState, { |
||||
currentView: defaultView, |
||||
transForward: false, |
||||
warning: null, |
||||
}) |
||||
|
||||
// accounts
|
||||
|
||||
case actions.SET_SELECTED_ACCOUNT: |
||||
return extend(appState, { |
||||
activeAddress: action.value, |
||||
}) |
||||
|
||||
case actions.SHOW_ACCOUNT_DETAIL: |
||||
return extend(appState, { |
||||
currentView: { |
||||
name: 'accountDetail', |
||||
context: action.value, |
||||
}, |
||||
accountDetail: { |
||||
accountExport: 'none', |
||||
privateKey: '', |
||||
}, |
||||
transForward: true, |
||||
}) |
||||
|
||||
case actions.BACK_TO_ACCOUNT_DETAIL: |
||||
return extend(appState, { |
||||
currentView: { |
||||
name: 'accountDetail', |
||||
context: action.value, |
||||
}, |
||||
accountDetail: { |
||||
accountExport: 'none', |
||||
privateKey: '', |
||||
}, |
||||
transForward: false, |
||||
}) |
||||
|
||||
case actions.SHOW_ACCOUNTS_PAGE: |
||||
var seedWords = state.metamask.seedWords |
||||
return extend(appState, { |
||||
currentView: { |
||||
name: seedWords ? 'createVaultComplete' : 'accounts', |
||||
}, |
||||
transForward: appState.currentView.name == 'locked', |
||||
isLoading: false, |
||||
warning: null, |
||||
}) |
||||
|
||||
case actions.SHOW_CONF_TX_PAGE: |
||||
return extend(appState, { |
||||
currentView: { |
||||
name: 'confTx', |
||||
context: 0, |
||||
}, |
||||
transForward: true, |
||||
warning: null, |
||||
}) |
||||
|
||||
case actions.COMPLETED_TX: |
||||
var unconfTxs = Object.keys(state.metamask.unconfTxs).filter(tx => tx !== tx.id) |
||||
if (unconfTxs && unconfTxs.length > 0) { |
||||
return extend(appState, { |
||||
transForward: false, |
||||
currentView: { |
||||
name: 'confTx', |
||||
context: 0, |
||||
}, |
||||
warning: null, |
||||
}) |
||||
} else { |
||||
return extend(appState, { |
||||
transForward: false, |
||||
currentView: { |
||||
name: 'accounts', |
||||
context: 0, |
||||
}, |
||||
transForward: false, |
||||
warning: null, |
||||
}) |
||||
} |
||||
|
||||
case actions.NEXT_TX: |
||||
return extend(appState, { |
||||
transForward: true, |
||||
currentView: { |
||||
name: 'confTx', |
||||
context: ++appState.currentView.context, |
||||
warning: null, |
||||
} |
||||
}) |
||||
|
||||
case actions.PREVIOUS_TX: |
||||
return extend(appState, { |
||||
transForward: false, |
||||
currentView: { |
||||
name: 'confTx', |
||||
context: --appState.currentView.context, |
||||
warning: null, |
||||
} |
||||
}) |
||||
|
||||
case actions.TRANSACTION_ERROR: |
||||
return extend(appState, { |
||||
currentView: { |
||||
name: 'confTx', |
||||
errorMessage: 'There was a problem submitting this transaction.', |
||||
}, |
||||
}) |
||||
|
||||
case actions.UNLOCK_FAILED: |
||||
return extend(appState, { |
||||
warning: 'Incorrect password. Try again.' |
||||
}) |
||||
|
||||
case actions.SHOW_LOADING: |
||||
return extend(appState, { |
||||
isLoading: true, |
||||
}) |
||||
|
||||
case actions.HIDE_LOADING: |
||||
return extend(appState, { |
||||
isLoading: false, |
||||
}) |
||||
|
||||
case actions.CLEAR_SEED_WORD_CACHE: |
||||
return extend(appState, { |
||||
transForward: true, |
||||
currentView: { |
||||
name: 'accounts', |
||||
}, |
||||
isLoading: false, |
||||
}) |
||||
|
||||
case actions.DISPLAY_WARNING: |
||||
return extend(appState, { |
||||
warning: action.value, |
||||
}) |
||||
|
||||
case actions.HIDE_WARNING: |
||||
return extend(appState, { |
||||
warning: undefined, |
||||
}) |
||||
|
||||
case actions.REQUEST_ACCOUNT_EXPORT: |
||||
return extend(appState, { |
||||
accountDetail: { |
||||
accountExport: 'requested', |
||||
}, |
||||
}) |
||||
|
||||
case actions.EXPORT_ACCOUNT: |
||||
return extend(appState, { |
||||
accountDetail: { |
||||
accountExport: 'completed', |
||||
}, |
||||
}) |
||||
|
||||
case actions.SHOW_PRIVATE_KEY: |
||||
return extend(appState, { |
||||
accountDetail: { |
||||
accountExport: 'completed', |
||||
privateKey: action.value, |
||||
}, |
||||
}) |
||||
|
||||
default: |
||||
return appState |
||||
|
||||
} |
||||
} |
@ -0,0 +1,18 @@ |
||||
const extend = require('xtend') |
||||
const actions = require('../actions') |
||||
|
||||
module.exports = reduceIdentities |
||||
|
||||
function reduceIdentities(state, action) { |
||||
|
||||
// clone + defaults
|
||||
var idState = extend({ |
||||
|
||||
}, state.identities) |
||||
|
||||
switch (action.type) { |
||||
default: |
||||
return idState |
||||
} |
||||
|
||||
} |
@ -0,0 +1,73 @@ |
||||
const extend = require('xtend') |
||||
const actions = require('../actions') |
||||
|
||||
module.exports = reduceMetamask |
||||
|
||||
function reduceMetamask(state, action) { |
||||
|
||||
// clone + defaults
|
||||
var metamaskState = extend({ |
||||
isInitialized: false, |
||||
isUnlocked: false, |
||||
currentDomain: 'example.com', |
||||
rpcTarget: 'https://rawtestrpc.metamask.io/', |
||||
identities: {}, |
||||
unconfTxs: {}, |
||||
}, state.metamask) |
||||
|
||||
switch (action.type) { |
||||
|
||||
case actions.SHOW_ACCOUNTS_PAGE: |
||||
var state = extend(metamaskState) |
||||
delete state.seedWords |
||||
return state |
||||
|
||||
case actions.UPDATE_METAMASK_STATE: |
||||
return extend(metamaskState, action.value) |
||||
|
||||
case actions.UNLOCK_METAMASK: |
||||
return extend(metamaskState, { |
||||
isUnlocked: true, |
||||
isInitialized: true, |
||||
}) |
||||
|
||||
case actions.LOCK_METAMASK: |
||||
return extend(metamaskState, { |
||||
isUnlocked: false, |
||||
}) |
||||
|
||||
case actions.SET_RPC_TARGET: |
||||
return extend(metamaskState, { |
||||
rpcTarget: action.value, |
||||
}) |
||||
|
||||
case actions.COMPLETED_TX: |
||||
var stringId = String(action.id) |
||||
var newState = extend(metamaskState, { |
||||
unconfTxs: {} |
||||
}) |
||||
for (var id in metamaskState.unconfTxs) { |
||||
if (id !== stringId) { |
||||
newState.unconfTxs[id] = metamaskState.unconfTxs[id] |
||||
} |
||||
} |
||||
return newState |
||||
|
||||
case actions.CLEAR_SEED_WORD_CACHE: |
||||
var newState = extend(metamaskState, { |
||||
isInitialized: true, |
||||
}) |
||||
delete newState.seedWords |
||||
return newState |
||||
|
||||
case actions.CREATE_NEW_VAULT_IN_PROGRESS: |
||||
return extend(metamaskState, { |
||||
isUnlocked: true, |
||||
isInitialized: true, |
||||
}) |
||||
|
||||
default: |
||||
return metamaskState |
||||
|
||||
} |
||||
} |
@ -0,0 +1,24 @@ |
||||
const inherits = require('util').inherits |
||||
const React = require('react') |
||||
const Component = require('react').Component |
||||
const Provider = require('react-redux').Provider |
||||
const h = require('react-hyperscript') |
||||
const App = require('./app') |
||||
|
||||
module.exports = Root |
||||
|
||||
|
||||
inherits(Root, Component) |
||||
function Root() { Component.call(this) } |
||||
|
||||
Root.prototype.render = function() { |
||||
return ( |
||||
|
||||
h(Provider, { |
||||
store: this.props.store, |
||||
}, [ |
||||
h(App) |
||||
]) |
||||
|
||||
) |
||||
} |
@ -0,0 +1,139 @@ |
||||
const inherits = require('util').inherits |
||||
const Component = require('react').Component |
||||
const h = require('react-hyperscript') |
||||
const connect = require('react-redux').connect |
||||
const actions = require('./actions') |
||||
const util = require('./util') |
||||
const numericBalance = require('./util').numericBalance |
||||
const AccountPanel = require('./components/account-panel') |
||||
const ethUtil = require('ethereumjs-util') |
||||
|
||||
module.exports = connect(mapStateToProps)(SendTransactionScreen) |
||||
|
||||
function mapStateToProps(state) { |
||||
var result = { |
||||
address: state.appState.currentView.context, |
||||
accounts: state.metamask.accounts, |
||||
identities: state.metamask.identities, |
||||
warning: state.appState.warning, |
||||
} |
||||
|
||||
result.account = result.accounts[result.address] |
||||
result.identity = result.identities[result.address] |
||||
result.balance = result.account ? numericBalance(result.account.balance) : null |
||||
|
||||
return result |
||||
} |
||||
|
||||
inherits(SendTransactionScreen, Component) |
||||
function SendTransactionScreen() { |
||||
Component.call(this) |
||||
} |
||||
|
||||
SendTransactionScreen.prototype.render = function() { |
||||
var state = this.props |
||||
var account = state.account |
||||
var identity = state.identity |
||||
|
||||
return ( |
||||
h('.send-screen.flex-column.flex-grow', [ |
||||
|
||||
// subtitle and nav
|
||||
h('.section-title.flex-row.flex-center', [ |
||||
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { |
||||
onClick: this.back.bind(this), |
||||
}), |
||||
h('h2.page-subtitle', 'Send Transaction'), |
||||
]), |
||||
|
||||
h(AccountPanel, { |
||||
showFullAddress: true, |
||||
identity: identity, |
||||
account: account, |
||||
}), |
||||
|
||||
h('section.recipient', [ |
||||
h('input.address', { |
||||
placeholder: 'Recipient Address', |
||||
}) |
||||
]), |
||||
|
||||
h('section.ammount', [ |
||||
h('input.ether', { |
||||
placeholder: 'Amount', |
||||
type: 'number', |
||||
style: { marginRight: '6px' } |
||||
}), |
||||
h('select.currency', { |
||||
name: 'currency', |
||||
}, [ |
||||
h('option', { value: 'ether' }, 'Ether (1e18 wei)'), |
||||
h('option', { value: 'wei' }, 'Wei'), |
||||
]), |
||||
]), |
||||
|
||||
h('section.data', [ |
||||
h('details', [ |
||||
h('summary', { |
||||
style: {cursor: 'pointer'}, |
||||
}, 'Advanced'), |
||||
h('textarea.txData', { |
||||
type: 'textarea', |
||||
placeholder: 'Transaction data (optional)', |
||||
style: { |
||||
height: '100px', |
||||
width: '100%', |
||||
resize: 'none', |
||||
} |
||||
}) |
||||
]) |
||||
]), |
||||
|
||||
h('section', { |
||||
}, [ |
||||
h('button', { |
||||
onClick: this.onSubmit.bind(this), |
||||
}, 'Send') |
||||
]), |
||||
|
||||
state.warning ? h('span.error', state.warning) : null, |
||||
]) |
||||
) |
||||
} |
||||
|
||||
SendTransactionScreen.prototype.back = function() { |
||||
var address = this.props.address |
||||
this.props.dispatch(actions.backToAccountDetail(address)) |
||||
} |
||||
|
||||
SendTransactionScreen.prototype.onSubmit = function(event) { |
||||
var recipient = document.querySelector('input.address').value |
||||
var amount = new ethUtil.BN(document.querySelector('input.ether').value, 10) |
||||
var currency = document.querySelector('select.currency').value |
||||
var txData = document.querySelector('textarea.txData').value |
||||
|
||||
var value = util.normalizeToWei(amount, currency) |
||||
var balance = this.props.balance |
||||
|
||||
if (value.gt(balance)) { |
||||
var message = 'Insufficient funds.' |
||||
return this.props.dispatch(actions.displayWarning(message)) |
||||
} |
||||
if (recipient.length !== 42) { |
||||
var message = 'Recipient address is the incorrect length.' |
||||
return this.props.dispatch(actions.displayWarning(message)) |
||||
} |
||||
|
||||
this.props.dispatch(actions.hideWarning()) |
||||
this.props.dispatch(actions.showLoadingIndication()) |
||||
|
||||
var txParams = { |
||||
to: recipient, |
||||
from: this.props.address, |
||||
value: '0x' + value.toString(16), |
||||
} |
||||
if (txData) txParams.data = txData |
||||
|
||||
this.props.dispatch(actions.signTx(txParams)) |
||||
} |
||||
|
@ -0,0 +1,69 @@ |
||||
const inherits = require('util').inherits |
||||
const Component = require('react').Component |
||||
const h = require('react-hyperscript') |
||||
const connect = require('react-redux').connect |
||||
const copyToClipboard = require('copy-to-clipboard') |
||||
const actions = require('./actions') |
||||
const AccountPanel = require('./components/account-panel') |
||||
|
||||
module.exports = connect(mapStateToProps)(AppSettingsPage) |
||||
|
||||
function mapStateToProps(state) { |
||||
return { |
||||
identities: state.metamask.identities, |
||||
address: state.appState.currentView.context, |
||||
} |
||||
} |
||||
|
||||
inherits(AppSettingsPage, Component) |
||||
function AppSettingsPage() { |
||||
Component.call(this) |
||||
} |
||||
|
||||
|
||||
AppSettingsPage.prototype.render = function() { |
||||
var state = this.props |
||||
var identity = state.identities[state.address] |
||||
return ( |
||||
|
||||
h('.account-detail-section.flex-column.flex-grow', [ |
||||
|
||||
// subtitle and nav
|
||||
h('.flex-row.flex-center', [ |
||||
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { |
||||
onClick: this.navigateToAccounts.bind(this), |
||||
}), |
||||
h('h2.page-subtitle', 'Settings'), |
||||
]), |
||||
|
||||
h('label', { |
||||
htmlFor: 'settings-rpc-endpoint', |
||||
}, 'RPC Endpoint:'), |
||||
h('input', { |
||||
// value: '//testrpc.metamask.io',
|
||||
type: 'url', |
||||
id: 'settings-rpc-endpoint', |
||||
onKeyPress: this.onKeyPress.bind(this), |
||||
}), |
||||
|
||||
]) |
||||
|
||||
) |
||||
} |
||||
|
||||
AppSettingsPage.prototype.componentDidMount = function(){ |
||||
document.querySelector('input').focus() |
||||
} |
||||
|
||||
AppSettingsPage.prototype.onKeyPress = function(event) { |
||||
// get submit event
|
||||
if (event.key === 'Enter') { |
||||
// this.submitPassword(event)
|
||||
} |
||||
} |
||||
|
||||
|
||||
AppSettingsPage.prototype.navigateToAccounts = function(event){ |
||||
event.stopPropagation() |
||||
this.props.dispatch(actions.showAccountsPage()) |
||||
} |
@ -0,0 +1,19 @@ |
||||
const createStore = require('redux').createStore |
||||
const applyMiddleware = require('redux').applyMiddleware |
||||
const thunkMiddleware = require('redux-thunk') |
||||
const createLogger = require('redux-logger') |
||||
const rootReducer = require('./reducers') |
||||
|
||||
module.exports = configureStore |
||||
|
||||
|
||||
const loggerMiddleware = createLogger() |
||||
|
||||
const createStoreWithMiddleware = applyMiddleware( |
||||
thunkMiddleware, |
||||
loggerMiddleware |
||||
)(createStore) |
||||
|
||||
function configureStore(initialState) { |
||||
return createStoreWithMiddleware(rootReducer, initialState) |
||||
} |
@ -0,0 +1,31 @@ |
||||
const inherits = require('util').inherits |
||||
const Component = require('react').Component |
||||
const h = require('react-hyperscript') |
||||
const connect = require('react-redux').connect |
||||
const actions = require('./actions') |
||||
|
||||
module.exports = connect(mapStateToProps)(COMPONENTNAME) |
||||
|
||||
function mapStateToProps(state) { |
||||
return {} |
||||
} |
||||
|
||||
inherits(COMPONENTNAME, Component) |
||||
function COMPONENTNAME() { |
||||
Component.call(this) |
||||
} |
||||
|
||||
COMPONENTNAME.prototype.render = function() { |
||||
var state = this.props |
||||
var rpc = state.rpc |
||||
|
||||
return ( |
||||
h('div', { |
||||
style: { |
||||
display: 'none', |
||||
} |
||||
}, [ |
||||
]) |
||||
) |
||||
} |
||||
|
@ -0,0 +1,101 @@ |
||||
const inherits = require('util').inherits |
||||
const Component = require('react').Component |
||||
const h = require('react-hyperscript') |
||||
const connect = require('react-redux').connect |
||||
const actions = require('./actions') |
||||
const Mascot = require('./components/mascot') |
||||
const getCaretCoordinates = require('textarea-caret') |
||||
const EventEmitter = require('events').EventEmitter |
||||
|
||||
module.exports = connect(mapStateToProps)(UnlockScreen) |
||||
|
||||
|
||||
inherits(UnlockScreen, Component) |
||||
function UnlockScreen() { |
||||
Component.call(this) |
||||
this.animationEventEmitter = new EventEmitter() |
||||
} |
||||
|
||||
function mapStateToProps(state) { |
||||
return { |
||||
warning: state.appState.warning, |
||||
} |
||||
} |
||||
|
||||
UnlockScreen.prototype.render = function() { |
||||
const state = this.props |
||||
const warning = state.warning |
||||
return ( |
||||
|
||||
h('.unlock-screen.flex-column.flex-center.flex-grow', [ |
||||
|
||||
h('h2.page-subtitle', 'Welcome!'), |
||||
|
||||
h(Mascot, { |
||||
animationEventEmitter: this.animationEventEmitter, |
||||
}), |
||||
|
||||
h('label', { |
||||
htmlFor: 'password-box', |
||||
}, 'Enter Password:'), |
||||
|
||||
h('input', { |
||||
type: 'password', |
||||
id: 'password-box', |
||||
onKeyPress: this.onKeyPress.bind(this), |
||||
onInput: this.inputChanged.bind(this), |
||||
}), |
||||
|
||||
h('.error', { |
||||
style: { |
||||
display: warning ? 'block' : 'none', |
||||
} |
||||
}, warning), |
||||
|
||||
h('button.primary.cursor-pointer', { |
||||
onClick: this.onSubmit.bind(this), |
||||
}, 'Unlock'), |
||||
|
||||
]) |
||||
|
||||
) |
||||
} |
||||
|
||||
UnlockScreen.prototype.componentDidMount = function(){ |
||||
document.getElementById('password-box').focus() |
||||
} |
||||
|
||||
UnlockScreen.prototype.onSubmit = function(event) { |
||||
const input = document.getElementById('password-box') |
||||
const password = input.value |
||||
this.props.dispatch(actions.tryUnlockMetamask(password)) |
||||
} |
||||
|
||||
UnlockScreen.prototype.onKeyPress = function(event) { |
||||
if (event.key === 'Enter') { |
||||
this.submitPassword(event) |
||||
} |
||||
} |
||||
|
||||
UnlockScreen.prototype.submitPassword = function(event){ |
||||
var element = event.target |
||||
var password = element.value |
||||
// reset input
|
||||
element.value = '' |
||||
this.props.dispatch(actions.tryUnlockMetamask(password)) |
||||
} |
||||
|
||||
UnlockScreen.prototype.inputChanged = function(event){ |
||||
// tell mascot to look at page action
|
||||
var element = event.target |
||||
var boundingRect = element.getBoundingClientRect() |
||||
var coordinates = getCaretCoordinates(element, element.selectionEnd) |
||||
this.animationEventEmitter.emit('point', { |
||||
x: boundingRect.left + coordinates.left - element.scrollLeft, |
||||
y: boundingRect.top + coordinates.top - element.scrollTop, |
||||
}) |
||||
} |
||||
|
||||
UnlockScreen.prototype.emitAnim = function(name, a, b, c){ |
||||
this.animationEventEmitter.emit(name, a, b, c) |
||||
} |
@ -0,0 +1,102 @@ |
||||
const ethUtil = require('ethereumjs-util') |
||||
|
||||
var valueTable = { |
||||
wei: '1000000000000000000', |
||||
kwei: '1000000000000000', |
||||
mwei: '1000000000000', |
||||
gwei: '1000000000', |
||||
szabo: '1000000', |
||||
finney:'1000', |
||||
ether: '1', |
||||
kether:'0.001', |
||||
mether:'0.000001', |
||||
gether:'0.000000001', |
||||
tether:'0.000000000001', |
||||
} |
||||
var bnTable = {} |
||||
for (var currency in valueTable) { |
||||
bnTable[currency] = new ethUtil.BN(valueTable[currency], 10) |
||||
} |
||||
|
||||
module.exports = { |
||||
valuesFor: valuesFor, |
||||
addressSummary: addressSummary, |
||||
numericBalance: numericBalance, |
||||
formatBalance: formatBalance, |
||||
dataSize: dataSize, |
||||
readableDate: readableDate, |
||||
ethToWei: ethToWei, |
||||
weiToEth: weiToEth, |
||||
normalizeToWei: normalizeToWei, |
||||
valueTable: valueTable, |
||||
bnTable: bnTable, |
||||
} |
||||
|
||||
|
||||
function valuesFor(obj) { |
||||
if (!obj) return [] |
||||
return Object.keys(obj) |
||||
.map(function(key){ return obj[key] }) |
||||
} |
||||
|
||||
function addressSummary(address) { |
||||
return address ? address.slice(0,2+8)+'...'+address.slice(-4) : '...' |
||||
} |
||||
|
||||
// Takes wei Hex, returns wei BN, even if input is null
|
||||
function numericBalance(balance) { |
||||
if (!balance) return new ethUtil.BN(0, 16) |
||||
var stripped = ethUtil.stripHexPrefix(balance) |
||||
return new ethUtil.BN(stripped, 16) |
||||
} |
||||
|
||||
// Takes eth BN, returns BN wei
|
||||
function ethToWei(bn) { |
||||
var eth = new ethUtil.BN('1000000000000000000') |
||||
var wei = bn.mul(eth) |
||||
return wei |
||||
} |
||||
|
||||
// Takes BN in Wei, returns BN in eth
|
||||
function weiToEth(bn) { |
||||
var diff = new ethUtil.BN('1000000000000000000') |
||||
var eth = bn.div(diff) |
||||
return eth |
||||
} |
||||
|
||||
function formatBalance(balance) { |
||||
if (!balance) return 'None' |
||||
var wei = numericBalance(balance) |
||||
var eth = weiToEth(wei) |
||||
return eth.toString(10) + ' ETH' |
||||
} |
||||
|
||||
function dataSize(data) { |
||||
var size = data ? ethUtil.stripHexPrefix(data).length : 0 |
||||
return size+' bytes' |
||||
} |
||||
|
||||
// Takes a BN and an ethereum currency name,
|
||||
// returns a BN in wei
|
||||
function normalizeToWei(amount, currency) { |
||||
try { |
||||
var ether = amount.div(bnTable[currency]) |
||||
var wei = ether.mul(bnTable.wei) |
||||
return wei |
||||
} catch (e) {} |
||||
return amount |
||||
} |
||||
|
||||
function readableDate(ms) { |
||||
var date = new Date(ms) |
||||
var month = date.getMonth() |
||||
var day = date.getDate() |
||||
var year = date.getFullYear() |
||||
var hours = date.getHours() |
||||
var minutes = "0" + date.getMinutes() |
||||
var seconds = "0" + date.getSeconds() |
||||
|
||||
var date = `${month}/${day}/${year}` |
||||
var time = `${hours}:${minutes.substr(-2)}:${seconds.substr(-2)}` |
||||
return `${date} ${time}` |
||||
} |
@ -0,0 +1,26 @@ |
||||
const fs = require('fs') |
||||
|
||||
module.exports = bundleCss |
||||
|
||||
var cssFiles = { |
||||
'fonts.css': fs.readFileSync(__dirname+'/app/css/fonts.css', 'utf8'), |
||||
'reset.css': fs.readFileSync(__dirname+'/app/css/reset.css', 'utf8'), |
||||
'lib.css': fs.readFileSync(__dirname+'/app/css/lib.css', 'utf8'), |
||||
'index.css': fs.readFileSync(__dirname+'/app/css/index.css', 'utf8'), |
||||
'transitions.css': fs.readFileSync(__dirname+'/app/css/transitions.css', 'utf8'), |
||||
} |
||||
|
||||
function bundleCss() { |
||||
var cssBundle = Object.keys(cssFiles).reduce(function(bundle, fileName){ |
||||
var fileContent = cssFiles[fileName] |
||||
var output = String() |
||||
|
||||
output += '/*========== '+fileName+' ==========*/\n\n' |
||||
output += fileContent |
||||
output += '\n\n' |
||||
|
||||
return bundle+output |
||||
}, String()) |
||||
|
||||
return cssBundle |
||||
} |
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 409 KiB |
Binary file not shown.
@ -0,0 +1,123 @@ |
||||
const injectCss = require('inject-css') |
||||
const MetaMaskUi = require('./index.js') |
||||
const MetaMaskUiCss = require('./css.js') |
||||
const EventEmitter = require('events').EventEmitter |
||||
|
||||
// account management
|
||||
|
||||
var identities = { |
||||
'0x1113462427bcc9133bb46e88bcbe39cd7ef0e111': { |
||||
name: 'Walrus', |
||||
img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', |
||||
address: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', |
||||
balance: 220, |
||||
txCount: 4, |
||||
}, |
||||
'0x222462427bcc9133bb46e88bcbe39cd7ef0e7222': { |
||||
name: 'Tardus', |
||||
img: 'QmQYaRdrf2EhRhJWaHnts8Meu1mZiXrNib5W1P6cYmXWRL', |
||||
address: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222', |
||||
balance: 10.005, |
||||
txCount: 16, |
||||
}, |
||||
'0x333462427bcc9133bb46e88bcbe39cd7ef0e7333': { |
||||
name: 'Gambler', |
||||
img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', |
||||
address: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333', |
||||
balance: 0.000001, |
||||
txCount: 1, |
||||
} |
||||
} |
||||
|
||||
var unconfTxs = {} |
||||
addUnconfTx({ |
||||
from: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222', |
||||
to: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', |
||||
value: '0x123', |
||||
}) |
||||
addUnconfTx({ |
||||
from: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', |
||||
to: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333', |
||||
value: '0x0000', |
||||
data: '0x000462427bcc9133bb46e88bcbe39cd7ef0e7000', |
||||
}) |
||||
|
||||
function addUnconfTx(txParams){ |
||||
var time = (new Date()).getTime() |
||||
var id = createRandomId() |
||||
unconfTxs[id] = { |
||||
id: id, |
||||
txParams: txParams, |
||||
time: time, |
||||
} |
||||
} |
||||
|
||||
var isUnlocked = false |
||||
var selectedAddress = null |
||||
|
||||
function getState(){ |
||||
return { |
||||
isUnlocked: isUnlocked, |
||||
identities: isUnlocked ? identities : {}, |
||||
unconfTxs: isUnlocked ? unconfTxs : {}, |
||||
selectedAddress: selectedAddress, |
||||
} |
||||
} |
||||
|
||||
var accountManager = new EventEmitter() |
||||
|
||||
accountManager.getState = function(cb){ |
||||
cb(null, getState()) |
||||
} |
||||
|
||||
accountManager.setLocked = function(){ |
||||
isUnlocked = false |
||||
this._didUpdate() |
||||
} |
||||
|
||||
accountManager.submitPassword = function(password, cb){ |
||||
if (password === 'test') { |
||||
isUnlocked = true |
||||
cb(null, getState()) |
||||
this._didUpdate() |
||||
} else { |
||||
cb(new Error('Bad password -- try "test"')) |
||||
} |
||||
} |
||||
|
||||
accountManager.setSelectedAddress = function(address, cb){ |
||||
selectedAddress = address |
||||
cb(null, getState()) |
||||
this._didUpdate() |
||||
} |
||||
|
||||
accountManager.signTransaction = function(txParams, cb){ |
||||
alert('signing tx....') |
||||
} |
||||
|
||||
accountManager._didUpdate = function(){ |
||||
this.emit('update', getState()) |
||||
} |
||||
|
||||
// start app
|
||||
|
||||
var container = document.getElementById('app-content') |
||||
|
||||
var css = MetaMaskUiCss() |
||||
injectCss(css) |
||||
|
||||
var app = MetaMaskUi({ |
||||
container: container, |
||||
accountManager: accountManager |
||||
}) |
||||
|
||||
// util
|
||||
|
||||
function createRandomId(){ |
||||
// 13 time digits
|
||||
var datePart = new Date().getTime()*Math.pow(10, 3) |
||||
// 3 random digits
|
||||
var extraPart = Math.floor(Math.random()*Math.pow(10, 3)) |
||||
// 16 digits
|
||||
return datePart+extraPart |
||||
} |
@ -0,0 +1,38 @@ |
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<title>MetaMask</title> |
||||
</head> |
||||
<body> |
||||
|
||||
<!-- app content --> |
||||
<div id="app-content"></div> |
||||
<script src="./bundle.js" type="text/javascript" charset="utf-8"></script> |
||||
|
||||
<!-- design reference --> |
||||
<link rel="stylesheet" type="text/css" href="./app/css/debug.css"> |
||||
<div id="design-container"> |
||||
<img id="design-img" src="./design/metamask_wfs_jan_13.png"> |
||||
<!-- persist scroll position on refresh --> |
||||
<script type="text/javascript"> |
||||
var scrollElement = document.getElementById('design-container') |
||||
function getScrollPosition () { |
||||
var scrollTop = scrollElement.scrollTop, scrollLeft = scrollElement.scrollLeft |
||||
window.location.hash = 'scrollTop='+scrollTop+'&scrollLeft='+scrollLeft |
||||
} |
||||
window.onload = function () { |
||||
setInterval(getScrollPosition, 1000) |
||||
var hashLocation = window.location.hash.split('#')[1] |
||||
if (!hashLocation) return |
||||
var sections = hashLocation.split('&') |
||||
var scrollTop = sections[0].split('=')[1] |
||||
var scrollLeft = sections[1].split('=')[1] |
||||
scrollElement.scrollTop = scrollTop |
||||
scrollElement.scrollLeft = scrollLeft |
||||
} |
||||
</script> |
||||
</div> |
||||
|
||||
</body> |
||||
</html> |
@ -0,0 +1,55 @@ |
||||
const React = require('react') |
||||
const render = require('react-dom').render |
||||
const h = require('react-hyperscript') |
||||
const extend = require('xtend') |
||||
const Root = require('./app/root') |
||||
const actions = require('./app/actions') |
||||
const configureStore = require('./app/store') |
||||
|
||||
module.exports = launchApp |
||||
|
||||
function launchApp(opts) { |
||||
|
||||
var accountManager = opts.accountManager |
||||
actions._setAccountManager(accountManager) |
||||
|
||||
// check if we are unlocked first
|
||||
accountManager.getState(function(err, metamaskState){ |
||||
if (err) throw err |
||||
startApp(metamaskState, accountManager, opts) |
||||
}) |
||||
|
||||
} |
||||
|
||||
function startApp(metamaskState, accountManager, opts){ |
||||
|
||||
// parse opts
|
||||
var store = configureStore({ |
||||
|
||||
// metamaskState represents the cross-tab state
|
||||
metamask: metamaskState, |
||||
|
||||
// appState represents the current tab's popup state
|
||||
appState: { |
||||
currentDomain: opts.currentDomain, |
||||
} |
||||
}) |
||||
|
||||
// if unconfirmed txs, start on txConf page
|
||||
if (Object.keys(metamaskState.unconfTxs || {}).length) { |
||||
store.dispatch(actions.showConfTxPage()) |
||||
} |
||||
|
||||
accountManager.on('update', function(metamaskState){ |
||||
store.dispatch(actions.updateMetamaskState(metamaskState)) |
||||
}) |
||||
|
||||
// start app
|
||||
render( |
||||
h(Root, { |
||||
// inject initial state
|
||||
store: store, |
||||
} |
||||
), opts.container) |
||||
|
||||
} |
@ -0,0 +1,58 @@ |
||||
{ |
||||
"name": "metamask-ui", |
||||
"version": "1.5.0", |
||||
"description": "", |
||||
"main": "index.js", |
||||
"scripts": { |
||||
"test": "mocha test/**/*test.js", |
||||
"watch": "mocha watch test/**/*test.js", |
||||
"start": "beefy example.js:bundle.js --live --open", |
||||
"build": "browserify example.js -g uglifyify -o bundle.js" |
||||
}, |
||||
"author": "", |
||||
"license": "ISC", |
||||
"devDependencies": { |
||||
"beefy": "^2.1.5", |
||||
"chai": "^3.5.0", |
||||
"deep-freeze-strict": "^1.1.1", |
||||
"jsdom": "^8.1.0", |
||||
"mocha": "^2.4.5", |
||||
"mocha-jsdom": "^1.1.0", |
||||
"sinon": "^1.17.3", |
||||
"uglifyify": "^3.0.1" |
||||
}, |
||||
"browserify": { |
||||
"transform": [ |
||||
[ |
||||
"babelify", |
||||
{ |
||||
"presets": [ |
||||
"es2015" |
||||
] |
||||
} |
||||
], |
||||
"brfs" |
||||
] |
||||
}, |
||||
"dependencies": { |
||||
"babel-preset-es2015": "^6.3.13", |
||||
"babelify": "^7.2.0", |
||||
"brfs": "^1.4.2", |
||||
"browserify": "^12.0.1", |
||||
"copy-to-clipboard": "^1.1.1", |
||||
"debounce": "^1.0.0", |
||||
"ethereumjs-util": "^2.6.0", |
||||
"inject-css": "^0.1.1", |
||||
"metamask-logo": "^1.1.3", |
||||
"react": "^0.14.3", |
||||
"react-addons-css-transition-group": "^0.14.7", |
||||
"react-dom": "^0.14.3", |
||||
"react-hyperscript": "^2.2.2", |
||||
"react-redux": "^4.0.3", |
||||
"redux": "^3.0.5", |
||||
"redux-logger": "^2.3.1", |
||||
"redux-thunk": "^1.0.2", |
||||
"textarea-caret": "^3.0.1", |
||||
"xtend": "^4.0.1" |
||||
} |
||||
} |
@ -0,0 +1,8 @@ |
||||
if (typeof process === 'object') { |
||||
// Initialize node environment
|
||||
global.expect = require('chai').expect |
||||
require('mocha-jsdom')() |
||||
} else { |
||||
window.expect = window.chai.expect |
||||
window.require = function () { /* noop */ } |
||||
} |
@ -0,0 +1,43 @@ |
||||
var jsdom = require('mocha-jsdom') |
||||
var assert = require('assert') |
||||
var freeze = require('deep-freeze-strict') |
||||
var path = require('path') |
||||
|
||||
var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js')) |
||||
var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js')) |
||||
|
||||
describe ('config view actions', function() { |
||||
|
||||
var initialState = { |
||||
metamask: { |
||||
rpcTarget: 'foo', |
||||
}, |
||||
appState: { |
||||
currentView: { |
||||
name: 'accounts', |
||||
} |
||||
} |
||||
} |
||||
freeze(initialState) |
||||
|
||||
describe('SHOW_CONFIG_PAGE', function() { |
||||
it('should set appState.currentView.name to config', function() { |
||||
var result = reducers(initialState, actions.showConfigPage()) |
||||
assert.equal(result.appState.currentView.name, 'config') |
||||
}) |
||||
}) |
||||
|
||||
describe('SET_RPC_TARGET', function() { |
||||
|
||||
it('sets the state.metamask.rpcTarget property of the state to the action.value', function() { |
||||
const action = { |
||||
type: actions.SET_RPC_TARGET, |
||||
value: 'bar', |
||||
} |
||||
|
||||
var result = reducers(initialState, action) |
||||
assert.equal(result.metamask.rpcTarget, action.value) |
||||
}) |
||||
}) |
||||
}) |
||||
|
@ -0,0 +1,54 @@ |
||||
var jsdom = require('mocha-jsdom') |
||||
var assert = require('assert') |
||||
var freeze = require('deep-freeze-strict') |
||||
var path = require('path') |
||||
var sinon = require('sinon') |
||||
|
||||
var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js')) |
||||
var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js')) |
||||
|
||||
describe('#recoverFromSeed(password, seed)', function() { |
||||
|
||||
beforeEach(function() { |
||||
// sinon allows stubbing methods that are easily verified
|
||||
this.sinon = sinon.sandbox.create() |
||||
}) |
||||
|
||||
afterEach(function() { |
||||
// sinon requires cleanup otherwise it will overwrite context
|
||||
this.sinon.restore() |
||||
}) |
||||
|
||||
// stub out account manager
|
||||
actions._setAccountManager({ |
||||
recoverFromSeed(pw, seed, cb) { cb() }, |
||||
}) |
||||
|
||||
it('sets metamask.isUnlocked to true', function() { |
||||
var initialState = { |
||||
metamask: { |
||||
isUnlocked: false, |
||||
isInitialized: false, |
||||
} |
||||
} |
||||
freeze(initialState) |
||||
|
||||
const restorePhrase = 'invite heavy among daring outdoor dice jelly coil stable note seat vicious' |
||||
const password = 'foo' |
||||
const dispatchFunc = actions.recoverFromSeed(password, restorePhrase) |
||||
|
||||
var dispatchStub = this.sinon.stub() |
||||
dispatchStub.withArgs({ TYPE: actions.unlockMetamask() }).onCall(0) |
||||
dispatchStub.withArgs({ TYPE: actions.showAccountsPage() }).onCall(1) |
||||
|
||||
var action |
||||
var resultingState = initialState |
||||
dispatchFunc((newAction) => { |
||||
action = newAction |
||||
resultingState = reducers(resultingState, action) |
||||
}) |
||||
|
||||
assert.equal(resultingState.metamask.isUnlocked, true, 'was unlocked') |
||||
assert.equal(resultingState.metamask.isInitialized, true, 'was initialized') |
||||
}); |
||||
}); |
@ -0,0 +1,28 @@ |
||||
var jsdom = require('mocha-jsdom') |
||||
var assert = require('assert') |
||||
var freeze = require('deep-freeze-strict') |
||||
var path = require('path') |
||||
|
||||
var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js')) |
||||
var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js')) |
||||
|
||||
describe('SET_SELECTED_ACCOUNT', function() { |
||||
|
||||
it('sets the state.appState.activeAddress property of the state to the action.value', function() { |
||||
var initialState = { |
||||
appState: { |
||||
activeAddress: 'foo', |
||||
} |
||||
} |
||||
freeze(initialState) |
||||
|
||||
const action = { |
||||
type: actions.SET_SELECTED_ACCOUNT, |
||||
value: 'bar', |
||||
} |
||||
freeze(action) |
||||
|
||||
var resultingState = reducers(initialState, action) |
||||
assert.equal(resultingState.appState.activeAddress, action.value) |
||||
}); |
||||
}); |
@ -0,0 +1,168 @@ |
||||
var jsdom = require('mocha-jsdom') |
||||
var assert = require('assert') |
||||
var freeze = require('deep-freeze-strict') |
||||
var path = require('path') |
||||
|
||||
var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js')) |
||||
var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js')) |
||||
|
||||
describe('tx confirmation screen', function() { |
||||
var initialState, result |
||||
|
||||
describe('when there is only one tx', function() { |
||||
var firstTxId = 1457634084250832 |
||||
|
||||
beforeEach(function() { |
||||
|
||||
initialState = { |
||||
appState: { |
||||
currentView: { |
||||
name: 'confTx', |
||||
}, |
||||
}, |
||||
metamask: { |
||||
unconfTxs: { |
||||
'1457634084250832': { |
||||
id: 1457634084250832, |
||||
status: "unconfirmed", |
||||
time: 1457634084250, |
||||
} |
||||
}, |
||||
} |
||||
} |
||||
freeze(initialState) |
||||
}) |
||||
|
||||
describe('cancelTx', function() { |
||||
|
||||
before(function(done) { |
||||
actions._setAccountManager({ |
||||
approveTransaction(txId, cb) { cb('An error!') }, |
||||
cancelTransaction(txId) { /* noop */ }, |
||||
clearSeedWordCache(cb) { cb() }, |
||||
}) |
||||
|
||||
actions.cancelTx({id: firstTxId})(function(action) { |
||||
result = reducers(initialState, action) |
||||
done() |
||||
}) |
||||
}) |
||||
|
||||
it('should transition to the accounts list', function() { |
||||
assert.equal(result.appState.currentView.name, 'accounts') |
||||
}) |
||||
|
||||
it('should have no unconfirmed txs remaining', function() { |
||||
var count = getUnconfirmedTxCount(result) |
||||
assert.equal(count, 0) |
||||
}) |
||||
}) |
||||
|
||||
describe('sendTx', function() { |
||||
var result |
||||
|
||||
describe('when there is an error', function() { |
||||
|
||||
before(function(done) { |
||||
alert = () => {/* noop */} |
||||
|
||||
actions._setAccountManager({ |
||||
approveTransaction(txId, cb) { cb('An error!') }, |
||||
}) |
||||
|
||||
actions.sendTx({id: firstTxId})(function(action) { |
||||
result = reducers(initialState, action) |
||||
done() |
||||
}) |
||||
}) |
||||
|
||||
it('should stay on the page', function() { |
||||
assert.equal(result.appState.currentView.name, 'confTx') |
||||
}) |
||||
|
||||
it('should set errorMessage on the currentView', function() { |
||||
assert(result.appState.currentView.errorMessage) |
||||
}) |
||||
}) |
||||
|
||||
describe('when there is success', function() { |
||||
before(function(done) { |
||||
actions._setAccountManager({ |
||||
approveTransaction(txId, cb) { cb() }, |
||||
}) |
||||
|
||||
actions.sendTx({id: firstTxId})(function(action) { |
||||
result = reducers(initialState, action) |
||||
done() |
||||
}) |
||||
}) |
||||
|
||||
it('should navigate away from the tx page', function() { |
||||
assert.equal(result.appState.currentView.name, 'accounts') |
||||
}) |
||||
|
||||
it('should clear the tx from the unconfirmed transactions', function() { |
||||
assert(!(firstTxId in result.metamask.unconfTxs), 'tx is cleared') |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('when there are two pending txs', function() { |
||||
var firstTxId = 1457634084250832 |
||||
var result, initialState |
||||
before(function(done) { |
||||
initialState = { |
||||
appState: { |
||||
currentView: { |
||||
name: 'confTx', |
||||
}, |
||||
}, |
||||
metamask: { |
||||
unconfTxs: { |
||||
'1457634084250832': { |
||||
id: 1457634084250832, |
||||
status: "unconfirmed", |
||||
time: 1457634084250, |
||||
}, |
||||
'1457634084250833': { |
||||
id: 1457634084250833, |
||||
status: "unconfirmed", |
||||
time: 1457634084255, |
||||
}, |
||||
}, |
||||
} |
||||
} |
||||
freeze(initialState) |
||||
|
||||
|
||||
actions._setAccountManager({ |
||||
approveTransaction(txId, cb) { cb() }, |
||||
}) |
||||
|
||||
actions.sendTx({id: firstTxId})(function(action) { |
||||
result = reducers(initialState, action) |
||||
done() |
||||
}) |
||||
}) |
||||
|
||||
it('should stay on the confTx view', function() { |
||||
assert.equal(result.appState.currentView.name, 'confTx') |
||||
}) |
||||
|
||||
it('should transition to the first tx', function() { |
||||
assert.equal(result.appState.currentView.context, 0) |
||||
}) |
||||
|
||||
it('should only have one unconfirmed tx remaining', function() { |
||||
var count = getUnconfirmedTxCount(result) |
||||
assert.equal(count, 1) |
||||
}) |
||||
}) |
||||
}) |
||||
}); |
||||
|
||||
function getUnconfirmedTxCount(state) { |
||||
var txs = state.metamask.unconfTxs |
||||
var count = Object.keys(txs).length |
||||
return count |
||||
} |
@ -0,0 +1,23 @@ |
||||
var jsdom = require('mocha-jsdom') |
||||
var assert = require('assert') |
||||
var freeze = require('deep-freeze-strict') |
||||
var path = require('path') |
||||
|
||||
var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js')) |
||||
var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js')) |
||||
|
||||
describe('SHOW_INFO_PAGE', function() { |
||||
|
||||
it('sets the state.appState.currentView.name property to info', function() { |
||||
var initialState = { |
||||
appState: { |
||||
activeAddress: 'foo', |
||||
} |
||||
} |
||||
freeze(initialState) |
||||
|
||||
const action = actions.showInfoPage() |
||||
var resultingState = reducers(initialState, action) |
||||
assert.equal(resultingState.appState.currentView.name, 'info') |
||||
}); |
||||
}); |
@ -0,0 +1,24 @@ |
||||
var jsdom = require('mocha-jsdom') |
||||
var assert = require('assert') |
||||
var freeze = require('deep-freeze-strict') |
||||
var path = require('path') |
||||
|
||||
var actions = require(path.join(__dirname, '..', '..', '..', 'app', 'actions.js')) |
||||
var reducers = require(path.join(__dirname, '..', '..', '..', 'app', 'reducers.js')) |
||||
|
||||
describe('action DISPLAY_WARNING', function() { |
||||
|
||||
it('sets appState.warning to provided value', function() { |
||||
var initialState = { |
||||
appState: {}, |
||||
} |
||||
freeze(initialState) |
||||
|
||||
const warningText = 'This is a sample warning message' |
||||
|
||||
const action = actions.displayWarning(warningText) |
||||
const resultingState = reducers(initialState, action) |
||||
|
||||
assert.equal(resultingState.appState.warning, warningText, 'warning text set') |
||||
}); |
||||
}); |
@ -0,0 +1,102 @@ |
||||
var assert = require('assert') |
||||
var sinon = require('sinon') |
||||
const ethUtil = require('ethereumjs-util') |
||||
|
||||
var path = require('path') |
||||
var util = require(path.join(__dirname, '..', '..', 'app', 'util.js')) |
||||
|
||||
describe('util', function() { |
||||
var ethInWei = '1' |
||||
for (var i = 0; i < 18; i++ ) { ethInWei += '0' } |
||||
|
||||
beforeEach(function() { |
||||
this.sinon = sinon.sandbox.create() |
||||
}) |
||||
|
||||
afterEach(function() { |
||||
this.sinon.restore() |
||||
}) |
||||
|
||||
describe('numericBalance', function() { |
||||
|
||||
it('should return a BN 0 if given nothing', function() { |
||||
var result = util.numericBalance() |
||||
assert.equal(result.toString(10), 0) |
||||
}) |
||||
|
||||
it('should work with hex prefix', function() { |
||||
var result = util.numericBalance('0x012') |
||||
assert.equal(result.toString(10), '18') |
||||
}) |
||||
|
||||
it('should work with no hex prefix', function() { |
||||
var result = util.numericBalance('012') |
||||
assert.equal(result.toString(10), '18') |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('#ethToWei', function() { |
||||
|
||||
it('should take an eth BN, returns wei BN', function() { |
||||
var input = new ethUtil.BN(1, 10) |
||||
var result = util.ethToWei(input) |
||||
assert.equal(result, ethInWei, '18 zeroes') |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('#weiToEth', function() { |
||||
|
||||
it('should take a wei BN and return an eth BN', function() { |
||||
var result = util.weiToEth(new ethUtil.BN(ethInWei)) |
||||
assert.equal(result, '1', 'equals 1 eth') |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('#formatBalance', function() { |
||||
|
||||
it('when given nothing', function() { |
||||
var result = util.formatBalance() |
||||
assert.equal(result, 'None', 'should return "None"') |
||||
}) |
||||
|
||||
it('should return eth as string followed by ETH', function() { |
||||
var input = new ethUtil.BN(ethInWei).toJSON() |
||||
var result = util.formatBalance(input) |
||||
assert.equal(result, '1 ETH') |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('#normalizeToWei', function() { |
||||
it('should convert an eth to the appropriate equivalent values', function() { |
||||
var valueTable = { |
||||
wei: '1000000000000000000', |
||||
kwei: '1000000000000000', |
||||
mwei: '1000000000000', |
||||
gwei: '1000000000', |
||||
szabo: '1000000', |
||||
finney:'1000', |
||||
ether: '1', |
||||
kether:'0.001', |
||||
mether:'0.000001', |
||||
// AUDIT: We're getting BN numbers on these ones.
|
||||
// I think they're big enough to ignore for now.
|
||||
// gether:'0.000000001',
|
||||
// tether:'0.000000000001',
|
||||
} |
||||
var oneEthBn = new ethUtil.BN(ethInWei, 10) |
||||
|
||||
for(var currency in valueTable) { |
||||
|
||||
var value = new ethUtil.BN(valueTable[currency], 10) |
||||
var output = util.normalizeToWei(value, currency) |
||||
assert.equal(output.toString(10), valueTable.wei, `value of ${output.toString(10)} ${currency} should convert to ${oneEthBn}`) |
||||
|
||||
} |
||||
}) |
||||
}) |
||||
|
||||
}) |
Loading…
Reference in new issue