Commit ade904d2 authored by Kenny Tran's avatar Kenny Tran Committed by Chi Kei Chan

Approve / unlock functionality (#66)

* Initial approve / unlock functionality

* Refactor out getdecimal/getbalance. Add in approval limit

* Change validateBalance to shouldValidateBalance

* Add deepEqual and shouldComponentUpdate

* Use web3 toHex util

* Add pending icon and use
parent 370d9e10
......@@ -95,6 +95,18 @@
}
}
&__sub-currency-select {
@extend %row-nowrap;
line-height: 0;
background: $zumthor-blue;
border: 1px solid $royal-blue;
color: $royal-blue;
height: 2rem;
padding: 10px 50px 10px 10px;
margin-right: -40px;
border-radius: 2.5rem;
}
&__dropdown-icon {
height: 1rem;
width: .75rem;
......
......@@ -45,10 +45,15 @@ class CurrencyInputPanel extends Component {
errors: PropTypes.arrayOf(PropTypes.string),
addError: PropTypes.func,
removeError: PropTypes.func,
showSubButton: PropTypes.bool,
subButtonContent: PropTypes.node,
onSubButtonClick: PropTypes.func,
shouldValidateBalance: PropTypes.bool,
};
static defaultProps = {
onCurrencySelected() {},
onSubButtonClick() {},
onValueChange() {},
addError() {},
removeError() {},
......@@ -143,14 +148,16 @@ class CurrencyInputPanel extends Component {
};
validate = (balance) => {
const { value, addError, removeError, errors } = this.props;
const { value, addError, removeError, errors, shouldValidateBalance } = this.props;
const hasInsufficientBalance = errors.indexOf(INSUFFICIENT_BALANCE) > -1;
const balanceIsLess = BN(value).isGreaterThan(BN(balance));
if (balanceIsLess && !hasInsufficientBalance) {
addError(INSUFFICIENT_BALANCE);
} else if (!balanceIsLess && hasInsufficientBalance) {
removeError(INSUFFICIENT_BALANCE);
if (shouldValidateBalance) {
if (balanceIsLess && !hasInsufficientBalance) {
addError(INSUFFICIENT_BALANCE);
} else if (!balanceIsLess && hasInsufficientBalance) {
removeError(INSUFFICIENT_BALANCE);
}
}
};
......@@ -278,7 +285,10 @@ class CurrencyInputPanel extends Component {
title,
description,
value,
errors
errors,
showSubButton,
subButtonContent,
onSubButtonClick,
} = this.props;
const { selectedTokenAddress } = this.state;
......@@ -318,6 +328,18 @@ class CurrencyInputPanel extends Component {
onChange={e => this.props.onValueChange(e.target.value)}
value={this.props.value}
/>
{
showSubButton
? (
<button
onClick={onSubButtonClick}
className={classnames("currency-input-panel__sub-currency-select")}
>
{ subButtonContent }
</button>
)
: null
}
<button
className={classnames("currency-input-panel__currency-select", {
'currency-input-panel__currency-select--selected': selectedTokenAddress,
......
import {BigNumber as BN} from "bignumber.js";
import { getDecimals } from './contract-utils';
export const isExchangeUnapproved = opts => {
const {
value,
currency,
drizzleCtx,
account,
contractStore,
exchangeAddresses,
} = opts;
if (!currency || currency === 'ETH') {
return false;
}
const inputExchange = exchangeAddresses.fromToken[currency];
if (!inputExchange) {
return false;
}
const allowanceKey = drizzleCtx.contracts[inputExchange].methods.allowance.cacheCall(account, inputExchange);
const allowance = contractStore[inputExchange].allowance[allowanceKey];
if (!allowance) {
return false;
}
return BN(value).isGreaterThan(BN(allowance.value));
};
export const approveExchange = async opts => {
const {
currency,
contractStore,
drizzleCtx,
account,
exchangeAddresses,
} = opts;
const { web3 } = drizzleCtx;
const inputExchange = exchangeAddresses.fromToken[currency];
if (!inputExchange) {
return;
}
const decimals = await getDecimals({ address: currency, drizzleCtx, contractStore });
return drizzleCtx.contracts[inputExchange].methods.approve.cacheSend(
inputExchange,
web3.utils.toHex(decimals*10**18),
{ from: account }
);
};
export const getApprovalTxStatus = opts => {
const {
drizzleCtx,
txId
} = opts;
const st = drizzleCtx.store.getState();
const tx = st.transactionStack[txId];
const status = st.transactions[tx] && st.transactions[tx].status;
return status;
};
export function getDecimals({ address, drizzleCtx, contractStore }) {
return new Promise(async (resolve, reject) => {
if (address === 'ETH') {
resolve('18');
return;
}
const decimalsKey = drizzleCtx.contracts[address].methods.decimals.cacheCall();
const decimals = contractStore[address].decimals[decimalsKey];
resolve(decimals && decimals.value);
});
}
const BALANCE_KEY = {};
export function getBalance({ currency, address, drizzleCtx, contractStore }) {
return new Promise(async (resolve, reject) => {
if (currency === 'ETH') {
drizzleCtx.web3.eth.getBalance(address, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
} else {
const token = drizzleCtx.contracts[currency];
if (!token) {
return;
}
let balanceKey = BALANCE_KEY[address];
if (!balanceKey) {
balanceKey = token.methods.balanceOf.cacheCall(address);
BALANCE_KEY[address] = balanceKey;
}
const tokenStore = contractStore[currency];
if (!tokenStore) {
reject(new Error(`Cannot find ${currency} in contract store`));
return;
}
let balance = tokenStore.balanceOf[balanceKey];
resolve(balance && balance.value);
}
});
}
import {BigNumber as BN} from "bignumber.js";
import promisify from "./web3-promisfy";
import { getDecimals, getBalance } from './contract-utils';
export const calculateExchangeRateFromInput = async opts => {
const { inputCurrency, outputCurrency } = opts;
......@@ -570,54 +571,3 @@ const ERC20_TO_ERC20 = {
);
},
};
function getDecimals({ address, drizzleCtx, contractStore }) {
return new Promise(async (resolve, reject) => {
if (address === 'ETH') {
resolve('18');
return;
}
const decimalsKey = drizzleCtx.contracts[address].methods.decimals.cacheCall();
const decimals = contractStore[address].decimals[decimalsKey];
resolve(decimals && decimals.value);
});
}
const BALANCE_KEY = {};
function getBalance({ currency, address, drizzleCtx, contractStore }) {
return new Promise(async (resolve, reject) => {
if (currency === 'ETH') {
drizzleCtx.web3.eth.getBalance(address, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
} else {
const token = drizzleCtx.contracts[currency];
if (!token) {
return;
}
let balanceKey = BALANCE_KEY[address];
if (!balanceKey) {
balanceKey = token.methods.balanceOf.cacheCall(address);
BALANCE_KEY[address] = balanceKey;
}
const tokenStore = contractStore[currency];
if (!tokenStore) {
reject(new Error(`Cannot find ${currency} in contract store`));
return;
}
let balance = tokenStore.balanceOf[balanceKey];
resolve(balance && balance.value);
}
});
}
......@@ -4,17 +4,24 @@ import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import {BigNumber as BN} from "bignumber.js";
import deepEqual from 'deep-equal';
import { isValidSwap, updateField, addError, removeError } from '../../ducks/swap';
import Header from '../../components/Header';
import CurrencyInputPanel from '../../components/CurrencyInputPanel';
import OversizedPanel from '../../components/OversizedPanel';
import ArrowDown from '../../assets/images/arrow-down-blue.svg';
import Pending from '../../assets/images/pending.svg';
import {
calculateExchangeRateFromInput,
calculateExchangeRateFromOutput,
swapInput,
swapOutput,
} from '../../helpers/exchange-utils';
import {
isExchangeUnapproved,
getApprovalTxStatus,
approveExchange,
} from '../../helpers/approval-utils';
import promisify from '../../helpers/web3-promisfy';
import "./swap.scss";
......@@ -43,6 +50,7 @@ class Swap extends Component {
state = {
exchangeRate: BN(0),
approvalTxId: null,
};
componentWillReceiveProps(nextProps) {
......@@ -61,6 +69,11 @@ class Swap extends Component {
});
}
shouldComponentUpdate(nextProps, nextState) {
return !deepEqual(nextProps, this.props) ||
!deepEqual(nextState, this.state);
}
componentWillUnmount() {
this.props.updateField('output', '');
this.props.updateField('input', '');
......@@ -146,6 +159,57 @@ class Swap extends Component {
}) ;
}
getIsUnapproved() {
const {
input,
inputCurrency,
account,
contracts,
exchangeAddresses
} = this.props;
const { drizzle } = this.context;
return isExchangeUnapproved({
value: input,
currency: inputCurrency,
drizzleCtx: drizzle,
contractStore: contracts,
account,
exchangeAddresses,
});
}
approveExchange = async () => {
const {
inputCurrency,
exchangeAddresses,
account,
contracts,
} = this.props;
const { drizzle } = this.context;
if (this.getIsUnapproved()) {
const approvalTxId = await approveExchange({
currency: inputCurrency,
drizzleCtx: drizzle,
contractStore: contracts,
account,
exchangeAddresses,
});
this.setState({ approvalTxId })
}
}
getApprovalStatus() {
const { drizzle } = this.context;
return getApprovalTxStatus({
drizzleCtx: drizzle,
txId: this.state.approvalTxId,
});
}
onSwap = async () => {
const {
input,
......@@ -194,6 +258,23 @@ class Swap extends Component {
// }));
};
handleSubButtonClick = () => {
if (this.getIsUnapproved() && this.getApprovalStatus() !== 'pending') {
this.approveExchange();
}
}
renderSubButtonText() {
if (this.getApprovalStatus() === 'pending') {
return [
(<img key="pending" className="swap__sub-icon" src={Pending} />),
(<span key="text" className="swap__sub-text">Pending</span>)
];
} else {
return '🔒 Unlock'
}
}
render() {
const { lastEditedField, inputCurrency, outputCurrency, input, output, isValid, outputErrors, inputErrors } = this.props;
const { exchangeRate } = this.state;
......@@ -219,6 +300,10 @@ class Swap extends Component {
removeError={error => this.props.removeError('inputErrors', error)}
errors={inputErrors}
value={input}
shouldValidateBalance
showSubButton={this.getIsUnapproved()}
subButtonContent={this.renderSubButtonText()}
onSubButtonClick={this.handleSubButtonClick}
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
......
......@@ -64,4 +64,12 @@
background: $mercury-gray;
}
}
&__sub-icon {
margin-right: 5px;
}
&__sub-text {
margin-top: 5px;
}
}
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