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 @@
}
&__qr-container {
display: none;
padding: 10px;
background: $concrete-gray;
border: 1px solid $mercury-gray;
......
......@@ -113,6 +113,9 @@
padding: 10px 50px 10px 10px;
margin-right: -40px;
border-radius: 2.5rem;
outline: none;
cursor: pointer;
user-select: none;
}
&__dropdown-icon {
......
......@@ -7,9 +7,12 @@ import Fuse from '../../helpers/fuse';
import Modal from '../Modal';
import TokenLogo from '../TokenLogo';
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 ERC20_ABI from '../../abi/erc20';
import EXCHANGE_ABI from "../../abi/exchange";
const FUSE_OPTIONS = {
......@@ -44,9 +47,12 @@ class CurrencyInputPanel extends Component {
}).isRequired,
selectedTokens: PropTypes.array.isRequired,
errorMessage: PropTypes.string,
account: PropTypes.string,
selectedTokenAddress: PropTypes.string,
disableTokenSelect: PropTypes.bool,
selectors: PropTypes.func.isRequired,
filteredTokens: PropTypes.arrayOf(PropTypes.string),
disableUnlock: PropTypes.bool,
};
static defaultProps = {
......@@ -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() {
const {
title,
......@@ -234,11 +279,7 @@ class CurrencyInputPanel extends Component {
onChange={e => onValueChange(e.target.value)}
value={value}
/>
{/*<button*/}
{/*className='currency-input-panel__sub-currency-select'*/}
{/*>*/}
{/*Unlock*/}
{/*</button>*/}
{ this.renderUnlockButton() }
<button
className={classnames("currency-input-panel__currency-select", {
'currency-input-panel__currency-select--selected': selectedTokenAddress,
......@@ -277,5 +318,11 @@ export default drizzleConnect(
exchangeAddresses: state.addresses.exchangeAddresses,
tokenAddresses: state.addresses.tokenAddresses,
contracts: state.contracts,
account: state.web3connect.account,
approvals: state.web3connect.approvals,
web3: state.web3connect.web3,
}),
dispatch => ({
selectors: () => dispatch(selectors()),
}),
);
import { combineReducers } from 'redux';
import { drizzleReducers } from 'drizzle'
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';
export default combineReducers({
addresses,
exchangeContracts,
tokenContracts,
exchange,
// exchangeContracts,
// tokenContracts,
// exchange,
send,
swap,
// swap,
web3connect,
...drizzleReducers,
});
......@@ -12,6 +12,8 @@ export const WATCH_ETH_BALANCE = 'web3connect/watchEthBalance';
export const WATCH_TOKEN_BALANCE = 'web3connect/watchTokenBalance';
export const UPDATE_ETH_BALANCE = 'web3connect/updateEthBalance';
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';
......@@ -22,6 +24,13 @@ const initialState = {
balances: {
ethereum: {},
},
approvals: {
'0x0': {
TOKEN_OWNER: {
SPENDER: {},
},
},
},
pendingTransactions: [],
transactions: {},
errorMessage: '',
......@@ -29,6 +38,7 @@ const initialState = {
balances: {
ethereum: [],
},
approvals: {},
},
contracts: {},
};
......@@ -54,7 +64,7 @@ export const selectors = () => (dispatch, getState) => {
};
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');
}
......@@ -73,10 +83,23 @@ export const selectors = () => (dispatch, getState) => {
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 {
getBalance,
getTokenBalance,
}
getApprovals,
};
};
const Balance = (value, label = '', decimals = 18) => ({
......@@ -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) => {
const { getBalance, getTokenBalance } = dispatch(selectors());
const { getBalance, getTokenBalance, getApprovals } = dispatch(selectors());
const web3 = await dispatch(initialize());
const {
account,
......@@ -195,7 +245,6 @@ export const sync = () => async (dispatch, getState) => {
}
const contract = contracts[tokenAddress] || new web3.eth.Contract(ERC20_ABI, tokenAddress);
if (!contracts[tokenAddress]) {
dispatch({
type: ADD_CONTRACT,
......@@ -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) => {
......@@ -305,6 +381,40 @@ export default function web3connectReducer(state = initialState, { type, payload
[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:
return state;
}
......
......@@ -22,6 +22,9 @@ html, body {
background-color: $white;
z-index: 100;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
@media only screen and (min-device-width : 768px) {
background-color: $concrete-gray;
}
}
#modal-root {
......
......@@ -5,6 +5,10 @@
height: 100%;
position: relative;
@media only screen and (min-device-width : 768px) {
box-shadow: 0 0 8px 1px $mercury-gray;
}
& > div {
position: absolute;
width: 100%;
......@@ -19,4 +23,10 @@
width: 100vw;
height: 100vh;
@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 {
value={output}
selectedTokenAddress={outputCurrency}
extraText={this.getBalance(outputCurrency)}
disableUnlock
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
......
......@@ -562,6 +562,7 @@ class Swap extends Component {
value={outputValue}
selectedTokenAddress={outputCurrency}
errorMessage={outputError}
disableUnlock
/>
{ this.renderExchangeRate() }
{ this.renderSummary() }
......
......@@ -2,33 +2,4 @@ import { generateContractsInitialState } from 'drizzle'
export default {
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