Add option to swap SPL tokens for native tokens
This commit is contained in:
parent
0f2014fcde
commit
948d0e411c
|
@ -5,9 +5,9 @@ import { makeStyles } from '@material-ui/core/styles';
|
|||
import { useSnackbar } from 'notistack';
|
||||
import QrcodeIcon from 'mdi-material-ui/Qrcode';
|
||||
import QRCode from 'qrcode.react';
|
||||
import DialogForm from './DialogForm';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
|
@ -81,11 +81,11 @@ function Qrcode({ value }) {
|
|||
<IconButton onClick={() => setShowQrcode(true)}>
|
||||
<QrcodeIcon />
|
||||
</IconButton>
|
||||
<DialogForm open={showQrcode} onClose={() => setShowQrcode(false)}>
|
||||
<Dialog open={showQrcode} onClose={() => setShowQrcode(false)}>
|
||||
<DialogContent className={classes.qrcodeContainer}>
|
||||
<QRCode value={value} size={256} includeMargin />
|
||||
</DialogContent>
|
||||
</DialogForm>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,11 @@ import { useUpdateTokenName } from '../utils/tokens/names';
|
|||
import { useCallAsync, useSendTransaction } from '../utils/notifications';
|
||||
import { Account, LAMPORTS_PER_SOL } from '@solana/web3.js';
|
||||
import { abbreviateAddress, sleep } from '../utils/utils';
|
||||
import { refreshAccountInfo, useConnectionConfig, MAINNET_URL } from '../utils/connection';
|
||||
import {
|
||||
refreshAccountInfo,
|
||||
useConnectionConfig,
|
||||
MAINNET_URL,
|
||||
} from '../utils/connection';
|
||||
import { createAndInitializeMint } from '../utils/tokens';
|
||||
import { Tooltip, Button } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
|
|
|
@ -17,6 +17,7 @@ import { makeStyles } from '@material-ui/core/styles';
|
|||
import { useCallAsync } from '../utils/notifications';
|
||||
import { SwapApiError, swapApiRequest } from '../utils/swap/api';
|
||||
import {
|
||||
ConnectToMetamaskButton,
|
||||
getErc20Balance,
|
||||
swapErc20ToSpl,
|
||||
useEthAccount,
|
||||
|
@ -104,7 +105,7 @@ function SolletSwapDepositAddress({ publicKey, balanceInfo }) {
|
|||
try {
|
||||
return await swapApiRequest('POST', 'swap_to', {
|
||||
blockchain: 'sol',
|
||||
coin: balanceInfo.mint.toBase58(),
|
||||
coin: balanceInfo.mint?.toBase58(),
|
||||
address: publicKey.toBase58(),
|
||||
});
|
||||
} catch (e) {
|
||||
|
@ -115,7 +116,7 @@ function SolletSwapDepositAddress({ publicKey, balanceInfo }) {
|
|||
}
|
||||
throw e;
|
||||
}
|
||||
}, tuple('swapInfo', balanceInfo.mint.toBase58(), publicKey.toBase58()));
|
||||
}, tuple('swapInfo', balanceInfo.mint?.toBase58(), publicKey.toBase58()));
|
||||
|
||||
if (!loaded) {
|
||||
return <LoadingIndicator />;
|
||||
|
@ -153,9 +154,7 @@ function SolletSwapDepositAddress({ publicKey, balanceInfo }) {
|
|||
{coin.erc20Contract ? 'ERC20' : 'Native'} {coin.ticker} can be
|
||||
converted to SPL {tokenName} via MetaMask.
|
||||
</Typography>
|
||||
{window.ethereum ? (
|
||||
<MetamaskDeposit swapInfo={swapInfo} ethereum={window.ethereum} />
|
||||
) : null}
|
||||
<MetamaskDeposit swapInfo={swapInfo} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -163,7 +162,7 @@ function SolletSwapDepositAddress({ publicKey, balanceInfo }) {
|
|||
return null;
|
||||
}
|
||||
|
||||
function MetamaskDeposit({ ethereum, swapInfo }) {
|
||||
function MetamaskDeposit({ swapInfo }) {
|
||||
const ethAccount = useEthAccount();
|
||||
const [amount, setAmount] = useState('');
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
@ -184,22 +183,7 @@ function MetamaskDeposit({ ethereum, swapInfo }) {
|
|||
}, tuple(getErc20Balance, ethAccount, erc20Address));
|
||||
|
||||
if (!ethAccount) {
|
||||
function connect() {
|
||||
callAsync(
|
||||
ethereum.request({
|
||||
method: 'eth_requestAccounts',
|
||||
}),
|
||||
{
|
||||
progressMessage: 'Connecting to MetaMask...',
|
||||
successMessage: 'Connected to MetaMask',
|
||||
},
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button color="primary" variant="outlined" onClick={connect}>
|
||||
Connect to MetaMask
|
||||
</Button>
|
||||
);
|
||||
return <ConnectToMetamaskButton />;
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
|
@ -208,6 +192,7 @@ function MetamaskDeposit({ ethereum, swapInfo }) {
|
|||
await callAsync(
|
||||
(async () => {
|
||||
let parsedAmount = parseFloat(amount);
|
||||
|
||||
if (!parsedAmount || parsedAmount > maxAmount) {
|
||||
throw new Error('Invalid amount');
|
||||
}
|
||||
|
@ -243,7 +228,13 @@ function MetamaskDeposit({ ethereum, swapInfo }) {
|
|||
}}
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value.trim())}
|
||||
helperText={maxAmountLoaded ? `Max: ${maxAmount.toFixed(6)}` : null}
|
||||
helperText={
|
||||
maxAmountLoaded ? (
|
||||
<span onClick={() => setAmount(maxAmount.toFixed(6))}>
|
||||
Max: {maxAmount.toFixed(6)}
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<Button color="primary" style={{ marginLeft: 8 }} onClick={submit}>
|
||||
Convert
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
|
@ -10,78 +10,301 @@ import { PublicKey } from '@solana/web3.js';
|
|||
import { abbreviateAddress } from '../utils/utils';
|
||||
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||
import { useSendTransaction } from '../utils/notifications';
|
||||
import { useAsyncData } from '../utils/fetch-loop';
|
||||
import { SwapApiError, swapApiRequest } from '../utils/swap/api';
|
||||
import { showSwapAddress } from '../utils/config';
|
||||
import Tabs from '@material-ui/core/Tabs';
|
||||
import Tab from '@material-ui/core/Tab';
|
||||
import DialogContentText from '@material-ui/core/DialogContentText';
|
||||
import {
|
||||
ConnectToMetamaskButton,
|
||||
useEthAccount,
|
||||
withdrawEth,
|
||||
} from '../utils/swap/eth';
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
export default function SendDialog({ open, onClose, publicKey, balanceInfo }) {
|
||||
const [tab, setTab] = useState(0);
|
||||
const onSubmitRef = useRef();
|
||||
|
||||
const [swapCoinInfo] = useAsyncData(async () => {
|
||||
if (!showSwapAddress) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return await swapApiRequest(
|
||||
'GET',
|
||||
`coins/sol/${balanceInfo.mint?.toBase58()}`,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof SwapApiError) {
|
||||
if (e.status === 404) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}, ['swapCoinInfo', balanceInfo.mint?.toBase58()]);
|
||||
const ethAccount = useEthAccount();
|
||||
|
||||
const { mint, tokenName, tokenSymbol } = balanceInfo;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogForm
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onSubmit={() => onSubmitRef.current()}
|
||||
>
|
||||
<DialogTitle>
|
||||
Send {tokenName ?? abbreviateAddress(mint)}
|
||||
{tokenSymbol ? ` (${tokenSymbol})` : null}
|
||||
</DialogTitle>
|
||||
{swapCoinInfo ? (
|
||||
<Tabs
|
||||
value={tab}
|
||||
variant="fullWidth"
|
||||
onChange={(e, value) => setTab(value)}
|
||||
>
|
||||
<Tab label={`SPL ${swapCoinInfo.ticker}`} />
|
||||
<Tab label={describeSwap(swapCoinInfo)} />
|
||||
</Tabs>
|
||||
) : null}
|
||||
{tab === 0 ? (
|
||||
<SendSplDialog
|
||||
onClose={onClose}
|
||||
publicKey={publicKey}
|
||||
balanceInfo={balanceInfo}
|
||||
onSubmitRef={onSubmitRef}
|
||||
/>
|
||||
) : (
|
||||
<SendSwapDialog
|
||||
onClose={onClose}
|
||||
publicKey={publicKey}
|
||||
balanceInfo={balanceInfo}
|
||||
swapCoinInfo={swapCoinInfo}
|
||||
ethAccount={ethAccount}
|
||||
onSubmitRef={onSubmitRef}
|
||||
/>
|
||||
)}
|
||||
</DialogForm>
|
||||
{ethAccount &&
|
||||
(swapCoinInfo?.blockchain === 'eth' || swapCoinInfo?.erc20Contract) ? (
|
||||
<EthWithdrawalCompleter ethAccount={ethAccount} publicKey={publicKey} />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function describeSwap(swapCoinInfo) {
|
||||
if (swapCoinInfo.blockchain === 'eth' && swapCoinInfo.erc20Contract) {
|
||||
return `ERC20 ${swapCoinInfo.ticker}`;
|
||||
}
|
||||
return `native ${swapCoinInfo.ticker}`;
|
||||
}
|
||||
|
||||
function SendSplDialog({ onClose, publicKey, balanceInfo, onSubmitRef }) {
|
||||
const wallet = useWallet();
|
||||
const [destinationAddress, setDestinationAddress] = useState('');
|
||||
const [transferAmountString, setTransferAmountString] = useState('');
|
||||
const [sendTransaction, sending] = useSendTransaction();
|
||||
const { fields, destinationAddress, transferAmountString } = useForm(
|
||||
balanceInfo,
|
||||
);
|
||||
const { decimals } = balanceInfo;
|
||||
|
||||
let {
|
||||
amount: balanceAmount,
|
||||
decimals,
|
||||
mint,
|
||||
tokenName,
|
||||
tokenSymbol,
|
||||
} = balanceInfo;
|
||||
|
||||
function onSubmit() {
|
||||
let amount = Math.round(
|
||||
parseFloat(transferAmountString) * Math.pow(10, decimals),
|
||||
);
|
||||
async function makeTransaction() {
|
||||
let amount = Math.round(parseFloat(transferAmountString) * 10 ** decimals);
|
||||
if (!amount || amount <= 0) {
|
||||
throw new Error('Invalid amount');
|
||||
}
|
||||
sendTransaction(
|
||||
wallet.transferToken(
|
||||
publicKey,
|
||||
new PublicKey(destinationAddress),
|
||||
amount,
|
||||
),
|
||||
{ onSuccess: onClose },
|
||||
return wallet.transferToken(
|
||||
publicKey,
|
||||
new PublicKey(destinationAddress),
|
||||
amount,
|
||||
);
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
return sendTransaction(makeTransaction(), { onSuccess: onClose });
|
||||
}
|
||||
onSubmitRef.current = onSubmit;
|
||||
return (
|
||||
<DialogForm open={open} onClose={onClose} onSubmit={onSubmit}>
|
||||
<DialogTitle>
|
||||
Send {tokenName ?? abbreviateAddress(mint)}
|
||||
{tokenSymbol ? ` (${tokenSymbol})` : null}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
label="Recipient Address"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
value={destinationAddress}
|
||||
onChange={(e) => setDestinationAddress(e.target.value.trim())}
|
||||
/>
|
||||
<TextField
|
||||
label="Amount"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
type="number"
|
||||
InputProps={{
|
||||
endAdornment: tokenSymbol ? (
|
||||
<InputAdornment position="end">{tokenSymbol}</InputAdornment>
|
||||
) : null,
|
||||
inputProps: {
|
||||
step: Math.pow(10, -decimals),
|
||||
},
|
||||
}}
|
||||
value={transferAmountString}
|
||||
onChange={(e) => setTransferAmountString(e.target.value.trim())}
|
||||
helperText={`Max: ${balanceAmount / Math.pow(10, decimals)}`}
|
||||
/>
|
||||
</DialogContent>
|
||||
<>
|
||||
<DialogContent>{fields}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button type="submit" color="primary" disabled={sending}>
|
||||
Send
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogForm>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SendSwapDialog({
|
||||
onClose,
|
||||
publicKey,
|
||||
balanceInfo,
|
||||
swapCoinInfo,
|
||||
ethAccount,
|
||||
onSubmitRef,
|
||||
}) {
|
||||
const wallet = useWallet();
|
||||
const [sendTransaction, sending] = useSendTransaction();
|
||||
const {
|
||||
fields,
|
||||
destinationAddress,
|
||||
transferAmountString,
|
||||
setDestinationAddress,
|
||||
} = useForm(balanceInfo);
|
||||
|
||||
const { tokenName, decimals } = balanceInfo;
|
||||
const blockchain =
|
||||
swapCoinInfo.blockchain === 'sol' ? 'eth' : swapCoinInfo.blockchain;
|
||||
const needMetamask = blockchain === 'eth';
|
||||
|
||||
useEffect(() => {
|
||||
if (blockchain === 'eth' && ethAccount) {
|
||||
setDestinationAddress(ethAccount);
|
||||
}
|
||||
}, [blockchain, ethAccount, setDestinationAddress]);
|
||||
|
||||
async function makeTransaction() {
|
||||
let amount = Math.round(parseFloat(transferAmountString) * 10 ** decimals);
|
||||
if (!amount || amount <= 0) {
|
||||
throw new Error('Invalid amount');
|
||||
}
|
||||
const swapInfo = await swapApiRequest('POST', 'swap_to', {
|
||||
blockchain,
|
||||
coin: swapCoinInfo.erc20Contract,
|
||||
address: destinationAddress,
|
||||
});
|
||||
if (swapInfo.blockchain !== 'sol') {
|
||||
throw new Error('Unexpected blockchain');
|
||||
}
|
||||
return wallet.transferToken(
|
||||
publicKey,
|
||||
new PublicKey(swapInfo.address),
|
||||
amount,
|
||||
swapInfo.memo,
|
||||
);
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
return sendTransaction(makeTransaction(), { onSuccess: onClose });
|
||||
}
|
||||
onSubmitRef.current = onSubmit;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
SPL {tokenName} can be converted to {describeSwap(swapCoinInfo)}
|
||||
{needMetamask ? ' via MetaMask' : null}.
|
||||
</DialogContentText>
|
||||
{needMetamask && !ethAccount ? <ConnectToMetamaskButton /> : fields}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
disabled={sending || (needMetamask && !ethAccount)}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function useForm(balanceInfo) {
|
||||
const [destinationAddress, setDestinationAddress] = useState('');
|
||||
const [transferAmountString, setTransferAmountString] = useState('');
|
||||
const { amount: balanceAmount, decimals, tokenSymbol } = balanceInfo;
|
||||
|
||||
const fields = (
|
||||
<>
|
||||
<TextField
|
||||
label="Recipient Address"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
value={destinationAddress}
|
||||
onChange={(e) => setDestinationAddress(e.target.value.trim())}
|
||||
/>
|
||||
<TextField
|
||||
label="Amount"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
margin="normal"
|
||||
type="number"
|
||||
InputProps={{
|
||||
endAdornment: tokenSymbol ? (
|
||||
<InputAdornment position="end">{tokenSymbol}</InputAdornment>
|
||||
) : null,
|
||||
inputProps: {
|
||||
step: Math.pow(10, -decimals),
|
||||
},
|
||||
}}
|
||||
value={transferAmountString}
|
||||
onChange={(e) => setTransferAmountString(e.target.value.trim())}
|
||||
helperText={
|
||||
<span
|
||||
onClick={() =>
|
||||
setTransferAmountString(
|
||||
(balanceAmount / Math.pow(10, decimals)).toFixed(decimals),
|
||||
)
|
||||
}
|
||||
>
|
||||
Max: {balanceAmount / Math.pow(10, decimals)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return {
|
||||
fields,
|
||||
destinationAddress,
|
||||
transferAmountString,
|
||||
setDestinationAddress,
|
||||
};
|
||||
}
|
||||
|
||||
function EthWithdrawalCompleter({ ethAccount, publicKey }) {
|
||||
const [swaps] = useAsyncData(
|
||||
() => swapApiRequest('GET', `swaps_from/sol/${publicKey.toBase58()}`),
|
||||
`swaps_from/sol/${publicKey.toBase58()}`,
|
||||
{ refreshInterval: 10000 },
|
||||
);
|
||||
if (!swaps) {
|
||||
return null;
|
||||
}
|
||||
return swaps.map((swap) => (
|
||||
<EthWithdrawalCompleterItem
|
||||
key={swap.deposit.txid}
|
||||
ethAccount={ethAccount}
|
||||
swap={swap}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
function EthWithdrawalCompleterItem({ ethAccount, swap }) {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const { withdrawal } = swap;
|
||||
useEffect(() => {
|
||||
if (
|
||||
withdrawal.status === 'sent' &&
|
||||
withdrawal.blockchain === 'eth' &&
|
||||
withdrawal.txid &&
|
||||
!withdrawal.txid.startsWith('0x') &&
|
||||
withdrawal.txData
|
||||
) {
|
||||
withdrawEth(ethAccount, withdrawal).catch((e) =>
|
||||
enqueueSnackbar(e.message, { variant: 'error' }),
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [withdrawal.txid, withdrawal.status]);
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,9 @@ body {
|
|||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ ReactDOM.render(
|
|||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
document.getElementById('root'),
|
||||
);
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import assert from 'assert';
|
||||
import { useEffect, useReducer } from 'react';
|
||||
import tuple from 'immutable-tuple';
|
||||
|
||||
const pageLoadTime = new Date();
|
||||
|
||||
|
@ -142,6 +143,8 @@ export function useAsyncData(
|
|||
cacheKey,
|
||||
{ refreshInterval = 60000 } = {},
|
||||
) {
|
||||
cacheKey = formatCacheKey(cacheKey);
|
||||
|
||||
const [, rerender] = useReducer((i) => i + 1, 0);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -169,6 +172,7 @@ export function useAsyncData(
|
|||
}
|
||||
|
||||
export function refreshCache(cacheKey, clearCache = false) {
|
||||
cacheKey = formatCacheKey(cacheKey);
|
||||
if (clearCache) {
|
||||
globalCache.delete(cacheKey);
|
||||
}
|
||||
|
@ -182,6 +186,7 @@ export function refreshCache(cacheKey, clearCache = false) {
|
|||
}
|
||||
|
||||
export function setCache(cacheKey, value, { initializeOnly = false } = {}) {
|
||||
cacheKey = formatCacheKey(cacheKey);
|
||||
if (initializeOnly && globalCache.has(cacheKey)) {
|
||||
return;
|
||||
}
|
||||
|
@ -191,3 +196,10 @@ export function setCache(cacheKey, value, { initializeOnly = false } = {}) {
|
|||
loop.notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
function formatCacheKey(cacheKey) {
|
||||
if (Array.isArray(cacheKey)) {
|
||||
return tuple(...cacheKey);
|
||||
}
|
||||
return cacheKey;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Web3 from 'web3';
|
||||
import ERC20_ABI from './erc20-abi.json';
|
||||
import SWAP_ABI from './swap-abi.json';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import { useCallAsync } from '../notifications';
|
||||
|
||||
const web3 = new Web3(window.ethereum);
|
||||
|
||||
|
@ -58,14 +60,14 @@ export async function swapErc20ToSpl({
|
|||
const decimals = parseInt(await erc20.methods.decimals().call(), 10);
|
||||
|
||||
const approveTx = erc20.methods
|
||||
.approve(swapAddress, Math.floor(amount * 10 ** decimals))
|
||||
.approve(swapAddress, Math.round(amount * 10 ** decimals))
|
||||
.send({ from: ethAccount });
|
||||
await waitForTxid(approveTx);
|
||||
|
||||
onStatusChange({ step: 1 });
|
||||
|
||||
const swapTx = swap.methods
|
||||
.swapErc20(erc20Address, destination, Math.floor(amount * 10 ** decimals))
|
||||
.swapErc20(erc20Address, destination, Math.round(amount * 10 ** decimals))
|
||||
.send({ from: ethAccount, gasLimit: 100000 });
|
||||
const swapTxid = await waitForTxid(swapTx);
|
||||
|
||||
|
@ -101,6 +103,36 @@ export async function swapEthToSpl({
|
|||
onStatusChange({ step: 3 });
|
||||
}
|
||||
|
||||
export async function withdrawEth(from, withdrawal) {
|
||||
const { params, signature } = withdrawal.txData;
|
||||
const swap = new web3.eth.Contract(SWAP_ABI, params[1]);
|
||||
let method;
|
||||
if (params[0] === 'withdrawErc20') {
|
||||
method = swap.methods.withdrawErc20(
|
||||
params[2],
|
||||
params[3],
|
||||
params[4],
|
||||
params[5],
|
||||
signature,
|
||||
);
|
||||
} else if (params[0] === 'withdrawEth') {
|
||||
method = swap.methods.withdrawEth(
|
||||
params[2],
|
||||
params[3],
|
||||
params[4],
|
||||
signature,
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await method.estimateGas();
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
await method.send({ from });
|
||||
}
|
||||
|
||||
function waitForTxid(tx) {
|
||||
return new Promise((resolve, reject) => {
|
||||
tx.once('transactionHash', resolve).catch(reject);
|
||||
|
@ -124,3 +156,40 @@ function waitForConfirms(tx, onStatusChange) {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function ConnectToMetamaskButton() {
|
||||
const callAsync = useCallAsync();
|
||||
|
||||
if (!window.ethereum) {
|
||||
return (
|
||||
<Button
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
component="a"
|
||||
href="https://metamask.io/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
Connect to MetaMask
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function connect() {
|
||||
callAsync(
|
||||
window.ethereum.request({
|
||||
method: 'eth_requestAccounts',
|
||||
}),
|
||||
{
|
||||
progressMessage: 'Connecting to MetaMask...',
|
||||
successMessage: 'Connected to MetaMask',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button color="primary" variant="outlined" onClick={connect}>
|
||||
Connect to MetaMask
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,23 @@
|
|||
[
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "uint256",
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bytes",
|
||||
"name": "signature",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "NonceUsed",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
|
@ -18,6 +37,19 @@
|
|||
"name": "OwnershipTransferred",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "address",
|
||||
"name": "account",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "Paused",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
|
@ -43,6 +75,44 @@
|
|||
"name": "Swap",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "address",
|
||||
"name": "account",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "Unpaused",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Withdraw",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "owner",
|
||||
|
@ -56,6 +126,19 @@
|
|||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "paused",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "renounceOwnership",
|
||||
|
@ -63,6 +146,19 @@
|
|||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "newOwner",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "transferOwnership",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
|
@ -102,12 +198,105 @@
|
|||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "newOwner",
|
||||
"internalType": "contract IERC20",
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "signature",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "transferOwnership",
|
||||
"name": "withdrawErc20",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address payable",
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "signature",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "withdrawEth",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "pause",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "unpause",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "contract IERC20",
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "ownerWithdrawErc20",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "ownerWithdrawEth",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
|
|
|
@ -2,6 +2,7 @@ import { PublicKey, SystemProgram, Transaction } from '@solana/web3.js';
|
|||
import {
|
||||
initializeAccount,
|
||||
initializeMint,
|
||||
memoInstruction,
|
||||
mintTo,
|
||||
TOKEN_PROGRAM_ID,
|
||||
transfer,
|
||||
|
@ -144,6 +145,7 @@ export async function transferTokens({
|
|||
sourcePublicKey,
|
||||
destinationPublicKey,
|
||||
amount,
|
||||
memo,
|
||||
}) {
|
||||
let transaction = new Transaction().add(
|
||||
transfer({
|
||||
|
@ -153,6 +155,9 @@ export async function transferTokens({
|
|||
amount,
|
||||
}),
|
||||
);
|
||||
if (memo) {
|
||||
transaction.add(memoInstruction(memo));
|
||||
}
|
||||
let signers = [owner];
|
||||
return await connection.sendTransaction(transaction, signers);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,10 @@ export const WRAPPED_SOL_MINT = new PublicKey(
|
|||
'So11111111111111111111111111111111111111111',
|
||||
);
|
||||
|
||||
export const MEMO_PROGRAM_ID = new PublicKey(
|
||||
'Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo',
|
||||
);
|
||||
|
||||
const LAYOUT = BufferLayout.union(BufferLayout.u8('instruction'));
|
||||
LAYOUT.addVariant(
|
||||
0,
|
||||
|
@ -122,3 +126,11 @@ export function mintTo({ mint, destination, amount, mintAuthority }) {
|
|||
programId: TOKEN_PROGRAM_ID,
|
||||
});
|
||||
}
|
||||
|
||||
export function memoInstruction(memo) {
|
||||
return new TransactionInstruction({
|
||||
keys: [],
|
||||
data: Buffer.from(memo, 'utf-8'),
|
||||
programId: MEMO_PROGRAM_ID,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -68,8 +68,11 @@ export class Wallet {
|
|||
);
|
||||
};
|
||||
|
||||
transferToken = async (source, destination, amount) => {
|
||||
transferToken = async (source, destination, amount, memo = null) => {
|
||||
if (source.equals(this.publicKey)) {
|
||||
if (memo) {
|
||||
throw new Error('Memo not implemented');
|
||||
}
|
||||
return this.transferSol(destination, amount);
|
||||
}
|
||||
return await transferTokens({
|
||||
|
@ -78,6 +81,7 @@ export class Wallet {
|
|||
sourcePublicKey: source,
|
||||
destinationPublicKey: destination,
|
||||
amount,
|
||||
memo,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue