Merge pull request #108 from project-serum/fee_estimate
Add ETH Fee Estimate to Withdrawal Dialog
This commit is contained in:
commit
469a3955d2
|
@ -34,7 +34,6 @@ import Tooltip from '@material-ui/core/Tooltip';
|
||||||
import EditIcon from '@material-ui/icons/Edit';
|
import EditIcon from '@material-ui/icons/Edit';
|
||||||
import MergeType from '@material-ui/icons/MergeType';
|
import MergeType from '@material-ui/icons/MergeType';
|
||||||
import FingerprintIcon from '@material-ui/icons/Fingerprint';
|
import FingerprintIcon from '@material-ui/icons/Fingerprint';
|
||||||
import { MARKETS } from '@project-serum/serum';
|
|
||||||
import AddTokenDialog from './AddTokenDialog';
|
import AddTokenDialog from './AddTokenDialog';
|
||||||
import ExportAccountDialog from './ExportAccountDialog';
|
import ExportAccountDialog from './ExportAccountDialog';
|
||||||
import SendDialog from './SendDialog';
|
import SendDialog from './SendDialog';
|
||||||
|
@ -44,11 +43,12 @@ import {
|
||||||
refreshAccountInfo,
|
refreshAccountInfo,
|
||||||
useSolanaExplorerUrlSuffix,
|
useSolanaExplorerUrlSuffix,
|
||||||
} from '../utils/connection';
|
} from '../utils/connection';
|
||||||
|
import { serumMarkets, priceStore } from '../utils/markets';
|
||||||
import { swapApiRequest } from '../utils/swap/api';
|
import { swapApiRequest } from '../utils/swap/api';
|
||||||
import { showSwapAddress } from '../utils/config';
|
import { showSwapAddress } from '../utils/config';
|
||||||
import { useAsyncData } from '../utils/fetch-loop';
|
import { useAsyncData } from '../utils/fetch-loop';
|
||||||
import { showTokenInfoDialog } from '../utils/config';
|
import { showTokenInfoDialog } from '../utils/config';
|
||||||
import { useConnection, MAINNET_URL } from '../utils/connection';
|
import { useConnection } from '../utils/connection';
|
||||||
import CloseTokenAccountDialog from './CloseTokenAccountButton';
|
import CloseTokenAccountDialog from './CloseTokenAccountButton';
|
||||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||||
import TokenIcon from './TokenIcon';
|
import TokenIcon from './TokenIcon';
|
||||||
|
@ -61,28 +61,6 @@ const balanceFormat = new Intl.NumberFormat(undefined, {
|
||||||
useGrouping: true,
|
useGrouping: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const serumMarkets = (() => {
|
|
||||||
const m = {};
|
|
||||||
MARKETS.forEach((market) => {
|
|
||||||
const coin = market.name.split('/')[0];
|
|
||||||
if (m[coin]) {
|
|
||||||
// Only override a market if it's not deprecated .
|
|
||||||
if (!m.deprecated) {
|
|
||||||
m[coin] = {
|
|
||||||
publicKey: market.address,
|
|
||||||
name: market.name.split('/').join(''),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
m[coin] = {
|
|
||||||
publicKey: market.address,
|
|
||||||
name: market.name.split('/').join(''),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return m;
|
|
||||||
})();
|
|
||||||
|
|
||||||
export default function BalancesList() {
|
export default function BalancesList() {
|
||||||
const wallet = useWallet();
|
const wallet = useWallet();
|
||||||
const [publicKeys, loaded] = useWalletPublicKeys();
|
const [publicKeys, loaded] = useWalletPublicKeys();
|
||||||
|
@ -202,7 +180,7 @@ export function BalanceListItem({ publicKey, expandable }) {
|
||||||
// A Serum market exists. Fetch the price.
|
// A Serum market exists. Fetch the price.
|
||||||
else if (serumMarkets[coin]) {
|
else if (serumMarkets[coin]) {
|
||||||
let m = serumMarkets[coin];
|
let m = serumMarkets[coin];
|
||||||
_priceStore.getPrice(connection, m.name).then((price) => {
|
priceStore.getPrice(connection, m.name).then((price) => {
|
||||||
setPrice(price);
|
setPrice(price);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -491,43 +469,3 @@ function BalanceListItemDetails({ publicKey, serumMarkets, balanceInfo }) {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a cached API wrapper to avoid rate limits.
|
|
||||||
class PriceStore {
|
|
||||||
constructor() {
|
|
||||||
this.cache = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPrice(connection, marketName) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (connection._rpcEndpoint !== MAINNET_URL) {
|
|
||||||
resolve(undefined);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.cache[marketName] === undefined) {
|
|
||||||
fetch(`https://serum-api.bonfida.com/orderbooks/${marketName}`).then(
|
|
||||||
(resp) => {
|
|
||||||
resp.json().then((resp) => {
|
|
||||||
if (resp.data.asks.length === 0 && resp.data.bids.length === 0) {
|
|
||||||
resolve(undefined);
|
|
||||||
} else if (resp.data.asks.length === 0) {
|
|
||||||
resolve(resp.data.bids[0]);
|
|
||||||
} else if (resp.data.bids.length === 0) {
|
|
||||||
resolve(resp.data.asks[0]);
|
|
||||||
} else {
|
|
||||||
const mid =
|
|
||||||
(resp.data.asks[0].price + resp.data.bids[0].price) / 2.0;
|
|
||||||
this.cache[marketName] = mid;
|
|
||||||
resolve(this.cache[marketName]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return resolve(this.cache[marketName]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const _priceStore = new PriceStore();
|
|
||||||
|
|
|
@ -17,9 +17,11 @@ import Tab from '@material-ui/core/Tab';
|
||||||
import DialogContentText from '@material-ui/core/DialogContentText';
|
import DialogContentText from '@material-ui/core/DialogContentText';
|
||||||
import {
|
import {
|
||||||
ConnectToMetamaskButton,
|
ConnectToMetamaskButton,
|
||||||
|
getErc20Balance,
|
||||||
useEthAccount,
|
useEthAccount,
|
||||||
withdrawEth,
|
withdrawEth,
|
||||||
} from '../utils/swap/eth';
|
} from '../utils/swap/eth';
|
||||||
|
import { serumMarkets, priceStore } from '../utils/markets';
|
||||||
import { useConnection, useIsProdNetwork } from '../utils/connection';
|
import { useConnection, useIsProdNetwork } from '../utils/connection';
|
||||||
import Stepper from '@material-ui/core/Stepper';
|
import Stepper from '@material-ui/core/Stepper';
|
||||||
import Step from '@material-ui/core/Step';
|
import Step from '@material-ui/core/Step';
|
||||||
|
@ -33,7 +35,7 @@ import {
|
||||||
WRAPPED_SOL_MINT,
|
WRAPPED_SOL_MINT,
|
||||||
} from '../utils/tokens/instructions';
|
} from '../utils/tokens/instructions';
|
||||||
import { parseTokenAccountData } from '../utils/tokens/data';
|
import { parseTokenAccountData } from '../utils/tokens/data';
|
||||||
import { Switch } from "@material-ui/core";
|
import { Switch, Tooltip } from '@material-ui/core';
|
||||||
|
|
||||||
const WUSDC_MINT = new PublicKey(
|
const WUSDC_MINT = new PublicKey(
|
||||||
'BXXkv6z8ykpG1yuvUDPgh732wzVHB69RnB9YgSYh3itW',
|
'BXXkv6z8ykpG1yuvUDPgh732wzVHB69RnB9YgSYh3itW',
|
||||||
|
@ -65,13 +67,13 @@ export default function SendDialog({ open, onClose, publicKey, balanceInfo }) {
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
Send {tokenName ?? abbreviateAddress(mint)}
|
Send {tokenName ?? abbreviateAddress(mint)}
|
||||||
{tokenSymbol ? ` (${tokenSymbol})` : null}
|
{tokenSymbol ? ` (${tokenSymbol})` : null}
|
||||||
{ethAccount && (
|
{ethAccount && (
|
||||||
<div>
|
<div>
|
||||||
<Typography color="textSecondary" style={{ fontSize: '14px' }}>
|
<Typography color="textSecondary" style={{ fontSize: '14px' }}>
|
||||||
Metamask connected: {ethAccount}
|
Metamask connected: {ethAccount}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
{swapCoinInfo ? (
|
{swapCoinInfo ? (
|
||||||
<Tabs
|
<Tabs
|
||||||
|
@ -216,7 +218,7 @@ function SendSplDialog({ onClose, publicKey, balanceInfo, onSubmitRef }) {
|
||||||
amount,
|
amount,
|
||||||
balanceInfo.mint,
|
balanceInfo.mint,
|
||||||
null,
|
null,
|
||||||
overrideDestinationCheck
|
overrideDestinationCheck,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,12 +230,18 @@ function SendSplDialog({ onClose, publicKey, balanceInfo, onSubmitRef }) {
|
||||||
<>
|
<>
|
||||||
<DialogContent>{fields}</DialogContent>
|
<DialogContent>{fields}</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
{ shouldShowOverride && (
|
{shouldShowOverride && (
|
||||||
<div style={{'align-items': 'center', 'display': 'flex', 'text-align': 'left'}}>
|
<div
|
||||||
|
style={{
|
||||||
|
'align-items': 'center',
|
||||||
|
display: 'flex',
|
||||||
|
'text-align': 'left',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<b>This address has no funds. Are you sure it's correct?</b>
|
<b>This address has no funds. Are you sure it's correct?</b>
|
||||||
<Switch
|
<Switch
|
||||||
checked={overrideDestinationCheck}
|
checked={overrideDestinationCheck}
|
||||||
onChange={e => setOverrideDestinationCheck(e.target.checked)}
|
onChange={(e) => setOverrideDestinationCheck(e.target.checked)}
|
||||||
color="primary"
|
color="primary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -279,6 +287,25 @@ function SendSwapDialog({
|
||||||
: swapCoinInfo.blockchain;
|
: swapCoinInfo.blockchain;
|
||||||
const needMetamask = blockchain === 'eth';
|
const needMetamask = blockchain === 'eth';
|
||||||
|
|
||||||
|
const [ethBalance] = useAsyncData(
|
||||||
|
() => getErc20Balance(ethAccount),
|
||||||
|
'ethBalance',
|
||||||
|
{
|
||||||
|
refreshInterval: 2000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const ethFeeData = useSwapApiGet(
|
||||||
|
blockchain === 'eth' &&
|
||||||
|
`fees/eth/${ethAccount}` +
|
||||||
|
(swapCoinInfo.erc20Contract ? '/' + swapCoinInfo.erc20Contract : ''),
|
||||||
|
{ refreshInterval: 2000 },
|
||||||
|
);
|
||||||
|
const [ethFeeEstimate] = ethFeeData;
|
||||||
|
const insufficientEthBalance =
|
||||||
|
typeof ethBalance === 'number' &&
|
||||||
|
typeof ethFeeEstimate === 'number' &&
|
||||||
|
ethBalance < ethFeeEstimate;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (blockchain === 'eth' && ethAccount) {
|
if (blockchain === 'eth' && ethAccount) {
|
||||||
setDestinationAddress(ethAccount);
|
setDestinationAddress(ethAccount);
|
||||||
|
@ -342,6 +369,32 @@ function SendSwapDialog({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let sendButton = (
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
disabled={
|
||||||
|
sending ||
|
||||||
|
(needMetamask && !ethAccount) ||
|
||||||
|
!validAmount ||
|
||||||
|
insufficientEthBalance
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (insufficientEthBalance) {
|
||||||
|
sendButton = (
|
||||||
|
<Tooltip
|
||||||
|
title="Insufficient ETH for withdrawal transaction fee"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<span>{sendButton}</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DialogContent style={{ paddingTop: 16 }}>
|
<DialogContent style={{ paddingTop: 16 }}>
|
||||||
|
@ -355,17 +408,20 @@ function SendSwapDialog({
|
||||||
{swapCoinInfo.ticker}
|
{swapCoinInfo.ticker}
|
||||||
{needMetamask ? ' via MetaMask' : null}.
|
{needMetamask ? ' via MetaMask' : null}.
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
|
{blockchain === 'eth' && (
|
||||||
|
<DialogContentText>
|
||||||
|
Estimated withdrawal transaction fee:
|
||||||
|
<EthWithdrawalFeeEstimate
|
||||||
|
ethFeeData={ethFeeData}
|
||||||
|
insufficientEthBalance={insufficientEthBalance}
|
||||||
|
/>
|
||||||
|
</DialogContentText>
|
||||||
|
)}
|
||||||
{needMetamask && !ethAccount ? <ConnectToMetamaskButton /> : fields}
|
{needMetamask && !ethAccount ? <ConnectToMetamaskButton /> : fields}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={onClose}>Cancel</Button>
|
<Button onClick={onClose}>Cancel</Button>
|
||||||
<Button
|
{sendButton}
|
||||||
type="submit"
|
|
||||||
color="primary"
|
|
||||||
disabled={sending || (needMetamask && !ethAccount) || !validAmount}
|
|
||||||
>
|
|
||||||
Send
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -457,7 +513,12 @@ function SendSwapProgress({ publicKey, signature, onClose, blockchain }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useForm(balanceInfo, addressHelperText, passAddressValidation, overrideValidation) {
|
function useForm(
|
||||||
|
balanceInfo,
|
||||||
|
addressHelperText,
|
||||||
|
passAddressValidation,
|
||||||
|
overrideValidation,
|
||||||
|
) {
|
||||||
const [destinationAddress, setDestinationAddress] = useState('');
|
const [destinationAddress, setDestinationAddress] = useState('');
|
||||||
const [transferAmountString, setTransferAmountString] = useState('');
|
const [transferAmountString, setTransferAmountString] = useState('');
|
||||||
const { amount: balanceAmount, decimals, tokenSymbol } = balanceInfo;
|
const { amount: balanceAmount, decimals, tokenSymbol } = balanceInfo;
|
||||||
|
@ -489,13 +550,15 @@ function useForm(balanceInfo, addressHelperText, passAddressValidation, override
|
||||||
margin="normal"
|
margin="normal"
|
||||||
type="number"
|
type="number"
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
<Button onClick={() =>
|
<Button
|
||||||
setTransferAmountString(
|
onClick={() =>
|
||||||
balanceAmountToUserAmount(balanceAmount, decimals),
|
setTransferAmountString(
|
||||||
)
|
balanceAmountToUserAmount(balanceAmount, decimals),
|
||||||
}>
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
MAX
|
MAX
|
||||||
</Button>
|
</Button>
|
||||||
{tokenSymbol ? tokenSymbol : null}
|
{tokenSymbol ? tokenSymbol : null}
|
||||||
|
@ -532,7 +595,7 @@ function useForm(balanceInfo, addressHelperText, passAddressValidation, override
|
||||||
}
|
}
|
||||||
|
|
||||||
function balanceAmountToUserAmount(balanceAmount, decimals) {
|
function balanceAmountToUserAmount(balanceAmount, decimals) {
|
||||||
return (balanceAmount / Math.pow(10, decimals)).toFixed(decimals)
|
return (balanceAmount / Math.pow(10, decimals)).toFixed(decimals);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EthWithdrawalCompleter({ ethAccount, publicKey }) {
|
function EthWithdrawalCompleter({ ethAccount, publicKey }) {
|
||||||
|
@ -568,3 +631,42 @@ function EthWithdrawalCompleterItem({ ethAccount, swap }) {
|
||||||
}, [withdrawal.txid, withdrawal.status]);
|
}, [withdrawal.txid, withdrawal.status]);
|
||||||
return null;
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -273,8 +273,8 @@ function RestoreWalletForm({ goBack }) {
|
||||||
Restore Existing Wallet
|
Restore Existing Wallet
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography>
|
<Typography>
|
||||||
Restore your wallet using your twelve or twenty-four seed words. Note that this
|
Restore your wallet using your twelve or twenty-four seed words.
|
||||||
will delete any existing wallet on this device.
|
Note that this will delete any existing wallet on this device.
|
||||||
</Typography>
|
</Typography>
|
||||||
<TextField
|
<TextField
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
|
|
@ -5,6 +5,7 @@ import tuple from 'immutable-tuple';
|
||||||
const pageLoadTime = new Date();
|
const pageLoadTime = new Date();
|
||||||
|
|
||||||
const globalCache: Map<any, any> = new Map();
|
const globalCache: Map<any, any> = new Map();
|
||||||
|
const errorCache: Map<any, any> = new Map();
|
||||||
|
|
||||||
class FetchLoops {
|
class FetchLoops {
|
||||||
loops = new Map();
|
loops = new Map();
|
||||||
|
@ -117,13 +118,16 @@ class FetchLoopInternal<T = any> {
|
||||||
try {
|
try {
|
||||||
const data = await this.fn();
|
const data = await this.fn();
|
||||||
globalCache.set(this.cacheKey, data);
|
globalCache.set(this.cacheKey, data);
|
||||||
|
errorCache.delete(this.cacheKey);
|
||||||
this.errors = 0;
|
this.errors = 0;
|
||||||
this.notifyListeners();
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
++this.errors;
|
++this.errors;
|
||||||
|
globalCache.delete(this.cacheKey);
|
||||||
|
errorCache.set(this.cacheKey, error);
|
||||||
console.warn(error);
|
console.warn(error);
|
||||||
} finally {
|
} finally {
|
||||||
|
this.notifyListeners();
|
||||||
if (!this.timeoutId && !this.stopped) {
|
if (!this.timeoutId && !this.stopped) {
|
||||||
let waitTime = this.refreshInterval;
|
let waitTime = this.refreshInterval;
|
||||||
|
|
||||||
|
@ -154,11 +158,13 @@ class FetchLoopInternal<T = any> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns [data, loaded, error]
|
||||||
|
// loaded is false when error is present for backwards compatibility
|
||||||
export function useAsyncData<T = any>(
|
export function useAsyncData<T = any>(
|
||||||
asyncFn: () => Promise<T>,
|
asyncFn: () => Promise<T>,
|
||||||
cacheKey: any,
|
cacheKey: any,
|
||||||
{ refreshInterval = 60000 } = {},
|
{ refreshInterval = 60000 } = {},
|
||||||
): [null | undefined | T, boolean] {
|
): [null | undefined | T, boolean, any] {
|
||||||
const [, rerender] = useReducer((i) => i + 1, 0);
|
const [, rerender] = useReducer((i) => i + 1, 0);
|
||||||
cacheKey = formatCacheKey(cacheKey);
|
cacheKey = formatCacheKey(cacheKey);
|
||||||
|
|
||||||
|
@ -178,12 +184,13 @@ export function useAsyncData<T = any>(
|
||||||
}, [cacheKey, refreshInterval]);
|
}, [cacheKey, refreshInterval]);
|
||||||
|
|
||||||
if (!cacheKey) {
|
if (!cacheKey) {
|
||||||
return [null, false];
|
return [null, false, undefined];
|
||||||
}
|
}
|
||||||
|
|
||||||
const loaded = globalCache.has(cacheKey);
|
const loaded = globalCache.has(cacheKey);
|
||||||
|
const error = errorCache.has(cacheKey) ? errorCache.get(cacheKey) : undefined;
|
||||||
const data = loaded ? globalCache.get(cacheKey) : undefined;
|
const data = loaded ? globalCache.get(cacheKey) : undefined;
|
||||||
return [data, loaded];
|
return [data, loaded, error];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function refreshCache(cacheKey, clearCache = false) {
|
export function refreshCache(cacheKey, clearCache = false) {
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { MARKETS } from '@project-serum/serum';
|
||||||
|
import { PublicKey } from '@solana/web3.js';
|
||||||
|
import { MAINNET_URL } from './connection';
|
||||||
|
|
||||||
|
interface Markets {
|
||||||
|
[coin: string]: {
|
||||||
|
publicKey: PublicKey;
|
||||||
|
name: string;
|
||||||
|
deprecated?: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const serumMarkets = (() => {
|
||||||
|
const m: Markets = {};
|
||||||
|
MARKETS.forEach((market) => {
|
||||||
|
const coin = market.name.split('/')[0];
|
||||||
|
if (m[coin]) {
|
||||||
|
// Only override a market if it's not deprecated .
|
||||||
|
if (!m.deprecated) {
|
||||||
|
m[coin] = {
|
||||||
|
publicKey: market.address,
|
||||||
|
name: market.name.split('/').join(''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m[coin] = {
|
||||||
|
publicKey: market.address,
|
||||||
|
name: market.name.split('/').join(''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return m;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Create a cached API wrapper to avoid rate limits.
|
||||||
|
class PriceStore {
|
||||||
|
cache: {}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.cache = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPrice(connection, marketName) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (connection._rpcEndpoint !== MAINNET_URL) {
|
||||||
|
resolve(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.cache[marketName] === undefined) {
|
||||||
|
fetch(`https://serum-api.bonfida.com/orderbooks/${marketName}`).then(
|
||||||
|
(resp) => {
|
||||||
|
resp.json().then((resp) => {
|
||||||
|
if (resp.data.asks.length === 0 && resp.data.bids.length === 0) {
|
||||||
|
resolve(undefined);
|
||||||
|
} else if (resp.data.asks.length === 0) {
|
||||||
|
resolve(resp.data.bids[0]);
|
||||||
|
} else if (resp.data.bids.length === 0) {
|
||||||
|
resolve(resp.data.asks[0]);
|
||||||
|
} else {
|
||||||
|
const mid =
|
||||||
|
(resp.data.asks[0].price + resp.data.bids[0].price) / 2.0;
|
||||||
|
this.cache[marketName] = mid;
|
||||||
|
resolve(this.cache[marketName]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return resolve(this.cache[marketName]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const priceStore = new PriceStore();
|
|
@ -14,8 +14,12 @@ import {
|
||||||
import { PublicKey } from '@solana/web3.js';
|
import { PublicKey } from '@solana/web3.js';
|
||||||
import { TOKEN_PROGRAM_ID } from './tokens/instructions';
|
import { TOKEN_PROGRAM_ID } from './tokens/instructions';
|
||||||
|
|
||||||
const RAYDIUM_STAKE_PROGRAM_ID = new PublicKey('EhhTKczWMGQt46ynNeRX1WfeagwwJd7ufHvCDjRxjo5Q');
|
const RAYDIUM_STAKE_PROGRAM_ID = new PublicKey(
|
||||||
const RAYDIUM_LP_PROGRAM_ID = new PublicKey('RVKd61ztZW9GUwhRbbLoYVRE5Xf1B2tVscKqwZqXgEr');
|
'EhhTKczWMGQt46ynNeRX1WfeagwwJd7ufHvCDjRxjo5Q',
|
||||||
|
);
|
||||||
|
const RAYDIUM_LP_PROGRAM_ID = new PublicKey(
|
||||||
|
'RVKd61ztZW9GUwhRbbLoYVRE5Xf1B2tVscKqwZqXgEr',
|
||||||
|
);
|
||||||
|
|
||||||
const marketCache = {};
|
const marketCache = {};
|
||||||
let marketCacheConnection = null;
|
let marketCacheConnection = null;
|
||||||
|
|
Loading…
Reference in New Issue