Add Desposit Fee Estimation (#118)

* init

* fix

* fix
This commit is contained in:
jhl-alameda 2021-03-01 01:01:20 -05:00 committed by GitHub
parent 658f7640ff
commit d629d8d27a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 183 additions and 51 deletions

View File

@ -16,6 +16,7 @@ import {
getErc20Balance,
swapErc20ToSpl,
useEthAccount,
estimateErc20SwapFees,
} from '../utils/swap/eth';
import InputAdornment from '@material-ui/core/InputAdornment';
import TextField from '@material-ui/core/TextField';
@ -26,7 +27,8 @@ import CircularProgress from '@material-ui/core/CircularProgress';
import Link from '@material-ui/core/Link';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import { DialogContentText } from '@material-ui/core';
import { DialogContentText, Tooltip } from '@material-ui/core';
import { EthFeeEstimate } from './EthFeeEstimate';
export default function DepositDialog({
open,
@ -117,6 +119,7 @@ export default function DepositDialog({
<SolletSwapDepositAddress
balanceInfo={balanceInfo}
swapInfo={swapInfo}
ethAccount={ethAccount}
/>
)}
</DialogContent>
@ -127,11 +130,41 @@ export default function DepositDialog({
);
}
function SolletSwapDepositAddress({ balanceInfo, swapInfo }) {
function SolletSwapDepositAddress({ balanceInfo, swapInfo, ethAccount }) {
const [ethBalance] = useAsyncData(
() => getErc20Balance(ethAccount),
'ethBalance',
{
refreshInterval: 2000,
},
);
const ethFeeData = useAsyncData(
swapInfo.coin &&
(() =>
estimateErc20SwapFees({
erc20Address: swapInfo.coin.erc20Contract,
swapAddress: swapInfo.address,
ethAccount,
})),
'depositEthFee',
{
refreshInterval: 2000,
},
);
if (!swapInfo) {
return null;
}
const ethFeeEstimate = Array.isArray(ethFeeData[0])
? ethFeeData[0].reduce((acc, elem) => acc + elem)
: ethFeeData[0];
const insufficientEthBalance =
typeof ethBalance === 'number' &&
typeof ethFeeEstimate === 'number' &&
ethBalance < ethFeeEstimate;
const { blockchain, address, memo, coin } = swapInfo;
const { mint, tokenName } = balanceInfo;
@ -160,7 +193,17 @@ function SolletSwapDepositAddress({ balanceInfo, swapInfo }) {
converted to {mint ? 'SPL' : 'native'} {tokenName} via MetaMask. To
convert, you must already have SOL in your wallet.
</DialogContentText>
<MetamaskDeposit swapInfo={swapInfo} />
<DialogContentText>
Estimated withdrawal transaction fee:
<EthFeeEstimate
ethFeeData={ethFeeData}
insufficientEthBalance={insufficientEthBalance}
/>
</DialogContentText>
<MetamaskDeposit
swapInfo={swapInfo}
insufficientEthBalance={insufficientEthBalance}
/>
</>
);
}
@ -168,7 +211,7 @@ function SolletSwapDepositAddress({ balanceInfo, swapInfo }) {
return null;
}
function MetamaskDeposit({ swapInfo }) {
function MetamaskDeposit({ swapInfo, insufficientEthBalance }) {
const ethAccount = useEthAccount();
const [amount, setAmount] = useState('');
const [submitted, setSubmitted] = useState(false);
@ -219,6 +262,28 @@ function MetamaskDeposit({ swapInfo }) {
}
if (!submitted) {
let convertButton = (
<Button
color="primary"
style={{ marginLeft: 8 }}
onClick={submit}
disabled={insufficientEthBalance}
>
Convert
</Button>
);
if (insufficientEthBalance) {
convertButton = (
<Tooltip
title="Insufficient ETH for withdrawal transaction fee"
placement="top"
>
<span>{convertButton}</span>
</Tooltip>
);
}
return (
<div style={{ display: 'flex', alignItems: 'baseline' }}>
<TextField
@ -245,9 +310,7 @@ function MetamaskDeposit({ swapInfo }) {
) : null
}
/>
<Button color="primary" style={{ marginLeft: 8 }} onClick={submit}>
Convert
</Button>
{convertButton}
</div>
);
}

View File

@ -0,0 +1,70 @@
import { DialogContentText } from "@material-ui/core";
import { useEffect, useState } from "react";
import { useConnection } from "../utils/connection";
import { priceStore, serumMarkets } from "../utils/markets";
function FeeContentText({ ethFee, ethPrice, warning = false, prefix = "", bold = false }) {
let usdFeeEstimate = ethPrice !== undefined ? ethPrice * ethFee : null;
return (
<DialogContentText
color={warning ? 'secondary' : 'textPrimary'}
// @ts-ignore
style={{ marginBottom: '0px', fontWeight: bold ? '500' : undefined }}
>
{prefix}
{ethFee.toFixed(4)}
{' ETH'}
{usdFeeEstimate && ` (${usdFeeEstimate.toFixed(2)} USD)`}
</DialogContentText>
);
}
export function EthFeeEstimate({ ethFeeData, insufficientEthBalance }) {
let [ethFeeEstimate, loaded, error] = ethFeeData;
const [ethPrice, setEthPrice] = useState<number | undefined>(undefined);
const connection = useConnection();
useEffect(() => {
if (ethPrice === undefined) {
let m = serumMarkets['ETH'];
priceStore.getPrice(connection, m.name).then(setEthPrice);
}
}, [ethPrice, connection]);
if (!loaded && !error) {
return (
<DialogContentText color="textPrimary">Loading...</DialogContentText>
);
} else if (error) {
return (
<DialogContentText color="textPrimary">
Unable to estimate
</DialogContentText>
);
}
if (Array.isArray(ethFeeEstimate)) {
const [approveFee, swapFee] = ethFeeEstimate;
return (
<DialogContentText>
<FeeContentText ethFee={approveFee} ethPrice={ethPrice} prefix={"Approve: "} />
<FeeContentText ethFee={swapFee} ethPrice={ethPrice} prefix={"Swap: "} />
<FeeContentText
warning={insufficientEthBalance}
ethFee={approveFee + swapFee}
ethPrice={ethPrice}
prefix={"Total: "}
bold
/>
</DialogContentText>
);
}
return (
<FeeContentText
warning={insufficientEthBalance}
ethFee={ethFeeEstimate}
ethPrice={ethPrice}
/>
);
}

View File

@ -21,7 +21,6 @@ import {
useEthAccount,
withdrawEth,
} from '../utils/swap/eth';
import { serumMarkets, priceStore } from '../utils/markets';
import { useConnection, useIsProdNetwork } from '../utils/connection';
import Stepper from '@material-ui/core/Stepper';
import Step from '@material-ui/core/Step';
@ -36,6 +35,7 @@ import {
} from '../utils/tokens/instructions';
import { parseTokenAccountData } from '../utils/tokens/data';
import { Switch, Tooltip } from '@material-ui/core';
import { EthFeeEstimate } from './EthFeeEstimate';
const WUSDC_MINT = new PublicKey(
'BXXkv6z8ykpG1yuvUDPgh732wzVHB69RnB9YgSYh3itW',
@ -411,7 +411,7 @@ function SendSwapDialog({
{blockchain === 'eth' && (
<DialogContentText>
Estimated withdrawal transaction fee:
<EthWithdrawalFeeEstimate
<EthFeeEstimate
ethFeeData={ethFeeData}
insufficientEthBalance={insufficientEthBalance}
/>
@ -631,42 +631,3 @@ function EthWithdrawalCompleterItem({ ethAccount, swap }) {
}, [withdrawal.txid, withdrawal.status]);
return null;
}
export function EthWithdrawalFeeEstimate({
ethFeeData,
insufficientEthBalance,
}) {
let [ethFeeEstimate, loaded, error] = ethFeeData;
const [ethPrice, setEthPrice] = useState(null);
const connection = useConnection();
useEffect(() => {
if (ethPrice === null) {
let m = serumMarkets['ETH'];
priceStore.getPrice(connection, m.name).then(setEthPrice);
}
}, [ethPrice, connection]);
if (!loaded && !error) {
return (
<DialogContentText color="textPrimary">Loading...</DialogContentText>
);
} else if (error) {
return (
<DialogContentText color="textPrimary">
Unable to estimate
</DialogContentText>
);
}
let usdFeeEstimate = ethPrice !== null ? ethPrice * ethFeeEstimate : null;
return (
<DialogContentText
color={insufficientEthBalance ? 'secondary' : 'textPrimary'}
>
{ethFeeEstimate.toFixed(4)}
{' ETH'}
{usdFeeEstimate && ` (${usdFeeEstimate.toFixed(2)} USD)`}
</DialogContentText>
);
}

View File

@ -40,7 +40,7 @@ class PriceStore {
this.cache = {};
}
async getPrice(connection, marketName) {
async getPrice(connection, marketName): Promise<number | undefined> {
return new Promise((resolve, reject) => {
if (connection._rpcEndpoint !== MAINNET_URL) {
resolve(undefined);

View File

@ -6,6 +6,8 @@ import Button from '@material-ui/core/Button';
import { useCallAsync } from '../notifications';
const web3 = new Web3(window.ethereum);
// Change to use estimated gas limit
const SUGGESTED_GAS_LIMIT = 200000;
export function useEthAccount() {
const [account, setAccount] = useState(null);
@ -37,6 +39,42 @@ export async function getErc20Balance(account, erc20Address) {
return parseInt(value, 10) / 10 ** parseInt(decimals, 10);
}
export async function estimateErc20SwapFees({
erc20Address,
swapAddress,
ethAccount,
}) {
if (!erc20Address) {
return estimateEthSwapFees({ swapAddress });
}
const erc20 = new web3.eth.Contract(ERC20_ABI, erc20Address);
const decimals = parseInt(await erc20.methods.decimals().call(), 10);
const approveAmount = addDecimals('100000000', decimals);
let approveEstimatedGas = await erc20.methods
.approve(swapAddress, approveAmount)
.estimateGas({ from: ethAccount });
// Account for Metamask over-estimation
approveEstimatedGas *= 1.5;
// Use estimated gas limit for now
const swapEstimatedGas = SUGGESTED_GAS_LIMIT;
const gasPrice = (await web3.eth.getGasPrice()) * 1e-18;
return [approveEstimatedGas * gasPrice, swapEstimatedGas * gasPrice];
}
export async function estimateEthSwapFees() {
const estimatedGas = SUGGESTED_GAS_LIMIT;
const gasPrice = (await web3.eth.getGasPrice()) * 1e-18;
return estimatedGas * gasPrice;
}
export async function swapErc20ToSpl({
ethAccount,
erc20Address,
@ -70,7 +108,7 @@ export async function swapErc20ToSpl({
const swapTx = swap.methods
.swapErc20(erc20Address, destination, encodedAmount)
.send({ from: ethAccount, gasLimit: 200000 });
.send({ from: ethAccount, gasLimit: SUGGESTED_GAS_LIMIT });
const swapTxid = await waitForTxid(swapTx);
onStatusChange({ step: 2, txid: swapTxid, confirms: 0 });
@ -157,7 +195,7 @@ export async function withdrawEth(from, withdrawal, callAsync) {
return;
}
pendingNonces.add(nonce);
await callAsync(method.send({ from, gasLimit: 200000 }), {
await callAsync(method.send({ from, gasLimit: SUGGESTED_GAS_LIMIT }), {
progressMessage: `Completing ${withdrawal.coin.ticker} transfer...`,
});
pendingNonces.delete(nonce);