spl-token-wallet/src/components/DepositDialog.js

327 lines
9.0 KiB
JavaScript
Raw Normal View History

import React, { useState } from 'react';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent';
import DialogForm from './DialogForm';
import { abbreviateAddress } from '../utils/utils';
import CopyableDisplay from './CopyableDisplay';
2020-09-12 22:56:07 -07:00
import {
useIsProdNetwork,
useSolanaExplorerUrlSuffix,
} from '../utils/connection';
import Typography from '@material-ui/core/Typography';
2020-09-10 00:25:53 -07:00
import DialogActions from '@material-ui/core/DialogActions';
import Button from '@material-ui/core/Button';
import { useAsyncData } from '../utils/fetch-loop';
import tuple from 'immutable-tuple';
import { showSwapAddress } from '../utils/config';
import { useCallAsync } from '../utils/notifications';
2020-09-11 22:24:16 -07:00
import { swapApiRequest } from '../utils/swap/api';
import {
ConnectToMetamaskButton,
getErc20Balance,
swapErc20ToSpl,
useEthAccount,
} from '../utils/swap/eth';
import InputAdornment from '@material-ui/core/InputAdornment';
import TextField from '@material-ui/core/TextField';
import Stepper from '@material-ui/core/Stepper';
import Step from '@material-ui/core/Step';
import StepLabel from '@material-ui/core/StepLabel';
import CircularProgress from '@material-ui/core/CircularProgress';
import Link from '@material-ui/core/Link';
2020-09-11 04:57:57 -07:00
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import { DialogContentText } from '@material-ui/core';
export default function DepositDialog({
open,
onClose,
publicKey,
balanceInfo,
}) {
const ethAccount = useEthAccount();
2020-09-12 22:56:07 -07:00
const isProdNetwork = useIsProdNetwork();
2020-09-10 00:25:53 -07:00
const urlSuffix = useSolanaExplorerUrlSuffix();
2020-09-11 22:24:16 -07:00
const { mint, tokenName, tokenSymbol, owner } = balanceInfo;
2020-09-11 04:57:57 -07:00
const [tab, setTab] = useState(0);
const [swapInfo] = useAsyncData(async () => {
2020-09-12 22:56:07 -07:00
if (!showSwapAddress || !isProdNetwork) {
2020-09-11 04:57:57 -07:00
return null;
}
2020-09-11 22:24:16 -07:00
return await swapApiRequest(
'POST',
'swap_to',
{
2020-09-11 04:57:57 -07:00
blockchain: 'sol',
coin: balanceInfo.mint?.toBase58(),
address: publicKey.toBase58(),
2020-09-11 22:24:16 -07:00
},
{ ignoreUserErrors: true },
);
2020-09-12 22:56:07 -07:00
}, [
'swapInfo',
isProdNetwork,
balanceInfo.mint?.toBase58(),
publicKey.toBase58(),
]);
2020-09-11 22:24:16 -07:00
let tabs = null;
if (swapInfo) {
let firstTab = `SPL ${tokenSymbol ?? swapInfo.coin.ticker}`;
let secondTab = swapInfo.coin.ticker;
if (!mint) {
firstTab = 'SOL';
} else {
secondTab = `${
swapInfo.coin.erc20Contract ? 'ERC20' : 'Native'
} ${secondTab}`;
2020-09-11 04:57:57 -07:00
}
2020-09-11 22:24:16 -07:00
tabs = (
<Tabs
value={tab}
variant="fullWidth"
onChange={(e, value) => setTab(value)}
textColor="primary"
indicatorColor="primary"
>
<Tab label={firstTab} />
<Tab label={secondTab} />
</Tabs>
);
}
return (
<DialogForm open={open} onClose={onClose}>
<DialogTitle>
2020-09-10 00:25:53 -07:00
Deposit {tokenName ?? mint.toBase58()}
{tokenSymbol ? ` (${tokenSymbol})` : null}
{ethAccount && (
<div>
<Typography color="textSecondary" style={{ fontSize: '14px' }}>
Metamask connected: {ethAccount}
</Typography>
</div>
)}
</DialogTitle>
2020-09-11 22:24:16 -07:00
{tabs}
2020-09-11 04:57:57 -07:00
<DialogContent style={{ paddingTop: 16 }}>
{tab === 0 ? (
<>
{publicKey.equals(owner) ? (
<DialogContentText>
This address can only be used to receive SOL. Do not send other
tokens to this address.
</DialogContentText>
) : (
<DialogContentText>
This address can only be used to receive{' '}
{tokenSymbol ?? abbreviateAddress(mint)}. Do not send SOL to
this address.
</DialogContentText>
)}
<CopyableDisplay
value={publicKey.toBase58()}
label={'Deposit Address'}
autoFocus
qrCode
/>
<DialogContentText variant="body2">
<Link
href={
`https://explorer.solana.com/account/${publicKey.toBase58()}` +
urlSuffix
}
target="_blank"
rel="noopener"
>
View on Solana Explorer
</Link>
</DialogContentText>
</>
) : (
<SolletSwapDepositAddress
balanceInfo={balanceInfo}
2020-09-11 04:57:57 -07:00
swapInfo={swapInfo}
/>
2020-09-11 04:57:57 -07:00
)}
</DialogContent>
2020-09-10 00:25:53 -07:00
<DialogActions>
<Button onClick={onClose}>Close</Button>
</DialogActions>
</DialogForm>
);
}
2020-09-11 05:25:19 -07:00
function SolletSwapDepositAddress({ balanceInfo, swapInfo }) {
if (!swapInfo) {
return null;
}
const { blockchain, address, memo, coin } = swapInfo;
2020-09-11 22:24:16 -07:00
const { mint, tokenName } = balanceInfo;
if (blockchain === 'btc' && memo === null) {
return (
<>
2020-09-11 04:57:57 -07:00
<DialogContentText>
Native BTC can be converted to SPL {tokenName} by sending it to the
following address:
2020-09-11 04:57:57 -07:00
</DialogContentText>
<CopyableDisplay
value={address}
label="Native BTC Deposit Address"
2020-09-11 05:25:19 -07:00
autoFocus
qrCode={`bitcoin:${address}`}
/>
</>
);
}
if (blockchain === 'eth') {
return (
<>
2020-09-11 04:57:57 -07:00
<DialogContentText>
{coin.erc20Contract ? 'ERC20' : 'Native'} {coin.ticker} can be
2020-09-11 22:24:16 -07:00
converted to {mint ? 'SPL' : 'native'} {tokenName} via MetaMask.
To convert, you must already have SOL in your wallet.
2020-09-11 04:57:57 -07:00
</DialogContentText>
<MetamaskDeposit swapInfo={swapInfo} />
</>
);
}
return null;
}
function MetamaskDeposit({ swapInfo }) {
const ethAccount = useEthAccount();
const [amount, setAmount] = useState('');
const [submitted, setSubmitted] = useState(false);
const [status, setStatus] = useState(null);
const callAsync = useCallAsync();
const {
address: swapAddress,
memo: destination,
coin: { erc20Contract: erc20Address, ticker },
} = swapInfo;
2020-09-11 22:24:16 -07:00
const [maxAmount, maxAmountLoaded] = useAsyncData(async () => {
if (ethAccount) {
2020-09-11 22:24:16 -07:00
return Math.min(
await getErc20Balance(ethAccount, erc20Address),
swapInfo.maxSize ?? Infinity,
);
}
return 0;
}, tuple(getErc20Balance, ethAccount, erc20Address));
if (!ethAccount) {
return <ConnectToMetamaskButton />;
}
async function submit() {
setSubmitted(true);
setStatus({ step: 0 });
await callAsync(
(async () => {
let parsedAmount = parseFloat(amount);
2020-09-16 05:22:09 -07:00
if (!parsedAmount || parsedAmount > maxAmount || parsedAmount <= 0) {
throw new Error('Invalid amount');
}
await swapErc20ToSpl({
ethAccount,
erc20Address,
swapAddress,
destination,
2020-09-16 05:22:09 -07:00
amount,
onStatusChange: (e) => setStatus((status) => ({ ...status, ...e })),
});
})(),
{ onError: () => setSubmitted(false) },
);
}
if (!submitted) {
return (
<div style={{ display: 'flex', alignItems: 'baseline' }}>
<TextField
label="Amount"
fullWidth
variant="outlined"
margin="normal"
type="number"
InputProps={{
endAdornment: (
<InputAdornment position="end">{ticker}</InputAdornment>
),
inputProps: {
step: 'any',
},
}}
value={amount}
onChange={(e) => setAmount(e.target.value.trim())}
helperText={
maxAmountLoaded ? (
<span onClick={() => setAmount(maxAmount.toFixed(6))}>
Max: {maxAmount.toFixed(6)}
</span>
) : null
}
/>
<Button color="primary" style={{ marginLeft: 8 }} onClick={submit}>
Convert
</Button>
</div>
);
}
return (
<>
<Stepper activeStep={status.step}>
<Step>
<StepLabel>Approve Conversion</StepLabel>
</Step>
<Step>
<StepLabel>Send Funds</StepLabel>
</Step>
<Step>
<StepLabel>Wait for Confirmations</StepLabel>
</Step>
</Stepper>
{status.step === 2 ? (
<>
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<div style={{ marginRight: 16 }}>
<CircularProgress />
</div>
<div>
{status.confirms ? (
<Typography>{status.confirms} / 12 Confirmations</Typography>
) : (
<Typography>Transaction Pending</Typography>
)}
<Typography variant="body2">
<Link
href={`https://etherscan.io/tx/${status.txid}`}
target="_blank"
rel="noopener"
>
View on Etherscan
</Link>
</Typography>
</div>
</div>
</>
) : null}
</>
);
}