diff options
Diffstat (limited to 'old-ui/app/components')
44 files changed, 4852 insertions, 0 deletions
diff --git a/old-ui/app/components/account-dropdowns.js b/old-ui/app/components/account-dropdowns.js new file mode 100644 index 000000000..aa7a3ad67 --- /dev/null +++ b/old-ui/app/components/account-dropdowns.js @@ -0,0 +1,320 @@ +const Component = require('react').Component +const PropTypes = require('prop-types') +const h = require('react-hyperscript') +const actions = require('../../../ui/app/actions') +const genAccountLink = require('etherscan-link').createAccountLink +const connect = require('react-redux').connect +const Dropdown = require('./dropdown').Dropdown +const DropdownMenuItem = require('./dropdown').DropdownMenuItem +const Identicon = require('./identicon') +const ethUtil = require('ethereumjs-util') +const copyToClipboard = require('copy-to-clipboard') + +class AccountDropdowns extends Component { + constructor (props) { + super(props) + this.state = { + accountSelectorActive: false, + optionsMenuActive: false, + } + this.accountSelectorToggleClassName = 'accounts-selector' + this.optionsMenuToggleClassName = 'fa-ellipsis-h' + } + + renderAccounts () { + const { identities, selected, keyrings } = this.props + + return Object.keys(identities).map((key, index) => { + const identity = identities[key] + const isSelected = identity.address === selected + + const simpleAddress = identity.address.substring(2).toLowerCase() + + const keyring = keyrings.find((kr) => { + return kr.accounts.includes(simpleAddress) || + kr.accounts.includes(identity.address) + }) + + return h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + this.props.actions.showAccountDetail(identity.address) + }, + style: { + marginTop: index === 0 ? '5px' : '', + fontSize: '24px', + }, + }, + [ + h( + Identicon, + { + address: identity.address, + diameter: 32, + style: { + marginLeft: '10px', + }, + }, + ), + this.indicateIfLoose(keyring), + h('span', { + style: { + marginLeft: '20px', + fontSize: '24px', + maxWidth: '145px', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + }, identity.name || ''), + h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null), + ] + ) + }) + } + + indicateIfLoose (keyring) { + try { // Sometimes keyrings aren't loaded yet: + const type = keyring.type + const isLoose = type !== 'HD Key Tree' + return isLoose ? h('.keyring-label', 'LOOSE') : null + } catch (e) { return } + } + + renderAccountSelector () { + const { actions } = this.props + const { accountSelectorActive } = this.state + + return h( + Dropdown, + { + useCssTransition: true, // Hardcoded because account selector is temporarily in app-header + style: { + marginLeft: '-238px', + marginTop: '38px', + minWidth: '180px', + overflowY: 'auto', + maxHeight: '300px', + width: '300px', + }, + innerStyle: { + padding: '8px 25px', + }, + isOpen: accountSelectorActive, + onClickOutside: (event) => { + const { classList } = event.target + const isNotToggleElement = !classList.contains(this.accountSelectorToggleClassName) + if (accountSelectorActive && isNotToggleElement) { + this.setState({ accountSelectorActive: false }) + } + }, + }, + [ + ...this.renderAccounts(), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => actions.addNewAccount(), + }, + [ + h( + Identicon, + { + style: { + marginLeft: '10px', + }, + diameter: 32, + }, + ), + h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, 'Create Account'), + ], + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => actions.showImportPage(), + }, + [ + h( + Identicon, + { + style: { + marginLeft: '10px', + }, + diameter: 32, + }, + ), + h('span', { + style: { + marginLeft: '20px', + fontSize: '24px', + marginBottom: '5px', + }, + }, 'Import Account'), + ] + ), + ] + ) + } + + renderAccountOptions () { + const { actions } = this.props + const { optionsMenuActive } = this.state + + return h( + Dropdown, + { + style: { + marginLeft: '-215px', + minWidth: '180px', + }, + isOpen: optionsMenuActive, + onClickOutside: () => { + const { classList } = event.target + const isNotToggleElement = !classList.contains(this.optionsMenuToggleClassName) + if (optionsMenuActive && isNotToggleElement) { + this.setState({ optionsMenuActive: false }) + } + }, + }, + [ + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + const { selected, network } = this.props + const url = genAccountLink(selected, network) + global.platform.openWindow({ url }) + }, + }, + 'View account on Etherscan', + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + const { selected, identities } = this.props + var identity = identities[selected] + actions.showQrView(selected, identity ? identity.name : '') + }, + }, + 'Show QR Code', + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + const { selected } = this.props + const checkSumAddress = selected && ethUtil.toChecksumAddress(selected) + copyToClipboard(checkSumAddress) + }, + }, + 'Copy Address to clipboard', + ), + h( + DropdownMenuItem, + { + closeMenu: () => {}, + onClick: () => { + actions.requestAccountExport() + }, + }, + 'Export Private Key', + ), + ] + ) + } + + render () { + const { style, enableAccountsSelector, enableAccountOptions } = this.props + const { optionsMenuActive, accountSelectorActive } = this.state + + return h( + 'span', + { + style: style, + }, + [ + enableAccountsSelector && h( + // 'i.fa.fa-angle-down', + 'div.cursor-pointer.color-orange.accounts-selector', + { + style: { + // fontSize: '1.8em', + background: 'url(images/switch_acc.svg) white center center no-repeat', + height: '25px', + width: '25px', + transform: 'scale(0.75)', + marginRight: '3px', + }, + onClick: (event) => { + event.stopPropagation() + this.setState({ + accountSelectorActive: !accountSelectorActive, + optionsMenuActive: false, + }) + }, + }, + this.renderAccountSelector(), + ), + enableAccountOptions && h( + 'i.fa.fa-ellipsis-h', + { + style: { + margin: '0.5em', + fontSize: '1.8em', + }, + onClick: (event) => { + event.stopPropagation() + this.setState({ + accountSelectorActive: false, + optionsMenuActive: !optionsMenuActive, + }) + }, + }, + this.renderAccountOptions() + ), + ] + ) + } +} + +AccountDropdowns.defaultProps = { + enableAccountsSelector: false, + enableAccountOptions: false, +} + +AccountDropdowns.propTypes = { + identities: PropTypes.objectOf(PropTypes.object), + selected: PropTypes.string, + keyrings: PropTypes.array, + actions: PropTypes.objectOf(PropTypes.func), + network: PropTypes.string, + style: PropTypes.object, + enableAccountOptions: PropTypes.bool, + enableAccountsSelector: PropTypes.bool, +} + +const mapDispatchToProps = (dispatch) => { + return { + actions: { + showConfigPage: () => dispatch(actions.showConfigPage()), + requestAccountExport: () => dispatch(actions.requestExportAccount()), + showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)), + addNewAccount: () => dispatch(actions.addNewAccount()), + showImportPage: () => dispatch(actions.showImportPage()), + showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)), + }, + } +} + +module.exports = { + AccountDropdowns: connect(null, mapDispatchToProps)(AccountDropdowns), +} diff --git a/old-ui/app/components/account-export.js b/old-ui/app/components/account-export.js new file mode 100644 index 000000000..51b85b786 --- /dev/null +++ b/old-ui/app/components/account-export.js @@ -0,0 +1,132 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const exportAsFile = require('../util').exportAsFile +const copyToClipboard = require('copy-to-clipboard') +const actions = require('../../../ui/app/actions') +const ethUtil = require('ethereumjs-util') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(ExportAccountView) + +inherits(ExportAccountView, Component) +function ExportAccountView () { + Component.call(this) +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +ExportAccountView.prototype.render = function () { + const state = this.props + const accountDetail = state.accountDetail + const nickname = state.identities[state.address].name + + if (!accountDetail) return h('div') + const accountExport = accountDetail.accountExport + + const notExporting = accountExport === 'none' + const exportRequested = accountExport === 'requested' + const accountExported = accountExport === 'completed' + + if (notExporting) return h('div') + + if (exportRequested) { + const warning = `Export private keys at your own risk.` + return ( + h('div', { + style: { + display: 'inline-block', + textAlign: 'center', + }, + }, + [ + h('div', { + key: 'exporting', + style: { + margin: '0 20px', + }, + }, [ + h('p.error', warning), + h('input#exportAccount.sizing-input', { + type: 'password', + placeholder: 'confirm password', + onKeyPress: this.onExportKeyPress.bind(this), + style: { + position: 'relative', + top: '1.5px', + marginBottom: '7px', + }, + }), + ]), + h('div', { + key: 'buttons', + style: { + margin: '0 20px', + }, + }, + [ + h('button', { + onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }), + style: { + marginRight: '10px', + }, + }, 'Submit'), + h('button', { + onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), + }, 'Cancel'), + ]), + (this.props.warning) && ( + h('span.error', { + style: { + margin: '20px', + }, + }, this.props.warning.split('-')) + ), + ]) + ) + } + + if (accountExported) { + const plainKey = ethUtil.stripHexPrefix(accountDetail.privateKey) + + return h('div.privateKey', { + style: { + margin: '0 20px', + }, + }, [ + h('label', 'Your private key (click to copy):'), + h('p.error.cursor-pointer', { + style: { + textOverflow: 'ellipsis', + overflow: 'hidden', + webkitUserSelect: 'text', + maxWidth: '275px', + }, + onClick: function (event) { + copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey)) + }, + }, plainKey), + h('button', { + onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), + }, 'Done'), + h('button', { + style: { + marginLeft: '10px', + }, + onClick: () => exportAsFile(`MetaMask ${nickname} Private Key`, plainKey), + }, 'Save as File'), + ]) + } +} + +ExportAccountView.prototype.onExportKeyPress = function (event) { + if (event.key !== 'Enter') return + event.preventDefault() + + const input = document.getElementById('exportAccount').value + this.props.dispatch(actions.exportAccount(input, this.props.address)) +} diff --git a/old-ui/app/components/account-panel.js b/old-ui/app/components/account-panel.js new file mode 100644 index 000000000..abaaf8163 --- /dev/null +++ b/old-ui/app/components/account-panel.js @@ -0,0 +1,86 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const Identicon = require('./identicon') +const formatBalance = require('../util').formatBalance +const addressSummary = require('../util').addressSummary + +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 + + var panelState = { + key: `accountPanel${identity.address}`, + identiconKey: identity.address, + identiconLabel: identity.name || '', + attributes: [ + { + key: 'ADDRESS', + value: addressSummary(identity.address), + }, + balanceOrFaucetingIndication(account, isFauceting), + ], + } + + return ( + + h('.identity-panel.flex-row.flex-space-between', { + style: { + flex: '1 0 auto', + cursor: panelState.onClick ? 'pointer' : undefined, + }, + onClick: panelState.onClick, + }, [ + + // account identicon + h('.identicon-wrapper.flex-column.select-none', [ + h(Identicon, { + address: panelState.identiconKey, + imageify: state.imageifyIdenticons, + }), + h('span.font-small', panelState.identiconLabel.substring(0, 7) + '...'), + ]), + + // account address, balance + h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [ + + panelState.attributes.map((attr) => { + return h('.flex-row.flex-space-between', { + key: '' + Math.round(Math.random() * 1000000), + }, [ + h('label.font-small.no-select', attr.key), + h('span.font-small', attr.value), + ]) + }), + ]), + + ]) + + ) +} + +function balanceOrFaucetingIndication (account, isFauceting) { + // Temporarily deactivating isFauceting indication + // because it shows fauceting for empty restored accounts. + if (/* isFauceting*/ false) { + return { + key: 'Account is auto-funding.', + value: 'Please wait.', + } + } else { + return { + key: 'BALANCE', + value: formatBalance(account.balance), + } + } +} diff --git a/old-ui/app/components/balance.js b/old-ui/app/components/balance.js new file mode 100644 index 000000000..57ca84564 --- /dev/null +++ b/old-ui/app/components/balance.js @@ -0,0 +1,89 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance +const generateBalanceObject = require('../util').generateBalanceObject +const Tooltip = require('./tooltip.js') +const FiatValue = require('./fiat-value.js') + +module.exports = EthBalanceComponent + +inherits(EthBalanceComponent, Component) +function EthBalanceComponent () { + Component.call(this) +} + +EthBalanceComponent.prototype.render = function () { + var props = this.props + let { value } = props + var style = props.style + var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true + value = value ? formatBalance(value, 6, needsParse) : '...' + var width = props.width + + return ( + + h('.ether-balance.ether-balance-amount', { + style: style, + }, [ + h('div', { + style: { + display: 'inline', + width: width, + }, + }, this.renderBalance(value)), + ]) + + ) +} +EthBalanceComponent.prototype.renderBalance = function (value) { + var props = this.props + if (value === 'None') return value + if (value === '...') return value + var balanceObj = generateBalanceObject(value, props.shorten ? 1 : 3) + var balance + var splitBalance = value.split(' ') + var ethNumber = splitBalance[0] + var ethSuffix = splitBalance[1] + const showFiat = 'showFiat' in props ? props.showFiat : true + + if (props.shorten) { + balance = balanceObj.shortBalance + } else { + balance = balanceObj.balance + } + + var label = balanceObj.label + + return ( + h(Tooltip, { + position: 'bottom', + title: `${ethNumber} ${ethSuffix}`, + }, h('div.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + }, + }, this.props.incoming ? `+${balance}` : balance), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + }, + }, label), + ]), + + showFiat ? h(FiatValue, { value: props.value }) : null, + ])) + ) +} diff --git a/old-ui/app/components/binary-renderer.js b/old-ui/app/components/binary-renderer.js new file mode 100644 index 000000000..0b6a1f5c2 --- /dev/null +++ b/old-ui/app/components/binary-renderer.js @@ -0,0 +1,46 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const extend = require('xtend') + +module.exports = BinaryRenderer + +inherits(BinaryRenderer, Component) +function BinaryRenderer () { + Component.call(this) +} + +BinaryRenderer.prototype.render = function () { + const props = this.props + const { value, style } = props + const text = this.hexToText(value) + + const defaultStyle = extend({ + width: '315px', + maxHeight: '210px', + resize: 'none', + border: 'none', + background: 'white', + padding: '3px', + }, style) + + return ( + h('textarea.font-small', { + readOnly: true, + style: defaultStyle, + defaultValue: text, + }) + ) +} + +BinaryRenderer.prototype.hexToText = function (hex) { + try { + const stripped = ethUtil.stripHexPrefix(hex) + const buff = Buffer.from(stripped, 'hex') + return buff.toString('utf8') + } catch (e) { + return hex + } +} + diff --git a/old-ui/app/components/bn-as-decimal-input.js b/old-ui/app/components/bn-as-decimal-input.js new file mode 100644 index 000000000..22e37602e --- /dev/null +++ b/old-ui/app/components/bn-as-decimal-input.js @@ -0,0 +1,181 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const extend = require('xtend') + +module.exports = BnAsDecimalInput + +inherits(BnAsDecimalInput, Component) +function BnAsDecimalInput () { + this.state = { invalid: null } + Component.call(this) +} + +/* Bn as Decimal Input + * + * A component for allowing easy, decimal editing + * of a passed in bn string value. + * + * On change, calls back its `onChange` function parameter + * and passes it an updated bn string. + */ + +BnAsDecimalInput.prototype.render = function () { + const props = this.props + const state = this.state + + const { value, scale, precision, onChange, min, max } = props + + const suffix = props.suffix + const style = props.style + const valueString = value.toString(10) + const newMin = min && this.downsize(min.toString(10), scale) + const newMax = max && this.downsize(max.toString(10), scale) + const newValue = this.downsize(valueString, scale) + + return ( + h('.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('input.hex-input', { + type: 'number', + step: 'any', + required: true, + min: newMin, + max: newMax, + style: extend({ + display: 'block', + textAlign: 'right', + backgroundColor: 'transparent', + border: '1px solid #bdbdbd', + + }, style), + value: newValue, + onBlur: (event) => { + this.updateValidity(event) + }, + onChange: (event) => { + this.updateValidity(event) + const value = (event.target.value === '') ? '' : event.target.value + + + const scaledNumber = this.upsize(value, scale, precision) + const precisionBN = new BN(scaledNumber, 10) + onChange(precisionBN, event.target.checkValidity()) + }, + onInvalid: (event) => { + const msg = this.constructWarning() + if (msg === state.invalid) { + return + } + this.setState({ invalid: msg }) + event.preventDefault() + return false + }, + }), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + marginRight: '6px', + width: '20px', + }, + }, suffix), + ]), + + state.invalid ? h('span.error', { + style: { + position: 'absolute', + right: '0px', + textAlign: 'right', + transform: 'translateY(26px)', + padding: '3px', + background: 'rgba(255,255,255,0.85)', + zIndex: '1', + textTransform: 'capitalize', + border: '2px solid #E20202', + }, + }, state.invalid) : null, + ]) + ) +} + +BnAsDecimalInput.prototype.setValid = function (message) { + this.setState({ invalid: null }) +} + +BnAsDecimalInput.prototype.updateValidity = function (event) { + const target = event.target + const value = this.props.value + const newValue = target.value + + if (value === newValue) { + return + } + + const valid = target.checkValidity() + + if (valid) { + this.setState({ invalid: null }) + } +} + +BnAsDecimalInput.prototype.constructWarning = function () { + const { name, min, max, scale, suffix } = this.props + const newMin = min && this.downsize(min.toString(10), scale) + const newMax = max && this.downsize(max.toString(10), scale) + let message = name ? name + ' ' : '' + + if (min && max) { + message += `must be greater than or equal to ${newMin} ${suffix} and less than or equal to ${newMax} ${suffix}.` + } else if (min) { + message += `must be greater than or equal to ${newMin} ${suffix}.` + } else if (max) { + message += `must be less than or equal to ${newMax} ${suffix}.` + } else { + message += 'Invalid input.' + } + + return message +} + + +BnAsDecimalInput.prototype.downsize = function (number, scale) { + // if there is no scaling, simply return the number + if (scale === 0) { + return Number(number) + } else { + // if the scale is the same as the precision, account for this edge case. + var adjustedNumber = number + while (adjustedNumber.length < scale) { + adjustedNumber = '0' + adjustedNumber + } + return Number(adjustedNumber.slice(0, -scale) + '.' + adjustedNumber.slice(-scale)) + } +} + +BnAsDecimalInput.prototype.upsize = function (number, scale, precision) { + var stringArray = number.toString().split('.') + var decimalLength = stringArray[1] ? stringArray[1].length : 0 + var newString = stringArray[0] + + // If there is scaling and decimal parts exist, integrate them in. + if ((scale !== 0) && (decimalLength !== 0)) { + newString += stringArray[1].slice(0, precision) + } + + // Add 0s to account for the upscaling. + for (var i = decimalLength; i < scale; i++) { + newString += '0' + } + return newString +} diff --git a/old-ui/app/components/buy-button-subview.js b/old-ui/app/components/buy-button-subview.js new file mode 100644 index 000000000..843627c33 --- /dev/null +++ b/old-ui/app/components/buy-button-subview.js @@ -0,0 +1,262 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../../../ui/app/actions') +const CoinbaseForm = require('./coinbase-form') +const ShapeshiftForm = require('./shapeshift-form') +const Loading = require('./loading') +const AccountPanel = require('./account-panel') +const RadioList = require('./custom-radio-list') +const networkNames = require('../../../app/scripts/config.js').networkNames + +module.exports = connect(mapStateToProps)(BuyButtonSubview) + +function mapStateToProps (state) { + return { + identity: state.appState.identity, + account: state.metamask.accounts[state.appState.buyView.buyAddress], + warning: state.appState.warning, + buyView: state.appState.buyView, + network: state.metamask.network, + provider: state.metamask.provider, + context: state.appState.currentView.context, + isSubLoading: state.appState.isSubLoading, + } +} + +inherits(BuyButtonSubview, Component) +function BuyButtonSubview () { + Component.call(this) +} + +BuyButtonSubview.prototype.render = function () { + return ( + h('div', { + style: { + width: '100%', + }, + }, [ + this.headerSubview(), + this.primarySubview(), + ]) + ) +} + +BuyButtonSubview.prototype.headerSubview = function () { + const props = this.props + const isLoading = props.isSubLoading + return ( + + h('.flex-column', { + style: { + alignItems: 'center', + }, + }, [ + + // header bar (back button, label) + h('.flex-row', { + style: { + alignItems: 'center', + justifyContent: 'center', + }, + }, [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: this.backButtonContext.bind(this), + style: { + position: 'absolute', + left: '10px', + }, + }), + h('h2.text-transform-uppercase.flex-center', { + style: { + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, 'Buy Eth'), + ]), + + // loading indication + h('div', { + style: { + position: 'absolute', + top: '57vh', + left: '49vw', + }, + }, [ + h(Loading, { isLoading }), + ]), + + // account panel + h('div', { + style: { + width: '80%', + }, + }, [ + h(AccountPanel, { + showFullAddress: true, + identity: props.identity, + account: props.account, + }), + ]), + + h('.flex-row', { + style: { + alignItems: 'center', + justifyContent: 'center', + }, + }, [ + h('h3.text-transform-uppercase.flex-center', { + style: { + paddingLeft: '15px', + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, 'Select Service'), + ]), + + ]) + + ) +} + + +BuyButtonSubview.prototype.primarySubview = function () { + const props = this.props + const network = props.network + + switch (network) { + case 'loading': + return + + case '1': + return this.mainnetSubview() + + // Ropsten, Rinkeby, Kovan + case '3': + case '4': + case '42': + const networkName = networkNames[network] + const label = `${networkName} Test Faucet` + return ( + h('div.flex-column', { + style: { + alignItems: 'center', + margin: '20px 50px', + }, + }, [ + h('button.text-transform-uppercase', { + onClick: () => this.props.dispatch(actions.buyEth({ network })), + style: { + marginTop: '15px', + }, + }, label), + // Kovan only: Dharma loans beta + network === '42' ? ( + h('button.text-transform-uppercase', { + onClick: () => this.navigateTo('https://borrow.dharma.io/'), + style: { + marginTop: '15px', + }, + }, 'Borrow With Dharma (Beta)') + ) : null, + ]) + ) + + default: + return ( + h('h2.error', 'Unknown network ID') + ) + + } +} + +BuyButtonSubview.prototype.mainnetSubview = function () { + const props = this.props + + return ( + + h('.flex-column', { + style: { + alignItems: 'center', + }, + }, [ + + h('.flex-row.selected-exchange', { + style: { + position: 'relative', + right: '35px', + marginTop: '20px', + marginBottom: '20px', + }, + }, [ + h(RadioList, { + defaultFocus: props.buyView.subview, + labels: [ + 'Coinbase', + 'ShapeShift', + ], + subtext: { + 'Coinbase': 'Crypto/FIAT (USA only)', + 'ShapeShift': 'Crypto', + }, + onClick: this.radioHandler.bind(this), + }), + ]), + + h('h3.text-transform-uppercase', { + style: { + paddingLeft: '15px', + fontFamily: 'Montserrat Light', + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, props.buyView.subview), + + this.formVersionSubview(), + ]) + + ) +} + +BuyButtonSubview.prototype.formVersionSubview = function () { + const network = this.props.network + if (network === '1') { + if (this.props.buyView.formView.coinbase) { + return h(CoinbaseForm, this.props) + } else if (this.props.buyView.formView.shapeshift) { + return h(ShapeshiftForm, this.props) + } + } +} + +BuyButtonSubview.prototype.navigateTo = function (url) { + global.platform.openWindow({ url }) +} + +BuyButtonSubview.prototype.backButtonContext = function () { + if (this.props.context === 'confTx') { + this.props.dispatch(actions.showConfTxPage(false)) + } else { + console.log(`actions.goHome`, actions.goHome); + this.props.dispatch(actions.goHome()) + } +} + +BuyButtonSubview.prototype.radioHandler = function (event) { + switch (event.target.title) { + case 'Coinbase': + return this.props.dispatch(actions.coinBaseSubview()) + case 'ShapeShift': + return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type)) + } +} diff --git a/old-ui/app/components/coinbase-form.js b/old-ui/app/components/coinbase-form.js new file mode 100644 index 000000000..35b2111ff --- /dev/null +++ b/old-ui/app/components/coinbase-form.js @@ -0,0 +1,63 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../../../ui/app/actions') + +module.exports = connect(mapStateToProps)(CoinbaseForm) + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +inherits(CoinbaseForm, Component) + +function CoinbaseForm () { + Component.call(this) +} + +CoinbaseForm.prototype.render = function () { + var props = this.props + + return h('.flex-column', { + style: { + marginTop: '35px', + padding: '25px', + width: '100%', + }, + }, [ + h('.flex-row', { + style: { + justifyContent: 'space-around', + margin: '33px', + marginTop: '0px', + }, + }, [ + h('button.btn-green', { + onClick: this.toCoinbase.bind(this), + }, 'Continue to Coinbase'), + + h('button.btn-red', { + onClick: () => props.dispatch(actions.backTobuyView(props.accounts.address)), + }, 'Cancel'), + ]), + ]) +} + +CoinbaseForm.prototype.toCoinbase = function () { + const props = this.props + const address = props.buyView.buyAddress + props.dispatch(actions.buyEth({ network: '1', address, amount: 0 })) +} + +CoinbaseForm.prototype.renderLoading = function () { + return h('img', { + style: { + width: '27px', + marginRight: '-27px', + }, + src: 'images/loading.svg', + }) +} diff --git a/old-ui/app/components/copyButton.js b/old-ui/app/components/copyButton.js new file mode 100644 index 000000000..a25d0719c --- /dev/null +++ b/old-ui/app/components/copyButton.js @@ -0,0 +1,59 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const copyToClipboard = require('copy-to-clipboard') + +const Tooltip = require('./tooltip') + +module.exports = CopyButton + +inherits(CopyButton, Component) +function CopyButton () { + Component.call(this) +} + +// As parameters, accepts: +// "value", which is the value to copy (mandatory) +// "title", which is the text to show on hover (optional, defaults to 'Copy') +CopyButton.prototype.render = function () { + const props = this.props + const state = this.state || {} + + const value = props.value + const copied = state.copied + + const message = copied ? 'Copied' : props.title || ' Copy ' + + return h('.copy-button', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + + h(Tooltip, { + title: message, + }, [ + h('i.fa.fa-clipboard.cursor-pointer.color-orange', { + style: { + margin: '5px', + }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(value) + this.debounceRestore() + }, + }), + ]), + + ]) +} + +CopyButton.prototype.debounceRestore = function () { + this.setState({ copied: true }) + clearTimeout(this.timeout) + this.timeout = setTimeout(() => { + this.setState({ copied: false }) + }, 850) +} diff --git a/old-ui/app/components/copyable.js b/old-ui/app/components/copyable.js new file mode 100644 index 000000000..a4f6f4bc6 --- /dev/null +++ b/old-ui/app/components/copyable.js @@ -0,0 +1,46 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const Tooltip = require('./tooltip') +const copyToClipboard = require('copy-to-clipboard') + +module.exports = Copyable + +inherits(Copyable, Component) +function Copyable () { + Component.call(this) + this.state = { + copied: false, + } +} + +Copyable.prototype.render = function () { + const props = this.props + const state = this.state + const { value, children } = props + const { copied } = state + + return h(Tooltip, { + title: copied ? 'Copied!' : 'Copy', + position: 'bottom', + }, h('span', { + style: { + cursor: 'pointer', + }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(value) + this.debounceRestore() + }, + }, children)) +} + +Copyable.prototype.debounceRestore = function () { + this.setState({ copied: true }) + clearTimeout(this.timeout) + this.timeout = setTimeout(() => { + this.setState({ copied: false }) + }, 850) +} diff --git a/old-ui/app/components/custom-radio-list.js b/old-ui/app/components/custom-radio-list.js new file mode 100644 index 000000000..a4c525396 --- /dev/null +++ b/old-ui/app/components/custom-radio-list.js @@ -0,0 +1,60 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = RadioList + +inherits(RadioList, Component) +function RadioList () { + Component.call(this) +} + +RadioList.prototype.render = function () { + const props = this.props + const activeClass = '.custom-radio-selected' + const inactiveClass = '.custom-radio-inactive' + const { + labels, + defaultFocus, + } = props + + + return ( + h('.flex-row', { + style: { + fontSize: '12px', + }, + }, [ + h('.flex-column.custom-radios', { + style: { + marginRight: '5px', + }, + }, + labels.map((lable, i) => { + let isSelcted = (this.state !== null) + isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable) + return h(isSelcted ? activeClass : inactiveClass, { + title: lable, + onClick: (event) => { + this.setState({selected: event.target.title}) + props.onClick(event) + }, + }) + }) + ), + h('.text', {}, + labels.map((lable) => { + if (props.subtext) { + return h('.flex-row', {}, [ + h('.radio-titles', lable), + h('.radio-titles-subtext', `- ${props.subtext[lable]}`), + ]) + } else { + return h('.radio-titles', lable) + } + }) + ), + ]) + ) +} + diff --git a/old-ui/app/components/dropdown.js b/old-ui/app/components/dropdown.js new file mode 100644 index 000000000..fb606d2c5 --- /dev/null +++ b/old-ui/app/components/dropdown.js @@ -0,0 +1,98 @@ +const Component = require('react').Component +const PropTypes = require('prop-types') +const h = require('react-hyperscript') +const MenuDroppo = require('./menu-droppo') +const extend = require('xtend') + +const noop = () => {} + +class Dropdown extends Component { + render () { + const { isOpen, onClickOutside, style, innerStyle, children, useCssTransition } = this.props + + const innerStyleDefaults = extend({ + borderRadius: '4px', + padding: '8px 16px', + background: 'rgba(0, 0, 0, 0.8)', + boxShadow: 'rgba(0, 0, 0, 0.15) 0px 2px 2px 2px', + }, innerStyle) + + return h( + MenuDroppo, + { + useCssTransition, + isOpen, + zIndex: 11, + onClickOutside, + style, + innerStyle: innerStyleDefaults, + }, + [ + h( + 'style', + ` + li.dropdown-menu-item:hover { color:rgb(225, 225, 225); } + li.dropdown-menu-item { color: rgb(185, 185, 185); position: relative } + ` + ), + ...children, + ] + ) + } +} + +Dropdown.defaultProps = { + isOpen: false, + onClick: noop, + useCssTransition: false, +} + +Dropdown.propTypes = { + isOpen: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, + children: PropTypes.node, + style: PropTypes.object.isRequired, + onClickOutside: PropTypes.func, + innerStyle: PropTypes.object, + useCssTransition: PropTypes.bool, +} + +class DropdownMenuItem extends Component { + render () { + const { onClick, closeMenu, children, style } = this.props + + return h( + 'li.dropdown-menu-item', + { + onClick: () => { + onClick() + closeMenu() + }, + style: Object.assign({ + listStyle: 'none', + padding: '8px 0px 8px 0px', + fontSize: '18px', + fontStyle: 'normal', + fontFamily: 'Montserrat Regular', + cursor: 'pointer', + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center', + }, style), + }, + children + ) + } +} + +DropdownMenuItem.propTypes = { + closeMenu: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, + children: PropTypes.node, + style: PropTypes.object, +} + +module.exports = { + Dropdown, + DropdownMenuItem, +} diff --git a/old-ui/app/components/editable-label.js b/old-ui/app/components/editable-label.js new file mode 100644 index 000000000..8a5c8954f --- /dev/null +++ b/old-ui/app/components/editable-label.js @@ -0,0 +1,57 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const findDOMNode = require('react-dom').findDOMNode + +module.exports = EditableLabel + +inherits(EditableLabel, Component) +function EditableLabel () { + Component.call(this) +} + +EditableLabel.prototype.render = function () { + const props = this.props + const state = this.state + + if (state && state.isEditingLabel) { + return h('div.editable-label', [ + h('input.sizing-input', { + defaultValue: props.textValue, + maxLength: '20', + onKeyPress: (event) => { + this.saveIfEnter(event) + }, + }), + h('button.editable-button', { + onClick: () => this.saveText(), + }, 'Save'), + ]) + } else { + return h('div.name-label', { + onClick: (event) => { + const nameAttribute = event.target.getAttribute('name') + // checks for class to handle smaller CTA above the account name + const classAttribute = event.target.getAttribute('class') + if (nameAttribute === 'edit' || classAttribute === 'edit-text') { + this.setState({ isEditingLabel: true }) + } + }, + }, this.props.children) + } +} + +EditableLabel.prototype.saveIfEnter = function (event) { + if (event.key === 'Enter') { + this.saveText() + } +} + +EditableLabel.prototype.saveText = function () { + // eslint-disable-next-line react/no-find-dom-node + var container = findDOMNode(this) + var text = container.querySelector('.editable-label input').value + var truncatedText = text.substring(0, 20) + this.props.saveText(truncatedText) + this.setState({ isEditingLabel: false, textLabel: truncatedText }) +} diff --git a/old-ui/app/components/ens-input.js b/old-ui/app/components/ens-input.js new file mode 100644 index 000000000..c85a23514 --- /dev/null +++ b/old-ui/app/components/ens-input.js @@ -0,0 +1,170 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const extend = require('xtend') +const debounce = require('debounce') +const copyToClipboard = require('copy-to-clipboard') +const ENS = require('ethjs-ens') +const networkMap = require('ethjs-ens/lib/network-map.json') +const ensRE = /.+\..+$/ +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + + +module.exports = EnsInput + +inherits(EnsInput, Component) +function EnsInput () { + Component.call(this) +} + +EnsInput.prototype.render = function () { + const props = this.props + const opts = extend(props, { + list: 'addresses', + onChange: () => { + const network = this.props.network + const networkHasEnsSupport = getNetworkEnsSupport(network) + if (!networkHasEnsSupport) return + + const recipient = document.querySelector('input[name="address"]').value + if (recipient.match(ensRE) === null) { + return this.setState({ + loadingEns: false, + ensResolution: null, + ensFailure: null, + }) + } + + this.setState({ + loadingEns: true, + }) + this.checkName() + }, + }) + return h('div', { + style: { width: '100%' }, + }, [ + h('input.large-input', opts), + // The address book functionality. + h('datalist#addresses', + [ + // Corresponds to the addresses owned. + Object.keys(props.identities).map((key) => { + const identity = props.identities[key] + return h('option', { + value: identity.address, + label: identity.name, + key: identity.address, + }) + }), + // Corresponds to previously sent-to addresses. + props.addressBook.map((identity) => { + return h('option', { + value: identity.address, + label: identity.name, + key: identity.address, + }) + }), + ]), + this.ensIcon(), + ]) +} + +EnsInput.prototype.componentDidMount = function () { + const network = this.props.network + const networkHasEnsSupport = getNetworkEnsSupport(network) + this.setState({ ensResolution: ZERO_ADDRESS }) + + if (networkHasEnsSupport) { + const provider = global.ethereumProvider + this.ens = new ENS({ provider, network }) + this.checkName = debounce(this.lookupEnsName.bind(this), 200) + } +} + +EnsInput.prototype.lookupEnsName = function () { + const recipient = document.querySelector('input[name="address"]').value + const { ensResolution } = this.state + + log.info(`ENS attempting to resolve name: ${recipient}`) + this.ens.lookup(recipient.trim()) + .then((address) => { + if (address === ZERO_ADDRESS) throw new Error('No address has been set for this name.') + if (address !== ensResolution) { + this.setState({ + loadingEns: false, + ensResolution: address, + nickname: recipient.trim(), + hoverText: address + '\nClick to Copy', + ensFailure: false, + }) + } + }) + .catch((reason) => { + log.error(reason) + return this.setState({ + loadingEns: false, + ensResolution: ZERO_ADDRESS, + ensFailure: true, + hoverText: reason.message, + }) + }) +} + +EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { + const state = this.state || {} + const ensResolution = state.ensResolution + // If an address is sent without a nickname, meaning not from ENS or from + // the user's own accounts, a default of a one-space string is used. + const nickname = state.nickname || ' ' + if (prevState && ensResolution && this.props.onChange && + ensResolution !== prevState.ensResolution) { + this.props.onChange(ensResolution, nickname) + } +} + +EnsInput.prototype.ensIcon = function (recipient) { + const { hoverText } = this.state || {} + return h('span', { + title: hoverText, + style: { + position: 'absolute', + padding: '9px', + transform: 'translatex(-40px)', + }, + }, this.ensIconContents(recipient)) +} + +EnsInput.prototype.ensIconContents = function (recipient) { + const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: ZERO_ADDRESS} + + if (loadingEns) { + return h('img', { + src: 'images/loading.svg', + style: { + width: '30px', + height: '30px', + transform: 'translateY(-6px)', + }, + }) + } + + if (ensFailure) { + return h('i.fa.fa-warning.fa-lg.warning') + } + + if (ensResolution && (ensResolution !== ZERO_ADDRESS)) { + return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', { + style: { color: 'green' }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(ensResolution) + }, + }) + } +} + +function getNetworkEnsSupport (network) { + return Boolean(networkMap[network]) +} diff --git a/old-ui/app/components/eth-balance.js b/old-ui/app/components/eth-balance.js new file mode 100644 index 000000000..4f538fd31 --- /dev/null +++ b/old-ui/app/components/eth-balance.js @@ -0,0 +1,89 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance +const generateBalanceObject = require('../util').generateBalanceObject +const Tooltip = require('./tooltip.js') +const FiatValue = require('./fiat-value.js') + +module.exports = EthBalanceComponent + +inherits(EthBalanceComponent, Component) +function EthBalanceComponent () { + Component.call(this) +} + +EthBalanceComponent.prototype.render = function () { + var props = this.props + let { value } = props + const { style, width } = props + var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true + value = value ? formatBalance(value, 6, needsParse) : '...' + + return ( + + h('.ether-balance.ether-balance-amount', { + style, + }, [ + h('div', { + style: { + display: 'inline', + width, + }, + }, this.renderBalance(value)), + ]) + + ) +} +EthBalanceComponent.prototype.renderBalance = function (value) { + var props = this.props + const { conversionRate, shorten, incoming, currentCurrency } = props + if (value === 'None') return value + if (value === '...') return value + var balanceObj = generateBalanceObject(value, shorten ? 1 : 3) + var balance + var splitBalance = value.split(' ') + var ethNumber = splitBalance[0] + var ethSuffix = splitBalance[1] + const showFiat = 'showFiat' in props ? props.showFiat : true + + if (shorten) { + balance = balanceObj.shortBalance + } else { + balance = balanceObj.balance + } + + var label = balanceObj.label + + return ( + h(Tooltip, { + position: 'bottom', + title: `${ethNumber} ${ethSuffix}`, + }, h('div.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + }, + }, incoming ? `+${balance}` : balance), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + }, + }, label), + ]), + + showFiat ? h(FiatValue, { value: props.value, conversionRate, currentCurrency }) : null, + ])) + ) +} diff --git a/old-ui/app/components/fiat-value.js b/old-ui/app/components/fiat-value.js new file mode 100644 index 000000000..d69f41d11 --- /dev/null +++ b/old-ui/app/components/fiat-value.js @@ -0,0 +1,64 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance + +module.exports = FiatValue + +inherits(FiatValue, Component) +function FiatValue () { + Component.call(this) +} + +FiatValue.prototype.render = function () { + const props = this.props + const { conversionRate, currentCurrency } = props + const renderedCurrency = currentCurrency || '' + + const value = formatBalance(props.value, 6) + + if (value === 'None') return value + var fiatDisplayNumber, fiatTooltipNumber + var splitBalance = value.split(' ') + + if (conversionRate !== 0) { + fiatTooltipNumber = Number(splitBalance[0]) * conversionRate + fiatDisplayNumber = fiatTooltipNumber.toFixed(2) + } else { + fiatDisplayNumber = 'N/A' + fiatTooltipNumber = 'Unknown' + } + + return fiatDisplay(fiatDisplayNumber, renderedCurrency.toUpperCase()) +} + +function fiatDisplay (fiatDisplayNumber, fiatSuffix) { + if (fiatDisplayNumber !== 'N/A') { + return h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + fontSize: '12px', + color: '#333333', + }, + }, fiatDisplayNumber), + h('div', { + style: { + color: '#AEAEAE', + marginLeft: '5px', + fontSize: '12px', + }, + }, fiatSuffix), + ]) + } else { + return h('div') + } +} diff --git a/old-ui/app/components/hex-as-decimal-input.js b/old-ui/app/components/hex-as-decimal-input.js new file mode 100644 index 000000000..4a71e9585 --- /dev/null +++ b/old-ui/app/components/hex-as-decimal-input.js @@ -0,0 +1,154 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const extend = require('xtend') + +module.exports = HexAsDecimalInput + +inherits(HexAsDecimalInput, Component) +function HexAsDecimalInput () { + this.state = { invalid: null } + Component.call(this) +} + +/* Hex as Decimal Input + * + * A component for allowing easy, decimal editing + * of a passed in hex string value. + * + * On change, calls back its `onChange` function parameter + * and passes it an updated hex string. + */ + +HexAsDecimalInput.prototype.render = function () { + const props = this.props + const state = this.state + + const { value, onChange, min, max } = props + + const toEth = props.toEth + const suffix = props.suffix + const decimalValue = decimalize(value, toEth) + const style = props.style + + return ( + h('.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('input.hex-input', { + type: 'number', + required: true, + min: min, + max: max, + style: extend({ + display: 'block', + textAlign: 'right', + backgroundColor: 'transparent', + border: '1px solid #bdbdbd', + + }, style), + value: parseInt(decimalValue), + onBlur: (event) => { + this.updateValidity(event) + }, + onChange: (event) => { + this.updateValidity(event) + const hexString = (event.target.value === '') ? '' : hexify(event.target.value) + onChange(hexString) + }, + onInvalid: (event) => { + const msg = this.constructWarning() + if (msg === state.invalid) { + return + } + this.setState({ invalid: msg }) + event.preventDefault() + return false + }, + }), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + marginRight: '6px', + width: '20px', + }, + }, suffix), + ]), + + state.invalid ? h('span.error', { + style: { + position: 'absolute', + right: '0px', + textAlign: 'right', + transform: 'translateY(26px)', + padding: '3px', + background: 'rgba(255,255,255,0.85)', + zIndex: '1', + textTransform: 'capitalize', + border: '2px solid #E20202', + }, + }, state.invalid) : null, + ]) + ) +} + +HexAsDecimalInput.prototype.setValid = function (message) { + this.setState({ invalid: null }) +} + +HexAsDecimalInput.prototype.updateValidity = function (event) { + const target = event.target + const value = this.props.value + const newValue = target.value + + if (value === newValue) { + return + } + + const valid = target.checkValidity() + if (valid) { + this.setState({ invalid: null }) + } +} + +HexAsDecimalInput.prototype.constructWarning = function () { + const { name, min, max } = this.props + let message = name ? name + ' ' : '' + + if (min && max) { + message += `must be greater than or equal to ${min} and less than or equal to ${max}.` + } else if (min) { + message += `must be greater than or equal to ${min}.` + } else if (max) { + message += `must be less than or equal to ${max}.` + } else { + message += 'Invalid input.' + } + + return message +} + +function hexify (decimalString) { + const hexBN = new BN(parseInt(decimalString), 10) + return '0x' + hexBN.toString('hex') +} + +function decimalize (input, toEth) { + if (input === '') { + return '' + } else { + const strippedInput = ethUtil.stripHexPrefix(input) + const inputBN = new BN(strippedInput, 'hex') + return inputBN.toString(10) + } +} diff --git a/old-ui/app/components/identicon.js b/old-ui/app/components/identicon.js new file mode 100644 index 000000000..bb476ca7b --- /dev/null +++ b/old-ui/app/components/identicon.js @@ -0,0 +1,74 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const isNode = require('detect-node') +const findDOMNode = require('react-dom').findDOMNode +const jazzicon = require('jazzicon') +const iconFactoryGen = require('../../lib/icon-factory') +const iconFactory = iconFactoryGen(jazzicon) + +module.exports = IdenticonComponent + +inherits(IdenticonComponent, Component) +function IdenticonComponent () { + Component.call(this) + + this.defaultDiameter = 46 +} + +IdenticonComponent.prototype.render = function () { + var props = this.props + var diameter = props.diameter || this.defaultDiameter + return ( + h('div', { + key: 'identicon-' + this.props.address, + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: diameter, + width: diameter, + borderRadius: diameter / 2, + overflow: 'hidden', + }, + }) + ) +} + +IdenticonComponent.prototype.componentDidMount = function () { + var props = this.props + const { address } = props + + if (!address) return + + // eslint-disable-next-line react/no-find-dom-node + var container = findDOMNode(this) + + var diameter = props.diameter || this.defaultDiameter + if (!isNode) { + var img = iconFactory.iconForAddress(address, diameter) + container.appendChild(img) + } +} + +IdenticonComponent.prototype.componentDidUpdate = function () { + var props = this.props + const { address } = props + + if (!address) return + + // eslint-disable-next-line react/no-find-dom-node + var container = findDOMNode(this) + + var children = container.children + for (var i = 0; i < children.length; i++) { + container.removeChild(children[i]) + } + + var diameter = props.diameter || this.defaultDiameter + if (!isNode) { + var img = iconFactory.iconForAddress(address, diameter) + container.appendChild(img) + } +} + diff --git a/old-ui/app/components/loading.js b/old-ui/app/components/loading.js new file mode 100644 index 000000000..b8e2eb599 --- /dev/null +++ b/old-ui/app/components/loading.js @@ -0,0 +1,55 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') + + +inherits(LoadingIndicator, Component) +module.exports = LoadingIndicator + +function LoadingIndicator () { + Component.call(this) +} + +LoadingIndicator.prototype.render = function () { + const { isLoading, loadingMessage, canBypass, bypass } = this.props + + return ( + isLoading ? h('.full-flex-height', { + style: { + left: '0px', + zIndex: 10, + position: 'absolute', + flexDirection: 'column', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '100%', + width: '100%', + background: 'rgba(255, 255, 255, 0.8)', + }, + }, [ + canBypass ? h( 'i.fa.fa-close.cursor-pointer.close-loading', { + style: { + position: 'absolute', + top: '1px', + right: '15px', + color: '#AEAEAE', + }, + onClick: bypass, + }) : null, + + h('img', { + src: 'images/loading.svg', + }), + + h('br'), + + showMessageIfAny(loadingMessage), + ]) : null + ) +} + +function showMessageIfAny (loadingMessage) { + if (!loadingMessage) return null + return h('span', loadingMessage) +} diff --git a/old-ui/app/components/mascot.js b/old-ui/app/components/mascot.js new file mode 100644 index 000000000..973ec2cad --- /dev/null +++ b/old-ui/app/components/mascot.js @@ -0,0 +1,59 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const metamaskLogo = require('metamask-logo') +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, + }) + + 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', { + style: { zIndex: 0 }, + }) +} + +Mascot.prototype.componentDidMount = function () { + var targetDivId = 'metamask-mascot-container' + var container = document.getElementById(targetDivId) + container.appendChild(this.logo.container) +} + +Mascot.prototype.componentWillUnmount = function () { + this.animations = this.props.animationEventEmitter + this.animations.removeAllListeners() + this.logo.container.remove() + this.logo.stopAnimation() +} + +Mascot.prototype.handleAnimationEvents = function () { + // 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) { + this.unfollowMouse() + this.logo.lookAt(target) + this.refollowMouse() +} diff --git a/old-ui/app/components/menu-droppo.js b/old-ui/app/components/menu-droppo.js new file mode 100644 index 000000000..e6276f3b1 --- /dev/null +++ b/old-ui/app/components/menu-droppo.js @@ -0,0 +1,132 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const findDOMNode = require('react-dom').findDOMNode +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') + +module.exports = MenuDroppoComponent + + +inherits(MenuDroppoComponent, Component) +function MenuDroppoComponent () { + Component.call(this) +} + +MenuDroppoComponent.prototype.render = function () { + const speed = this.props.speed || '300ms' + const useCssTransition = this.props.useCssTransition + const zIndex = ('zIndex' in this.props) ? this.props.zIndex : 0 + + this.manageListeners() + + const style = this.props.style || {} + if (!('position' in style)) { + style.position = 'fixed' + } + style.zIndex = zIndex + + return ( + h('.menu-droppo-container', { + style, + }, [ + h('style', ` + .menu-droppo-enter { + transition: transform ${speed} ease-in-out; + transform: translateY(-200%); + } + + .menu-droppo-enter.menu-droppo-enter-active { + transition: transform ${speed} ease-in-out; + transform: translateY(0%); + } + + .menu-droppo-leave { + transition: transform ${speed} ease-in-out; + transform: translateY(0%); + } + + .menu-droppo-leave.menu-droppo-leave-active { + transition: transform ${speed} ease-in-out; + transform: translateY(-200%); + } + `), + + useCssTransition + ? h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'menu-droppo', + transitionEnterTimeout: parseInt(speed), + transitionLeaveTimeout: parseInt(speed), + }, this.renderPrimary()) + : this.renderPrimary(), + ]) + ) +} + +MenuDroppoComponent.prototype.renderPrimary = function () { + const isOpen = this.props.isOpen + if (!isOpen) { + return null + } + + const innerStyle = this.props.innerStyle || {} + + return ( + h('.menu-droppo', { + key: 'menu-droppo-drawer', + style: innerStyle, + }, + [ this.props.children ]) + ) +} + +MenuDroppoComponent.prototype.manageListeners = function () { + const isOpen = this.props.isOpen + const onClickOutside = this.props.onClickOutside + + if (isOpen) { + this.outsideClickHandler = onClickOutside + } else if (isOpen) { + this.outsideClickHandler = null + } +} + +MenuDroppoComponent.prototype.componentDidMount = function () { + if (this && document.body) { + this.globalClickHandler = this.globalClickOccurred.bind(this) + document.body.addEventListener('click', this.globalClickHandler) + // eslint-disable-next-line react/no-find-dom-node + var container = findDOMNode(this) + this.container = container + } +} + +MenuDroppoComponent.prototype.componentWillUnmount = function () { + if (this && document.body) { + document.body.removeEventListener('click', this.globalClickHandler) + } +} + +MenuDroppoComponent.prototype.globalClickOccurred = function (event) { + const target = event.target + // eslint-disable-next-line react/no-find-dom-node + const container = findDOMNode(this) + + if (target !== container && + !isDescendant(this.container, event.target) && + this.outsideClickHandler) { + this.outsideClickHandler(event) + } +} + +function isDescendant (parent, child) { + var node = child.parentNode + while (node !== null) { + if (node === parent) { + return true + } + node = node.parentNode + } + + return false +} diff --git a/old-ui/app/components/mini-account-panel.js b/old-ui/app/components/mini-account-panel.js new file mode 100644 index 000000000..c09cf5b7a --- /dev/null +++ b/old-ui/app/components/mini-account-panel.js @@ -0,0 +1,74 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const Identicon = require('./identicon') + +module.exports = AccountPanel + + +inherits(AccountPanel, Component) +function AccountPanel () { + Component.call(this) +} + +AccountPanel.prototype.render = function () { + var props = this.props + var picOrder = props.picOrder || 'left' + const { imageSeed } = props + + return ( + + h('.identity-panel.flex-row.flex-left', { + style: { + cursor: props.onClick ? 'pointer' : undefined, + }, + onClick: props.onClick, + }, [ + + this.genIcon(imageSeed, picOrder), + + h('div.flex-column.flex-justify-center', { + style: { + lineHeight: '15px', + order: 2, + display: 'flex', + alignItems: picOrder === 'left' ? 'flex-begin' : 'flex-end', + }, + }, this.props.children), + ]) + ) +} + +AccountPanel.prototype.genIcon = function (seed, picOrder) { + const props = this.props + + // When there is no seed value, this is a contract creation. + // We then show the contract icon. + if (!seed) { + return h('.identicon-wrapper.flex-column.select-none', { + style: { + order: picOrder === 'left' ? 1 : 3, + }, + }, [ + h('i.fa.fa-file-text-o.fa-lg', { + style: { + fontSize: '42px', + transform: 'translate(0px, -16px)', + }, + }), + ]) + } + + // If there was a seed, we return an identicon for that address. + return h('.identicon-wrapper.flex-column.select-none', { + style: { + order: picOrder === 'left' ? 1 : 3, + }, + }, [ + h(Identicon, { + address: seed, + imageify: props.imageifyIdenticons, + }), + ]) +} + diff --git a/old-ui/app/components/network.js b/old-ui/app/components/network.js new file mode 100644 index 000000000..0dbe37cdd --- /dev/null +++ b/old-ui/app/components/network.js @@ -0,0 +1,129 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = Network + +inherits(Network, Component) + +function Network () { + Component.call(this) +} + +Network.prototype.render = function () { + const props = this.props + const networkNumber = props.network + let providerName + try { + providerName = props.provider.type + } catch (e) { + providerName = null + } + let iconName, hoverText + + if (networkNumber === 'loading') { + return h('span.pointer', { + style: { + display: 'flex', + alignItems: 'center', + flexDirection: 'row', + }, + onClick: (event) => this.props.onClick(event), + }, [ + h('img', { + title: 'Attempting to connect to blockchain.', + style: { + width: '27px', + }, + src: 'images/loading.svg', + }), + h('i.fa.fa-caret-down'), + ]) + } else if (providerName === 'mainnet') { + hoverText = 'Main Ethereum Network' + iconName = 'ethereum-network' + } else if (providerName === 'ropsten') { + hoverText = 'Ropsten Test Network' + iconName = 'ropsten-test-network' + } else if (parseInt(networkNumber) === 3) { + hoverText = 'Ropsten Test Network' + iconName = 'ropsten-test-network' + } else if (providerName === 'kovan') { + hoverText = 'Kovan Test Network' + iconName = 'kovan-test-network' + } else if (providerName === 'rinkeby') { + hoverText = 'Rinkeby Test Network' + iconName = 'rinkeby-test-network' + } else { + hoverText = 'Unknown Private Network' + iconName = 'unknown-private-network' + } + + return ( + h('#network_component.pointer', { + title: hoverText, + onClick: (event) => this.props.onClick(event), + }, [ + (function () { + switch (iconName) { + case 'ethereum-network': + return h('.network-indicator', [ + h('.menu-icon.diamond'), + h('.network-name', { + style: { + color: '#039396', + }}, + 'Main Network'), + h('i.fa.fa-caret-down.fa-lg'), + ]) + case 'ropsten-test-network': + return h('.network-indicator', [ + h('.menu-icon.red-dot'), + h('.network-name', { + style: { + color: '#ff6666', + }}, + 'Ropsten Test Net'), + h('i.fa.fa-caret-down.fa-lg'), + ]) + case 'kovan-test-network': + return h('.network-indicator', [ + h('.menu-icon.hollow-diamond'), + h('.network-name', { + style: { + color: '#690496', + }}, + 'Kovan Test Net'), + h('i.fa.fa-caret-down.fa-lg'), + ]) + case 'rinkeby-test-network': + return h('.network-indicator', [ + h('.menu-icon.golden-square'), + h('.network-name', { + style: { + color: '#e7a218', + }}, + 'Rinkeby Test Net'), + h('i.fa.fa-caret-down.fa-lg'), + ]) + default: + return h('.network-indicator', [ + h('i.fa.fa-question-circle.fa-lg', { + style: { + margin: '10px', + color: 'rgb(125, 128, 130)', + }, + }), + + h('.network-name', { + style: { + color: '#AEAEAE', + }}, + 'Private Network'), + h('i.fa.fa-caret-down.fa-lg'), + ]) + } + })(), + ]) + ) +} diff --git a/old-ui/app/components/notice.js b/old-ui/app/components/notice.js new file mode 100644 index 000000000..09d461c7b --- /dev/null +++ b/old-ui/app/components/notice.js @@ -0,0 +1,132 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const ReactMarkdown = require('react-markdown') +const linker = require('extension-link-enabler') +const findDOMNode = require('react-dom').findDOMNode + +module.exports = Notice + +inherits(Notice, Component) +function Notice () { + Component.call(this) +} + +Notice.prototype.render = function () { + const { notice, onConfirm } = this.props + const { title, date, body } = notice + const state = this.state || { disclaimerDisabled: true } + const disabled = state.disclaimerDisabled + + return ( + h('.flex-column.flex-center.flex-grow', { + style: { + width: '100%', + }, + }, [ + h('h3.flex-center.text-transform-uppercase.terms-header', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + width: '100%', + fontSize: '20px', + textAlign: 'center', + padding: 6, + }, + }, [ + title, + ]), + + h('h5.flex-center.text-transform-uppercase.terms-header', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + textAlign: 'center', + padding: 6, + }, + }, [ + date, + ]), + + h('style', ` + + .markdown { + overflow-x: hidden; + } + + .markdown h1, .markdown h2, .markdown h3 { + margin: 10px 0; + font-weight: bold; + } + + .markdown strong { + font-weight: bold; + } + .markdown em { + font-style: italic; + } + + .markdown p { + margin: 10px 0; + } + + .markdown a { + color: #df6b0e; + } + + `), + + h('div.markdown', { + onScroll: (e) => { + var object = e.currentTarget + if (object.offsetHeight + object.scrollTop + 100 >= object.scrollHeight) { + this.setState({disclaimerDisabled: false}) + } + }, + style: { + background: 'rgb(235, 235, 235)', + height: '310px', + padding: '6px', + width: '90%', + overflowY: 'scroll', + scroll: 'auto', + }, + }, [ + h(ReactMarkdown, { + className: 'notice-box', + source: body, + skipHtml: true, + }), + ]), + + h('button', { + disabled, + onClick: () => { + this.setState({disclaimerDisabled: true}) + onConfirm() + }, + style: { + marginTop: '18px', + }, + }, 'Accept'), + ]) + ) +} + +Notice.prototype.componentDidMount = function () { + // eslint-disable-next-line react/no-find-dom-node + var node = findDOMNode(this) + linker.setupListener(node) + if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) { + this.setState({disclaimerDisabled: false}) + } +} + +Notice.prototype.componentWillUnmount = function () { + // eslint-disable-next-line react/no-find-dom-node + var node = findDOMNode(this) + linker.teardownListener(node) +} diff --git a/old-ui/app/components/pending-msg-details.js b/old-ui/app/components/pending-msg-details.js new file mode 100644 index 000000000..718a22de0 --- /dev/null +++ b/old-ui/app/components/pending-msg-details.js @@ -0,0 +1,50 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const AccountPanel = require('./account-panel') + +module.exports = PendingMsgDetails + +inherits(PendingMsgDetails, Component) +function PendingMsgDetails () { + Component.call(this) +} + +PendingMsgDetails.prototype.render = function () { + var state = this.props + var msgData = state.txData + + var msgParams = msgData.msgParams || {} + var address = msgParams.from || state.selectedAddress + var identity = state.identities[address] || { address: address } + var account = state.accounts[address] || { address: address } + + return ( + h('div', { + key: msgData.id, + style: { + margin: '10px 20px', + }, + }, [ + + // account that will sign + h(AccountPanel, { + showFullAddress: true, + identity: identity, + account: account, + imageifyIdenticons: state.imageifyIdenticons, + }), + + // message data + h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-column.flex-space-between', [ + h('label.font-small', 'MESSAGE'), + h('span.font-small', msgParams.data), + ]), + ]), + + ]) + ) +} + diff --git a/old-ui/app/components/pending-msg.js b/old-ui/app/components/pending-msg.js new file mode 100644 index 000000000..834719c53 --- /dev/null +++ b/old-ui/app/components/pending-msg.js @@ -0,0 +1,70 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const PendingTxDetails = require('./pending-msg-details') + +module.exports = PendingMsg + +inherits(PendingMsg, Component) +function PendingMsg () { + Component.call(this) +} + +PendingMsg.prototype.render = function () { + var state = this.props + var msgData = state.txData + + return ( + + h('div', { + key: msgData.id, + style: { + maxWidth: '350px', + }, + }, [ + + // header + h('h3', { + style: { + fontWeight: 'bold', + textAlign: 'center', + }, + }, 'Sign Message'), + + h('.error', { + style: { + margin: '10px', + }, + }, [ + `Signing this message can have + dangerous side effects. Only sign messages from + sites you fully trust with your entire account. + This dangerous method will be removed in a future version. `, + h('a', { + href: 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527', + style: { color: 'rgb(247, 134, 28)' }, + onClick: (event) => { + event.preventDefault() + const url = 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527' + global.platform.openWindow({ url }) + }, + }, 'Read more here.'), + ]), + + // message details + h(PendingTxDetails, state), + + // sign + cancel + h('.flex-row.flex-space-around', [ + h('button', { + onClick: state.cancelMessage, + }, 'Cancel'), + h('button', { + onClick: state.signMessage, + }, 'Sign'), + ]), + ]) + + ) +} + diff --git a/old-ui/app/components/pending-personal-msg-details.js b/old-ui/app/components/pending-personal-msg-details.js new file mode 100644 index 000000000..1050513f2 --- /dev/null +++ b/old-ui/app/components/pending-personal-msg-details.js @@ -0,0 +1,60 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const AccountPanel = require('./account-panel') +const BinaryRenderer = require('./binary-renderer') + +module.exports = PendingMsgDetails + +inherits(PendingMsgDetails, Component) +function PendingMsgDetails () { + Component.call(this) +} + +PendingMsgDetails.prototype.render = function () { + var state = this.props + var msgData = state.txData + + var msgParams = msgData.msgParams || {} + var address = msgParams.from || state.selectedAddress + var identity = state.identities[address] || { address: address } + var account = state.accounts[address] || { address: address } + + var { data } = msgParams + + return ( + h('div', { + key: msgData.id, + style: { + margin: '10px 20px', + }, + }, [ + + // account that will sign + h(AccountPanel, { + showFullAddress: true, + identity: identity, + account: account, + imageifyIdenticons: state.imageifyIdenticons, + }), + + // message data + h('div', { + style: { + height: '260px', + }, + }, [ + h('label.font-small', { style: { display: 'block' } }, 'MESSAGE'), + h(BinaryRenderer, { + value: data, + style: { + height: '215px', + }, + }), + ]), + + ]) + ) +} + diff --git a/old-ui/app/components/pending-personal-msg.js b/old-ui/app/components/pending-personal-msg.js new file mode 100644 index 000000000..4542adb28 --- /dev/null +++ b/old-ui/app/components/pending-personal-msg.js @@ -0,0 +1,47 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const PendingTxDetails = require('./pending-personal-msg-details') + +module.exports = PendingMsg + +inherits(PendingMsg, Component) +function PendingMsg () { + Component.call(this) +} + +PendingMsg.prototype.render = function () { + var state = this.props + var msgData = state.txData + + return ( + + h('div', { + key: msgData.id, + }, [ + + // header + h('h3', { + style: { + fontWeight: 'bold', + textAlign: 'center', + }, + }, 'Sign Message'), + + // message details + h(PendingTxDetails, state), + + // sign + cancel + h('.flex-row.flex-space-around', [ + h('button', { + onClick: state.cancelPersonalMessage, + }, 'Cancel'), + h('button', { + onClick: state.signPersonalMessage, + }, 'Sign'), + ]), + ]) + + ) +} + diff --git a/old-ui/app/components/pending-tx.js b/old-ui/app/components/pending-tx.js new file mode 100644 index 000000000..720df2243 --- /dev/null +++ b/old-ui/app/components/pending-tx.js @@ -0,0 +1,511 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const actions = require('../../../ui/app/actions') +const clone = require('clone') + +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const hexToBn = require('../../../app/scripts/lib/hex-to-bn') +const util = require('../util') +const MiniAccountPanel = require('./mini-account-panel') +const Copyable = require('./copyable') +const EthBalance = require('./eth-balance') +const addressSummary = util.addressSummary +const nameForAddress = require('../../lib/contract-namer') +const BNInput = require('./bn-as-decimal-input') + +// corresponds with 0.1 GWEI +const MIN_GAS_PRICE_BN = new BN('100000000') +const MIN_GAS_LIMIT_BN = new BN('21000') + +module.exports = PendingTx +inherits(PendingTx, Component) +function PendingTx () { + Component.call(this) + this.state = { + valid: true, + txData: null, + submitting: false, + } +} + +PendingTx.prototype.render = function () { + const props = this.props + const { currentCurrency, blockGasLimit } = props + + const conversionRate = props.conversionRate + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + + // Allow retry txs + const { lastGasPrice } = txMeta + let forceGasMin + if (lastGasPrice) { + const stripped = ethUtil.stripHexPrefix(lastGasPrice) + const lastGas = new BN(stripped, 16) + const priceBump = lastGas.divn('10') + forceGasMin = lastGas.add(priceBump) + } + + // Account Details + const address = txParams.from || props.selectedAddress + const identity = props.identities[address] || { address: address } + const account = props.accounts[address] + const balance = account ? account.balance : '0x0' + + // recipient check + const isValidAddress = !txParams.to || util.isValidAddress(txParams.to) + + // Gas + const gas = txParams.gas + const gasBn = hexToBn(gas) + // default to 8MM gas limit + const gasLimit = new BN(parseInt(blockGasLimit) || '8000000') + const safeGasLimitBN = this.bnMultiplyByFraction(gasLimit, 19, 20) + const saferGasLimitBN = this.bnMultiplyByFraction(gasLimit, 18, 20) + const safeGasLimit = safeGasLimitBN.toString(10) + + // Gas Price + const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) + const gasPriceBn = hexToBn(gasPrice) + + const txFeeBn = gasBn.mul(gasPriceBn) + const valueBn = hexToBn(txParams.value) + const maxCost = txFeeBn.add(valueBn) + + const dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0 + + const balanceBn = hexToBn(balance) + const insufficientBalance = balanceBn.lt(maxCost) + const dangerousGasLimit = gasBn.gte(saferGasLimitBN) + const gasLimitSpecified = txMeta.gasLimitSpecified + const buyDisabled = insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting + const showRejectAll = props.unconfTxListLength > 1 + + this.inputs = [] + + return ( + + h('div', { + key: txMeta.id, + }, [ + + h('form#pending-tx-form', { + onSubmit: this.onSubmit.bind(this), + + }, [ + + // tx info + h('div', [ + + h('.flex-row.flex-center', { + style: { + maxWidth: '100%', + }, + }, [ + + h(MiniAccountPanel, { + imageSeed: address, + picOrder: 'right', + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, identity.name), + + h(Copyable, { + value: ethUtil.toChecksumAddress(address), + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, addressSummary(address, 6, 4, false)), + ]), + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, [ + h(EthBalance, { + value: balance, + conversionRate, + currentCurrency, + inline: true, + labelColor: '#F7861C', + }), + ]), + ]), + + forwardCarrat(), + + this.miniAccountPanelForRecipient(), + ]), + + h('style', ` + .table-box { + margin: 7px 0px 0px 0px; + width: 100%; + } + .table-box .row { + margin: 0px; + background: rgb(236,236,236); + display: flex; + justify-content: space-between; + font-family: Montserrat Light, sans-serif; + font-size: 13px; + padding: 5px 25px; + } + .table-box .row .value { + font-family: Montserrat Regular; + } + `), + + h('.table-box', [ + + // Ether Value + // Currently not customizable, but easily modified + // in the way that gas and gasLimit currently are. + h('.row', [ + h('.cell.label', 'Amount'), + h(EthBalance, { value: txParams.value, currentCurrency, conversionRate }), + ]), + + // Gas Limit (customizable) + h('.cell.row', [ + h('.cell.label', 'Gas Limit'), + h('.cell.value', { + }, [ + h(BNInput, { + name: 'Gas Limit', + value: gasBn, + precision: 0, + scale: 0, + // The hard lower limit for gas. + min: MIN_GAS_LIMIT_BN, + max: safeGasLimit, + suffix: 'UNITS', + style: { + position: 'relative', + top: '5px', + }, + onChange: this.gasLimitChanged.bind(this), + + ref: (hexInput) => { this.inputs.push(hexInput) }, + }), + ]), + ]), + + // Gas Price (customizable) + h('.cell.row', [ + h('.cell.label', 'Gas Price'), + h('.cell.value', { + }, [ + h(BNInput, { + name: 'Gas Price', + value: gasPriceBn, + precision: 9, + scale: 9, + suffix: 'GWEI', + min: forceGasMin || MIN_GAS_PRICE_BN, + style: { + position: 'relative', + top: '5px', + }, + onChange: this.gasPriceChanged.bind(this), + ref: (hexInput) => { this.inputs.push(hexInput) }, + }), + ]), + ]), + + // Max Transaction Fee (calculated) + h('.cell.row', [ + h('.cell.label', 'Max Transaction Fee'), + h(EthBalance, { value: txFeeBn.toString(16), currentCurrency, conversionRate }), + ]), + + h('.cell.row', { + style: { + fontFamily: 'Montserrat Regular', + background: 'white', + padding: '10px 25px', + }, + }, [ + h('.cell.label', 'Max Total'), + h('.cell.value', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + h(EthBalance, { + value: maxCost.toString(16), + currentCurrency, + conversionRate, + inline: true, + labelColor: 'black', + fontSize: '16px', + }), + ]), + ]), + + // Data size row: + h('.cell.row', { + style: { + background: '#f7f7f7', + paddingBottom: '0px', + }, + }, [ + h('.cell.label'), + h('.cell.value', { + style: { + fontFamily: 'Montserrat Light', + fontSize: '11px', + }, + }, `Data included: ${dataLength} bytes`), + ]), + ]), // End of Table + + ]), + + h('style', ` + .conf-buttons button { + margin-left: 10px; + text-transform: uppercase; + } + `), + h('.cell.row', { + style: { + textAlign: 'center', + }, + }, [ + txMeta.simulationFails ? + h('.error', { + style: { + fontSize: '0.9em', + }, + }, 'Transaction Error. Exception thrown in contract code.') + : null, + + !isValidAddress ? + h('.error', { + style: { + fontSize: '0.9em', + }, + }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.') + : null, + + insufficientBalance ? + h('span.error', { + style: { + fontSize: '0.9em', + }, + }, 'Insufficient balance for transaction') + : null, + + (dangerousGasLimit && !gasLimitSpecified) ? + h('span.error', { + style: { + fontSize: '0.9em', + }, + }, 'Gas limit set dangerously high. Approving this transaction is likely to fail.') + : null, + ]), + + + // send + cancel + h('.flex-row.flex-space-around.conf-buttons', { + style: { + display: 'flex', + justifyContent: 'flex-end', + margin: '14px 25px', + }, + }, [ + h('button', { + onClick: (event) => { + this.resetGasFields() + event.preventDefault() + }, + }, 'Reset'), + + // Accept Button or Buy Button + insufficientBalance ? h('button.btn-green', { onClick: props.buyEth }, 'Buy Ether') : + h('input.confirm.btn-green', { + type: 'submit', + value: 'SUBMIT', + style: { marginLeft: '10px' }, + disabled: buyDisabled, + }), + + h('button.cancel.btn-red', { + onClick: props.cancelTransaction, + }, 'Reject'), + ]), + showRejectAll ? h('.flex-row.flex-space-around.conf-buttons', { + style: { + display: 'flex', + justifyContent: 'flex-end', + margin: '14px 25px', + }, + }, [ + h('button.cancel.btn-red', { + onClick: props.cancelAllTransactions, + }, 'Reject All'), + ]) : null, + ]), + ]) + ) +} + +PendingTx.prototype.miniAccountPanelForRecipient = function () { + const props = this.props + const txData = props.txData + const txParams = txData.txParams || {} + const isContractDeploy = !('to' in txParams) + + // If it's not a contract deploy, send to the account + if (!isContractDeploy) { + return h(MiniAccountPanel, { + imageSeed: txParams.to, + picOrder: 'left', + }, [ + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, nameForAddress(txParams.to, props.identities)), + + h(Copyable, { + value: ethUtil.toChecksumAddress(txParams.to), + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, addressSummary(txParams.to, 6, 4, false)), + ]), + + ]) + } else { + return h(MiniAccountPanel, { + picOrder: 'left', + }, [ + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, 'New Contract'), + + ]) + } +} + +PendingTx.prototype.gasPriceChanged = function (newBN, valid) { + log.info(`Gas price changed to: ${newBN.toString(10)}`) + const txMeta = this.gatherTxMeta() + txMeta.txParams.gasPrice = '0x' + newBN.toString('hex') + this.setState({ + txData: clone(txMeta), + valid, + }) +} + +PendingTx.prototype.gasLimitChanged = function (newBN, valid) { + log.info(`Gas limit changed to ${newBN.toString(10)}`) + const txMeta = this.gatherTxMeta() + txMeta.txParams.gas = '0x' + newBN.toString('hex') + this.setState({ + txData: clone(txMeta), + valid, + }) +} + +PendingTx.prototype.resetGasFields = function () { + log.debug(`pending-tx resetGasFields`) + + this.inputs.forEach((hexInput) => { + if (hexInput) { + hexInput.setValid() + } + }) + + this.setState({ + txData: null, + valid: true, + }) +} + +PendingTx.prototype.onSubmit = function (event) { + event.preventDefault() + const txMeta = this.gatherTxMeta() + const valid = this.checkValidity() + this.setState({ valid, submitting: true }) + if (valid && this.verifyGasParams()) { + this.props.sendTransaction(txMeta, event) + } else { + this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) + this.setState({ submitting: false }) + } +} + +PendingTx.prototype.checkValidity = function () { + const form = this.getFormEl() + const valid = form.checkValidity() + return valid +} + +PendingTx.prototype.getFormEl = function () { + const form = document.querySelector('form#pending-tx-form') + // Stub out form for unit tests: + if (!form) { + return { checkValidity () { return true } } + } + return form +} + +// After a customizable state value has been updated, +PendingTx.prototype.gatherTxMeta = function () { + log.debug(`pending-tx gatherTxMeta`) + const props = this.props + const state = this.state + const txData = clone(state.txData) || clone(props.txData) + + log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) + return txData +} + +PendingTx.prototype.verifyGasParams = function () { + // We call this in case the gas has not been modified at all + if (!this.state) { return true } + return ( + this._notZeroOrEmptyString(this.state.gas) && + this._notZeroOrEmptyString(this.state.gasPrice) + ) +} + +PendingTx.prototype._notZeroOrEmptyString = function (obj) { + return obj !== '' && obj !== '0x0' +} + +PendingTx.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) { + const numBN = new BN(numerator) + const denomBN = new BN(denominator) + return targetBN.mul(numBN).div(denomBN) +} + +function forwardCarrat () { + return ( + h('img', { + src: 'images/forward-carrat.svg', + style: { + padding: '5px 6px 0px 10px', + height: '37px', + }, + }) + ) +} diff --git a/old-ui/app/components/pending-typed-msg-details.js b/old-ui/app/components/pending-typed-msg-details.js new file mode 100644 index 000000000..b5fd29f71 --- /dev/null +++ b/old-ui/app/components/pending-typed-msg-details.js @@ -0,0 +1,59 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const AccountPanel = require('./account-panel') +const TypedMessageRenderer = require('./typed-message-renderer') + +module.exports = PendingMsgDetails + +inherits(PendingMsgDetails, Component) +function PendingMsgDetails () { + Component.call(this) +} + +PendingMsgDetails.prototype.render = function () { + var state = this.props + var msgData = state.txData + + var msgParams = msgData.msgParams || {} + var address = msgParams.from || state.selectedAddress + var identity = state.identities[address] || { address: address } + var account = state.accounts[address] || { address: address } + + var { data } = msgParams + + return ( + h('div', { + key: msgData.id, + style: { + margin: '10px 20px', + }, + }, [ + + // account that will sign + h(AccountPanel, { + showFullAddress: true, + identity: identity, + account: account, + imageifyIdenticons: state.imageifyIdenticons, + }), + + // message data + h('div', { + style: { + height: '260px', + }, + }, [ + h('label.font-small', { style: { display: 'block' } }, 'YOU ARE SIGNING'), + h(TypedMessageRenderer, { + value: data, + style: { + height: '215px', + }, + }), + ]), + + ]) + ) +} diff --git a/old-ui/app/components/pending-typed-msg.js b/old-ui/app/components/pending-typed-msg.js new file mode 100644 index 000000000..f8926d0a3 --- /dev/null +++ b/old-ui/app/components/pending-typed-msg.js @@ -0,0 +1,46 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const PendingTxDetails = require('./pending-typed-msg-details') + +module.exports = PendingMsg + +inherits(PendingMsg, Component) +function PendingMsg () { + Component.call(this) +} + +PendingMsg.prototype.render = function () { + var state = this.props + var msgData = state.txData + + return ( + + h('div', { + key: msgData.id, + }, [ + + // header + h('h3', { + style: { + fontWeight: 'bold', + textAlign: 'center', + }, + }, 'Sign Message'), + + // message details + h(PendingTxDetails, state), + + // sign + cancel + h('.flex-row.flex-space-around', [ + h('button', { + onClick: state.cancelTypedMessage, + }, 'Cancel'), + h('button', { + onClick: state.signTypedMessage, + }, 'Sign'), + ]), + ]) + + ) +} diff --git a/old-ui/app/components/qr-code.js b/old-ui/app/components/qr-code.js new file mode 100644 index 000000000..fa38dcd92 --- /dev/null +++ b/old-ui/app/components/qr-code.js @@ -0,0 +1,80 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const qrCode = require('qrcode-npm').qrcode +const inherits = require('util').inherits +const connect = require('react-redux').connect +const isHexPrefixed = require('ethereumjs-util').isHexPrefixed +const CopyButton = require('./copyButton') + +module.exports = connect(mapStateToProps)(QrCodeView) + +function mapStateToProps (state) { + return { + Qr: state.appState.Qr, + buyView: state.appState.buyView, + warning: state.appState.warning, + } +} + +inherits(QrCodeView, Component) + +function QrCodeView () { + Component.call(this) +} + +QrCodeView.prototype.render = function () { + const props = this.props + const Qr = props.Qr + console.log(`QrCodeView Qr`, Qr); + const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}` + const qrImage = qrCode(4, 'M') + qrImage.addData(address) + qrImage.make() + return h('.main-container.flex-column', { + key: 'qr', + style: { + justifyContent: 'center', + paddingBottom: '45px', + paddingLeft: '45px', + paddingRight: '45px', + alignItems: 'center', + }, + }, [ + Array.isArray(Qr.message) ? h('.message-container', this.renderMultiMessage()) : h('.qr-header', Qr.message), + + this.props.warning ? this.props.warning && h('span.error.flex-center', { + style: { + textAlign: 'center', + width: '229px', + height: '82px', + }, + }, + this.props.warning) : null, + + h('#qr-container.flex-column', { + style: { + marginTop: '25px', + marginBottom: '15px', + }, + dangerouslySetInnerHTML: { + __html: qrImage.createTableTag(4), + }, + }), + h('.flex-row', [ + h('h3.ellip-address', { + style: { + width: '247px', + }, + }, Qr.data), + h(CopyButton, { + value: Qr.data, + }), + ]), + ]) +} + +QrCodeView.prototype.renderMultiMessage = function () { + var Qr = this.props.Qr + var multiMessage = Qr.message.map((message) => h('.qr-message', message)) + return multiMessage +} diff --git a/old-ui/app/components/range-slider.js b/old-ui/app/components/range-slider.js new file mode 100644 index 000000000..823f5eb01 --- /dev/null +++ b/old-ui/app/components/range-slider.js @@ -0,0 +1,58 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = RangeSlider + +inherits(RangeSlider, Component) +function RangeSlider () { + Component.call(this) +} + +RangeSlider.prototype.render = function () { + const state = this.state || {} + const props = this.props + const onInput = props.onInput || function () {} + const name = props.name + const { + min = 0, + max = 100, + increment = 1, + defaultValue = 50, + mirrorInput = false, + } = this.props.options + const {container, input, range} = props.style + + return ( + h('.flex-row', { + style: container, + }, [ + h('input', { + type: 'range', + name: name, + min: min, + max: max, + step: increment, + style: range, + value: state.value || defaultValue, + onChange: mirrorInput ? this.mirrorInputs.bind(this, event) : onInput, + }), + + // Mirrored input for range + mirrorInput ? h('input.large-input', { + type: 'number', + name: `${name}Mirror`, + min: min, + max: max, + value: state.value || defaultValue, + step: increment, + style: input, + onChange: this.mirrorInputs.bind(this, event), + }) : null, + ]) + ) +} + +RangeSlider.prototype.mirrorInputs = function (event) { + this.setState({value: event.target.value}) +} diff --git a/old-ui/app/components/shapeshift-form.js b/old-ui/app/components/shapeshift-form.js new file mode 100644 index 000000000..a54987c04 --- /dev/null +++ b/old-ui/app/components/shapeshift-form.js @@ -0,0 +1,308 @@ +const PersistentForm = require('../../lib/persistent-form') +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../../../ui/app/actions') +const Qr = require('./qr-code') +const isValidAddress = require('../util').isValidAddress +module.exports = connect(mapStateToProps)(ShapeshiftForm) + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + isSubLoading: state.appState.isSubLoading, + qrRequested: state.appState.qrRequested, + } +} + +inherits(ShapeshiftForm, PersistentForm) + +function ShapeshiftForm () { + PersistentForm.call(this) + this.persistentFormParentId = 'shapeshift-buy-form' +} + +ShapeshiftForm.prototype.render = function () { + return this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain() +} + +ShapeshiftForm.prototype.renderMain = function () { + const marketinfo = this.props.buyView.formView.marketinfo + const coinOptions = this.props.buyView.formView.coinOptions + var coin = marketinfo.pair.split('_')[0].toUpperCase() + + return h('.flex-column', { + style: { + position: 'relative', + padding: '25px', + paddingTop: '5px', + width: '90%', + minHeight: '215px', + alignItems: 'center', + overflowY: 'auto', + }, + }, [ + h('.flex-row', { + style: { + justifyContent: 'center', + alignItems: 'baseline', + height: '42px', + }, + }, [ + h('img', { + src: coinOptions[coin].image, + width: '25px', + height: '25px', + style: { + marginRight: '5px', + }, + }), + + h('.input-container', { + position: 'relative', + }, [ + h('input#fromCoin.buy-inputs.ex-coins', { + type: 'text', + list: 'coinList', + autoFocus: true, + dataset: { + persistentFormId: 'input-coin', + }, + style: { + boxSizing: 'border-box', + }, + onChange: this.handleLiveInput.bind(this), + defaultValue: 'BTC', + }), + + this.renderCoinList(), + + h('i.fa.fa-pencil-square-o.edit-text', { + style: { + fontSize: '12px', + color: '#F7861C', + position: 'absolute', + }, + }), + ]), + + h('.icon-control', { + style: { + position: 'relative', + }, + }, [ + // Not visible on the screen, can't see it on master. + + // h('i.fa.fa-refresh.fa-4.orange', { + // style: { + // bottom: '5px', + // left: '5px', + // color: '#F7861C', + // }, + // onClick: this.updateCoin.bind(this), + // }), + h('i.fa.fa-chevron-right.fa-4.orange', { + style: { + position: 'absolute', + bottom: '35%', + left: '0%', + color: '#F7861C', + }, + onClick: this.updateCoin.bind(this), + }), + ]), + + h('#toCoin.ex-coins', marketinfo.pair.split('_')[1].toUpperCase()), + + h('img', { + src: coinOptions[marketinfo.pair.split('_')[1].toUpperCase()].image, + width: '25px', + height: '25px', + style: { + marginLeft: '5px', + }, + }), + ]), + + h('.flex-column', { + style: { + marginTop: '1%', + alignItems: 'flex-start', + }, + }, [ + this.props.warning ? + this.props.warning && + h('span.error.flex-center', { + style: { + textAlign: 'center', + width: '229px', + height: '82px', + }, + }, this.props.warning) + : this.renderInfo(), + + this.renderRefundAddressForCoin(coin), + ]), + + ]) +} + +ShapeshiftForm.prototype.renderRefundAddressForCoin = function (coin) { + return h(this.activeToggle('.input-container'), { + style: { + marginTop: '1%', + }, + }, [ + + h('div', `${coin} Address:`), + + h('input#fromCoinAddress.buy-inputs', { + type: 'text', + placeholder: `Your ${coin} Refund Address`, + dataset: { + persistentFormId: 'refund-address', + + }, + style: { + boxSizing: 'border-box', + width: '227px', + height: '30px', + padding: ' 5px ', + }, + }), + + h('i.fa.fa-pencil-square-o.edit-text', { + style: { + fontSize: '12px', + color: '#F7861C', + position: 'absolute', + }, + }), + h('div.flex-row', { + style: { + justifyContent: 'flex-start', + }, + }, [ + h('button', { + onClick: this.shift.bind(this), + style: { + marginTop: '1%', + }, + }, + 'Submit'), + ]), + ]) +} + +ShapeshiftForm.prototype.shift = function () { + var props = this.props + var withdrawal = this.props.buyView.buyAddress + var returnAddress = document.getElementById('fromCoinAddress').value + var pair = this.props.buyView.formView.marketinfo.pair + var data = { + 'withdrawal': withdrawal, + 'pair': pair, + 'returnAddress': returnAddress, + // Public api key + 'apiKey': '803d1f5df2ed1b1476e4b9e6bcd089e34d8874595dda6a23b67d93c56ea9cc2445e98a6748b219b2b6ad654d9f075f1f1db139abfa93158c04e825db122c14b6', + } + var message = [ + `Deposit Limit: ${props.buyView.formView.marketinfo.limit}`, + `Deposit Minimum:${props.buyView.formView.marketinfo.minimum}`, + ] + if (isValidAddress(withdrawal)) { + this.props.dispatch(actions.coinShiftRquest(data, message)) + } +} + +ShapeshiftForm.prototype.renderCoinList = function () { + var list = Object.keys(this.props.buyView.formView.coinOptions).map((item) => { + return h('option', { + value: item, + }, item) + }) + + return h('datalist#coinList', { + onClick: (event) => { + event.preventDefault() + }, + }, list) +} + +ShapeshiftForm.prototype.updateCoin = function (event) { + event.preventDefault() + const props = this.props + var coinOptions = this.props.buyView.formView.coinOptions + var coin = document.getElementById('fromCoin').value + + if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { + var message = 'Not a valid coin' + return props.dispatch(actions.displayWarning(message)) + } else { + return props.dispatch(actions.pairUpdate(coin)) + } +} + +ShapeshiftForm.prototype.handleLiveInput = function () { + const props = this.props + var coinOptions = this.props.buyView.formView.coinOptions + var coin = document.getElementById('fromCoin').value + + if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { + return null + } else { + return props.dispatch(actions.pairUpdate(coin)) + } +} + +ShapeshiftForm.prototype.renderInfo = function () { + const marketinfo = this.props.buyView.formView.marketinfo + const coinOptions = this.props.buyView.formView.coinOptions + var coin = marketinfo.pair.split('_')[0].toUpperCase() + + return h('span', { + style: { + }, + }, [ + h('h3.flex-row.text-transform-uppercase', { + style: { + color: '#868686', + paddingTop: '4px', + justifyContent: 'space-around', + textAlign: 'center', + fontSize: '17px', + }, + }, `Market Info for ${marketinfo.pair.replace('_', ' to ').toUpperCase()}:`), + h('.marketinfo', ['Status : ', `${coinOptions[coin].status}`]), + h('.marketinfo', ['Exchange Rate: ', `${marketinfo.rate}`]), + h('.marketinfo', ['Limit: ', `${marketinfo.limit}`]), + h('.marketinfo', ['Minimum : ', `${marketinfo.minimum}`]), + ]) +} + +ShapeshiftForm.prototype.activeToggle = function (elementType) { + if (!this.props.buyView.formView.response || this.props.warning) return elementType + return `${elementType}.inactive` +} + +ShapeshiftForm.prototype.renderLoading = function () { + return h('span', { + style: { + position: 'absolute', + left: '70px', + bottom: '194px', + background: 'transparent', + width: '229px', + height: '82px', + display: 'flex', + justifyContent: 'center', + }, + }, [ + h('img', { + style: { + width: '60px', + }, + src: 'images/loading.svg', + }), + ]) +} diff --git a/old-ui/app/components/shift-list-item.js b/old-ui/app/components/shift-list-item.js new file mode 100644 index 000000000..5454a90bc --- /dev/null +++ b/old-ui/app/components/shift-list-item.js @@ -0,0 +1,204 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const vreme = new (require('vreme'))() +const explorerLink = require('etherscan-link').createExplorerLink +const actions = require('../../../ui/app/actions') +const addressSummary = require('../util').addressSummary + +const CopyButton = require('./copyButton') +const EthBalance = require('./eth-balance') +const Tooltip = require('./tooltip') + + +module.exports = connect(mapStateToProps)(ShiftListItem) + +function mapStateToProps (state) { + return { + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + } +} + +inherits(ShiftListItem, Component) + +function ShiftListItem () { + Component.call(this) +} + +ShiftListItem.prototype.render = function () { + return ( + h('.transaction-list-item.flex-row', { + style: { + paddingTop: '20px', + paddingBottom: '20px', + justifyContent: 'space-around', + alignItems: 'center', + }, + }, [ + h('div', { + style: { + width: '0px', + position: 'relative', + bottom: '19px', + }, + }, [ + h('img', { + src: 'https://info.shapeshift.io/sites/default/files/logo.png', + style: { + height: '35px', + width: '132px', + position: 'absolute', + clip: 'rect(0px,23px,34px,0px)', + }, + }), + ]), + + this.renderInfo(), + this.renderUtilComponents(), + ]) + ) +} + +function formatDate (date) { + return vreme.format(new Date(date), 'March 16 2014 14:30') +} + +ShiftListItem.prototype.renderUtilComponents = function () { + var props = this.props + const { conversionRate, currentCurrency } = props + + switch (props.response.status) { + case 'no_deposits': + return h('.flex-row', [ + h(CopyButton, { + value: this.props.depositAddress, + }), + h(Tooltip, { + title: 'QR Code', + }, [ + h('i.fa.fa-qrcode.pointer.pop-hover', { + onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType)), + style: { + margin: '5px', + marginLeft: '23px', + marginRight: '12px', + fontSize: '20px', + color: '#F7861C', + }, + }), + ]), + ]) + case 'received': + return h('.flex-row') + + case 'complete': + return h('.flex-row', [ + h(CopyButton, { + value: this.props.response.transaction, + }), + h(EthBalance, { + value: `${props.response.outgoingCoin}`, + conversionRate, + currentCurrency, + width: '55px', + shorten: true, + needsParse: false, + incoming: true, + style: { + fontSize: '15px', + color: '#01888C', + }, + }), + ]) + + case 'failed': + return '' + default: + return '' + } +} + +ShiftListItem.prototype.renderInfo = function () { + var props = this.props + switch (props.response.status) { + case 'no_deposits': + return h('.flex-column', { + style: { + width: '200px', + overflow: 'hidden', + }, + }, [ + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, `${props.depositType} to ETH via ShapeShift`), + h('div', 'No deposits received'), + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, formatDate(props.time)), + ]) + case 'received': + return h('.flex-column', { + style: { + width: '200px', + overflow: 'hidden', + }, + }, [ + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, `${props.depositType} to ETH via ShapeShift`), + h('div', 'Conversion in progress'), + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, formatDate(props.time)), + ]) + case 'complete': + var url = explorerLink(props.response.transaction, parseInt('1')) + + return h('.flex-column.pointer', { + style: { + width: '200px', + overflow: 'hidden', + }, + onClick: () => global.platform.openWindow({ url }), + }, [ + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, 'From ShapeShift'), + h('div', formatDate(props.time)), + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, addressSummary(props.response.transaction)), + ]) + + case 'failed': + return h('span.error', '(Failed)') + default: + return '' + } +} diff --git a/old-ui/app/components/tab-bar.js b/old-ui/app/components/tab-bar.js new file mode 100644 index 000000000..bef444a48 --- /dev/null +++ b/old-ui/app/components/tab-bar.js @@ -0,0 +1,37 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = TabBar + +inherits(TabBar, Component) +function TabBar () { + Component.call(this) +} + +TabBar.prototype.render = function () { + const props = this.props + const state = this.state || {} + const { tabs = [], defaultTab, tabSelected } = props + const { subview = defaultTab } = state + + return ( + h('.flex-row.space-around.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + paddingTop: '4px', + minHeight: '30px', + }, + }, tabs.map((tab) => { + const { key, content } = tab + return h(subview === key ? '.activeForm' : '.inactiveForm.pointer', { + onClick: () => { + this.setState({ subview: key }) + tabSelected(key) + }, + }, content) + })) + ) +} + diff --git a/old-ui/app/components/template.js b/old-ui/app/components/template.js new file mode 100644 index 000000000..b6ed8eaa0 --- /dev/null +++ b/old-ui/app/components/template.js @@ -0,0 +1,18 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = NewComponent + +inherits(NewComponent, Component) +function NewComponent () { + Component.call(this) +} + +NewComponent.prototype.render = function () { + const props = this.props + + return ( + h('span', props.message) + ) +} diff --git a/old-ui/app/components/token-cell.js b/old-ui/app/components/token-cell.js new file mode 100644 index 000000000..19d7139bb --- /dev/null +++ b/old-ui/app/components/token-cell.js @@ -0,0 +1,72 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Identicon = require('./identicon') +const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') + +module.exports = TokenCell + +inherits(TokenCell, Component) +function TokenCell () { + Component.call(this) +} + +TokenCell.prototype.render = function () { + const props = this.props + const { address, symbol, string, network, userAddress } = props + + return ( + h('li.token-cell', { + style: { cursor: network === '1' ? 'pointer' : 'default' }, + onClick: this.view.bind(this, address, userAddress, network), + }, [ + + h(Identicon, { + diameter: 50, + address, + network, + }), + + h('h3', `${string || 0} ${symbol}`), + + h('span', { style: { flex: '1 0 auto' } }), + + /* + h('button', { + onClick: this.send.bind(this, address), + }, 'SEND'), + */ + + ]) + ) +} + +TokenCell.prototype.send = function (address, event) { + event.preventDefault() + event.stopPropagation() + const url = tokenFactoryFor(address) + if (url) { + navigateTo(url) + } +} + +TokenCell.prototype.view = function (address, userAddress, network, event) { + const url = etherscanLinkFor(address, userAddress, network) + if (url) { + navigateTo(url) + } +} + +function navigateTo (url) { + global.platform.openWindow({ url }) +} + +function etherscanLinkFor (tokenAddress, address, network) { + const prefix = prefixForNetwork(network) + return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}` +} + +function tokenFactoryFor (tokenAddress) { + return `https://tokenfactory.surge.sh/#/token/${tokenAddress}` +} + diff --git a/old-ui/app/components/token-list.js b/old-ui/app/components/token-list.js new file mode 100644 index 000000000..998ec901d --- /dev/null +++ b/old-ui/app/components/token-list.js @@ -0,0 +1,207 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const TokenTracker = require('eth-token-tracker') +const TokenCell = require('./token-cell.js') + +module.exports = TokenList + +inherits(TokenList, Component) +function TokenList () { + this.state = { + tokens: [], + isLoading: true, + network: null, + } + Component.call(this) +} + +TokenList.prototype.render = function () { + const state = this.state + const { tokens, isLoading, error } = state + const { userAddress, network } = this.props + + if (isLoading) { + return this.message('Loading') + } + + if (error) { + log.error(error) + return h('.hotFix', { + style: { + padding: '80px', + }, + }, [ + 'We had trouble loading your token balances. You can view them ', + h('span.hotFix', { + style: { + color: 'rgba(247, 134, 28, 1)', + cursor: 'pointer', + }, + onClick: () => { + global.platform.openWindow({ + url: `https://ethplorer.io/address/${userAddress}`, + }) + }, + }, 'here'), + ]) + } + + const tokenViews = tokens.map((tokenData) => { + tokenData.network = network + tokenData.userAddress = userAddress + return h(TokenCell, tokenData) + }) + + return h('.full-flex-height', [ + this.renderTokenStatusBar(), + + h('ol.full-flex-height.flex-column', { + style: { + overflowY: 'auto', + display: 'flex', + flexDirection: 'column', + }, + }, [ + h('style', ` + + li.token-cell { + display: flex; + flex-direction: row; + align-items: center; + padding: 10px; + min-height: 50px; + } + + li.token-cell > h3 { + margin-left: 12px; + } + + li.token-cell:hover { + background: white; + cursor: pointer; + } + + `), + ...tokenViews, + h('.flex-grow'), + ]), + ]) +} + +TokenList.prototype.renderTokenStatusBar = function () { + const { tokens } = this.state + + let msg + if (tokens.length === 1) { + msg = `You own 1 token` + } else if (tokens.length > 1) { + msg = `You own ${tokens.length} tokens` + } else { + msg = `No tokens found` + } + + return h('div', { + style: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + minHeight: '70px', + padding: '10px', + }, + }, [ + h('span', msg), + h('button', { + key: 'reveal-account-bar', + onClick: (event) => { + event.preventDefault() + this.props.addToken() + }, + style: { + display: 'flex', + height: '40px', + padding: '10px', + justifyContent: 'center', + alignItems: 'center', + }, + }, [ + 'ADD TOKEN', + ]), + ]) +} + +TokenList.prototype.message = function (body) { + return h('div', { + style: { + display: 'flex', + height: '250px', + alignItems: 'center', + justifyContent: 'center', + padding: '30px', + }, + }, body) +} + +TokenList.prototype.componentDidMount = function () { + this.createFreshTokenTracker() +} + +TokenList.prototype.createFreshTokenTracker = function () { + if (this.tracker) { + // Clean up old trackers when refreshing: + this.tracker.stop() + this.tracker.removeListener('update', this.balanceUpdater) + this.tracker.removeListener('error', this.showError) + } + + if (!global.ethereumProvider) return + const { userAddress } = this.props + this.tracker = new TokenTracker({ + userAddress, + provider: global.ethereumProvider, + tokens: this.props.tokens, + pollingInterval: 8000, + }) + + + // Set up listener instances for cleaning up + this.balanceUpdater = this.updateBalances.bind(this) + this.showError = (error) => { + this.setState({ error, isLoading: false }) + } + this.tracker.on('update', this.balanceUpdater) + this.tracker.on('error', this.showError) + + this.tracker.updateBalances() + .then(() => { + this.updateBalances(this.tracker.serialize()) + }) + .catch((reason) => { + log.error(`Problem updating balances`, reason) + this.setState({ isLoading: false }) + }) +} + +TokenList.prototype.componentWillUpdate = function (nextProps) { + if (nextProps.network === 'loading') return + const oldNet = this.props.network + const newNet = nextProps.network + + if (oldNet && newNet && newNet !== oldNet) { + this.setState({ isLoading: true }) + this.createFreshTokenTracker() + } +} + +TokenList.prototype.updateBalances = function (tokens) { + const heldTokens = tokens.filter(token => { + return token.balance !== '0' && token.string !== '0.000' + }) + this.setState({ tokens: heldTokens, isLoading: false }) +} + +TokenList.prototype.componentWillUnmount = function () { + if (!this.tracker) return + this.tracker.stop() +} + diff --git a/old-ui/app/components/tooltip.js b/old-ui/app/components/tooltip.js new file mode 100644 index 000000000..efab2c497 --- /dev/null +++ b/old-ui/app/components/tooltip.js @@ -0,0 +1,22 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ReactTooltip = require('react-tooltip-component') + +module.exports = Tooltip + +inherits(Tooltip, Component) +function Tooltip () { + Component.call(this) +} + +Tooltip.prototype.render = function () { + const props = this.props + const { position, title, children } = props + + return h(ReactTooltip, { + position: position || 'left', + title, + fixed: true, + }, children) +} diff --git a/old-ui/app/components/transaction-list-item-icon.js b/old-ui/app/components/transaction-list-item-icon.js new file mode 100644 index 000000000..f442b05af --- /dev/null +++ b/old-ui/app/components/transaction-list-item-icon.js @@ -0,0 +1,68 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Tooltip = require('./tooltip') + +const Identicon = require('./identicon') + +module.exports = TransactionIcon + +inherits(TransactionIcon, Component) +function TransactionIcon () { + Component.call(this) +} + +TransactionIcon.prototype.render = function () { + const { transaction, txParams, isMsg } = this.props + switch (transaction.status) { + case 'unapproved': + return h(!isMsg ? '.unapproved-tx-icon' : 'i.fa.fa-certificate.fa-lg') + + case 'rejected': + return h('i.fa.fa-exclamation-triangle.fa-lg.warning', { + style: { + width: '24px', + }, + }) + + case 'failed': + return h('i.fa.fa-exclamation-triangle.fa-lg.error', { + style: { + width: '24px', + }, + }) + + case 'submitted': + return h(Tooltip, { + title: 'Pending', + position: 'right', + }, [ + h('i.fa.fa-ellipsis-h', { + style: { + fontSize: '27px', + }, + }), + ]) + } + + if (isMsg) { + return h('i.fa.fa-certificate.fa-lg', { + style: { + width: '24px', + }, + }) + } + + if (txParams.to) { + return h(Identicon, { + diameter: 24, + address: txParams.to || transaction.hash, + }) + } else { + return h('i.fa.fa-file-text-o.fa-lg', { + style: { + width: '24px', + }, + }) + } +} diff --git a/old-ui/app/components/transaction-list-item.js b/old-ui/app/components/transaction-list-item.js new file mode 100644 index 000000000..76a456d3f --- /dev/null +++ b/old-ui/app/components/transaction-list-item.js @@ -0,0 +1,175 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const EthBalance = require('./eth-balance') +const addressSummary = require('../util').addressSummary +const explorerLink = require('etherscan-link').createExplorerLink +const CopyButton = require('./copyButton') +const vreme = new (require('vreme'))() +const Tooltip = require('./tooltip') +const numberToBN = require('number-to-bn') + +const TransactionIcon = require('./transaction-list-item-icon') +const ShiftListItem = require('./shift-list-item') +module.exports = TransactionListItem + +inherits(TransactionListItem, Component) +function TransactionListItem () { + Component.call(this) +} + +TransactionListItem.prototype.render = function () { + const { transaction, network, conversionRate, currentCurrency } = this.props + if (transaction.key === 'shapeshift') { + if (network === '1') return h(ShiftListItem, transaction) + } + var date = formatDate(transaction.time) + + let isLinkable = false + const numericNet = parseInt(network) + isLinkable = numericNet === 1 || numericNet === 3 || numericNet === 4 || numericNet === 42 + + var isMsg = ('msgParams' in transaction) + var isTx = ('txParams' in transaction) + var isPending = transaction.status === 'unapproved' + let txParams + if (isTx) { + txParams = transaction.txParams + } else if (isMsg) { + txParams = transaction.msgParams + } + + const nonce = txParams.nonce ? numberToBN(txParams.nonce).toString(10) : '' + + const isClickable = ('hash' in transaction && isLinkable) || isPending + return ( + h(`.transaction-list-item.flex-row.flex-space-between${isClickable ? '.pointer' : ''}`, { + onClick: (event) => { + if (isPending) { + this.props.showTx(transaction.id) + } + event.stopPropagation() + if (!transaction.hash || !isLinkable) return + var url = explorerLink(transaction.hash, parseInt(network)) + global.platform.openWindow({ url }) + }, + style: { + padding: '20px 0', + display: 'flex', + justifyContent: 'space-between', + }, + }, [ + + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + h(TransactionIcon, { txParams, transaction, isTx, isMsg }), + ]), + + h(Tooltip, { + title: 'Transaction Number', + position: 'right', + }, [ + h('span', { + style: { + display: 'flex', + cursor: 'normal', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + }, + }, nonce), + ]), + + h('.flex-column', {style: {width: '150px', overflow: 'hidden'}}, [ + domainField(txParams), + h('div', date), + recipientField(txParams, transaction, isTx, isMsg), + ]), + + // Places a copy button if tx is successful, else places a placeholder empty div. + transaction.hash ? h(CopyButton, { value: transaction.hash }) : h('div', {style: { display: 'flex', alignItems: 'center', width: '26px' }}), + + isTx ? h(EthBalance, { + value: txParams.value, + conversionRate, + currentCurrency, + shorten: true, + showFiat: false, + style: {fontSize: '15px'}, + }) : h('.flex-column'), + ]) + ) +} + +function domainField (txParams) { + return h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + overflow: 'hidden', + textOverflow: 'ellipsis', + width: '100%', + }, + }, [ + txParams.origin, + ]) +} + +function recipientField (txParams, transaction, isTx, isMsg) { + let message + + if (isMsg) { + message = 'Signature Requested' + } else if (txParams.to) { + message = addressSummary(txParams.to) + } else { + message = 'Contract Published' + } + + return h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + }, + }, [ + message, + renderErrorOrWarning(transaction), + ]) +} + +function formatDate (date) { + return vreme.format(new Date(date), 'March 16 2014 14:30') +} + +function renderErrorOrWarning (transaction) { + const { status, err, warning } = transaction + + // show rejected + if (status === 'rejected') { + return h('span.error', ' (Rejected)') + } + + // show error + if (err) { + const message = err.message || '' + return ( + h(Tooltip, { + title: message, + position: 'bottom', + }, [ + h(`span.error`, ` (Failed)`), + ]) + ) + } + + // show warning + if (warning) { + const message = warning.message + return h(Tooltip, { + title: message, + position: 'bottom', + }, [ + h(`span.warning`, ` (Warning)`), + ]) + } +} diff --git a/old-ui/app/components/transaction-list.js b/old-ui/app/components/transaction-list.js new file mode 100644 index 000000000..345e3ca16 --- /dev/null +++ b/old-ui/app/components/transaction-list.js @@ -0,0 +1,87 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const TransactionListItem = require('./transaction-list-item') + +module.exports = TransactionList + + +inherits(TransactionList, Component) +function TransactionList () { + Component.call(this) +} + +TransactionList.prototype.render = function () { + const { transactions, network, unapprovedMsgs, conversionRate } = this.props + + var shapeShiftTxList + if (network === '1') { + shapeShiftTxList = this.props.shapeShiftTxList + } + const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList) + .sort((a, b) => b.time - a.time) + + return ( + + h('section.transaction-list.full-flex-height', { + style: { + justifyContent: 'center', + }, + }, [ + + h('style', ` + .transaction-list .transaction-list-item:not(:last-of-type) { + border-bottom: 1px solid #D4D4D4; + } + .transaction-list .transaction-list-item .ether-balance-label { + display: block !important; + font-size: small; + } + `), + + h('.tx-list', { + style: { + overflowY: 'auto', + height: '100%', + padding: '0 25px 0 15px', + textAlign: 'center', + }, + }, [ + + txsToRender.length + ? txsToRender.map((transaction, i) => { + let key + switch (transaction.key) { + case 'shapeshift': + const { depositAddress, time } = transaction + key = `shift-tx-${depositAddress}-${time}-${i}` + break + default: + key = `tx-${transaction.id}-${i}` + } + return h(TransactionListItem, { + transaction, i, network, key, + conversionRate, + showTx: (txId) => { + this.props.viewPendingTx(txId) + }, + }) + }) + : h('.flex-center.full-flex-height', { + style: { + flexDirection: 'column', + justifyContent: 'center', + }, + }, [ + h('p', { + style: { + marginTop: '50px', + }, + }, 'No transaction history.'), + ]), + ]), + ]) + ) +} + diff --git a/old-ui/app/components/typed-message-renderer.js b/old-ui/app/components/typed-message-renderer.js new file mode 100644 index 000000000..d170d63b7 --- /dev/null +++ b/old-ui/app/components/typed-message-renderer.js @@ -0,0 +1,42 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const extend = require('xtend') + +module.exports = TypedMessageRenderer + +inherits(TypedMessageRenderer, Component) +function TypedMessageRenderer () { + Component.call(this) +} + +TypedMessageRenderer.prototype.render = function () { + const props = this.props + const { value, style } = props + const text = renderTypedData(value) + + const defaultStyle = extend({ + width: '315px', + maxHeight: '210px', + resize: 'none', + border: 'none', + background: 'white', + padding: '3px', + overflow: 'scroll', + }, style) + + return ( + h('div.font-small', { + style: defaultStyle, + }, text) + ) +} + +function renderTypedData (values) { + return values.map(function (value) { + return h('div', {}, [ + h('strong', {style: {display: 'block', fontWeight: 'bold'}}, String(value.name) + ':'), + h('div', {}, value.value), + ]) + }) +} |