diff --git a/common/actions/swap.js b/common/actions/swap.js new file mode 100644 index 00000000..334d6a7e --- /dev/null +++ b/common/actions/swap.js @@ -0,0 +1,42 @@ +import { + SWAP_DESTINATION_AMOUNT, + SWAP_DESTINATION_KIND, + SWAP_ORIGIN_AMOUNT, + SWAP_ORIGIN_KIND, + SWAP_UPDATE_BITY_RATES +} from './swapConstants'; + +export const SWAP_ORIGIN_KIND_TO = value => { + return { + type: SWAP_ORIGIN_KIND, + value + }; +}; + +export const SWAP_DESTINATION_KIND_TO = value => { + return { + type: SWAP_DESTINATION_KIND, + value + }; +}; + +export const SWAP_ORIGIN_AMOUNT_TO = value => { + return { + type: SWAP_ORIGIN_AMOUNT, + value + }; +}; + +export const SWAP_DESTINATION_AMOUNT_TO = value => { + return { + type: SWAP_DESTINATION_AMOUNT, + value + }; +}; + +export const SWAP_UPDATE_BITY_RATES_TO = value => { + return { + type: SWAP_UPDATE_BITY_RATES, + value + }; +}; diff --git a/common/actions/swapConstants.js b/common/actions/swapConstants.js new file mode 100644 index 00000000..ca72fc2c --- /dev/null +++ b/common/actions/swapConstants.js @@ -0,0 +1,5 @@ +export const SWAP_ORIGIN_KIND = 'SWAP_ORIGIN_KIND'; +export const SWAP_DESTINATION_KIND = 'SWAP_DESTINATION_KIND'; +export const SWAP_ORIGIN_AMOUNT = 'SWAP_ORIGIN_AMOUNT'; +export const SWAP_DESTINATION_AMOUNT = 'SWAP_DESTINATION_AMOUNT'; +export const SWAP_UPDATE_BITY_RATES = 'SWAP_UPDATE_BITY_RATES'; diff --git a/common/api/bity.js b/common/api/bity.js new file mode 100644 index 00000000..cf3a7f9e --- /dev/null +++ b/common/api/bity.js @@ -0,0 +1,62 @@ +import axios from 'axios'; +import bityConfig from 'config/bity'; + +// https://stackoverflow.com/questions/9828684/how-to-get-all-arguments-of-a-callback-function +export function combineAndUpper() { + const args = []; + let newString = ''; + for (let i = 0; i < arguments.length; ++i) args[i] = arguments[i]; + args.forEach(each => { + newString = newString.concat(each.toUpperCase()); + }); + return newString; +} + +export default class Bity { + findRateFromBityRateList(rateObjects, pairName) { + return rateObjects.find(x => x.pair === pairName); + } + + _getRate(bityRates, origin, destination) { + const pairName = combineAndUpper(origin, destination); + const rateObjects = bityRates.data.objects; + return this.findRateFromBityRateList(rateObjects, pairName); + } + + /** + * Gives you multiple rates from Bitys API without making multiple API calls + * @param arrayOfOriginAndDestinationDicts - [{origin: 'BTC', destination: 'ETH'}, {origin: 'BTC', destination: 'REP}] + */ + getMultipleRates(arrayOfOriginAndDestinationDicts) { + const mappedRates = {}; + return this.requestAllRates().then(bityRates => { + arrayOfOriginAndDestinationDicts.forEach(each => { + const origin = each.origin; + const destination = each.destination; + const pairName = combineAndUpper(origin, destination); + const rate = this._getRate(bityRates, origin, destination); + mappedRates[pairName] = parseFloat(rate.rate_we_sell); + }); + return mappedRates; + }); + // TODO - catch errors + } + + getAllRates() { + const mappedRates = {}; + return this.requestAllRates().then(bityRates => { + bityRates.data.objects.forEach(each => { + const pairName = each.pair; + mappedRates[pairName] = parseFloat(each.rate_we_sell); + }); + return mappedRates; + }); + // TODO - catch errors + } + + requestAllRates() { + const path = '/v1/rate2/'; + const bityURL = bityConfig.bityAPI + path; + return axios.get(bityURL); + } +} diff --git a/common/config/bity.js b/common/config/bity.js new file mode 100644 index 00000000..b3350f3d --- /dev/null +++ b/common/config/bity.js @@ -0,0 +1,18 @@ +export default { + SERVERURL: 'https://myetherapi.com', + bityAPI: 'https://bity.com/api', + decimals: 6, + ethExplorer: 'https://etherscan.io/tx/[[txHash]]', + btcExplorer: 'https://blockchain.info/tx/[[txHash]]', + validStatus: ['RCVE', 'FILL', 'CONF', 'EXEC'], + invalidStatus: ['CANC'], + mainPairs: ['REP', 'ETH'], + min: 0.01, + max: 3, + priceLoaded: false, + postConfig: { + headers: { + 'Content-Type': 'application/json; charse:UTF-8' + } + } +}; diff --git a/common/containers/App/index.jsx b/common/containers/App/index.jsx index 80146f2e..4b78d126 100644 --- a/common/containers/App/index.jsx +++ b/common/containers/App/index.jsx @@ -27,11 +27,6 @@ class App extends Component { nodeSelection: PropTypes.object }; - componentWillMount() { - let { handleWindowResize } = this.props; - window.addEventListener('resize', handleWindowResize); - } - render() { let { children, @@ -46,7 +41,6 @@ class App extends Component { location, changeLanguage, languageSelection, - changeNode, nodeSelection }; diff --git a/common/containers/Tabs/Swap/components/currentRates.js b/common/containers/Tabs/Swap/components/currentRates.js new file mode 100644 index 00000000..577df5a4 --- /dev/null +++ b/common/containers/Tabs/Swap/components/currentRates.js @@ -0,0 +1,117 @@ +import React, { Component } from 'react'; +import translate from 'translations'; +import PropTypes from 'prop-types'; + +export default class CurrentRates extends Component { + constructor(props) { + super(props); + this.state = { + ETHBTCAmount: 1, + ETHREPAmount: 1, + BTCETHAmount: 1, + BTCREPAmount: 1 + }; + } + + static propTypes = { + ETHBTC: PropTypes.number, + ETHREP: PropTypes.number, + BTCETH: PropTypes.number, + BTCREP: PropTypes.number + }; + + onChange = event => { + const target = event.target; + const value = target.value; + const name = target.name; + this.setState({ + [name]: value + }); + }; + + // TODO - A little code duplication here, but simple enough to where it doesn't seem worth the time to fix. + render() { + return ( +
+
+
+ {translate('SWAP_rates')} +
+
+
+
+

+ + + ETH = {(this.state.ETHBTCAmount * this.props.ETHBTC).toFixed( + 6 + )}{' '} + BTC + +

+

+ + + ETH = {(this.state.ETHREPAmount * this.props.ETHREP).toFixed( + 6 + )}{' '} + REP + +

+
+
+

+ + + BTC = {(this.state.BTCETHAmount * this.props.BTCETH).toFixed( + 6 + )}{' '} + ETH + +

+

+ + + BTC = {(this.state.BTCREPAmount * this.props.BTCREP).toFixed( + 6 + )}{' '} + REP + +

+
+ + + +
+
+ ); + } +} diff --git a/common/containers/Tabs/Swap/components/wantToSwapMy.js b/common/containers/Tabs/Swap/components/wantToSwapMy.js new file mode 100644 index 00000000..a8efd912 --- /dev/null +++ b/common/containers/Tabs/Swap/components/wantToSwapMy.js @@ -0,0 +1,153 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import translate from 'translations'; +import { combineAndUpper } from 'api/bity'; + +class CoinTypeDropDown extends Component { + constructor(props, context) { + super(props, context); + } + + static propTypes = { + kind: PropTypes.any, + onChange: PropTypes.any, + kindOptions: PropTypes.any + }; + + render() { + return ( + + + + ); + } +} + +export default class WantToSwapMy extends Component { + constructor(props, context) { + super(props, context); + } + + static propTypes = { + bityRates: PropTypes.any, + originAmount: PropTypes.any, + destinationAmount: PropTypes.any, + originKind: PropTypes.string, + destinationKind: PropTypes.string, + destinationKindOptions: PropTypes.array, + originKindOptions: PropTypes.array, + SWAP_ORIGIN_KIND_TO: PropTypes.func, + SWAP_DESTINATION_KIND_TO: PropTypes.func, + SWAP_ORIGIN_AMOUNT_TO: PropTypes.func, + SWAP_DESTINATION_AMOUNT_TO: PropTypes.func + }; + + onClickStartSwap() {} + + onChangeOriginAmount = amount => { + let originAmountAsNumber = parseFloat(amount); + if (originAmountAsNumber) { + let pairName = combineAndUpper( + this.props.originKind, + this.props.destinationKind + ); + let bityRate = this.props.bityRates[pairName]; + this.props.SWAP_ORIGIN_AMOUNT_TO(originAmountAsNumber); + this.props.SWAP_DESTINATION_AMOUNT_TO(originAmountAsNumber * bityRate); + } else { + this.props.SWAP_ORIGIN_AMOUNT_TO(''); + this.props.SWAP_DESTINATION_AMOUNT_TO(''); + } + }; + + onChangeDestinationAmount(amount) { + let destinationAmountAsNumber = parseFloat(amount); + if (destinationAmountAsNumber) { + this.props.SWAP_DESTINATION_AMOUNT_TO(destinationAmountAsNumber); + let pairName = combineAndUpper( + this.props.destinationKind, + this.props.originKind + ); + let bityRate = this.props.bityRates[pairName]; + this.props.SWAP_ORIGIN_AMOUNT_TO(destinationAmountAsNumber * bityRate); + } else { + this.props.SWAP_ORIGIN_AMOUNT_TO(''); + this.props.SWAP_DESTINATION_AMOUNT_TO(''); + } + } + + async onChangeDestinationKind(event) { + let newDestinationKind = event.target.value; + this.props.SWAP_DESTINATION_KIND_TO(newDestinationKind); + } + + async onChangeOriginKind(event) { + let newOriginKind = event.target.value; + this.props.SWAP_ORIGIN_KIND_TO(newOriginKind); + } + + render() { + const { + originAmount, + destinationAmount, + originKind, + destinationKind, + destinationKindOptions, + originKindOptions + } = this.props; + + return ( +
+

{translate('SWAP_init_1')}

+ 0 + ? 'is-valid' + : 'is-invalid'}`} + type="number" + placeholder="Amount" + onChange={e => this.onChangeOriginAmount(e.target.value)} + value={originAmount} + /> + + + +

{translate('SWAP_init_2')}

+ + 0 + ? 'is-valid' + : 'is-invalid'}`} + type="number" + placeholder="Amount" + value={destinationAmount} + onChange={e => this.onChangeDestinationAmount(e.target.value)} + /> + + +
+ + {translate('SWAP_init_CTA')} + +
+
+ ); + } +} diff --git a/common/containers/Tabs/Swap/index.js b/common/containers/Tabs/Swap/index.js new file mode 100644 index 00000000..e428371b --- /dev/null +++ b/common/containers/Tabs/Swap/index.js @@ -0,0 +1,100 @@ +import React, { Component } from 'react'; +import WantToSwapMy from './components/wantToSwapMy'; +import CurrentRates from './components/currentRates'; +import { connect } from 'react-redux'; +import * as swapActions from 'actions/swap'; + +import PropTypes from 'prop-types'; +import Bity from 'api/bity'; + +class Swap extends Component { + constructor(props) { + super(props); + this.bity = new Bity(); + } + + static propTypes = { + bityRates: PropTypes.any, + originAmount: PropTypes.any, + destinationAmount: PropTypes.any, + originKind: PropTypes.string, + destinationKind: PropTypes.string, + destinationKindOptions: PropTypes.array, + originKindOptions: PropTypes.array, + SWAP_ORIGIN_KIND_TO: PropTypes.func, + SWAP_DESTINATION_KIND_TO: PropTypes.func, + SWAP_ORIGIN_AMOUNT_TO: PropTypes.func, + SWAP_DESTINATION_AMOUNT_TO: PropTypes.func, + SWAP_UPDATE_BITY_RATES_TO: PropTypes.func + }; + + componentDidMount() { + let { bityRates } = this.props; + + if ( + !bityRates.ETHBTC || + !bityRates.ETHREP || + !bityRates.BTCETH || + !bityRates.BTCREP + ) { + this.bity.getAllRates().then(data => { + this.props.SWAP_UPDATE_BITY_RATES_TO(data); + }); + } + } + + render() { + let { + bityRates, + originAmount, + destinationAmount, + originKind, + destinationKind, + destinationKindOptions, + originKindOptions, + SWAP_ORIGIN_KIND_TO, + SWAP_DESTINATION_KIND_TO, + SWAP_ORIGIN_AMOUNT_TO, + SWAP_DESTINATION_AMOUNT_TO + } = this.props; + + let wantToSwapMyProps = { + bityRates, + originAmount, + destinationAmount, + originKind, + destinationKind, + destinationKindOptions, + originKindOptions, + SWAP_ORIGIN_KIND_TO, + SWAP_DESTINATION_KIND_TO, + SWAP_ORIGIN_AMOUNT_TO, + SWAP_DESTINATION_AMOUNT_TO + }; + + return ( +
+
+
+ + +
+
+
+ ); + } +} + +function mapStateToProps(state) { + return { + originAmount: state.swap.originAmount, + destinationAmount: state.swap.destinationAmount, + originKind: state.swap.originKind, + destinationKind: state.swap.destinationKind, + destinationKindOptions: state.swap.destinationKindOptions, + originKindOptions: state.swap.originKindOptions, + bityRates: state.swap.bityRates + }; +} + +export default connect(mapStateToProps, swapActions)(Swap); diff --git a/common/reducers/index.js b/common/reducers/index.js index 28b1f155..03af9db1 100644 --- a/common/reducers/index.js +++ b/common/reducers/index.js @@ -1,5 +1,7 @@ import * as generateWallet from './generateWallet' import * as config from './config' +import * as swap from './swap' + import { reducer as formReducer } from 'redux-form' import {combineReducers} from 'redux'; import {routerReducer} from 'react-router-redux' @@ -7,6 +9,7 @@ import {routerReducer} from 'react-router-redux' export default combineReducers({ ...generateWallet, ...config, + ...swap, form: formReducer, routing: routerReducer }) diff --git a/common/reducers/swap.js b/common/reducers/swap.js new file mode 100644 index 00000000..b827df05 --- /dev/null +++ b/common/reducers/swap.js @@ -0,0 +1,100 @@ +import { + SWAP_DESTINATION_AMOUNT, + SWAP_DESTINATION_KIND, + SWAP_ORIGIN_AMOUNT, + SWAP_ORIGIN_KIND, + SWAP_UPDATE_BITY_RATES +} from 'actions/swapConstants'; +import { combineAndUpper } from 'api/bity'; + +export const ALL_CRYPTO_KIND_OPTIONS = ['BTC', 'ETH', 'REP']; + +const initialState = { + originAmount: 0, + destinationAmount: 0, + originKind: 'BTC', + destinationKind: 'ETH', + destinationKindOptions: ALL_CRYPTO_KIND_OPTIONS.filter( + element => element !== 'BTC' + ), + originKindOptions: ALL_CRYPTO_KIND_OPTIONS.filter( + element => element !== 'REP' + ), + bityRates: {} +}; + +const buildDestinationAmount = ( + originAmount, + originKind, + destinationKind, + bityRates +) => { + let pairName = combineAndUpper(originKind, destinationKind); + let bityRate = bityRates[pairName]; + return originAmount * bityRate; +}; + +const buildDestinationKind = (originKind, destinationKind) => { + if (originKind === destinationKind) { + return ALL_CRYPTO_KIND_OPTIONS.filter(element => element !== originKind)[0]; + } else { + return destinationKind; + } +}; + +export function swap(state = initialState, action) { + switch (action.type) { + case SWAP_ORIGIN_KIND: { + const newDestinationKind = buildDestinationKind( + action.value, + state.destinationKind + ); + return { + ...state, + originKind: action.value, + destinationKind: newDestinationKind, + destinationKindOptions: ALL_CRYPTO_KIND_OPTIONS.filter( + element => element !== action.value + ), + destinationAmount: buildDestinationAmount( + state.originAmount, + action.value, + newDestinationKind, + state.bityRates + ) + }; + } + case SWAP_DESTINATION_KIND: { + return { + ...state, + destinationKind: action.value, + destinationAmount: buildDestinationAmount( + state.originAmount, + state.originKind, + action.value, + state.bityRates + ) + }; + } + case SWAP_ORIGIN_AMOUNT: + return { + ...state, + originAmount: action.value + }; + case SWAP_DESTINATION_AMOUNT: + return { + ...state, + destinationAmount: action.value + }; + case SWAP_UPDATE_BITY_RATES: + return { + ...state, + bityRates: { + ...state.bityRates, + ...action.value + } + }; + default: + return state; + } +} diff --git a/common/routing/index.jsx b/common/routing/index.jsx index 3173bd88..17d500f7 100644 --- a/common/routing/index.jsx +++ b/common/routing/index.jsx @@ -5,6 +5,7 @@ import {App} from 'containers'; import GenerateWallet from 'containers/Tabs/GenerateWallet' import ViewWallet from 'containers/Tabs/ViewWallet' import Help from 'containers/Tabs/Help' +import Swap from 'containers/Tabs/Swap' export const history = getHistory() @@ -13,6 +14,7 @@ export const Routing = () => ( + diff --git a/package.json b/package.json index 9e46cf62..09b5355a 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,8 @@ "main": "common/index.jsx", "description": "MyEtherWallet v4", "dependencies": { + "axios": "^0.16.2", + "lodash": "^4.17.4", "prop-types": "^15.5.8", "react": "^15.4.2", "react-dom": "^15.4.2",