Commit f86f1948 authored by Chi Kei Chan's avatar Chi Kei Chan Committed by GitHub

Add Create Exchange (#94)

* MinLiquidity to zero in new exchange

* Add Create Exchange; Fix Generic Logo size

* Fix pool page routing
parent 2be295d1
...@@ -16,6 +16,10 @@ ...@@ -16,6 +16,10 @@
&::placeholder { &::placeholder {
color: $chalice-gray; color: $chalice-gray;
} }
&--error {
color: $salmon-red;
}
} }
&__recipient-row { &__recipient-row {
......
import React, { Component } from 'react'; import React, { Component } from 'react';
import { drizzleConnect } from 'drizzle-react'; import { drizzleConnect } from 'drizzle-react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import c from 'classnames';
import QrCode from '../QrCode'; import QrCode from '../QrCode';
import './address-input-panel.scss'; import './address-input-panel.scss';
...@@ -8,10 +9,9 @@ import './address-input-panel.scss'; ...@@ -8,10 +9,9 @@ import './address-input-panel.scss';
class AddressInputPanel extends Component { class AddressInputPanel extends Component {
static propTypes = { static propTypes = {
title: PropTypes.string, title: PropTypes.string,
description: PropTypes.string,
extraText: PropTypes.string,
onChange: PropTypes.func, onChange: PropTypes.func,
value: PropTypes.string, value: PropTypes.string,
errorMessage: PropTypes.string,
}; };
static defaultProps = { static defaultProps = {
...@@ -22,25 +22,28 @@ class AddressInputPanel extends Component { ...@@ -22,25 +22,28 @@ class AddressInputPanel extends Component {
render() { render() {
const { const {
title, title,
description,
extraText,
onChange, onChange,
value value,
errorMessage,
} = this.props; } = this.props;
return ( return (
<div className="currency-input-panel"> <div className="currency-input-panel">
<div className="currency-input-panel__container address-input-panel__recipient-row"> <div className={c('currency-input-panel__container address-input-panel__recipient-row', {
'currency-input-panel__container--error': errorMessage,
})}>
<div className="address-input-panel__input-container"> <div className="address-input-panel__input-container">
<div className="currency-input-panel__label-row"> <div className="currency-input-panel__label-row">
<div className="currency-input-panel__label-container"> <div className="currency-input-panel__label-container">
<span className="currency-input-panel__label">Recipient Address</span> <span className="currency-input-panel__label">{title || 'Recipient Address'}</span>
</div> </div>
</div> </div>
<div className="currency-input-panel__input-row"> <div className="currency-input-panel__input-row">
<input <input
type="text" type="text"
className="address-input-panel__input" className={c('address-input-panel__input',{
'address-input-panel__input--error': errorMessage,
})}
placeholder="0x1234..." placeholder="0x1234..."
onChange={e => onChange(e.target.value)} onChange={e => onChange(e.target.value)}
value={value} value={value}
......
...@@ -29,7 +29,7 @@ class NavigationTabs extends Component { ...@@ -29,7 +29,7 @@ class NavigationTabs extends Component {
push(path); push(path);
this.setState({ selectedPath: path }); this.setState({ selectedPath: path });
}} }}
isSelected={path === selectedPath } isSelected={selectedPath.indexOf(path) > -1}
/> />
) )
} }
......
...@@ -32,7 +32,8 @@ export default class TokenLogo extends Component { ...@@ -32,7 +32,8 @@ export default class TokenLogo extends Component {
render() { render() {
const { address, size, className } = this.props; const { address, size, className } = this.props;
let path = GenericTokenLogo; // let path = GenericTokenLogo;
let path = '';
const mainAddress = RINKEBY_TOKEN_MAP[address] ? RINKEBY_TOKEN_MAP[address] : address; const mainAddress = RINKEBY_TOKEN_MAP[address] ? RINKEBY_TOKEN_MAP[address] : address;
if (mainAddress === 'ETH') { if (mainAddress === 'ETH') {
...@@ -43,6 +44,10 @@ export default class TokenLogo extends Component { ...@@ -43,6 +44,10 @@ export default class TokenLogo extends Component {
path = `${TOKEN_ICON_API}/${mainAddress.toLowerCase()}.png`; path = `${TOKEN_ICON_API}/${mainAddress.toLowerCase()}.png`;
} }
if (!path) {
return <div className={className} style={{ width: size, fontSize: size }}>🤔</div>
}
return ( return (
<img <img
src={path} src={path}
......
...@@ -67,14 +67,30 @@ const ADD_EXCHANGE = 'app/addresses/addExchange'; ...@@ -67,14 +67,30 @@ const ADD_EXCHANGE = 'app/addresses/addExchange';
const initialState = RINKEBY; const initialState = RINKEBY;
export const addExchange = ({label, exchangeAddress, tokenAddress}) => ({ export const addExchange = ({label, exchangeAddress, tokenAddress}) => (dispatch, getState) => {
const { addresses: { tokenAddresses, exchangeAddresses } } = getState();
if (tokenAddresses.addresses.filter(([ symbol ]) => symbol === label).length) {
return;
}
if (tokenAddresses.addresses.filter(([ symbol ]) => symbol === label).length) {
return;
}
if (exchangeAddresses.fromToken[tokenAddresses]) {
return;
}
dispatch({
type: ADD_EXCHANGE, type: ADD_EXCHANGE,
payload: { payload: {
label, label,
exchangeAddress, exchangeAddress,
tokenAddress, tokenAddress,
}, },
}); });
};
export const setAddresses = networkId => { export const setAddresses = networkId => {
switch(networkId) { switch(networkId) {
...@@ -119,7 +135,7 @@ function handleAddExchange(state, { payload }) { ...@@ -119,7 +135,7 @@ function handleAddExchange(state, { payload }) {
...state.exchangeAddresses, ...state.exchangeAddresses,
addresses: [ addresses: [
...state.exchangeAddresses.addresses, ...state.exchangeAddresses.addresses,
[label,exchangeAddress] [label, exchangeAddress]
], ],
fromToken: { fromToken: {
...state.exchangeAddresses.fromToken, ...state.exchangeAddresses.fromToken,
......
...@@ -51,7 +51,7 @@ class App extends Component { ...@@ -51,7 +51,7 @@ class App extends Component {
> >
<Route exact path="/swap" component={Swap} /> <Route exact path="/swap" component={Swap} />
<Route exact path="/send" component={Send} /> <Route exact path="/send" component={Send} />
<Route exact path="/pool" component={Pool} /> <Route path="/pool" component={Pool} />
<Redirect exact from="/" to="/swap" /> <Redirect exact from="/" to="/swap" />
</AnimatedSwitch> </AnimatedSwitch>
</BrowserRouter> </BrowserRouter>
......
...@@ -139,7 +139,7 @@ class AddLiquidity extends Component { ...@@ -139,7 +139,7 @@ class AddLiquidity extends Component {
const block = await promisify(web3, 'getBlock', blockNumber); const block = await promisify(web3, 'getBlock', blockNumber);
const deadline = block.timestamp + 300; const deadline = block.timestamp + 300;
const MAX_LIQUIDITY_SLIPPAGE = 0.025; const MAX_LIQUIDITY_SLIPPAGE = 0.025;
const minLiquidity = liquidityMinted.multipliedBy(1 - MAX_LIQUIDITY_SLIPPAGE); const minLiquidity = this.isNewExchange() ? BN(0) : liquidityMinted.multipliedBy(1 - MAX_LIQUIDITY_SLIPPAGE);
const maxTokens = tokenAmount.multipliedBy(1 + MAX_LIQUIDITY_SLIPPAGE); const maxTokens = tokenAmount.multipliedBy(1 + MAX_LIQUIDITY_SLIPPAGE);
try { try {
...@@ -500,7 +500,7 @@ class AddLiquidity extends Component { ...@@ -500,7 +500,7 @@ class AddLiquidity extends Component {
) )
: null : null
} }
<ModeSelector /> <ModeSelector title="Add Liquidity" />
<CurrencyInputPanel <CurrencyInputPanel
title="Deposit" title="Deposit"
extraText={this.getBalance(inputCurrency)} extraText={this.getBalance(inputCurrency)}
......
import React, { Component } from 'react';
import { drizzleConnect } from 'drizzle-react';
import PropTypes from 'prop-types';
import {selectors} from "../../ducks/web3connect";
import classnames from "classnames";
import NavigationTabs from "../../components/NavigationTabs";
import ModeSelector from "./ModeSelector";
import AddressInputPanel from "../../components/AddressInputPanel";
import OversizedPanel from "../../components/OversizedPanel";
import FACTORY_ABI from "../../abi/factory";
import {addExchange} from "../../ducks/addresses";
class CreateExchange extends Component {
static propTypes = {
web3: PropTypes.object,
selectors: PropTypes.func.isRequired,
addExchange: PropTypes.func.isRequired,
account: PropTypes.string,
isConnected: PropTypes.bool.isRequired,
factoryAddress: PropTypes.string.isRequired,
exchangeAddresses: PropTypes.shape({
fromToken: PropTypes.object.isRequired,
}).isRequired,
};
state = {
tokenAddress: '',
label: '',
decimals: 0,
};
validate() {
const { tokenAddress } = this.state;
const {
web3,
account,
selectors,
factoryAddress,
exchangeAddresses: { fromToken },
addExchange,
} = this.props;
let isValid = true;
let errorMessage = '';
if (!tokenAddress) {
return {
isValid: false,
};
}
if (!web3.utils.isAddress(tokenAddress)) {
return {
isValid: false,
errorMessage: 'Not a valid token address',
};
}
const { label, decimals } = selectors().getBalance(account, tokenAddress);
const factory = new web3.eth.Contract(FACTORY_ABI, factoryAddress);
const exchangeAddress = fromToken[tokenAddress];
if (!exchangeAddress) {
factory.methods.getExchange(tokenAddress).call((err, data) => {
if (!err && data !== '0x0000000000000000000000000000000000000000') {
addExchange({ label, tokenAddress, exchangeAddress: data });
}
});
} else {
errorMessage = `Already has an exchange for ${label}`;
}
if (!label) {
errorMessage = 'Invalid symbol';
}
if (!decimals) {
errorMessage = 'Invalid decimals';
}
return {
isValid: isValid && !errorMessage,
errorMessage,
};
}
onChange = tokenAddress => {
const { selectors, account, web3 } = this.props;
if (web3.utils.isAddress(tokenAddress)) {
const { label, decimals } = selectors().getBalance(account, tokenAddress);
this.setState({
label,
decimals,
tokenAddress,
});
} else {
this.setState({
label: '',
decimals: 0,
tokenAddress,
});
}
};
onCreateExchange = () => {
const { tokenAddress } = this.state;
const { account, web3, factoryAddress } = this.props;
if (!web3.utils.isAddress(tokenAddress)) {
return;
}
const factory = new web3.eth.Contract(FACTORY_ABI, factoryAddress);
factory.methods.createExchange(tokenAddress).send({ from: account }, (err, data) => {
if (!err) {
this.setState({
label: '',
decimals: 0,
tokenAddress: '',
});
}
})
};
renderSummary() {
const { tokenAddress } = this.state;
const { errorMessage } = this.validate();
if (!tokenAddress) {
return (
<div className="create-exchange__summary-panel">
<div className="create-exchange__summary-text">Enter a token address to continue</div>
</div>
)
}
if (errorMessage) {
return (
<div className="create-exchange__summary-panel">
<div className="create-exchange__summary-text">{errorMessage}</div>
</div>
)
}
return null;
}
render() {
const { tokenAddress } = this.state;
const { isConnected, account, selectors, web3 } = this.props;
const { isValid, errorMessage } = this.validate();
let label, decimals;
if (web3.utils.isAddress(tokenAddress)) {
const { label: _label, decimals: _decimals } = selectors().getBalance(account, tokenAddress);
label = _label;
decimals = _decimals;
}
return (
<div
key="content"
className={classnames('swap__content', {
'swap--inactive': !isConnected,
})}
>
<NavigationTabs
className={classnames('header__navigation', {
'header--inactive': !isConnected,
})}
/>
<ModeSelector title="Create Exchange" />
<AddressInputPanel
title="Token Address"
value={tokenAddress}
onChange={this.onChange}
errorMessage={errorMessage}
/>
<OversizedPanel hideBottom>
<div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">Label</span>
<span>{label || ' - '}</span>
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">Decimals</span>
<span>{decimals || ' - '}</span>
</div>
</div>
</OversizedPanel>
<div className="pool__cta-container">
<button
className={classnames('pool__cta-btn', {
'swap--inactive': !isConnected,
})}
disabled={!isValid}
onClick={this.onCreateExchange}
>
Create Exchange
</button>
</div>
{ this.renderSummary() }
</div>
);
}
}
export default drizzleConnect(
CreateExchange,
state => ({
isConnected: Boolean(state.web3connect.account),
account: state.web3connect.account,
balances: state.web3connect.balances,
web3: state.web3connect.web3,
exchangeAddresses: state.addresses.exchangeAddresses,
factoryAddress: state.addresses.factoryAddress,
}),
dispatch => ({
selectors: () => dispatch(selectors()),
addExchange: opts => dispatch(addExchange(opts)),
})
);
\ No newline at end of file
import React, { Component } from 'react'; import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import { drizzleConnect } from 'drizzle-react'; import { drizzleConnect } from 'drizzle-react';
import OversizedPanel from "../../components/OversizedPanel"; import OversizedPanel from "../../components/OversizedPanel";
import Dropdown from "../../assets/images/dropdown.svg"; import Dropdown from "../../assets/images/dropdown.svg";
import Modal from "../../components/Modal"; import Modal from "../../components/Modal";
import {CSSTransitionGroup} from "react-transition-group"; import {CSSTransitionGroup} from "react-transition-group";
const ADD = 'Add Liquidity';
const REMOVE = 'Remove Liquidity';
const CREATE = 'Create Exchange';
class ModeSelector extends Component { class ModeSelector extends Component {
state = { state = {
isShowingModal: false, isShowingModal: false,
selected: ADD,
}; };
changeView(view) {
const { history } = this.props;
this.setState({
isShowingModal: false,
selected: view,
});
switch (view) {
case ADD:
return history.push('/pool/add');
case REMOVE:
return history.push('/pool/remove');
case CREATE:
return history.push('/pool/create');
default:
return;
}
}
renderModal() { renderModal() {
if (!this.state.isShowingModal) { if (!this.state.isShowingModal) {
return; return;
...@@ -28,15 +54,21 @@ class ModeSelector extends Component { ...@@ -28,15 +54,21 @@ class ModeSelector extends Component {
<div className="pool-modal"> <div className="pool-modal">
<div <div
className="pool-modal__item" className="pool-modal__item"
onClick={() => this.setState({ isShowingModal: false })} onClick={() => this.changeView(ADD)}
>
{ADD}
</div>
<div
className="pool-modal__item"
onClick={() => this.changeView(REMOVE)}
> >
Add Liquidity {REMOVE}
</div> </div>
<div <div
className="pool-modal__item" className="pool-modal__item"
onClick={() => this.setState({ isShowingModal: false })} onClick={() => this.changeView(CREATE)}
> >
Remove Liquidity {CREATE}
</div> </div>
</div> </div>
</CSSTransitionGroup> </CSSTransitionGroup>
...@@ -51,7 +83,9 @@ class ModeSelector extends Component { ...@@ -51,7 +83,9 @@ class ModeSelector extends Component {
className="pool__liquidity-container" className="pool__liquidity-container"
onClick={() => this.setState({ isShowingModal: true })} onClick={() => this.setState({ isShowingModal: true })}
> >
<span className="pool__liquidity-label">Add Liquidity</span> <span className="pool__liquidity-label">
{this.props.title}
</span>
<img src={Dropdown} /> <img src={Dropdown} />
</div> </div>
{this.renderModal()} {this.renderModal()}
...@@ -60,4 +94,4 @@ class ModeSelector extends Component { ...@@ -60,4 +94,4 @@ class ModeSelector extends Component {
} }
} }
export default drizzleConnect(ModeSelector); export default withRouter(ModeSelector);
import React, { Component } from 'react'; import React, { Component } from 'react';
import Header from '../../components/Header'; import Header from '../../components/Header';
import AddLiquidity from './AddLiquidity'; import AddLiquidity from './AddLiquidity';
import CreateExchange from './CreateExchange';
import { Switch, Redirect, Route } from 'react-router-dom';
import "./pool.scss"; import "./pool.scss";
import Swap from "../Swap";
import Send from "../Send";
import {AnimatedSwitch} from "react-router-transition";
const ADD_LIQUIDITY = 'Add Liquidity'; const ADD_LIQUIDITY = 'Add Liquidity';
const REMOVE_LIQUIDITY = 'Remove Liquidity'; const REMOVE_LIQUIDITY = 'Remove Liquidity';
const CREATE_EXCHANGE = 'Create Exchange';
class Pool extends Component { class Pool extends Component {
state = { state = {
selectedMode: ADD_LIQUIDITY, selectedMode: ADD_LIQUIDITY,
}; };
renderContent() {
switch (this.state.selectedMode) {
case ADD_LIQUIDITY:
default:
return <AddLiquidity />
}
}
render() { render() {
return ( return (
<div className="pool"> <div className="pool">
<Header /> <Header />
{ this.renderContent() } <Switch>
<Route exact path="/pool/add" component={AddLiquidity} />
{/*<Route exact path="/remove" component={Send} />*/}
<Route exact path="/pool/create" component={CreateExchange} />
<Redirect exact from="/pool" to="/pool/add" />
</Switch>
</div> </div>
); );
} }
......
...@@ -82,9 +82,9 @@ ...@@ -82,9 +82,9 @@
.pool-modal { .pool-modal {
background-color: $white; background-color: $white;
position: relative; position: relative;
bottom: 7.875rem; bottom: 11.0625rem;
width: 100%; width: 100%;
height: 7.875rem; height: 11.0625rem;
z-index: 2000; z-index: 2000;
border-top-left-radius: 1rem; border-top-left-radius: 1rem;
border-top-right-radius: 1rem; border-top-right-radius: 1rem;
...@@ -116,3 +116,22 @@ ...@@ -116,3 +116,22 @@
.pool-modal-appear.modal-container-appear-active { .pool-modal-appear.modal-container-appear-active {
bottom: 0; bottom: 0;
} }
.create-exchange {
&__summary-panel {
position: absolute;
bottom: 0;
left: 0;
background-color: $concrete-gray;
width: 100%;
text-align: center;
border-top-left-radius: .625rem;
border-top-right-radius: .625rem;
}
&__summary-text {
padding: .75rem;
font-size: .75rem;
color: $dove-gray;
}
}
...@@ -458,13 +458,6 @@ class Send extends Component { ...@@ -458,13 +458,6 @@ class Send extends Component {
return; return;
} }
console.log(
BN(outputValue).multipliedBy(10 ** outputDecimals).toFixed(0),
BN(inputValue).multipliedBy(10 ** inputDecimals).multipliedBy(1 + TOKEN_ALLOWED_SLIPPAGE).toFixed(0),
inputAmountB.multipliedBy(1.2).toFixed(0),
deadline,
outputCurrency,
)
new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency]) new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency])
.methods .methods
.tokenToTokenTransferOutput( .tokenToTokenTransferOutput(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment