Merge pull request #13 from MyEtherWallet/swap

WIP Swap Part 1
This commit is contained in:
Daniel Ternyak 2017-06-19 18:51:01 -05:00 committed by GitHub
commit 227abd5ff6
12 changed files with 604 additions and 6 deletions

42
common/actions/swap.js Normal file
View File

@ -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
};
};

View File

@ -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';

62
common/api/bity.js Normal file
View File

@ -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);
}
}

18
common/config/bity.js Normal file
View File

@ -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'
}
}
};

View File

@ -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
};

View File

@ -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 (
<article className="swap-rates">
<section className="row">
<h5 className="col-xs-6 col-xs-offset-3">
{translate('SWAP_rates')}
</h5>
</section>
<section className="row order-panel">
<div className="col-sm-6 order-info">
<p className="mono">
<input
className="form-control input-sm"
onChange={this.onChange}
value={this.state.ETHBTCAmount}
name="ETHBTCAmount"
/>
<span>
ETH = {(this.state.ETHBTCAmount * this.props.ETHBTC).toFixed(
6
)}{' '}
BTC
</span>
</p>
<p className="mono">
<input
className="form-control input-sm"
onChange={this.onChange}
value={this.state.ETHREPAmount}
name="ETHREPAmount"
/>
<span>
ETH = {(this.state.ETHREPAmount * this.props.ETHREP).toFixed(
6
)}{' '}
REP
</span>
</p>
</div>
<div className="col-sm-6 order-info">
<p className="mono">
<input
className="form-control input-sm"
onChange={this.onChange}
value={this.state.BTCETHAmount}
name="BTCETHAmount"
/>
<span>
BTC = {(this.state.BTCETHAmount * this.props.BTCETH).toFixed(
6
)}{' '}
ETH
</span>
</p>
<p className="mono">
<input
className="form-control input-sm"
onChange={this.onChange}
value={this.state.BTCREPAmount}
name="BTCREPAmount"
/>
<span>
BTC = {(this.state.BTCREPAmount * this.props.BTCREP).toFixed(
6
)}{' '}
REP
</span>
</p>
</div>
<a
className="link bity-logo"
href="https://bity.com/af/jshkb37v"
target="_blank"
>
<img
src={'https://www.myetherwallet.com/images/logo-bity-white.svg'}
width={120}
height={49}
/>
</a>
</section>
</article>
);
}
}

View File

@ -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 (
<span className="dropdown">
<select
value={this.props.kind}
className="btn btn-default"
onChange={this.props.onChange.bind(this)}
>
{this.props.kindOptions.map((obj, i) => {
return <option value={obj} key={i}>{obj}</option>;
})}
</select>
</span>
);
}
}
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 (
<article className="swap-panel">
<h1>{translate('SWAP_init_1')}</h1>
<input
className={`form-control ${this.props.originAmount !== '' &&
this.props.originAmount > 0
? 'is-valid'
: 'is-invalid'}`}
type="number"
placeholder="Amount"
onChange={e => this.onChangeOriginAmount(e.target.value)}
value={originAmount}
/>
<CoinTypeDropDown
kind={originKind}
onChange={this.onChangeOriginKind.bind(this)}
kindOptions={originKindOptions}
/>
<h1>{translate('SWAP_init_2')}</h1>
<input
className={`form-control ${this.props.destinationAmount !== '' &&
this.props.destinationAmount > 0
? 'is-valid'
: 'is-invalid'}`}
type="number"
placeholder="Amount"
value={destinationAmount}
onChange={e => this.onChangeDestinationAmount(e.target.value)}
/>
<CoinTypeDropDown
kind={destinationKind}
onChange={this.onChangeDestinationKind.bind(this)}
kindOptions={destinationKindOptions}
/>
<div className="col-xs-12 clearfix text-center">
<a onClick={this.onClickStartSwap} className="btn btn-info btn-lg">
<span>{translate('SWAP_init_CTA')}</span>
</a>
</div>
</article>
);
}
}

View File

@ -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 (
<section className="container" style={{ minHeight: '50%' }}>
<div className="tab-content">
<main className="tab-pane swap-tab">
<CurrentRates {...bityRates} />
<WantToSwapMy {...wantToSwapMyProps} />
</main>
</div>
</section>
);
}
}
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);

View File

@ -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
})

100
common/reducers/swap.js Normal file
View File

@ -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;
}
}

View File

@ -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 = () => (
<Route name="GenerateWallet" path="/" component={GenerateWallet}/>
<Route name="ViewWallet" path="/view-wallet" component={ViewWallet}/>
<Route name="Help" path="/help" component={Help}/>
<Route name="Swap" path="/swap" component={Swap}/>
<Redirect from="/*" to="/"/>
</Route>

View File

@ -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",