1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

first commit

This commit is contained in:
Dan 2018-04-06 19:59:51 -02:30
parent 77486a2365
commit 284dd85a99
94 changed files with 845 additions and 37 deletions

View File

@ -167,6 +167,8 @@ var actions = {
UPDATE_MAX_MODE: 'UPDATE_MAX_MODE',
UPDATE_SEND: 'UPDATE_SEND',
CLEAR_SEND: 'CLEAR_SEND',
OPEN_FROM_DROPDOWN: 'OPEN_FROM_DROPDOWN',
CLOSE_FROM_DROPDOWN: 'CLOSE_FROM_DROPDOWN',
updateGasLimit,
updateGasPrice,
updateGasTotal,

View File

@ -0,0 +1,18 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class PageContainerContent extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
};
render () {
return (
<div className="page-container__content">
{this.props.children}
</div>
);
}
}

View File

@ -0,0 +1,41 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class PageContainerFooter extends Component {
static propTypes = {
onCancel: PropTypes.func,
onSubmit: PropTypes.func,
disabled: PropTypes.bool,
};
render () {
const { onCancel, onSubmit, disabled } = this.props
return (
<div className="page-container__footer">
<button
className="btn-secondary--lg page-container__footer-button"
onClick={() => onCancel()}
>
{this.context.t('cancel')}
</button>
<button
className="btn-primary--lg page-container__footer-button"
disabled={disabled}
onClick={(e) => onSubmit(e)}
>
{this.context.t('next')}
</button>
</div>
);
}
}
PageContainerFooter.contextTypes = {
t: PropTypes.func,
}

View File

@ -0,0 +1,35 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class PageContainerHeader extends Component {
static propTypes = {
title: PropTypes.string,
subtitle: PropTypes.string,
onClose: PropTypes.func,
};
render () {
const { title, subtitle, onClose } = this.props
return (
<div className="page-container__header">
<div className="page-container__title">
{title}
</div>
<div className="page-container__subtitle">
{subtitle}
</div>
<div
className="page-container__header-close"
onClick={() => onClose()}
/>
</div>
);
}
}

View File

@ -0,0 +1,18 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class PageContainer extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
};
render () {
return (
<div className="page-container">
{this.props.children}
</div>
);
}
}

View File

View File

@ -0,0 +1,48 @@
import {
getSelectedToken,
getPrimaryCurrency,
getAmountConversionRate,
getConvertedCurrency,
getSendAmount,
getGasTotal,
getSelectedBalance,
getTokenBalance,
} from '../../send.selectors.js'
import {
getMaxModeOn,
getSendAmountError,
} from './send-amount-row.selectors.js'
import { getAmountErrorObject } from './send-to-row.utils.js'
import {
updateSendErrors,
updateSendTo,
} from '../../../actions'
import {
openToDropdown,
closeToDropdown,
} from '../../../ducks/send'
import SendToRow from './send-to-row.component'
export default connect(mapStateToProps, mapDispatchToProps)(SendToRow)
function mapStateToProps (state) {
updateSendTo
return {
to: getSendTo(state),
toAccounts: getSendToAccounts(state),
toDropdownOpen: getToDropdownOpen(state),
inError: sendToIsInError(state),
network: getCurrentNetwork(state),
}
}
function mapDispatchToProps (dispatch) {
return {
updateSendToError: (to) => {
dispatch(updateSendErrors(getToErrorObject(to)))
},
updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)),
openToDropdown: () => dispatch(()),
closeToDropdown: () => dispatch(()),
}
}

View File

@ -0,0 +1,64 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import SendRowWrapper from '../../../send/from-dropdown'
import FromDropdown from ''
export default class SendFromRow extends Component {
static propTypes = {
closeFromDropdown: PropTypes.func,
conversionRate: PropTypes.string,
from: PropTypes.string,
fromAccounts: PropTypes.array,
fromDropdownOpen: PropTypes.bool,
openFromDropdown: PropTypes.func,
tokenContract: PropTypes.object,
updateSendFrom: PropTypes.func,
updateSendTokenBalance: PropTypes.func,
};
async handleFromChange (newFrom) {
const {
updateSendFrom,
tokenContract,
updateSendTokenBalance,
} = this.props
if (tokenContract) {
const usersToken = await tokenContract.balanceOf(newFrom.address)
updateSendTokenBalance(usersToken)
}
updateSendFrom(newFrom)
}
render () {
const {
from,
fromAccounts,
conversionRate,
fromDropdownOpen,
tokenContract,
openFromDropdown,
closeFromDropdown,
} = this.props
return (
<SendRowWrapper label={`${this.context.t('from')}:`}>
<FromDropdown
dropdownOpen={fromDropdownOpen}
accounts={fromAccounts}
selectedAccount={from}
onSelect={newFrom => this.handleFromChange(newFrom)}
openDropdown={() => openFromDropdown()}
closeDropdown={() => closeFromDropdown()}
conversionRate={conversionRate}
/>
</SendRowWrapper>
);
}
}
SendFromRow.contextTypes = {
t: PropTypes.func,
}

View File

@ -0,0 +1,44 @@
import {
getSendFrom,
conversionRateSelector,
getSelectedTokenContract,
getCurrentAccountWithSendEtherInfo,
accountsWithSendEtherInfoSelector,
} from '../../send.selectors.js'
import { getFromDropdownOpen } from './send-from-row.selectors.js'
import { calcTokenUpdateAmount } from './send-from-row.utils.js'
import {
updateSendTokenBalance,
updateSendFrom,
} from '../../../actions'
import {
openFromDropdown,
closeFromDropdown,
} from '../../../ducks/send'
import SendFromRow from './send-from-row.component'
export default connect(mapStateToProps, mapDispatchToProps)(SendFromRow)
function mapStateToProps (state) {
return {
from: getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state),
fromAccounts: accountsWithSendEtherInfoSelector(state),
conversionRate: conversionRateSelector(state),
fromDropdownOpen: getFromDropdownOpen(state),
tokenContract: getSelectedTokenContract(state),
}
}
function mapDispatchToProps (dispatch) {
return {
updateSendTokenBalance: (usersToken, selectedToken) => {
if (!usersToken) return
const tokenBalance = calcTokenUpdateAmount(selectedToken, selectedToken)
dispatch(updateSendTokenBalance(tokenBalance))
},
updateSendFrom: newFrom => dispatch(updateSendFrom(newFrom)),
openFromDropdown: () => dispatch(()),
closeFromDropdown: () => dispatch(()),
}
}

View File

@ -0,0 +1,9 @@
const selectors = {
getFromDropdownOpen,
}
module.exports = selectors
function getFromDropdownOpen (state) {
return state.send.fromDropdownOpen
}

View File

@ -0,0 +1,12 @@
const {
calcTokenAmount,
} = require('../../token-util')
function calcTokenUpdateAmount (usersToken, selectedToken) {
const { decimals } = selectedToken || {}
return calcTokenAmount(usersToken.balance.toString(), decimals)
}
module.exports = {
calcTokenUpdateAmount
}

View File

@ -0,0 +1,23 @@
export default class SendRowErrorMessage extends Component {
static propTypes = {
errors: PropTypes.object,
errorType: PropTypes.string,
};
render () {
const { errors, errorType } = this.props
const errorMessage = errors[errorType]
return (
errorMessage
? <div className='send-v2__error'>{errorMessage}</div>
: null
);
}
}
SendRowErrorMessage.contextTypes = {
t: PropTypes.func,
}

View File

@ -0,0 +1,11 @@
import { getSendErrors } from '../../../send.selectors'
import SendRowErrorMessage from './send-row-error-message.component'
export default connect(mapStateToProps)(SendRowErrorMessage)
function mapStateToProps (state, ownProps) {
return {
errors: getSendErrors(state),
errorType: ownProps.errorType,
}
}

View File

@ -0,0 +1,39 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import SendRowErrorMessage from './send-row-error-message/send-row-error-message.container'
export default class SendRowWrapper extends Component {
static propTypes = {
label: PropTypes.string,
showError: PropTypes.bool,
children: PropTypes.node,
errorType: PropTypes.string,
};
render () {
const {
label,
errorType = '',
showError = false,
children,
} = this.props
return (
<div className="send-v2__form-row">
<div className="send-v2__form-label">
{label}
(showError && <SendRowErrorMessage errorType={errorType}/>)
</div>
<div className="send-v2__form-field">
{children}
</div>
</div>
);
}
}
SendRowWrapper.contextTypes = {
t: PropTypes.func,
}

View File

@ -0,0 +1,62 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import SendRowWrapper from '../../../send/from-dropdown'
import ToDropdown from '../../../ens-input'
export default class SendToRow extends Component {
static propTypes = {
to: PropTypes.string,
toAccounts: PropTypes.array,
toDropdownOpen: PropTypes.bool,
inError: PropTypes.bool,
updateSendTo: PropTypes.func,
updateSendToError: PropTypes.func,
openToDropdown: PropTypes.func,
closeToDropdown: PropTypes.func,
network: PropTypes.number,
};
handleToChange (to, nickname = '') {
const { updateSendTo, updateSendToError } = this.props
updateSendTo(to, nickname)
updateSendErrors(to)
}
render () {
const {
from,
fromAccounts,
conversionRate,
fromDropdownOpen,
tokenContract,
openToDropdown,
closeToDropdown,
network,
inError,
} = this.props
return (
<SendRowWrapper label={`${this.context.t('to')}:`}>
<EnsInput
name={'address'}
placeholder={this.context.t('recipient Address')}
network={network},
to={to},
accounts={toAccounts}
dropdownOpen={toDropdownOpen}
openDropdown={() => openToDropdown()}
closeDropdown={() => closeToDropdown()}
onChange={this.handleToChange}
inError={inError}
/>
</SendRowWrapper>
);
}
}
SendToRow.contextTypes = {
t: PropTypes.func,
}

View File

@ -0,0 +1,43 @@
import {
getSendTo,
getToAccounts,
getCurrentNetwork,
} from '../../send.selectors.js'
import {
getToDropdownOpen,
sendToIsInError,
} from './send-to-row.selectors.js'
import { getToErrorObject } from './send-to-row.utils.js'
import {
updateSendErrors,
updateSendTo,
} from '../../../actions'
import {
openToDropdown,
closeToDropdown,
} from '../../../ducks/send'
import SendToRow from './send-to-row.component'
export default connect(mapStateToProps, mapDispatchToProps)(SendToRow)
function mapStateToProps (state) {
updateSendTo
return {
to: getSendTo(state),
toAccounts: getSendToAccounts(state),
toDropdownOpen: getToDropdownOpen(state),
inError: sendToIsInError(state),
network: getCurrentNetwork(state),
}
}
function mapDispatchToProps (dispatch) {
return {
updateSendToError: (to) => {
dispatch(updateSendErrors(getToErrorObject(to)))
},
updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)),
openToDropdown: () => dispatch(()),
closeToDropdown: () => dispatch(()),
}
}

View File

@ -0,0 +1,14 @@
const selectors = {
getToDropdownOpen,
sendToIsInError,
}
module.exports = selectors
function getToDropdownOpen (state) {
return state.send.toDropdownOpen
}
function sendToIsInError (state) {
return Boolean(state.metamask.send.to)
}

View File

@ -0,0 +1,17 @@
const { isValidAddress } = require('../../../../util')
function getToErrorObject (to) {
let toError = null
if (!to) {
toError = 'required'
} else if (!isValidAddress(to)) {
toError = 'invalidAddressRecipient'
}
return { to: toError }
}
module.exports = {
getToErrorObject
}

View File

@ -0,0 +1,32 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import PageContainerHeader from '../../page-container/page-container-header.component'
export default class SendHeader extends Component {
static propTypes = {
isToken: PropTypes.bool,
clearSend: PropTypes.func,
goHome: PropTypes.func,
};
render () {
const { isToken, clearSend, goHome } = this.props
return (
<PageContainerHeader
title={isToken ? this.context.t('sendTokens') : this.context.t('sendETH')}
subtitle={this.context.t('onlySendToEtherAddress')}
onClose={() => {
clearSend()
goHome()
}}
/>
);
}
}
SendHeader.contextTypes = {
t: PropTypes.func,
}

View File

@ -0,0 +1,19 @@
import { connect } from 'react-redux'
import { goHome, clearSend } from '../../../actions'
import SendHeader from './send-header.component'
import { getSelectedToken } from '../../../selectors'
export default connect(mapStateToProps, mapDispatchToProps)(SendHeader)
function mapStateToProps (state) {
return {
isToken: Boolean(getSelectedToken(state))
}
}
function mapDispatchToProps (dispatch) {
return {
goHome: () => dispatch(goHome()),
clearSend: () => dispatch(clearSend()),
}
}

View File

View File

@ -0,0 +1,217 @@
import { valuesFor } from '../../util'
import abi from 'human-standard-token-abi'
import {
multiplyCurrencies,
} from './conversion-util'
const selectors = {
getSelectedAddress,
getSelectedIdentity,
getSelectedAccount,
getSelectedToken,
getSelectedTokenExchangeRate,
getTokenExchangeRate,
conversionRateSelector,
transactionsSelector,
accountsWithSendEtherInfoSelector,
getCurrentAccountWithSendEtherInfo,
getGasPrice,
getGasLimit,
getForceGasMin,
getAddressBook,
getSendFrom,
getCurrentCurrency,
getSendAmount,
getSelectedTokenToFiatRate,
getSelectedTokenContract,
autoAddToBetaUI,
getSendMaxModeState,
getCurrentViewContext,
getSendErrors,
getSendTo,
getCurrentNetwork,
}
module.exports = selectors
function getSelectedAddress (state) {
const selectedAddress = state.metamask.selectedAddress || Object.keys(state.metamask.accounts)[0]
return selectedAddress
}
function getSelectedIdentity (state) {
const selectedAddress = getSelectedAddress(state)
const identities = state.metamask.identities
return identities[selectedAddress]
}
function getSelectedAccount (state) {
const accounts = state.metamask.accounts
const selectedAddress = getSelectedAddress(state)
return accounts[selectedAddress]
}
function getSelectedToken (state) {
const tokens = state.metamask.tokens || []
const selectedTokenAddress = state.metamask.selectedTokenAddress
const selectedToken = tokens.filter(({ address }) => address === selectedTokenAddress)[0]
const sendToken = state.metamask.send.token
return selectedToken || sendToken || null
}
function getSelectedTokenExchangeRate (state) {
const tokenExchangeRates = state.metamask.tokenExchangeRates
const selectedToken = getSelectedToken(state) || {}
const { symbol = '' } = selectedToken
const pair = `${symbol.toLowerCase()}_eth`
const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {}
return tokenExchangeRate
}
function getTokenExchangeRate (state, tokenSymbol) {
const pair = `${tokenSymbol.toLowerCase()}_eth`
const tokenExchangeRates = state.metamask.tokenExchangeRates
const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {}
return tokenExchangeRate
}
function conversionRateSelector (state) {
return state.metamask.conversionRate
}
function getAddressBook (state) {
return state.metamask.addressBook
}
function accountsWithSendEtherInfoSelector (state) {
const {
accounts,
identities,
} = state.metamask
const accountsWithSendEtherInfo = Object.entries(accounts).map(([key, account]) => {
return Object.assign({}, account, identities[key])
})
return accountsWithSendEtherInfo
}
function getCurrentAccountWithSendEtherInfo (state) {
const currentAddress = getSelectedAddress(state)
const accounts = accountsWithSendEtherInfoSelector(state)
return accounts.find(({ address }) => address === currentAddress)
}
function transactionsSelector (state) {
const { network, selectedTokenAddress } = state.metamask
const unapprovedMsgs = valuesFor(state.metamask.unapprovedMsgs)
const shapeShiftTxList = (network === '1') ? state.metamask.shapeShiftTxList : undefined
const transactions = state.metamask.selectedAddressTxList || []
const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList)
// console.log({txsToRender, selectedTokenAddress})
return selectedTokenAddress
? txsToRender
.filter(({ txParams }) => txParams && txParams.to === selectedTokenAddress)
.sort((a, b) => b.time - a.time)
: txsToRender
.sort((a, b) => b.time - a.time)
}
function getGasPrice (state) {
return state.metamask.send.gasPrice
}
function getGasLimit (state) {
return state.metamask.send.gasLimit
}
function getForceGasMin (state) {
return state.metamask.send.forceGasMin
}
function getSendFrom (state) {
return state.metamask.send.from
}
function getSendAmount (state) {
return state.metamask.send.amount
}
function getSendMaxModeState (state) {
return state.metamask.send.maxModeOn
}
function getCurrentCurrency (state) {
return state.metamask.currentCurrency
}
function getSelectedTokenToFiatRate (state) {
const selectedTokenExchangeRate = getSelectedTokenExchangeRate(state)
const conversionRate = conversionRateSelector(state)
const tokenToFiatRate = multiplyCurrencies(
conversionRate,
selectedTokenExchangeRate,
{ toNumericBase: 'dec' }
)
return tokenToFiatRate
}
function getSelectedTokenContract (state) {
const selectedToken = getSelectedToken(state)
return selectedToken
? global.eth.contract(abi).at(selectedToken.address)
: null
}
function autoAddToBetaUI (state) {
const autoAddTransactionThreshold = 12
const autoAddAccountsThreshold = 2
const autoAddTokensThreshold = 1
const numberOfTransactions = state.metamask.selectedAddressTxList.length
const numberOfAccounts = Object.keys(state.metamask.accounts).length
const numberOfTokensAdded = state.metamask.tokens.length
const userPassesThreshold = (numberOfTransactions > autoAddTransactionThreshold) &&
(numberOfAccounts > autoAddAccountsThreshold) &&
(numberOfTokensAdded > autoAddTokensThreshold)
const userIsNotInBeta = !state.metamask.featureFlags.betaUI
return userIsNotInBeta && userPassesThreshold
}
function getCurrentViewContext (state) {
const { currentView = {} } = state.appState
return currentView.context
}
function getSendErrors (state) {
return state.metamask.send.errors
}
function getSendTo (state) {
return state.metamask.send.to
}
function getSendToAccounts (state) {
const fromAccounts = accountsWithSendEtherInfoSelector(state)
const addressBookAccounts = getAddressBook(state)
const allAccounts = [...fromAccounts, ...addressBookAccounts]
// TODO: figure out exactly what the below returns and put a descriptive variable name on it
return Object.entries(allAccounts).map(([key, account]) => account)
}
function getCurrentNetwork (state) {
return state.metamask.network
}

View File

54
ui/app/ducks/send.js Normal file
View File

@ -0,0 +1,54 @@
import extend from 'xtend'
// Actions
const OPEN_FROM_DROPDOWN = 'metamask/send/OPEN_FROM_DROPDOWN';
const CLOSE_FROM_DROPDOWN = 'metamask/send/CLOSE_FROM_DROPDOWN';
const OPEN_TO_DROPDOWN = 'metamask/send/OPEN_TO_DROPDOWN';
const CLOSE_TO_DROPDOWN = 'metamask/send/CLOSE_TO_DROPDOWN';
// TODO: determine if this approach to initState is consistent with conventional ducks pattern
const initState = {
fromDropdownOpen: false,
toDropdownOpen: false,
}
// Reducer
export default function reducer(state = initState, action = {}) {
switch (action.type) {
case OPEN_FROM_DROPDOWN:
return extend(sendState, {
fromDropdownOpen: true,
})
case CLOSE_FROM_DROPDOWN:
return extend(sendState, {
fromDropdownOpen: false,
})
case OPEN_TO_DROPDOWN:
return extend(sendState, {
toDropdownOpen: true,
})
case CLOSE_TO_DROPDOWN:
return extend(sendState, {
toDropdownOpen: false,
})
default:
return sendState
}
}
// Action Creators
export function openFromDropdown() {
return { type: OPEN_FROM_DROPDOWN };
}
export function closeFromDropdown() {
return { type: CLOSE_FROM_DROPDOWN };
}
export function openToDropdown() {
return { type: OPEN_TO_DROPDOWN };
}
export function closeToDropdown() {
return { type: CLOSE_TO_DROPDOWN };
}

View File

@ -8,6 +8,7 @@ const reduceIdentities = require('./reducers/identities')
const reduceMetamask = require('./reducers/metamask')
const reduceApp = require('./reducers/app')
const reduceLocale = require('./reducers/locale')
const reduceSend = require('./ducks/send')
window.METAMASK_CACHED_LOG_STATE = null
@ -45,6 +46,12 @@ function rootReducer (state, action) {
state.localeMessages = reduceLocale(state, action)
//
// Send
//
state.send = reduceSend(state, action)
window.METAMASK_CACHED_LOG_STATE = state
return state
}

View File

@ -31,6 +31,11 @@ const {
} = require('./components/send/send-utils')
const { isValidAddress } = require('./util')
import PageContainer from './components/page-container/page-container.component'
import SendHeader from './components/send_/send-header/send-header.container'
import PageContainerContent from './components/page-container/page-container-content.component'
import PageContainerFooter from './components/page-container/page-container-footer.component'
SendTransactionScreen.contextTypes = {
t: PropTypes.func,
}
@ -181,25 +186,6 @@ SendTransactionScreen.prototype.componentDidUpdate = function (prevProps) {
}
}
SendTransactionScreen.prototype.renderHeader = function () {
const { selectedToken, clearSend, goHome } = this.props
return h('div.page-container__header', [
h('div.page-container__title', selectedToken ? this.context.t('sendTokens') : this.context.t('sendETH')),
h('div.page-container__subtitle', this.context.t('onlySendToEtherAddress')),
h('div.page-container__header-close', {
onClick: () => {
clearSend()
goHome()
},
}),
])
}
SendTransactionScreen.prototype.renderErrorMessage = function (errorType) {
const { errors } = this.props
const errorMessage = errors[errorType]
@ -477,7 +463,7 @@ SendTransactionScreen.prototype.renderMemoRow = function () {
}
SendTransactionScreen.prototype.renderForm = function () {
return h('.page-container__content', {}, [
return h(PageContainerContent, [
h('.send-v2__form', [
this.renderFromRow(),
@ -486,9 +472,6 @@ SendTransactionScreen.prototype.renderForm = function () {
this.renderAmountRow(),
this.renderGasRow(),
// this.renderMemoRow(),
]),
])
}
@ -506,26 +489,22 @@ SendTransactionScreen.prototype.renderFooter = function () {
const missingTokenBalance = selectedToken && !tokenBalance
const noErrors = !amountError && toError === null
return h('div.page-container__footer', [
h('button.btn-secondary--lg.page-container__footer-button', {
onClick: () => {
clearSend()
goHome()
},
}, this.context.t('cancel')),
h('button.btn-primary--lg.page-container__footer-button', {
disabled: !noErrors || !gasTotal || missingTokenBalance,
onClick: event => this.onSubmit(event),
}, this.context.t('next')),
])
return h(PageContainerFooter, {
onCancel: () => {
clearSend()
goHome()
},
onSubmit: e => this.onSubmit(e),
disabled: !noErrors || !gasTotal || missingTokenBalance,
})
}
SendTransactionScreen.prototype.render = function () {
return (
h('div.page-container', [
h(PageContainer, [
this.renderHeader(),
h(SendHeader),
this.renderForm(),