commit
3e299ff243
3
src/assets/images/dropup-blue.svg
Normal file
3
src/assets/images/dropup-blue.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="12" height="7" viewBox="0 0 12 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.4673 6L6.23364 1L0.999995 6" stroke="#388DFF"/>
|
||||
</svg>
|
After Width: | Height: | Size: 162 B |
@ -4,12 +4,14 @@
|
||||
@extend %col-nowrap;
|
||||
|
||||
&__input {
|
||||
font-size: .75rem;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
border: none;
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
color: $royal-blue;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&::placeholder {
|
||||
color: $chalice-gray;
|
||||
|
@ -59,6 +59,15 @@
|
||||
&--error {
|
||||
color: $salmon-red;
|
||||
}
|
||||
|
||||
&[type='number'] {
|
||||
-moz-appearance:textfield;
|
||||
}
|
||||
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__extra-text {
|
||||
|
@ -210,15 +210,16 @@ class CurrencyInputPanel extends Component {
|
||||
exchangeAddresses: { fromToken },
|
||||
web3,
|
||||
disableUnlock,
|
||||
value,
|
||||
} = this.props;
|
||||
|
||||
if (disableUnlock || !selectedTokenAddress || selectedTokenAddress === 'ETH') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { value, decimals, label } = selectors().getApprovals(selectedTokenAddress, account, fromToken[selectedTokenAddress]);
|
||||
const { value: allowance, decimals, label } = selectors().getApprovals(selectedTokenAddress, account, fromToken[selectedTokenAddress]);
|
||||
|
||||
if (!label || value.isGreaterThan(BN(10 ** 22))) {
|
||||
if (!label || allowance.isGreaterThanOrEqualTo(BN(value * 10 ** decimals || 0))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -272,11 +273,21 @@ class CurrencyInputPanel extends Component {
|
||||
<div className="currency-input-panel__input-row">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
className={classnames('currency-input-panel__input',{
|
||||
'currency-input-panel__input--error': errorMessage,
|
||||
})}
|
||||
placeholder="0.0"
|
||||
onChange={e => onValueChange(e.target.value)}
|
||||
onKeyPress={e => {
|
||||
const charCode = e.which ? e.which : e.keyCode;
|
||||
|
||||
// Prevent 'minus' character
|
||||
if (charCode === 45) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
value={value}
|
||||
/>
|
||||
{ this.renderUnlockButton() }
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
&__top {
|
||||
@extend %row-nowrap;
|
||||
padding: 1.5rem;
|
||||
padding: 1.5rem 1.5rem .5rem 1.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
}
|
||||
|
||||
&__navigation {
|
||||
margin: 0 .75rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
&--inactive {
|
||||
|
@ -121,11 +121,6 @@ function Header (props) {
|
||||
</div>
|
||||
<Web3Status isConnected />
|
||||
</div>
|
||||
<NavigationTabs
|
||||
className={classnames('header__navigation', {
|
||||
'header--inactive': !props.isConnected,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -3,6 +3,14 @@ import PropTypes from 'prop-types';
|
||||
import EthereumLogo from '../../assets/images/ethereum-logo.png';
|
||||
import GenericTokenLogo from '../../assets/images/generic-token-logo.png';
|
||||
|
||||
const RINKEBY_TOKEN_MAP = {
|
||||
'0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B': '0x0d8775f648430679a709e98d2b0cb6250d2887ef',
|
||||
'0x2448eE2641d78CC42D7AD76498917359D961A783': '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359',
|
||||
'0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85': '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2',
|
||||
'0x879884c3C46A24f56089f3bBbe4d5e38dB5788C0': '0xd26114cd6ee289accf82350c8d8487fedb8a0c07',
|
||||
'0xF22e3F33768354c9805d046af3C0926f27741B43': '0xe41d2489571d322189246dafa5ebde1f4699f498',
|
||||
};
|
||||
|
||||
const TOKEN_ICON_API = 'https://raw.githubusercontent.com/TrustWallet/tokens/master/images';
|
||||
const BAD_IMAGES = {};
|
||||
export default class TokenLogo extends Component {
|
||||
@ -14,7 +22,7 @@ export default class TokenLogo extends Component {
|
||||
|
||||
static defaultProps = {
|
||||
address: '',
|
||||
size: '1.5rem',
|
||||
size: '1rem',
|
||||
className: '',
|
||||
};
|
||||
|
||||
@ -25,13 +33,14 @@ export default class TokenLogo extends Component {
|
||||
render() {
|
||||
const { address, size, className } = this.props;
|
||||
let path = GenericTokenLogo;
|
||||
const mainAddress = RINKEBY_TOKEN_MAP[address] ? RINKEBY_TOKEN_MAP[address] : address;
|
||||
|
||||
if (address === 'ETH') {
|
||||
if (mainAddress === 'ETH') {
|
||||
path = EthereumLogo;
|
||||
}
|
||||
|
||||
if (!this.state.error && !BAD_IMAGES[address]) {
|
||||
path = `${TOKEN_ICON_API}/${address}.png`;
|
||||
if (!this.state.error && !BAD_IMAGES[mainAddress] && mainAddress !== 'ETH') {
|
||||
path = `${TOKEN_ICON_API}/${mainAddress}.png`;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -44,7 +53,7 @@ export default class TokenLogo extends Component {
|
||||
}}
|
||||
onError={() => {
|
||||
this.setState({ error: true });
|
||||
BAD_IMAGES[address] = true;
|
||||
BAD_IMAGES[mainAddress] = true;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -7,7 +7,7 @@
|
||||
line-height: 1rem;
|
||||
align-items: center;
|
||||
border: 1px solid $mercury-gray;
|
||||
padding: .5rem 1rem;
|
||||
padding: .5rem;
|
||||
border-radius: 2rem;
|
||||
color: $dove-gray;
|
||||
font-weight: 400;
|
||||
|
@ -2,10 +2,15 @@ import React, { Component } from 'react';
|
||||
import { drizzleConnect } from 'drizzle-react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from "classnames";
|
||||
import { CSSTransitionGroup } from "react-transition-group";
|
||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel';
|
||||
import OversizedPanel from '../../components/OversizedPanel';
|
||||
import NavigationTabs from '../../components/NavigationTabs';
|
||||
import Modal from '../../components/Modal';
|
||||
import { selectors, sync } from '../../ducks/web3connect';
|
||||
import ArrowDown from '../../assets/images/arrow-down-blue.svg';
|
||||
import DropdownBlue from "../../assets/images/dropdown-blue.svg";
|
||||
import DropupBlue from "../../assets/images/dropup-blue.svg";
|
||||
import ModeSelector from './ModeSelector';
|
||||
import {BigNumber as BN} from 'bignumber.js';
|
||||
import EXCHANGE_ABI from '../../abi/exchange';
|
||||
@ -32,11 +37,12 @@ class AddLiquidity extends Component {
|
||||
inputCurrency: 'ETH',
|
||||
outputCurrency: '',
|
||||
lastEditedField: '',
|
||||
showSummaryModal: false,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const { isConnected, account, exchangeAddresses, balances, web3 } = this.props;
|
||||
const { inputValue, outputValue, inputCurrency, outputCurrency, lastEditedField } = this.state;
|
||||
const { inputValue, outputValue, inputCurrency, outputCurrency, lastEditedField, showSummaryModal } = this.state;
|
||||
|
||||
return isConnected !== nextProps.isConnected ||
|
||||
account !== nextProps.account ||
|
||||
@ -47,7 +53,8 @@ class AddLiquidity extends Component {
|
||||
outputValue !== nextState.outputValue ||
|
||||
inputCurrency !== nextState.inputCurrency ||
|
||||
outputCurrency !== nextState.outputCurrency ||
|
||||
lastEditedField !== nextState.lastEditedField;
|
||||
lastEditedField !== nextState.lastEditedField ||
|
||||
showSummaryModal !== nextState.showSummaryModal;
|
||||
}
|
||||
|
||||
getBalance(currency) {
|
||||
@ -61,6 +68,27 @@ class AddLiquidity extends Component {
|
||||
return `Balance: ${value.dividedBy(10 ** decimals).toFixed(4)}`;
|
||||
}
|
||||
|
||||
isUnapproved() {
|
||||
const { account, exchangeAddresses, selectors } = this.props;
|
||||
const { outputCurrency, outputValue } = this.state;
|
||||
|
||||
if (!outputCurrency) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { value: allowance, label, decimals } = selectors().getApprovals(
|
||||
outputCurrency,
|
||||
account,
|
||||
exchangeAddresses.fromToken[outputCurrency]
|
||||
);
|
||||
|
||||
if (label && allowance.isLessThan(BN(outputValue * 10 ** decimals || 0))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onAddLiquidity = async () => {
|
||||
const { account, web3, exchangeAddresses: { fromToken }, selectors } = this.props;
|
||||
const { inputValue, outputValue, outputCurrency } = this.state;
|
||||
@ -155,8 +183,10 @@ class AddLiquidity extends Component {
|
||||
let inputError;
|
||||
let outputError;
|
||||
let isValid = true;
|
||||
const inputIsZero = BN(inputValue).isZero();
|
||||
const outputIsZero = BN(outputValue).isZero();
|
||||
|
||||
if (!inputValue || !outputValue || !inputCurrency || !outputCurrency) {
|
||||
if (!inputValue || inputIsZero || !outputValue || outputIsZero || !inputCurrency || !outputCurrency || this.isUnapproved()) {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
@ -229,10 +259,12 @@ class AddLiquidity extends Component {
|
||||
inputCurrency,
|
||||
outputCurrency,
|
||||
} = this.state;
|
||||
const inputIsZero = BN(inputValue).isZero();
|
||||
const outputIsZero = BN(outputValue).isZero();
|
||||
|
||||
if (!inputCurrency || !outputCurrency) {
|
||||
return (
|
||||
<div className="swap__summary-wrapper">
|
||||
<div key="summary" className="swap__summary-wrapper">
|
||||
<div>Select a token to continue.</div>
|
||||
</div>
|
||||
)
|
||||
@ -240,7 +272,7 @@ class AddLiquidity extends Component {
|
||||
|
||||
if (inputCurrency === outputCurrency) {
|
||||
return (
|
||||
<div className="swap__summary-wrapper">
|
||||
<div key="summary" className="swap__summary-wrapper">
|
||||
<div>Must be different token.</div>
|
||||
</div>
|
||||
)
|
||||
@ -248,22 +280,66 @@ class AddLiquidity extends Component {
|
||||
|
||||
if (![inputCurrency, outputCurrency].includes('ETH')) {
|
||||
return (
|
||||
<div className="swap__summary-wrapper">
|
||||
<div key="summary" className="swap__summary-wrapper">
|
||||
<div>One of the input must be ETH.</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (inputIsZero || outputIsZero) {
|
||||
return (
|
||||
<div key="summary" className="swap__summary-wrapper">
|
||||
<div>Amount cannot be zero.</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (this.isUnapproved()) {
|
||||
return (
|
||||
<div key="summary" className="swap__summary-wrapper">
|
||||
<div>Please unlock token to continue.</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const { value, decimals, label } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency]);
|
||||
|
||||
if (!inputValue || !outputValue) {
|
||||
return (
|
||||
<div className="swap__summary-wrapper">
|
||||
<div key="summary" className="swap__summary-wrapper">
|
||||
<div>{`Enter a ${inputCurrency} or ${label} value to continue.`}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return [
|
||||
<div
|
||||
key="open-details"
|
||||
className="swap__summary-wrapper swap__open-details-container"
|
||||
onClick={() => this.setState({showSummaryModal: true})}
|
||||
>
|
||||
<span>Transaction Details</span>
|
||||
<img src={DropdownBlue} />
|
||||
</div>,
|
||||
this.renderSummaryModal()
|
||||
];
|
||||
}
|
||||
|
||||
renderSummaryModal() {
|
||||
const { selectors, exchangeAddresses: { fromToken } } = this.props;
|
||||
const {
|
||||
inputValue,
|
||||
outputValue,
|
||||
inputCurrency,
|
||||
outputCurrency,
|
||||
showSummaryModal,
|
||||
} = this.state;
|
||||
if (!showSummaryModal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { value, decimals, label } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency]);
|
||||
|
||||
const SLIPPAGE = 0.025;
|
||||
const minOutput = BN(outputValue).multipliedBy(1 - SLIPPAGE);
|
||||
const maxOutput = BN(outputValue).multipliedBy(1 + SLIPPAGE);
|
||||
@ -272,13 +348,34 @@ class AddLiquidity extends Component {
|
||||
const maxPercentage = maxOutput.dividedBy(maxOutput.plus(tokenReserve)).multipliedBy(100);
|
||||
|
||||
return (
|
||||
<div className="swap__summary-wrapper">
|
||||
<div>You are adding between {b(`${minOutput.toFixed(2)} - ${maxOutput.toFixed(2)} ${label}`)} + {b(`${BN(inputValue).toFixed(2)} ETH`)} into the liquidity pool.</div>
|
||||
<div className="pool__last-summary-text">
|
||||
You will receive between {b(`${minPercentage.toFixed(2)}%`)} and {b(`${maxPercentage.toFixed(2)}%`)} of the {`${label}/ETH`} pool tokens.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
<Modal key="modal" onClose={() => this.setState({ showSummaryModal: false })}>
|
||||
<CSSTransitionGroup
|
||||
transitionName="summary-modal"
|
||||
transitionAppear={true}
|
||||
transitionLeave={true}
|
||||
transitionAppearTimeout={200}
|
||||
transitionLeaveTimeout={200}
|
||||
transitionEnterTimeout={200}
|
||||
>
|
||||
<div className="swap__summary-modal">
|
||||
<div
|
||||
key="open-details"
|
||||
className="swap__open-details-container"
|
||||
onClick={() => this.setState({showSummaryModal: false})}
|
||||
>
|
||||
<span>Transaction Details</span>
|
||||
<img src={DropupBlue} />
|
||||
</div>
|
||||
<div>
|
||||
<div>You are adding between {b(`${minOutput.toFixed(5)} - ${maxOutput.toFixed(5)} ${label}`)} + {b(`${BN(inputValue).toFixed(5)} ETH`)} into the liquidity pool.</div>
|
||||
<div className="pool__last-summary-text">
|
||||
You will receive between {b(`${minPercentage.toFixed(5)}%`)} and {b(`${maxPercentage.toFixed(5)}%`)} of the {`${label}/ETH`} pool tokens.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CSSTransitionGroup>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -296,12 +393,18 @@ class AddLiquidity extends Component {
|
||||
|
||||
const { inputError, outputError, isValid } = this.validate();
|
||||
|
||||
return (
|
||||
return [
|
||||
<div
|
||||
key="content"
|
||||
className={classnames('swap__content', {
|
||||
'swap--inactive': !isConnected,
|
||||
})}
|
||||
>
|
||||
<NavigationTabs
|
||||
className={classnames('header__navigation', {
|
||||
'header--inactive': !isConnected,
|
||||
})}
|
||||
/>
|
||||
<ModeSelector />
|
||||
<CurrencyInputPanel
|
||||
title="Deposit"
|
||||
@ -333,7 +436,6 @@ class AddLiquidity extends Component {
|
||||
<OversizedPanel hideBottom>
|
||||
{ this.renderInfo() }
|
||||
</OversizedPanel>
|
||||
{ this.renderSummary() }
|
||||
<div className="pool__cta-container">
|
||||
<button
|
||||
className={classnames('pool__cta-btn', {
|
||||
@ -346,8 +448,9 @@ class AddLiquidity extends Component {
|
||||
Add Liquidity
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
</div>,
|
||||
this.renderSummary()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,11 +3,16 @@ import { drizzleConnect } from 'drizzle-react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import {BigNumber as BN} from "bignumber.js";
|
||||
import { CSSTransitionGroup } from "react-transition-group";
|
||||
import { selectors } from '../../ducks/web3connect';
|
||||
import Header from '../../components/Header';
|
||||
import NavigationTabs from '../../components/NavigationTabs';
|
||||
import AddressInputPanel from '../../components/AddressInputPanel';
|
||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel';
|
||||
import Modal from '../../components/Modal';
|
||||
import OversizedPanel from '../../components/OversizedPanel';
|
||||
import DropdownBlue from "../../assets/images/dropdown-blue.svg";
|
||||
import DropupBlue from "../../assets/images/dropup-blue.svg";
|
||||
import ArrowDown from '../../assets/images/arrow-down-blue.svg';
|
||||
import EXCHANGE_ABI from '../../abi/exchange';
|
||||
|
||||
@ -28,11 +33,12 @@ class Send extends Component {
|
||||
state = {
|
||||
inputValue: '',
|
||||
outputValue: '',
|
||||
inputCurrency: '',
|
||||
inputCurrency: 'ETH',
|
||||
outputCurrency: '',
|
||||
inputAmountB: '',
|
||||
lastEditedField: '',
|
||||
recipient: '',
|
||||
showSummaryModal: false,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
@ -48,6 +54,7 @@ class Send extends Component {
|
||||
inputAmountB: '',
|
||||
lastEditedField: '',
|
||||
recipient: '',
|
||||
showSummaryModal: false,
|
||||
});
|
||||
}
|
||||
|
||||
@ -56,7 +63,7 @@ class Send extends Component {
|
||||
}
|
||||
|
||||
validate() {
|
||||
const { selectors, account } = this.props;
|
||||
const { selectors, account, web3 } = this.props;
|
||||
const {
|
||||
inputValue, outputValue,
|
||||
inputCurrency, outputCurrency,
|
||||
@ -66,8 +73,11 @@ class Send extends Component {
|
||||
let inputError = '';
|
||||
let outputError = '';
|
||||
let isValid = true;
|
||||
const validRecipientAddress = web3 && web3.utils.isAddress(recipient);
|
||||
const inputIsZero = BN(inputValue).isZero();
|
||||
const outputIsZero = BN(outputValue).isZero();
|
||||
|
||||
if (!inputValue || !outputValue || !inputCurrency || !outputCurrency || !recipient) {
|
||||
if (!inputValue || inputIsZero || !outputValue || outputIsZero || !inputCurrency || !outputCurrency || !recipient || this.isUnapproved() || !validRecipientAddress) {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
@ -88,13 +98,40 @@ class Send extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
isUnapproved() {
|
||||
const { account, exchangeAddresses, selectors } = this.props;
|
||||
const { inputCurrency, inputValue } = this.state;
|
||||
|
||||
if (!inputCurrency || inputCurrency === 'ETH') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { value: allowance, label, decimals } = selectors().getApprovals(
|
||||
inputCurrency,
|
||||
account,
|
||||
exchangeAddresses.fromToken[inputCurrency]
|
||||
);
|
||||
|
||||
if (label && allowance.isLessThan(BN(inputValue * 10 ** decimals || 0))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
recalcForm() {
|
||||
const { inputCurrency, outputCurrency } = this.state;
|
||||
const { inputCurrency, outputCurrency, lastEditedField } = this.state;
|
||||
|
||||
if (!inputCurrency || !outputCurrency) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editedValue = lastEditedField === INPUT ? this.state.inputValue : this.state.outputValue;
|
||||
|
||||
if (BN(editedValue).isZero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputCurrency === outputCurrency) {
|
||||
this.setState({
|
||||
inputValue: '',
|
||||
@ -457,10 +494,14 @@ class Send extends Component {
|
||||
outputError,
|
||||
recipient,
|
||||
} = this.state;
|
||||
const { web3 } = this.props;
|
||||
|
||||
const { selectors, account } = this.props;
|
||||
const { label: inputLabel } = selectors().getBalance(account, inputCurrency);
|
||||
const { label: outputLabel } = selectors().getBalance(account, outputCurrency);
|
||||
const validRecipientAddress = web3 && web3.utils.isAddress(recipient);
|
||||
const inputIsZero = BN(inputValue).isZero();
|
||||
const outputIsZero = BN(outputValue).isZero();
|
||||
|
||||
let nextStepMessage;
|
||||
if (inputError || outputError) {
|
||||
@ -472,8 +513,14 @@ class Send extends Component {
|
||||
} else if (!inputValue || !outputValue) {
|
||||
const missingCurrencyValue = !inputValue ? inputLabel : outputLabel;
|
||||
nextStepMessage = `Enter a ${missingCurrencyValue} value to continue.`;
|
||||
} else if (inputIsZero || outputIsZero) {
|
||||
nextStepMessage = 'Amount cannot be zero.';
|
||||
} else if (this.isUnapproved()) {
|
||||
nextStepMessage = 'Please unlock token to continue.';
|
||||
} else if (!recipient) {
|
||||
nextStepMessage = 'Enter a wallet address to send to.';
|
||||
} else if (!validRecipientAddress) {
|
||||
nextStepMessage = 'Please enter a valid wallet address recipient.';
|
||||
}
|
||||
|
||||
if (nextStepMessage) {
|
||||
@ -484,20 +531,129 @@ class Send extends Component {
|
||||
)
|
||||
}
|
||||
|
||||
const SLIPPAGE = 0.025;
|
||||
const minOutput = BN(outputValue).multipliedBy(1 - SLIPPAGE).toFixed(2);
|
||||
const maxOutput = BN(outputValue).multipliedBy(1 + SLIPPAGE).toFixed(2);
|
||||
return [
|
||||
<div
|
||||
key="open-details"
|
||||
className="swap__summary-wrapper swap__open-details-container"
|
||||
onClick={() => this.setState({showSummaryModal: true})}
|
||||
>
|
||||
<span>Transaction Details</span>
|
||||
<img src={DropdownBlue} />
|
||||
</div>,
|
||||
this.renderSummaryModal()
|
||||
];
|
||||
}
|
||||
|
||||
renderSummaryModal() {
|
||||
const {
|
||||
inputValue,
|
||||
inputCurrency,
|
||||
inputError,
|
||||
outputValue,
|
||||
outputCurrency,
|
||||
outputError,
|
||||
recipient,
|
||||
showSummaryModal,
|
||||
inputAmountB,
|
||||
lastEditedField,
|
||||
} = this.state;
|
||||
const { selectors, account } = this.props;
|
||||
if (!this.state.showSummaryModal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ALLOWED_SLIPPAGE = 0.025;
|
||||
const TOKEN_ALLOWED_SLIPPAGE = 0.04;
|
||||
|
||||
const type = getSendType(inputCurrency, outputCurrency);
|
||||
const { label: inputLabel, decimals: inputDecimals } = selectors().getBalance(account, inputCurrency);
|
||||
const { label: outputLabel, decimals: outputDecimals } = selectors().getBalance(account, outputCurrency);
|
||||
|
||||
const label = lastEditedField === INPUT ? outputLabel : inputLabel;
|
||||
let minOutput;
|
||||
let maxInput;
|
||||
|
||||
if (lastEditedField === INPUT) {
|
||||
switch(type) {
|
||||
case 'ETH_TO_TOKEN':
|
||||
minOutput = BN(outputValue).multipliedBy(1 - ALLOWED_SLIPPAGE).toFixed(5)
|
||||
break;
|
||||
case 'TOKEN_TO_ETH':
|
||||
minOutput = BN(outputValue).multipliedBy(1 - ALLOWED_SLIPPAGE).toFixed(5);
|
||||
break;
|
||||
case 'TOKEN_TO_TOKEN':
|
||||
minOutput = BN(outputValue).multipliedBy(1 - TOKEN_ALLOWED_SLIPPAGE).toFixed(5);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastEditedField === OUTPUT) {
|
||||
switch (type) {
|
||||
case 'ETH_TO_TOKEN':
|
||||
maxInput = BN(inputValue).multipliedBy(1 + ALLOWED_SLIPPAGE).toFixed(5);
|
||||
break;
|
||||
case 'TOKEN_TO_ETH':
|
||||
maxInput = BN(inputValue).multipliedBy(1 + ALLOWED_SLIPPAGE).toFixed(5);
|
||||
break;
|
||||
case 'TOKEN_TO_TOKEN':
|
||||
maxInput = BN(inputValue).multipliedBy(1 + TOKEN_ALLOWED_SLIPPAGE).toFixed(5);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let description;
|
||||
if (lastEditedField === INPUT) {
|
||||
description = (
|
||||
<div>
|
||||
<div>
|
||||
You are selling {b(`${inputValue} ${inputLabel}`)}.
|
||||
</div>
|
||||
<div className="send__last-summary-text">
|
||||
<span className="swap__highlight-text">{recipient.slice(0, 6)}</span> will receive between {b(`${minOutput} ${outputLabel}`)} and {b(`${outputValue} ${outputLabel}`)}.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
description = (
|
||||
<div>
|
||||
<div>
|
||||
You are selling between {b(`${inputValue} ${inputLabel}`)} to {b(`${maxInput} ${inputLabel}`)}.
|
||||
</div>
|
||||
<div className="send__last-summary-text">
|
||||
<span className="swap__highlight-text">{recipient.slice(0, 6)}</span> will receive {b(`${outputValue} ${outputLabel}`)}.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="swap__summary-wrapper">
|
||||
<div>
|
||||
You are selling {b(`${inputValue} ${inputLabel}`)}
|
||||
</div>
|
||||
<div className="send__last-summary-text">
|
||||
<span className="swap__highlight-text">{recipient.slice(0, 6)}</span> will receive between {b(`${minOutput} ${outputLabel}`)} and {b(`${maxOutput} ${outputLabel}`)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
<Modal key="modal" onClose={() => this.setState({ showSummaryModal: false })}>
|
||||
<CSSTransitionGroup
|
||||
transitionName="summary-modal"
|
||||
transitionAppear={true}
|
||||
transitionLeave={true}
|
||||
transitionAppearTimeout={200}
|
||||
transitionLeaveTimeout={200}
|
||||
transitionEnterTimeout={200}
|
||||
>
|
||||
<div className="swap__summary-modal">
|
||||
<div
|
||||
key="open-details"
|
||||
className="swap__open-details-container"
|
||||
onClick={() => this.setState({showSummaryModal: false})}
|
||||
>
|
||||
<span>Transaction Details</span>
|
||||
<img src={DropupBlue} />
|
||||
</div>
|
||||
{description}
|
||||
</div>
|
||||
</CSSTransitionGroup>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
renderExchangeRate() {
|
||||
@ -547,13 +703,18 @@ class Send extends Component {
|
||||
const { inputError, outputError, isValid } = this.validate();
|
||||
|
||||
return (
|
||||
<div className="swap">
|
||||
<div className="send">
|
||||
<Header />
|
||||
<div
|
||||
className={classnames('swap__content', {
|
||||
'swap--inactive': !this.props.isConnected,
|
||||
})}
|
||||
>
|
||||
<NavigationTabs
|
||||
className={classnames('header__navigation', {
|
||||
'header--inactive': !this.props.isConnected,
|
||||
})}
|
||||
/>
|
||||
<CurrencyInputPanel
|
||||
title="Input"
|
||||
description={lastEditedField === OUTPUT ? estimatedText : ''}
|
||||
@ -598,17 +759,19 @@ class Send extends Component {
|
||||
onChange={address => this.setState({recipient: address})}
|
||||
/>
|
||||
{ this.renderExchangeRate() }
|
||||
{ this.renderSummary() }
|
||||
<div className="swap__cta-container">
|
||||
<button
|
||||
className={classnames('swap__cta-btn', {
|
||||
'swap--inactive': !this.props.isConnected,
|
||||
})}
|
||||
disabled={!isValid}
|
||||
onClick={this.onSend}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className={classnames('swap__cta-btn', {
|
||||
'swap--inactive': !this.props.isConnected,
|
||||
})}
|
||||
disabled={!isValid}
|
||||
onClick={this.onSend}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
{ this.renderSummary() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -4,9 +4,14 @@ import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import {BigNumber as BN} from "bignumber.js";
|
||||
import { selectors } from '../../ducks/web3connect';
|
||||
import { CSSTransitionGroup } from "react-transition-group";
|
||||
import Header from '../../components/Header';
|
||||
import NavigationTabs from '../../components/NavigationTabs';
|
||||
import Modal from '../../components/Modal';
|
||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel';
|
||||
import OversizedPanel from '../../components/OversizedPanel';
|
||||
import DropdownBlue from "../../assets/images/dropdown-blue.svg";
|
||||
import DropupBlue from "../../assets/images/dropup-blue.svg";
|
||||
import ArrowDown from '../../assets/images/arrow-down-blue.svg';
|
||||
import EXCHANGE_ABI from '../../abi/exchange';
|
||||
|
||||
@ -28,10 +33,11 @@ class Swap extends Component {
|
||||
state = {
|
||||
inputValue: '',
|
||||
outputValue: '',
|
||||
inputCurrency: '',
|
||||
inputCurrency: 'ETH',
|
||||
outputCurrency: '',
|
||||
inputAmountB: '',
|
||||
lastEditedField: '',
|
||||
showSummaryModal: false,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
@ -46,6 +52,7 @@ class Swap extends Component {
|
||||
outputCurrency: '',
|
||||
inputAmountB: '',
|
||||
lastEditedField: '',
|
||||
showSummaryModal: false,
|
||||
});
|
||||
}
|
||||
|
||||
@ -63,8 +70,11 @@ class Swap extends Component {
|
||||
let inputError = '';
|
||||
let outputError = '';
|
||||
let isValid = true;
|
||||
let isUnapproved = this.isUnapproved();
|
||||
const inputIsZero = BN(inputValue).isZero();
|
||||
const outputIsZero = BN(outputValue).isZero();
|
||||
|
||||
if (!inputValue || !outputValue || !inputCurrency || !outputCurrency) {
|
||||
if (!inputValue || inputIsZero || !outputValue || outputIsZero || !inputCurrency || !outputCurrency || isUnapproved) {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
@ -85,13 +95,40 @@ class Swap extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
isUnapproved() {
|
||||
const { account, exchangeAddresses, selectors } = this.props;
|
||||
const { inputCurrency, inputValue } = this.state;
|
||||
|
||||
if (!inputCurrency || inputCurrency === 'ETH') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { value: allowance, label, decmals } = selectors().getApprovals(
|
||||
inputCurrency,
|
||||
account,
|
||||
exchangeAddresses.fromToken[inputCurrency]
|
||||
);
|
||||
|
||||
if (label && allowance.isLessThan(BN(inputValue * 10 ** decimals || 0))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
recalcForm() {
|
||||
const { inputCurrency, outputCurrency } = this.state;
|
||||
const { inputCurrency, outputCurrency, lastEditedField } = this.state;
|
||||
|
||||
if (!inputCurrency || !outputCurrency) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editedValue = lastEditedField === INPUT ? this.state.inputValue : this.state.outputValue;
|
||||
|
||||
if (BN(editedValue).isZero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputCurrency === outputCurrency) {
|
||||
this.setState({
|
||||
inputValue: '',
|
||||
@ -446,12 +483,8 @@ class Swap extends Component {
|
||||
outputCurrency,
|
||||
} = this.state;
|
||||
|
||||
const { selectors, account } = this.props;
|
||||
const { label: inputLabel } = selectors().getBalance(account, inputCurrency);
|
||||
const { label: outputLabel } = selectors().getBalance(account, outputCurrency);
|
||||
const SLIPPAGE = 0.025;
|
||||
const minOutput = BN(outputValue).multipliedBy(1 - SLIPPAGE).toFixed(2);
|
||||
const maxOutput = BN(outputValue).multipliedBy(1 + SLIPPAGE).toFixed(2);
|
||||
const inputIsZero = BN(inputValue).isZero();
|
||||
const outputIsZero = BN(outputValue).isZero();
|
||||
|
||||
if (!inputCurrency || !outputCurrency) {
|
||||
return (
|
||||
@ -469,12 +502,144 @@ class Swap extends Component {
|
||||
)
|
||||
}
|
||||
|
||||
if (inputIsZero || outputIsZero) {
|
||||
return (
|
||||
<div className="swap__summary-wrapper">
|
||||
<div>Amount cannot be zero.</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (this.isUnapproved()) {
|
||||
return (
|
||||
<div className="swap__summary-wrapper">
|
||||
<div>Please unlock token to continue.</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
<div
|
||||
key="open-details"
|
||||
className="swap__summary-wrapper swap__open-details-container"
|
||||
onClick={() => this.setState({showSummaryModal: true})}
|
||||
>
|
||||
<span>Transaction Details</span>
|
||||
<img src={DropdownBlue} />
|
||||
</div>,
|
||||
this.renderSummaryModal()
|
||||
];
|
||||
}
|
||||
|
||||
renderSummaryModal() {
|
||||
const {
|
||||
inputValue,
|
||||
inputCurrency,
|
||||
inputError,
|
||||
outputValue,
|
||||
outputCurrency,
|
||||
outputError,
|
||||
showSummaryModal,
|
||||
inputAmountB,
|
||||
lastEditedField,
|
||||
} = this.state;
|
||||
const { selectors, account } = this.props;
|
||||
if (!this.state.showSummaryModal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ALLOWED_SLIPPAGE = 0.025;
|
||||
const TOKEN_ALLOWED_SLIPPAGE = 0.04;
|
||||
|
||||
const type = getSwapType(inputCurrency, outputCurrency);
|
||||
const { label: inputLabel, decimals: inputDecimals } = selectors().getBalance(account, inputCurrency);
|
||||
const { label: outputLabel, decimals: outputDecimals } = selectors().getBalance(account, outputCurrency);
|
||||
|
||||
const label = lastEditedField === INPUT ? outputLabel : inputLabel;
|
||||
let minOutput;
|
||||
let maxInput;
|
||||
|
||||
if (lastEditedField === INPUT) {
|
||||
switch(type) {
|
||||
case 'ETH_TO_TOKEN':
|
||||
minOutput = BN(outputValue).multipliedBy(1 - ALLOWED_SLIPPAGE).toFixed(5)
|
||||
break;
|
||||
case 'TOKEN_TO_ETH':
|
||||
minOutput = BN(outputValue).multipliedBy(1 - ALLOWED_SLIPPAGE).toFixed(5);
|
||||
break;
|
||||
case 'TOKEN_TO_TOKEN':
|
||||
minOutput = BN(outputValue).multipliedBy(1 - TOKEN_ALLOWED_SLIPPAGE).toFixed(5);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastEditedField === OUTPUT) {
|
||||
switch (type) {
|
||||
case 'ETH_TO_TOKEN':
|
||||
maxInput = BN(inputValue).multipliedBy(1 + ALLOWED_SLIPPAGE).toFixed(5);
|
||||
break;
|
||||
case 'TOKEN_TO_ETH':
|
||||
maxInput = BN(inputValue).multipliedBy(1 + ALLOWED_SLIPPAGE).toFixed(5);
|
||||
break;
|
||||
case 'TOKEN_TO_TOKEN':
|
||||
maxInput = BN(inputValue).multipliedBy(1 + TOKEN_ALLOWED_SLIPPAGE).toFixed(5);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let description;
|
||||
if (lastEditedField === INPUT) {
|
||||
description = (
|
||||
<div>
|
||||
<div>
|
||||
You are selling {b(`${inputValue} ${inputLabel}`)}.
|
||||
</div>
|
||||
<div className="send__last-summary-text">
|
||||
You will receive between {b(`${minOutput} ${outputLabel}`)} and {b(`${outputValue} ${outputLabel}`)}.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
description = (
|
||||
<div>
|
||||
<div>
|
||||
You are selling between {b(`${inputValue} ${inputLabel}`)} to {b(`${maxInput} ${inputLabel}`)}.
|
||||
</div>
|
||||
<div className="send__last-summary-text">
|
||||
You will receive {b(`${outputValue} ${outputLabel}`)}.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="swap__summary-wrapper">
|
||||
<div>You are selling {b(`${inputValue} ${inputLabel}`)}</div>
|
||||
<div>You will receive between {b(minOutput)} and {b(`${maxOutput} ${outputLabel}`)}</div>
|
||||
</div>
|
||||
)
|
||||
<Modal key="modal" onClose={() => this.setState({ showSummaryModal: false })}>
|
||||
<CSSTransitionGroup
|
||||
transitionName="summary-modal"
|
||||
transitionAppear={true}
|
||||
transitionLeave={true}
|
||||
transitionAppearTimeout={200}
|
||||
transitionLeaveTimeout={200}
|
||||
transitionEnterTimeout={200}
|
||||
>
|
||||
<div className="swap__summary-modal">
|
||||
<div
|
||||
key="open-details"
|
||||
className="swap__open-details-container"
|
||||
onClick={() => this.setState({showSummaryModal: false})}
|
||||
>
|
||||
<span>Transaction Details</span>
|
||||
<img src={DropupBlue} />
|
||||
</div>
|
||||
{description}
|
||||
</div>
|
||||
</CSSTransitionGroup>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
renderExchangeRate() {
|
||||
@ -530,6 +695,11 @@ class Swap extends Component {
|
||||
'swap--inactive': !this.props.isConnected,
|
||||
})}
|
||||
>
|
||||
<NavigationTabs
|
||||
className={classnames('header__navigation', {
|
||||
'header--inactive': !this.props.isConnected,
|
||||
})}
|
||||
/>
|
||||
<CurrencyInputPanel
|
||||
title="Input"
|
||||
description={lastEditedField === OUTPUT ? estimatedText : ''}
|
||||
@ -565,17 +735,19 @@ class Swap extends Component {
|
||||
disableUnlock
|
||||
/>
|
||||
{ this.renderExchangeRate() }
|
||||
{ this.renderSummary() }
|
||||
<div className="swap__cta-container">
|
||||
<button
|
||||
className={classnames('swap__cta-btn', {
|
||||
'swap--inactive': !this.props.isConnected,
|
||||
})}
|
||||
disabled={!isValid}
|
||||
onClick={this.onSwap}
|
||||
>
|
||||
Swap
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className={classnames('swap__cta-btn', {
|
||||
'swap--inactive': !this.props.isConnected,
|
||||
})}
|
||||
disabled={!isValid}
|
||||
onClick={this.onSwap}
|
||||
>
|
||||
Swap
|
||||
</button>
|
||||
{ this.renderSummary() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -11,7 +11,6 @@
|
||||
|
||||
&__content {
|
||||
padding: 1rem .75rem;
|
||||
margin-top: 1rem;
|
||||
flex: 1 1 auto;
|
||||
height: 0;
|
||||
overflow-y: auto;
|
||||
@ -46,16 +45,52 @@
|
||||
}
|
||||
|
||||
&__summary-wrapper {
|
||||
margin: 2rem 1rem 0;
|
||||
margin: 2rem 1rem;
|
||||
color: #737373;
|
||||
font-size: .75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__summary-modal {
|
||||
background-color: $white;
|
||||
position: relative;
|
||||
bottom: 12rem;
|
||||
height: 12rem;
|
||||
z-index: 2000;
|
||||
padding: 1rem;
|
||||
border-top-left-radius: 1rem;
|
||||
border-top-right-radius: 1rem;
|
||||
transition: 250ms ease-in-out;
|
||||
|
||||
.swap__open-details-container {
|
||||
padding: 0.5rem 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__open-details-container {
|
||||
@extend %row-nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: .625rem 1rem;
|
||||
font-size: .75rem;
|
||||
color: $royal-blue;
|
||||
|
||||
img {
|
||||
height: .75rem;
|
||||
width: .75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__highlight-text {
|
||||
color: $royal-blue;
|
||||
}
|
||||
|
||||
&__cta-container {
|
||||
display: flex;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
&__cta-btn {
|
||||
@extend %primary-button;
|
||||
margin: 1rem auto;
|
||||
@ -69,3 +104,11 @@
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-modal-appear {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.summary-modal-appear.modal-container-appear-active {
|
||||
bottom: 0;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user