Connect send page with exchange utils (#75)

* Connect send page with exchange utils

* Add exchangeRate and fix txId
This commit is contained in:
Kenny Tran 2018-10-23 09:19:21 -07:00 committed by Chi Kei Chan
parent b1a5a6c867
commit a4e0d11cef
6 changed files with 496 additions and 23 deletions

@ -9,6 +9,13 @@ class AddressInputPanel extends Component {
title: PropTypes.string,
description: PropTypes.string,
extraText: PropTypes.string,
onChange: PropTypes.func,
value: PropTypes.string,
};
static defaultProps = {
onChange() {},
value: '',
};
render() {
@ -16,6 +23,8 @@ class AddressInputPanel extends Component {
title,
description,
extraText,
onChange,
value
} = this.props;
return (
@ -31,6 +40,8 @@ class AddressInputPanel extends Component {
type="text"
className="address-input-panel__input"
placeholder="0x1234..."
onChange={e => onChange(e.target.value)}
value={value}
/>
</div>
</div>

@ -37,7 +37,7 @@ class CurrencyInputPanel extends Component {
onCurrencySelected: PropTypes.func,
onValueChange: PropTypes.func,
tokenAddresses: PropTypes.shape({
address: PropTypes.array.isRequired,
addresses: PropTypes.array.isRequired,
}).isRequired,
exchangeAddresses: PropTypes.shape({
fromToken: PropTypes.object.isRequired,

@ -4,6 +4,7 @@ import addresses from './addresses';
import exchangeContracts from './exchange-contract';
import tokenContracts from './token-contract';
import exchange from './exchange';
import send from './send';
import swap from './swap';
import web3connect from './web3connect';
@ -12,6 +13,7 @@ export default combineReducers({
exchangeContracts,
tokenContracts,
exchange,
send,
swap,
web3connect,
...drizzleReducers,

88
src/ducks/send.js Normal file

@ -0,0 +1,88 @@
const UPDATE_FIELD = 'app/send/updateField';
const ADD_ERROR = 'app/send/addError';
const REMOVE_ERROR = 'app/send/removeError';
const RESET_SEND = 'app/send/resetSend';
const getInitialState = () => {
return {
input: '',
output: '',
inputCurrency: '',
outputCurrency: '',
recipient: '',
lastEditedField: '',
inputErrors: [],
outputErrors: [],
};
};
export const isValidSend = (state) => {
const { send } = state;
return send.outputCurrency !== '' &&
send.inputCurrency !== '' &&
send.input !== '' &&
send.output !== '' &&
send.recipient !== '' &&
send.inputErrors.length === 0 &&
send.outputErrors.length === 0;
};
export const updateField = ({ name, value }) => ({
type: UPDATE_FIELD,
payload: { name, value },
});
export const addError = ({ name, value }) => ({
type: ADD_ERROR,
payload: { name, value },
});
export const removeError = ({ name, value }) => ({
type: REMOVE_ERROR,
payload: { name, value },
});
export const resetSend = () => ({
type: RESET_SEND,
});
function reduceAddError(state, payload) {
const { name, value } = payload;
let nextErrors = state[name];
if (nextErrors.indexOf(value) === -1) {
nextErrors = [...nextErrors, value];
}
return {
...state,
[name]: nextErrors,
};
}
function reduceRemoveError(state, payload) {
const { name, value } = payload;
return {
...state,
[name]: state[name].filter(error => error !== value),
};
}
export default function sendReducer(state = getInitialState(), { type, payload }) {
switch (type) {
case UPDATE_FIELD:
return {
...state,
[payload.name]: payload.value,
};
case ADD_ERROR:
return reduceAddError(state, payload);
case REMOVE_ERROR:
return reduceRemoveError(state, payload);
case RESET_SEND:
return getInitialState();
default:
return state;
}
}

@ -273,7 +273,7 @@ const ETH_TO_ERC20 = {
value: maxInput.toFixed(0),
});
},
sendInput: async ({drizzleCtx, contractStore, input, output, account, inputCurrency, outputCurrency, exchangeAddresses, recipient}) => {
sendInput: async ({drizzleCtx, contractStore, input, output, outputDecimals, account, inputCurrency, outputCurrency, exchangeAddresses, recipient}) => {
if (!validEthToErc20(inputCurrency, outputCurrency)) {
return;
}
@ -287,7 +287,6 @@ const ETH_TO_ERC20 = {
const deadline = await getDeadline(drizzleCtx, 300);
const ALLOWED_SLIPPAGE = BN(0.025);
const outputDecimals = await getDecimals({ address: outputCurrency, contractStore, drizzleCtx });
const minOutput = BN(output).multipliedBy(10 ** outputDecimals).multipliedBy(BN(1).minus(ALLOWED_SLIPPAGE));
return exchange.methods.ethToTokenTransferInput.cacheSend(
@ -300,7 +299,7 @@ const ETH_TO_ERC20 = {
}
);
},
sendOutput: async ({drizzleCtx, contractStore, input, output, account, inputCurrency, outputCurrency, exchangeAddresses, recipient}) => {
sendOutput: async ({drizzleCtx, contractStore, input, output, outputDecimals, account, inputCurrency, outputCurrency, exchangeAddresses, recipient}) => {
if (!validEthToErc20(inputCurrency, outputCurrency)) {
return;
}
@ -314,7 +313,6 @@ const ETH_TO_ERC20 = {
const deadline = await getDeadline(drizzleCtx, 300);
const ALLOWED_SLIPPAGE = BN(0.025);
const outputDecimals = await getDecimals({ address: outputCurrency, contractStore, drizzleCtx });
const outputAmount = BN(output).multipliedBy(BN(10 ** outputDecimals));
const maxInput = BN(input).multipliedBy(10 ** 18).multipliedBy(BN(1).plus(ALLOWED_SLIPPAGE));
return exchange.methods.ethToTokenTransferOutput.cacheSend(
@ -481,7 +479,7 @@ const ERC20_TO_ETH = {
{ from: account },
);
},
sendInput: async ({drizzleCtx, contractStore, input, output, account, inputCurrency, outputCurrency, exchangeAddresses, recipient}) => {
sendInput: async ({drizzleCtx, contractStore, input, output, inputDecimals, account, inputCurrency, outputCurrency, exchangeAddresses, recipient}) => {
if (!validErc20ToEth(inputCurrency, outputCurrency)) {
return;
}
@ -495,7 +493,6 @@ const ERC20_TO_ETH = {
const deadline = await getDeadline(drizzleCtx, 300);
const ALLOWED_SLIPPAGE = BN(0.025);
const inputDecimals = await getDecimals({ address: inputCurrency, contractStore, drizzleCtx });
const minOutput = BN(output).multipliedBy(10 ** 18).multipliedBy(BN(1).minus(ALLOWED_SLIPPAGE));
const inputAmount = BN(input).multipliedBy(10 ** inputDecimals);
@ -507,7 +504,7 @@ const ERC20_TO_ETH = {
{ from: account, value: '0x0' },
);
},
sendOutput: async ({drizzleCtx, contractStore, input, output, account, inputCurrency, outputCurrency, exchangeAddresses, recipient}) => {
sendOutput: async ({drizzleCtx, contractStore, input, output, inputDecimals, account, inputCurrency, outputCurrency, exchangeAddresses, recipient}) => {
if (!validErc20ToEth(inputCurrency, outputCurrency)) {
return;
}
@ -521,7 +518,6 @@ const ERC20_TO_ETH = {
const deadline = await getDeadline(drizzleCtx, 300);
const ALLOWED_SLIPPAGE = BN(0.025);
const inputDecimals = await getDecimals({ address: inputCurrency, contractStore, drizzleCtx });
const maxInput = BN(input).multipliedBy(10 ** inputDecimals).multipliedBy(BN(1).plus(ALLOWED_SLIPPAGE));
const outputAmount = BN(output).multipliedBy(10 ** 18);
@ -676,7 +672,7 @@ const ERC20_TO_ERC20 = {
{ from: account },
);
},
sendInput: async ({drizzleCtx, contractStore, input, output, account, inputCurrency, outputCurrency, exchangeAddresses, recipient}) => {
sendInput: async ({drizzleCtx, contractStore, input, output, inputDecimals, outputDecimals, account, inputCurrency, outputCurrency, exchangeAddresses, recipient}) => {
if (!validErc20ToErc20(inputCurrency, outputCurrency)) {
return;
}
@ -688,8 +684,6 @@ const ERC20_TO_ERC20 = {
}
const deadline = await getDeadline(drizzleCtx, 300);
const ALLOWED_SLIPPAGE = BN(0.04);
const inputDecimals = await getDecimals({ address: inputCurrency, contractStore, drizzleCtx });
const outputDecimals = await getDecimals({ address: outputCurrency, contractStore, drizzleCtx });
const inputAmount = BN(input).multipliedBy(BN(10 ** inputDecimals));
const outputAmount = BN(input).multipliedBy(BN(10 ** outputDecimals));
@ -719,6 +713,8 @@ const ERC20_TO_ERC20 = {
outputCurrency,
exchangeAddresses,
recipient,
inputDecimals,
outputDecimals,
} = opts;
const exchangeRateA = await ETH_TO_ERC20.calculateInput({ ...opts, inputCurrency: 'ETH' });
if (!exchangeRateA) {
@ -738,8 +734,6 @@ const ERC20_TO_ERC20 = {
const deadline = await getDeadline(drizzleCtx, 300);
const ALLOWED_SLIPPAGE = BN(0.04);
const inputDecimals = await getDecimals({ address: inputCurrency, contractStore, drizzleCtx });
const outputDecimals = await getDecimals({ address: outputCurrency, contractStore, drizzleCtx });
const inputAmount = BN(input).multipliedBy(BN(10 ** inputDecimals));
const outputAmount = BN(output).multipliedBy(BN(10 ** outputDecimals));
const inputAmountB = BN(output).dividedBy(exchangeRateA).multipliedBy(BN(10 ** 18));

@ -3,11 +3,32 @@ import { drizzleConnect } from 'drizzle-react';
import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { isValidSend, updateField, addError, removeError, resetSend } from '../../ducks/send';
import { selectors, sync } from '../../ducks/web3connect';
import {BigNumber as BN} from "bignumber.js";
import deepEqual from 'deep-equal';
import Header from '../../components/Header';
import CurrencyInputPanel from '../../components/CurrencyInputPanel';
import AddressInputPanel from '../../components/AddressInputPanel';
import OversizedPanel from '../../components/OversizedPanel';
import ArrowDown from '../../assets/images/arrow-down-blue.svg';
import Pending from '../../assets/images/pending.svg';
import {
calculateExchangeRateFromInput,
calculateExchangeRateFromOutput,
sendInput,
sendOutput,
} from '../../helpers/exchange-utils';
import {
isExchangeUnapproved,
approveExchange,
} from '../../helpers/approval-utils';
import {
getTxStatus
} from '../../helpers/contract-utils';
import "./send.scss";
@ -18,9 +39,300 @@ class Send extends Component {
pathname: PropTypes.string.isRequired,
currentAddress: PropTypes.string,
isConnected: PropTypes.bool.isRequired,
isValid: PropTypes.bool.isRequired,
updateField: PropTypes.func.isRequired,
input: PropTypes.string,
output: PropTypes.string,
inputCurrency: PropTypes.string,
outputCurrency: PropTypes.string,
recipient: PropTypes.string,
lastEditedField: PropTypes.string,
inputErrors: PropTypes.arrayOf(PropTypes.string),
outputErrors: PropTypes.arrayOf(PropTypes.string),
};
static contextTypes = {
drizzle: PropTypes.object,
};
state = {
exchangeRate: BN(0),
approvalTxId: null,
sendTxId: null,
};
shouldComponentUpdate(nextProps, nextState) {
return !deepEqual(nextProps, this.props) ||
!deepEqual(nextState, this.state);
}
componentDidUpdate() {
if (this.getSendStatus() === 'pending') {
this.resetSend();
}
this.getExchangeRate(this.props)
.then(exchangeRate => {
if (this.state.exchangeRate !== exchangeRate) {
this.setState({ exchangeRate });
}
if (!exchangeRate) {
return;
}
if (this.props.lastEditedField === 'input') {
this.props.updateField('output', `${BN(this.props.input).multipliedBy(exchangeRate).toFixed(7)}`);
} else if (this.props.lastEditedField === 'output') {
this.props.updateField('input', `${BN(this.props.output).multipliedBy(BN(1).dividedBy(exchangeRate)).toFixed(7)}`);
}
});
}
componentWillUnmount() {
this.resetSend();
}
resetSend() {
this.props.resetSend();
this.setState({approvalTxId: null, sendTxId: null});
}
getSendStatus() {
const { drizzle } = this.context;
return getTxStatus({
drizzleCtx: drizzle,
txId: this.state.sendTxId,
});
}
getTokenLabel(address) {
if (address === 'ETH') {
return 'ETH';
}
const {
initialized,
contracts,
} = this.props;
const { drizzle } = this.context;
const { web3 } = drizzle;
if (!initialized || !web3 || !address) {
return '';
}
const symbolKey = drizzle.contracts[address].methods.symbol.cacheCall();
const token = contracts[address];
const symbol = token.symbol[symbolKey];
if (!symbol) {
return '';
}
return symbol.value;
}
getBalance(currency) {
const { selectors, account } = this.props;
if (!currency) {
return '';
}
if (currency === 'ETH') {
const { value, decimals } = selectors().getBalance(account);
return `Balance: ${value.dividedBy(10 ** decimals).toFixed(4)}`;
}
const { value, decimals } = selectors().getTokenBalance(currency, account);
return `Balance: ${value.dividedBy(10 ** decimals).toFixed(4)}`;
}
updateInput(amount) {
this.props.updateField('input', amount);
if (!amount) {
this.props.updateField('output', '');
}
this.props.updateField('lastEditedField', 'input');
}
updateOutput(amount) {
this.props.updateField('output', amount);
if (!amount) {
this.props.updateField('input', '');
}
this.props.updateField('lastEditedField', 'output');
}
async getExchangeRate(props) {
const {
input,
output,
inputCurrency,
outputCurrency,
exchangeAddresses,
lastEditedField,
contracts,
} = props;
const { drizzle } = this.context;
return lastEditedField === 'input'
? await calculateExchangeRateFromInput({
drizzleCtx: drizzle,
contractStore: contracts,
input,
output,
inputCurrency,
outputCurrency,
exchangeAddresses,
})
: await calculateExchangeRateFromOutput({
drizzleCtx: drizzle,
contractStore: contracts,
input,
output,
inputCurrency,
outputCurrency,
exchangeAddresses,
}) ;
}
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 getTxStatus({
drizzleCtx: drizzle,
txId: this.state.approvalTxId,
});
}
onSend = async () => {
const {
input,
output,
inputCurrency,
outputCurrency,
recipient,
exchangeAddresses,
lastEditedField,
account,
contracts,
selectors,
} = this.props;
const { drizzle } = this.context;
const { decimals: inputDecimals } = inputCurrency === 'ETH' ?
selectors().getBalance(account)
: selectors().getTokenBalance(inputCurrency, account);
const { decimals: outputDecimals } = outputCurrency === 'ETH' ?
selectors().getBalance(account)
: selectors().getTokenBalance(outputCurrency, account);
let sendTxId;
if (lastEditedField === 'input') {
sendTxId = await sendInput({
drizzleCtx: drizzle,
contractStore: contracts,
input,
output,
inputCurrency,
outputCurrency,
recipient,
exchangeAddresses,
account,
inputDecimals,
outputDecimals,
});
}
if (lastEditedField === 'output') {
sendTxId = await sendOutput({
drizzleCtx: drizzle,
contractStore: contracts,
input,
output,
inputCurrency,
outputCurrency,
recipient,
exchangeAddresses,
account,
inputDecimals,
outputDecimals,
});
}
this.setState({ sendTxId });
};
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, recipient, isValid, outputErrors, inputErrors } = this.props;
const { exchangeRate } = this.state;
const inputLabel = this.getTokenLabel(inputCurrency);
const outputLabel = this.getTokenLabel(outputCurrency);
const estimatedText = '(estimated)';
// 0xc41c71CAeA8ccc9AE19c6d8a66c6870C6E9c3632
return (
<div className="send">
<Header />
@ -31,7 +343,19 @@ class Send extends Component {
>
<CurrencyInputPanel
title="Input"
extraText="Balance: 0.03141"
description={lastEditedField === 'output' ? estimatedText : ''}
onCurrencySelected={(d) => {
this.props.updateField('inputCurrency', d)
this.props.sync();
}}
onValueChange={d => this.updateInput(d)}
selectedTokens={[inputCurrency, outputCurrency]}
addError={error => this.props.addError('inputErrors', error)}
removeError={error => this.props.removeError('inputErrors', error)}
errors={inputErrors}
value={input}
selectedTokenAddress={inputCurrency}
extraText={this.getBalance(inputCurrency)}
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
@ -40,30 +364,56 @@ class Send extends Component {
</OversizedPanel>
<CurrencyInputPanel
title="Output"
description="(estimated)"
extraText="Balance: 0.0"
description={lastEditedField === 'input' ? estimatedText : ''}
onCurrencySelected={(d) => {
this.props.updateField('outputCurrency', d)
this.props.sync();
}}
onValueChange={d => this.updateOutput(d)}
selectedTokens={[inputCurrency, outputCurrency]}
addError={error => this.props.addError('outputErrors', error)}
removeError={error => this.props.removeError('outputErrors', error)}
errors={outputErrors}
value={output}
selectedTokenAddress={outputCurrency}
extraText={this.getBalance(outputCurrency)}
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
<img className="swap__down-arrow" src={ArrowDown} />
</div>
</OversizedPanel>
<AddressInputPanel />
<AddressInputPanel
value={recipient}
onChange={address => this.props.updateField('recipient', address)}
/>
<OversizedPanel hideBottom>
<div className="swap__exchange-rate-wrapper">
<span className="swap__exchange-rate">Exchange Rate</span>
<span>1 ETH = 1283.878 BAT</span>
<span>
{exchangeRate ? `1 ${inputLabel} = ${exchangeRate.toFixed(7)} ${outputLabel}` : ' - '}
</span>
</div>
</OversizedPanel>
<div className="swap__summary-wrapper">
<div>You are selling <span className="swap__highlight-text">0.01 ETH</span></div>
<div>You will receive between <span className="swap__highlight-text">12.80</span> and <span className="swap__highlight-text">12.83 BAT</span></div>
</div>
{
inputLabel && input
? (
<div className="swap__summary-wrapper">
<div>You are selling <span className="swap__highlight-text">{`${input} ${inputLabel}`}</span></div>
<div>You will receive between <span className="swap__highlight-text">12.80</span> and <span
className="swap__highlight-text">12.83 BAT</span></div>
</div>
)
: null
}
</div>
<button
className={classnames('swap__cta-btn', {
'swap--inactive': !this.props.isConnected,
'swap__cta-btn--inactive': !this.props.isValid,
})}
disabled={!this.props.isValid}
onClick={this.onSend}
>
Send
</button>
@ -77,10 +427,38 @@ export default withRouter(
drizzleConnect(
Send,
(state, ownProps) => ({
// React Router
push: ownProps.history.push,
pathname: ownProps.location.pathname,
// From Drizzle
initialized: state.drizzleStatus.initialized,
balance: state.accountBalances[state.accounts[0]] || null,
account: state.accounts[0],
contracts: state.contracts,
currentAddress: state.accounts[0],
isConnected: !!(state.drizzleStatus.initialized && state.accounts[0]),
// Redux Store
balances: state.web3connect.balances,
input: state.send.input,
output: state.send.output,
inputCurrency: state.send.inputCurrency,
outputCurrency: state.send.outputCurrency,
recipient: state.send.recipient,
lastEditedField: state.send.lastEditedField,
exchangeAddresses: state.addresses.exchangeAddresses,
isValid: isValidSend(state),
inputErrors: state.send.inputErrors,
outputErrors: state.send.outputErrors,
}),
dispatch => ({
updateField: (name, value) => dispatch(updateField({ name, value })),
addError: (name, value) => dispatch(addError({ name, value })),
removeError: (name, value) => dispatch(removeError({ name, value })),
resetSend: () => dispatch(resetSend()),
selectors: () => dispatch(selectors()),
sync: () => dispatch(sync()),
})
),
);