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

Add Approvals (#81)

* Add Approvals; no watching pending tx yet

* Various fixes

* Basic Desktop mode for now
parent dadbc4e4
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
} }
&__qr-container { &__qr-container {
display: none;
padding: 10px; padding: 10px;
background: $concrete-gray; background: $concrete-gray;
border: 1px solid $mercury-gray; border: 1px solid $mercury-gray;
......
...@@ -113,6 +113,9 @@ ...@@ -113,6 +113,9 @@
padding: 10px 50px 10px 10px; padding: 10px 50px 10px 10px;
margin-right: -40px; margin-right: -40px;
border-radius: 2.5rem; border-radius: 2.5rem;
outline: none;
cursor: pointer;
user-select: none;
} }
&__dropdown-icon { &__dropdown-icon {
......
...@@ -7,9 +7,12 @@ import Fuse from '../../helpers/fuse'; ...@@ -7,9 +7,12 @@ import Fuse from '../../helpers/fuse';
import Modal from '../Modal'; import Modal from '../Modal';
import TokenLogo from '../TokenLogo'; import TokenLogo from '../TokenLogo';
import SearchIcon from '../../assets/images/magnifying-glass.svg'; import SearchIcon from '../../assets/images/magnifying-glass.svg';
import ERC20_ABI from '../../abi/erc20'; import { selectors } from "../../ducks/web3connect";
import { BigNumber as BN } from 'bignumber.js';
import './currency-panel.scss'; import './currency-panel.scss';
import ERC20_ABI from '../../abi/erc20';
import EXCHANGE_ABI from "../../abi/exchange"; import EXCHANGE_ABI from "../../abi/exchange";
const FUSE_OPTIONS = { const FUSE_OPTIONS = {
...@@ -44,9 +47,12 @@ class CurrencyInputPanel extends Component { ...@@ -44,9 +47,12 @@ class CurrencyInputPanel extends Component {
}).isRequired, }).isRequired,
selectedTokens: PropTypes.array.isRequired, selectedTokens: PropTypes.array.isRequired,
errorMessage: PropTypes.string, errorMessage: PropTypes.string,
account: PropTypes.string,
selectedTokenAddress: PropTypes.string, selectedTokenAddress: PropTypes.string,
disableTokenSelect: PropTypes.bool, disableTokenSelect: PropTypes.bool,
selectors: PropTypes.func.isRequired,
filteredTokens: PropTypes.arrayOf(PropTypes.string), filteredTokens: PropTypes.arrayOf(PropTypes.string),
disableUnlock: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
...@@ -196,6 +202,45 @@ class CurrencyInputPanel extends Component { ...@@ -196,6 +202,45 @@ class CurrencyInputPanel extends Component {
); );
} }
renderUnlockButton() {
const {
selectors,
selectedTokenAddress,
account,
exchangeAddresses: { fromToken },
web3,
disableUnlock,
} = this.props;
if (disableUnlock || !selectedTokenAddress || selectedTokenAddress === 'ETH') {
return;
}
const { value, decimals, label } = selectors().getApprovals(selectedTokenAddress, account, fromToken[selectedTokenAddress]);
if (!label || value.isGreaterThan(BN(10 ** 22))) {
return;
}
return (
<button
className='currency-input-panel__sub-currency-select'
onClick={() => {
const contract = new web3.eth.Contract(ERC20_ABI, selectedTokenAddress);
const amount = BN(10 ** decimals).multipliedBy(10 ** 8).toFixed(0);
contract.methods.approve(fromToken[selectedTokenAddress], amount)
.send({ from: account }, (err, data) => {
if (data) {
// TODO: Handle Pending in Redux
}
});
}}
>
Unlock
</button>
);
}
render() { render() {
const { const {
title, title,
...@@ -234,11 +279,7 @@ class CurrencyInputPanel extends Component { ...@@ -234,11 +279,7 @@ class CurrencyInputPanel extends Component {
onChange={e => onValueChange(e.target.value)} onChange={e => onValueChange(e.target.value)}
value={value} value={value}
/> />
{/*<button*/} { this.renderUnlockButton() }
{/*className='currency-input-panel__sub-currency-select'*/}
{/*>*/}
{/*Unlock*/}
{/*</button>*/}
<button <button
className={classnames("currency-input-panel__currency-select", { className={classnames("currency-input-panel__currency-select", {
'currency-input-panel__currency-select--selected': selectedTokenAddress, 'currency-input-panel__currency-select--selected': selectedTokenAddress,
...@@ -277,5 +318,11 @@ export default drizzleConnect( ...@@ -277,5 +318,11 @@ export default drizzleConnect(
exchangeAddresses: state.addresses.exchangeAddresses, exchangeAddresses: state.addresses.exchangeAddresses,
tokenAddresses: state.addresses.tokenAddresses, tokenAddresses: state.addresses.tokenAddresses,
contracts: state.contracts, contracts: state.contracts,
account: state.web3connect.account,
approvals: state.web3connect.approvals,
web3: state.web3connect.web3,
}),
dispatch => ({
selectors: () => dispatch(selectors()),
}), }),
); );
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import { drizzleReducers } from 'drizzle' import { drizzleReducers } from 'drizzle'
import addresses from './addresses'; import addresses from './addresses';
import exchangeContracts from './exchange-contract';
import tokenContracts from './token-contract';
import exchange from './exchange';
import send from './send'; import send from './send';
import swap from './swap';
import web3connect from './web3connect'; import web3connect from './web3connect';
export default combineReducers({ export default combineReducers({
addresses, addresses,
exchangeContracts, // exchangeContracts,
tokenContracts, // tokenContracts,
exchange, // exchange,
send, send,
swap, // swap,
web3connect, web3connect,
...drizzleReducers, ...drizzleReducers,
}); });
...@@ -12,6 +12,8 @@ export const WATCH_ETH_BALANCE = 'web3connect/watchEthBalance'; ...@@ -12,6 +12,8 @@ export const WATCH_ETH_BALANCE = 'web3connect/watchEthBalance';
export const WATCH_TOKEN_BALANCE = 'web3connect/watchTokenBalance'; export const WATCH_TOKEN_BALANCE = 'web3connect/watchTokenBalance';
export const UPDATE_ETH_BALANCE = 'web3connect/updateEthBalance'; export const UPDATE_ETH_BALANCE = 'web3connect/updateEthBalance';
export const UPDATE_TOKEN_BALANCE = 'web3connect/updateTokenBalance'; export const UPDATE_TOKEN_BALANCE = 'web3connect/updateTokenBalance';
export const WATCH_APPROVALS = 'web3connect/watchApprovals';
export const UPDATE_APPROVALS = 'web3connect/updateApprovals';
export const ADD_CONTRACT = 'web3connect/addContract'; export const ADD_CONTRACT = 'web3connect/addContract';
...@@ -22,6 +24,13 @@ const initialState = { ...@@ -22,6 +24,13 @@ const initialState = {
balances: { balances: {
ethereum: {}, ethereum: {},
}, },
approvals: {
'0x0': {
TOKEN_OWNER: {
SPENDER: {},
},
},
},
pendingTransactions: [], pendingTransactions: [],
transactions: {}, transactions: {},
errorMessage: '', errorMessage: '',
...@@ -29,6 +38,7 @@ const initialState = { ...@@ -29,6 +38,7 @@ const initialState = {
balances: { balances: {
ethereum: [], ethereum: [],
}, },
approvals: {},
}, },
contracts: {}, contracts: {},
}; };
...@@ -54,7 +64,7 @@ export const selectors = () => (dispatch, getState) => { ...@@ -54,7 +64,7 @@ export const selectors = () => (dispatch, getState) => {
}; };
const getBalance = (address, tokenAddress) => { const getBalance = (address, tokenAddress) => {
if (process.env.NODE_ENV === 'production' || !tokenAddress) { if (process.env.NODE_ENV !== 'production' && !tokenAddress) {
console.warn('No token address found - return ETH balance'); console.warn('No token address found - return ETH balance');
} }
...@@ -73,10 +83,23 @@ export const selectors = () => (dispatch, getState) => { ...@@ -73,10 +83,23 @@ export const selectors = () => (dispatch, getState) => {
return Balance(NaN); return Balance(NaN);
}; };
const getApprovals = (tokenAddress, tokenOwner, spender) => {
const token = state.approvals[tokenAddress] || {};
const owner = token[tokenOwner] || {};
if (!owner[spender]) {
dispatch(watchApprovals({ tokenAddress, tokenOwner, spender }));
return Balance(0);
}
return owner[spender];
};
return { return {
getBalance, getBalance,
getTokenBalance, getTokenBalance,
} getApprovals,
};
}; };
const Balance = (value, label = '', decimals = 18) => ({ const Balance = (value, label = '', decimals = 18) => ({
...@@ -153,8 +176,35 @@ export const watchBalance = ({ balanceOf, tokenAddress }) => (dispatch, getState ...@@ -153,8 +176,35 @@ export const watchBalance = ({ balanceOf, tokenAddress }) => (dispatch, getState
} }
}; };
export const watchApprovals = ({ tokenAddress, tokenOwner, spender }) => (dispatch, getState) => {
const { web3connect: { watched } } = getState();
const token = watched.approvals[tokenAddress] || {};
const owner = token[tokenOwner] || [];
if (owner.includes(spender)) {
return;
}
return dispatch({
type: WATCH_APPROVALS,
payload: {
tokenAddress,
tokenOwner,
spender,
},
});
};
export const updateApprovals = ({ tokenAddress, tokenOwner, spender, balance }) => ({
type: UPDATE_APPROVALS,
payload: {
tokenAddress,
tokenOwner,
spender,
balance,
},
});
export const sync = () => async (dispatch, getState) => { export const sync = () => async (dispatch, getState) => {
const { getBalance, getTokenBalance } = dispatch(selectors()); const { getBalance, getTokenBalance, getApprovals } = dispatch(selectors());
const web3 = await dispatch(initialize()); const web3 = await dispatch(initialize());
const { const {
account, account,
...@@ -195,7 +245,6 @@ export const sync = () => async (dispatch, getState) => { ...@@ -195,7 +245,6 @@ export const sync = () => async (dispatch, getState) => {
} }
const contract = contracts[tokenAddress] || new web3.eth.Contract(ERC20_ABI, tokenAddress); const contract = contracts[tokenAddress] || new web3.eth.Contract(ERC20_ABI, tokenAddress);
if (!contracts[tokenAddress]) { if (!contracts[tokenAddress]) {
dispatch({ dispatch({
type: ADD_CONTRACT, type: ADD_CONTRACT,
...@@ -227,6 +276,33 @@ export const sync = () => async (dispatch, getState) => { ...@@ -227,6 +276,33 @@ export const sync = () => async (dispatch, getState) => {
}); });
}); });
}); });
// Update Approvals
Object.entries(watched.approvals)
.forEach(([tokenAddress, token]) => {
const contract = contracts[tokenAddress] || new web3.eth.Contract(ERC20_ABI, tokenAddress);
Object.entries(token)
.forEach(([ tokenOwnerAddress, tokenOwner ]) => {
tokenOwner.forEach(async spenderAddress => {
const approvalBalance = getApprovals(tokenAddress, tokenOwnerAddress, spenderAddress);
const balance = await contract.methods.allowance(tokenOwnerAddress, spenderAddress).call();
const decimals = approvalBalance.decimals || await contract.methods.decimals().call();
const symbol = approvalBalance.label || await contract.methods.symbol().call();
if (approvalBalance.label && approvalBalance.value.isEqualTo(BN(balance))) {
return;
}
dispatch(updateApprovals({
tokenAddress,
tokenOwner: tokenOwnerAddress,
spender: spenderAddress,
balance: Balance(balance, symbol, decimals),
}));
});
});
});
}; };
export const startWatching = () => async (dispatch, getState) => { export const startWatching = () => async (dispatch, getState) => {
...@@ -305,6 +381,40 @@ export default function web3connectReducer(state = initialState, { type, payload ...@@ -305,6 +381,40 @@ export default function web3connectReducer(state = initialState, { type, payload
[payload.address]: payload.contract, [payload.address]: payload.contract,
}, },
}; };
case WATCH_APPROVALS:
const token = state.watched.approvals[payload.tokenAddress] || {};
const tokenOwner = token[payload.tokenOwner] || [];
return {
...state,
watched: {
...state.watched,
approvals: {
...state.watched.approvals,
[payload.tokenAddress]: {
...token,
[payload.tokenOwner]: [ ...tokenOwner, payload.spender ],
},
},
},
};
case UPDATE_APPROVALS:
const erc20 = state.approvals[payload.tokenAddress] || {};
const erc20Owner = erc20[payload.tokenOwner] || {};
return {
...state,
approvals: {
...state.approvals,
[payload.tokenAddress]: {
...erc20,
[payload.tokenOwner]: {
...erc20Owner,
[payload.spender]: payload.balance,
},
},
},
};
default: default:
return state; return state;
} }
......
...@@ -22,6 +22,9 @@ html, body { ...@@ -22,6 +22,9 @@ html, body {
background-color: $white; background-color: $white;
z-index: 100; z-index: 100;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0); -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
@media only screen and (min-device-width : 768px) {
background-color: $concrete-gray;
}
} }
#modal-root { #modal-root {
......
...@@ -5,6 +5,10 @@ ...@@ -5,6 +5,10 @@
height: 100%; height: 100%;
position: relative; position: relative;
@media only screen and (min-device-width : 768px) {
box-shadow: 0 0 8px 1px $mercury-gray;
}
& > div { & > div {
position: absolute; position: absolute;
width: 100%; width: 100%;
...@@ -19,4 +23,10 @@ ...@@ -19,4 +23,10 @@
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
@extend %col-nowrap; @extend %col-nowrap;
@media only screen and (min-device-width : 768px) {
max-width: 400px;
max-height: 600px;
margin: auto;
}
} }
\ No newline at end of file
...@@ -444,6 +444,7 @@ class Send extends Component { ...@@ -444,6 +444,7 @@ class Send extends Component {
value={output} value={output}
selectedTokenAddress={outputCurrency} selectedTokenAddress={outputCurrency}
extraText={this.getBalance(outputCurrency)} extraText={this.getBalance(outputCurrency)}
disableUnlock
/> />
<OversizedPanel> <OversizedPanel>
<div className="swap__down-arrow-background"> <div className="swap__down-arrow-background">
......
...@@ -562,6 +562,7 @@ class Swap extends Component { ...@@ -562,6 +562,7 @@ class Swap extends Component {
value={outputValue} value={outputValue}
selectedTokenAddress={outputCurrency} selectedTokenAddress={outputCurrency}
errorMessage={outputError} errorMessage={outputError}
disableUnlock
/> />
{ this.renderExchangeRate() } { this.renderExchangeRate() }
{ this.renderSummary() } { this.renderSummary() }
......
...@@ -2,33 +2,4 @@ import { generateContractsInitialState } from 'drizzle' ...@@ -2,33 +2,4 @@ import { generateContractsInitialState } from 'drizzle'
export default { export default {
contracts: generateContractsInitialState({ contracts: [], events: [], polls: [] }), contracts: generateContractsInitialState({ contracts: [], events: [], polls: [] }),
exchangeContracts: {},
tokenContracts: {},
exchange: {
inputBalance: 0,
outputBalance: 0,
inputToken: { value: 'ETH', label: 'ETH', clearableValue: false },
outputToken: { value: 'BAT', label: 'BAT', clearableValue: false },
investToken: { value: 'BAT', label: 'BAT', clearableValue: false },
ethPool1: 0,
ethPool2: 0,
tokenPool1: 0,
tokenPool2: 0,
allowanceApproved: true,
inputValue: 0,
outputValue: 0,
rate: 0,
fee: 0,
investEthPool: 0,
investTokenPool: 0,
investShares: 0,
userShares: 0,
investTokenBalance: 0,
investEthBalance: 0,
investTokenAllowance: 0,
investSharesInput: 0,
investEthRequired: 0,
investTokensRequired: 0,
investChecked: true
}
} }
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