serum-dex-ui/src/utils/markets.tsx

1196 lines
34 KiB
TypeScript

import {
Market,
MARKETS,
OpenOrders,
Orderbook,
TOKEN_MINTS,
TokenInstructions,
} from '@project-serum/serum';
import { PublicKey } from '@solana/web3.js';
import React, { useContext, useEffect, useState } from 'react';
import {
divideBnToNumber,
floorToDecimal,
getTokenMultiplierFromDecimals,
sleep,
useLocalStorageState,
} from './utils';
import { refreshCache, useAsyncData } from './fetch-loop';
import { useAccountData, useAccountInfo, useConnection } from './connection';
import { useWallet } from '@solana/wallet-adapter-react';
import tuple from 'immutable-tuple';
import { notify } from './notifications';
import BN from 'bn.js';
import {
getTokenAccountInfo,
parseTokenAccountData,
useMintInfos,
} from './tokens';
import {
Balances,
CustomMarketInfo,
DeprecatedOpenOrdersBalances,
FullMarketInfo,
MarketContextValues,
MarketInfo,
OrderWithMarketAndMarketName,
SelectedTokenAccounts,
TokenAccount,
} from './types';
import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions';
import { Order } from '@project-serum/serum/lib/market';
import BonfidaApi from './bonfidaConnector';
// Used in debugging, should be false in production
const _IGNORE_DEPRECATED = false;
export const USE_MARKETS: MarketInfo[] = _IGNORE_DEPRECATED
? MARKETS.map((m) => ({ ...m, deprecated: false }))
: MARKETS;
export function useMarketsList() {
return USE_MARKETS.filter(
({ name, deprecated }) =>
!deprecated && !process.env.REACT_APP_EXCLUDE_MARKETS?.includes(name),
);
}
export function useAllMarkets() {
const connection = useConnection();
const { customMarkets } = useCustomMarkets();
const getAllMarkets = async () => {
const markets: Array<{
market: Market;
marketName: string;
programId: PublicKey;
} | null> = await Promise.all(
getMarketInfos(customMarkets).map(async (marketInfo) => {
try {
const market = await Market.load(
connection,
marketInfo.address,
{},
marketInfo.programId,
);
return {
market,
marketName: marketInfo.name,
programId: marketInfo.programId,
};
} catch (e) {
notify({
message: 'Error loading all market',
description: e.message,
type: 'error',
});
return null;
}
}),
);
return markets.filter(
(m): m is { market: Market; marketName: string; programId: PublicKey } =>
!!m,
);
};
return useAsyncData(
getAllMarkets,
tuple('getAllMarkets', customMarkets.length, connection),
{ refreshInterval: _VERY_SLOW_REFRESH_INTERVAL },
);
}
export function useUnmigratedOpenOrdersAccounts() {
const connection = useConnection();
const { publicKey } = useWallet();
async function getUnmigratedOpenOrdersAccounts(): Promise<OpenOrders[]> {
if (!connection || !publicKey) {
return [];
}
console.log('refreshing useUnmigratedOpenOrdersAccounts');
let deprecatedOpenOrdersAccounts: OpenOrders[] = [];
const deprecatedProgramIds = Array.from(
new Set(
USE_MARKETS.filter(
({ deprecated }) => deprecated,
).map(({ programId }) => programId.toBase58()),
),
).map((publicKeyStr) => new PublicKey(publicKeyStr));
let programId: PublicKey;
for (programId of deprecatedProgramIds) {
try {
const openOrdersAccounts = await OpenOrders.findForOwner(
connection,
publicKey,
programId,
);
deprecatedOpenOrdersAccounts = deprecatedOpenOrdersAccounts.concat(
openOrdersAccounts
.filter(
(openOrders) =>
openOrders.baseTokenTotal.toNumber() ||
openOrders.quoteTokenTotal.toNumber(),
)
.filter((openOrders) =>
USE_MARKETS.some(
(market) =>
market.deprecated && market.address.equals(openOrders.market),
),
),
);
} catch (e) {
console.log(
'Error loading deprecated markets',
programId?.toBase58(),
e.message,
);
}
}
// Maybe sort
return deprecatedOpenOrdersAccounts;
}
const cacheKey = tuple(
'getUnmigratedOpenOrdersAccounts',
connection,
publicKey?.toBase58(),
);
const [accounts] = useAsyncData(getUnmigratedOpenOrdersAccounts, cacheKey, {
refreshInterval: _VERY_SLOW_REFRESH_INTERVAL,
});
return {
accounts,
refresh: (clearCache: boolean) => refreshCache(cacheKey, clearCache),
};
}
const MarketContext: React.Context<null | MarketContextValues> = React.createContext<null | MarketContextValues>(
null,
);
const _VERY_SLOW_REFRESH_INTERVAL = 5000 * 1000;
// For things that don't really change
const _SLOW_REFRESH_INTERVAL = 5 * 1000;
// For things that change frequently
const _FAST_REFRESH_INTERVAL = 1000;
export const DEFAULT_MARKET = USE_MARKETS.find(
({ name, deprecated }) => name === 'SRM/USDT' && !deprecated,
);
export function getMarketDetails(
market: Market | undefined | null,
customMarkets: CustomMarketInfo[],
): FullMarketInfo {
if (!market) {
return {};
}
const marketInfos = getMarketInfos(customMarkets);
const marketInfo = marketInfos.find((otherMarket) =>
otherMarket.address.equals(market.address),
);
const baseCurrency =
(market?.baseMintAddress &&
TOKEN_MINTS.find((token) => token.address.equals(market.baseMintAddress))
?.name) ||
(marketInfo?.baseLabel && `${marketInfo?.baseLabel}*`) ||
'UNKNOWN';
const quoteCurrency =
(market?.quoteMintAddress &&
TOKEN_MINTS.find((token) => token.address.equals(market.quoteMintAddress))
?.name) ||
(marketInfo?.quoteLabel && `${marketInfo?.quoteLabel}*`) ||
'UNKNOWN';
return {
...marketInfo,
marketName: marketInfo?.name,
baseCurrency,
quoteCurrency,
marketInfo,
};
}
export function useCustomMarkets() {
const [customMarkets, setCustomMarkets] = useLocalStorageState<
CustomMarketInfo[]
>('customMarkets', []);
return { customMarkets, setCustomMarkets };
}
export function MarketProvider({ marketAddress, setMarketAddress, children }) {
const { customMarkets, setCustomMarkets } = useCustomMarkets();
const address = marketAddress && new PublicKey(marketAddress);
const connection = useConnection();
const marketInfos = getMarketInfos(customMarkets);
const marketInfo =
address && marketInfos.find((market) => market.address.equals(address));
// Replace existing market with a non-deprecated one on first load
useEffect(() => {
if (marketInfo && marketInfo.deprecated) {
console.log('Switching markets from deprecated', marketInfo);
if (DEFAULT_MARKET) {
setMarketAddress(DEFAULT_MARKET.address.toBase58());
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [market, setMarket] = useState<Market | null>();
useEffect(() => {
if (
market &&
marketInfo &&
// @ts-ignore
market._decoded.ownAddress?.equals(marketInfo?.address)
) {
return;
}
setMarket(null);
if (!marketInfo || !marketInfo.address) {
notify({
message: 'Error loading market',
description: 'Please select a market from the dropdown',
type: 'error',
});
return;
}
Market.load(connection, marketInfo.address, {}, marketInfo.programId)
.then(setMarket)
.catch((e) =>
notify({
message: 'Error loading market',
description: e.message,
type: 'error',
}),
);
// eslint-disable-next-line
}, [connection, marketInfo]);
return (
<MarketContext.Provider
value={{
market,
...getMarketDetails(market, customMarkets),
setMarketAddress,
customMarkets,
setCustomMarkets,
}}
>
{children}
</MarketContext.Provider>
);
}
export function getTradePageUrl(marketAddress?: string) {
if (!marketAddress) {
const saved = localStorage.getItem('marketAddress');
if (saved) {
marketAddress = JSON.parse(saved);
}
marketAddress = marketAddress || DEFAULT_MARKET?.address.toBase58() || '';
}
return `/market/${marketAddress}`;
}
export function useSelectedTokenAccounts(): [
SelectedTokenAccounts,
(newSelectedTokenAccounts: SelectedTokenAccounts) => void,
] {
const [
selectedTokenAccounts,
setSelectedTokenAccounts,
] = useLocalStorageState<SelectedTokenAccounts>('selectedTokenAccounts', {});
return [selectedTokenAccounts, setSelectedTokenAccounts];
}
export function useMarket() {
const context = useContext(MarketContext);
if (!context) {
throw new Error('Missing market context');
}
return context;
}
export function useMarkPrice() {
const [markPrice, setMarkPrice] = useState<null | number>(null);
const [orderbook] = useOrderbook();
const trades = useTrades();
useEffect(() => {
let bb = orderbook?.bids?.length > 0 && Number(orderbook.bids[0][0]);
let ba = orderbook?.asks?.length > 0 && Number(orderbook.asks[0][0]);
let last = trades && trades.length > 0 && trades[0].price;
let markPrice =
bb && ba
? last
? [bb, ba, last].sort((a, b) => a - b)[1]
: (bb + ba) / 2
: null;
setMarkPrice(markPrice);
}, [orderbook, trades]);
return markPrice;
}
export function _useUnfilteredTrades(limit = 10000) {
const { market } = useMarket();
const connection = useConnection();
async function getUnfilteredTrades(): Promise<any[] | null> {
if (!market || !connection) {
return null;
}
return await market.loadFills(connection, limit);
}
const [trades] = useAsyncData(
getUnfilteredTrades,
tuple('getUnfilteredTrades', market, connection),
{ refreshInterval: _SLOW_REFRESH_INTERVAL },
);
return trades;
// NOTE: For now, websocket is too expensive since the event queue is large
// and updates very frequently
// let data = useAccountData(market && market._decoded.eventQueue);
// if (!data) {
// return null;
// }
// const events = decodeEventQueue(data, limit);
// return events
// .filter((event) => event.eventFlags.fill && event.nativeQuantityPaid.gtn(0))
// .map(market.parseFillEvent.bind(market));
}
export function useBonfidaTrades() {
const { market } = useMarket();
const marketAddress = market?.address.toBase58();
async function getBonfidaTrades() {
if (!marketAddress) {
return null;
}
return await BonfidaApi.getRecentTrades(marketAddress);
}
return useAsyncData(
getBonfidaTrades,
tuple('getBonfidaTrades', marketAddress),
{ refreshInterval: _SLOW_REFRESH_INTERVAL },
false,
);
}
export function useOrderbookAccounts() {
const { market } = useMarket();
// @ts-ignore
let bidData = useAccountData(market && market._decoded.bids);
// @ts-ignore
let askData = useAccountData(market && market._decoded.asks);
return {
bidOrderbook: market && bidData ? Orderbook.decode(market, bidData) : null,
askOrderbook: market && askData ? Orderbook.decode(market, askData) : null,
};
}
export function useOrderbook(
depth = 20,
): [{ bids: number[][]; asks: number[][] }, boolean] {
const { bidOrderbook, askOrderbook } = useOrderbookAccounts();
const { market } = useMarket();
const bids =
!bidOrderbook || !market
? []
: bidOrderbook.getL2(depth).map(([price, size]) => [price, size]);
const asks =
!askOrderbook || !market
? []
: askOrderbook.getL2(depth).map(([price, size]) => [price, size]);
return [{ bids, asks }, !!bids || !!asks];
}
// Want the balances table to be fast-updating, dont want open orders to flicker
// TODO: Update to use websocket
export function useOpenOrdersAccounts(fast = false) {
const { market } = useMarket();
const { connected, publicKey, wallet } = useWallet();
const connection = useConnection();
async function getOpenOrdersAccounts() {
if (!connected || !publicKey || !wallet) {
return null;
}
if (!market) {
return null;
}
return await market.findOpenOrdersAccountsForOwner(connection, publicKey);
}
return useAsyncData<OpenOrders[] | null>(
getOpenOrdersAccounts,
tuple('getOpenOrdersAccounts', wallet, market, connected),
{ refreshInterval: fast ? _FAST_REFRESH_INTERVAL : _SLOW_REFRESH_INTERVAL },
);
}
export function useSelectedOpenOrdersAccount(fast = false) {
const [accounts] = useOpenOrdersAccounts(fast);
if (!accounts) {
return null;
}
return accounts[0];
}
export function useTokenAccounts(): [
TokenAccount[] | null | undefined,
boolean,
] {
const { connected, publicKey, wallet } = useWallet();
const connection = useConnection();
async function getTokenAccounts() {
if (!connected || !publicKey || !wallet) {
return null;
}
return await getTokenAccountInfo(connection, publicKey);
}
return useAsyncData(
getTokenAccounts,
tuple('getTokenAccounts', wallet, connected),
{ refreshInterval: _SLOW_REFRESH_INTERVAL },
);
}
export function getSelectedTokenAccountForMint(
accounts: TokenAccount[] | undefined | null,
mint: PublicKey | undefined,
selectedPubKey?: string | PublicKey | null,
) {
if (!accounts || !mint) {
return null;
}
const filtered = accounts.filter(
({ effectiveMint, pubkey }) =>
mint.equals(effectiveMint) &&
(!selectedPubKey ||
(typeof selectedPubKey === 'string'
? selectedPubKey
: selectedPubKey.toBase58()) === pubkey.toBase58()),
);
return filtered && filtered[0];
}
export function useSelectedQuoteCurrencyAccount() {
const [accounts] = useTokenAccounts();
const { market } = useMarket();
const [selectedTokenAccounts] = useSelectedTokenAccounts();
const mintAddress = market?.quoteMintAddress;
return getSelectedTokenAccountForMint(
accounts,
mintAddress,
mintAddress && selectedTokenAccounts[mintAddress.toBase58()],
);
}
export function useSelectedBaseCurrencyAccount() {
const [accounts] = useTokenAccounts();
const { market } = useMarket();
const [selectedTokenAccounts] = useSelectedTokenAccounts();
const mintAddress = market?.baseMintAddress;
return getSelectedTokenAccountForMint(
accounts,
mintAddress,
mintAddress && selectedTokenAccounts[mintAddress.toBase58()],
);
}
// TODO: Update to use websocket
export function useSelectedQuoteCurrencyBalances() {
const quoteCurrencyAccount = useSelectedQuoteCurrencyAccount();
const { market } = useMarket();
const [accountInfo, loaded] = useAccountInfo(quoteCurrencyAccount?.pubkey);
if (!market || !quoteCurrencyAccount || !loaded || !accountInfo) {
return null;
}
if (market.quoteMintAddress.equals(TokenInstructions.WRAPPED_SOL_MINT)) {
return accountInfo?.lamports / 1e9 ?? 0;
}
return market.quoteSplSizeToNumber(
new BN(accountInfo.data.slice(64, 72), 10, 'le'),
);
}
// TODO: Update to use websocket
export function useSelectedBaseCurrencyBalances() {
const baseCurrencyAccount = useSelectedBaseCurrencyAccount();
const { market } = useMarket();
const [accountInfo, loaded] = useAccountInfo(baseCurrencyAccount?.pubkey);
if (!market || !baseCurrencyAccount || !loaded || !accountInfo) {
return null;
}
if (market.baseMintAddress.equals(TokenInstructions.WRAPPED_SOL_MINT)) {
return accountInfo?.lamports / 1e9 ?? 0;
}
return market.baseSplSizeToNumber(
new BN(accountInfo.data.slice(64, 72), 10, 'le'),
);
}
export function useOpenOrders() {
const { market, marketName } = useMarket();
const openOrdersAccount = useSelectedOpenOrdersAccount();
const { bidOrderbook, askOrderbook } = useOrderbookAccounts();
if (!market || !openOrdersAccount || !bidOrderbook || !askOrderbook) {
return null;
}
return market
.filterForOpenOrders(bidOrderbook, askOrderbook, [openOrdersAccount])
.map((order) => ({ ...order, marketName, market }));
}
export function useTrades(limit = 100) {
const trades = _useUnfilteredTrades(limit);
if (!trades) {
return null;
}
// Until partial fills are each given their own fill, use maker fills
return trades
.filter(({ eventFlags }) => eventFlags.maker)
.map((trade) => ({
...trade,
side: trade.side === 'buy' ? 'sell' : 'buy',
}));
}
export function useLocallyStoredFeeDiscountKey(): {
storedFeeDiscountKey: PublicKey | undefined;
setStoredFeeDiscountKey: (key: string) => void;
} {
const [
storedFeeDiscountKey,
setStoredFeeDiscountKey,
] = useLocalStorageState<string>(`feeDiscountKey`, undefined);
return {
storedFeeDiscountKey: storedFeeDiscountKey
? new PublicKey(storedFeeDiscountKey)
: undefined,
setStoredFeeDiscountKey,
};
}
export function useFeeDiscountKeys(): [
(
| {
pubkey: PublicKey;
feeTier: number;
balance: number;
mint: PublicKey;
}[]
| null
| undefined
),
boolean,
] {
const { market } = useMarket();
const { connected, publicKey, wallet } = useWallet();
const connection = useConnection();
const { setStoredFeeDiscountKey } = useLocallyStoredFeeDiscountKey();
let getFeeDiscountKeys = async () => {
if (!connected || !publicKey || !wallet) {
return null;
}
if (!market) {
return null;
}
const feeDiscountKey = await market.findFeeDiscountKeys(
connection,
publicKey,
);
if (feeDiscountKey) {
setStoredFeeDiscountKey(feeDiscountKey[0].pubkey.toBase58());
}
return feeDiscountKey;
};
return useAsyncData(
getFeeDiscountKeys,
tuple('getFeeDiscountKeys', wallet, market, connected),
{ refreshInterval: _SLOW_REFRESH_INTERVAL },
);
}
export function useFills(limit = 100) {
const { marketName } = useMarket();
const fills = _useUnfilteredTrades(limit);
const [openOrdersAccounts] = useOpenOrdersAccounts();
if (!openOrdersAccounts || openOrdersAccounts.length === 0) {
return null;
}
if (!fills) {
return null;
}
return fills
.filter((fill) =>
openOrdersAccounts.some((openOrdersAccount) =>
fill.openOrders.equals(openOrdersAccount.publicKey),
),
)
.map((fill) => ({ ...fill, marketName }));
}
export function useAllOpenOrdersAccounts() {
const { connected, publicKey } = useWallet();
const connection = useConnection();
const marketInfos = useMarketInfos();
const programIds = [
...new Set(marketInfos.map((info) => info.programId.toBase58())),
].map((stringProgramId) => new PublicKey(stringProgramId));
const getAllOpenOrdersAccounts = async () => {
if (!connected || !publicKey) {
return [];
}
return (
await Promise.all(
programIds.map((programId) =>
OpenOrders.findForOwner(connection, publicKey, programId),
),
)
).flat();
};
return useAsyncData(
getAllOpenOrdersAccounts,
tuple(
'getAllOpenOrdersAccounts',
connection,
connected,
publicKey?.toBase58(),
marketInfos.length,
(programIds || []).length,
),
{ refreshInterval: _SLOW_REFRESH_INTERVAL },
);
}
export function useAllOpenOrdersBalances() {
const [
openOrdersAccounts,
loadedOpenOrdersAccounts,
] = useAllOpenOrdersAccounts();
const [mintInfos, mintInfosConnected] = useMintInfos();
const [allMarkets] = useAllMarkets();
if (!loadedOpenOrdersAccounts || !mintInfosConnected) {
return {};
}
const marketsByAddress = Object.fromEntries(
(allMarkets || []).map((m) => [m.market.address.toBase58(), m]),
);
const openOrdersBalances: {
[mint: string]: { market: PublicKey; free: number; total: number }[];
} = {};
for (let account of openOrdersAccounts || []) {
const marketInfo = marketsByAddress[account.market.toBase58()];
const baseMint = marketInfo?.market.baseMintAddress.toBase58();
const quoteMint = marketInfo?.market.quoteMintAddress.toBase58();
if (!(baseMint in openOrdersBalances)) {
openOrdersBalances[baseMint] = [];
}
if (!(quoteMint in openOrdersBalances)) {
openOrdersBalances[quoteMint] = [];
}
const baseMintInfo = mintInfos && mintInfos[baseMint];
const baseFree = divideBnToNumber(
new BN(account.baseTokenFree),
getTokenMultiplierFromDecimals(baseMintInfo?.decimals || 0),
);
const baseTotal = divideBnToNumber(
new BN(account.baseTokenTotal),
getTokenMultiplierFromDecimals(baseMintInfo?.decimals || 0),
);
const quoteMintInfo = mintInfos && mintInfos[quoteMint];
const quoteFree = divideBnToNumber(
new BN(account.quoteTokenFree),
getTokenMultiplierFromDecimals(quoteMintInfo?.decimals || 0),
);
const quoteTotal = divideBnToNumber(
new BN(account.quoteTokenTotal),
getTokenMultiplierFromDecimals(quoteMintInfo?.decimals || 0),
);
openOrdersBalances[baseMint].push({
market: account.market,
free: baseFree,
total: baseTotal,
});
openOrdersBalances[quoteMint].push({
market: account.market,
free: quoteFree,
total: quoteTotal,
});
}
return openOrdersBalances;
}
export const useAllOpenOrders = (): {
openOrders: { orders: Order[]; marketAddress: string }[] | null | undefined;
loaded: boolean;
refreshOpenOrders: () => void;
} => {
const connection = useConnection();
const { connected, publicKey } = useWallet();
const [loaded, setLoaded] = useState(false);
const [refresh, setRefresh] = useState(0);
const [openOrders, setOpenOrders] = useState<
{ orders: Order[]; marketAddress: string }[] | null | undefined
>(null);
const [lastRefresh, setLastRefresh] = useState(0);
const refreshOpenOrders = () => {
if (new Date().getTime() - lastRefresh > 10 * 1000) {
setRefresh((prev) => prev + 1);
} else {
console.log('not refreshing');
}
};
useEffect(() => {
if (connected && publicKey) {
const getAllOpenOrders = async () => {
setLoaded(false);
const _openOrders: { orders: Order[]; marketAddress: string }[] = [];
const getOpenOrdersForMarket = async (marketInfo: MarketInfo) => {
await sleep(1000 * Math.random()); // Try not to hit rate limit
try {
const market = await Market.load(
connection,
marketInfo.address,
undefined,
marketInfo.programId,
);
const orders = await market.loadOrdersForOwner(
connection,
publicKey,
30000,
);
_openOrders.push({
orders: orders,
marketAddress: marketInfo.address.toBase58(),
});
} catch (e) {
console.warn(`Error loading open order ${marketInfo.name} - ${e}`);
}
};
await Promise.all(USE_MARKETS.map((m) => getOpenOrdersForMarket(m)));
setOpenOrders(_openOrders);
setLastRefresh(new Date().getTime());
setLoaded(true);
};
getAllOpenOrders();
}
}, [connection, connected, refresh, publicKey]);
return {
openOrders: openOrders,
loaded: loaded,
refreshOpenOrders: refreshOpenOrders,
};
};
export function useBalances(): Balances[] {
const baseCurrencyBalances = useSelectedBaseCurrencyBalances();
const quoteCurrencyBalances = useSelectedQuoteCurrencyBalances();
const openOrders = useSelectedOpenOrdersAccount(true);
const { baseCurrency, quoteCurrency, market } = useMarket();
const baseExists =
openOrders && openOrders.baseTokenTotal && openOrders.baseTokenFree;
const quoteExists =
openOrders && openOrders.quoteTokenTotal && openOrders.quoteTokenFree;
if (
baseCurrency === 'UNKNOWN' ||
quoteCurrency === 'UNKNOWN' ||
!baseCurrency ||
!quoteCurrency
) {
return [];
}
return [
{
market,
key: `${baseCurrency}${quoteCurrency}${baseCurrency}`,
coin: baseCurrency,
wallet: baseCurrencyBalances,
orders:
baseExists && market && openOrders
? market.baseSplSizeToNumber(
openOrders.baseTokenTotal.sub(openOrders.baseTokenFree),
)
: null,
openOrders,
unsettled:
baseExists && market && openOrders
? market.baseSplSizeToNumber(openOrders.baseTokenFree)
: null,
},
{
market,
key: `${quoteCurrency}${baseCurrency}${quoteCurrency}`,
coin: quoteCurrency,
wallet: quoteCurrencyBalances,
openOrders,
orders:
quoteExists && market && openOrders
? market.quoteSplSizeToNumber(
openOrders.quoteTokenTotal.sub(openOrders.quoteTokenFree),
)
: null,
unsettled:
quoteExists && market && openOrders
? market.quoteSplSizeToNumber(openOrders.quoteTokenFree)
: null,
},
];
}
export function useWalletBalancesForAllMarkets(): {
mint: string;
balance: number;
}[] {
const [tokenAccounts] = useTokenAccounts();
const { connected } = useWallet();
const [mintInfos, mintInfosConnected] = useMintInfos();
if (!connected || !mintInfosConnected) {
return [];
}
let balances: { [mint: string]: number } = {};
for (let account of tokenAccounts || []) {
if (!account.account) {
continue;
}
let parsedAccount;
if (account.effectiveMint.equals(WRAPPED_SOL_MINT)) {
parsedAccount = {
mint: WRAPPED_SOL_MINT,
owner: account.pubkey,
amount: account.account.lamports,
};
} else {
parsedAccount = parseTokenAccountData(account.account.data);
}
if (!(parsedAccount.mint.toBase58() in balances)) {
balances[parsedAccount.mint.toBase58()] = 0;
}
const mintInfo = mintInfos && mintInfos[parsedAccount.mint.toBase58()];
const additionalAmount = divideBnToNumber(
new BN(parsedAccount.amount),
getTokenMultiplierFromDecimals(mintInfo?.decimals || 0),
);
balances[parsedAccount.mint.toBase58()] += additionalAmount;
}
return Object.entries(balances).map(([mint, balance]) => {
return { mint, balance };
});
}
export function useUnmigratedDeprecatedMarkets() {
const connection = useConnection();
const { accounts } = useUnmigratedOpenOrdersAccounts();
const marketsList =
accounts &&
Array.from(new Set(accounts.map((openOrders) => openOrders.market)));
const deps = marketsList && marketsList.map((m) => m.toBase58());
const useUnmigratedDeprecatedMarketsInner = async () => {
if (!marketsList) {
return null;
}
const getMarket = async (address) => {
const marketInfo = USE_MARKETS.find((market) =>
market.address.equals(address),
);
if (!marketInfo) {
console.log('Failed loading market');
notify({
message: 'Error loading market',
type: 'error',
});
return null;
}
try {
console.log('Loading market', marketInfo.name);
// NOTE: Should this just be cached by (connection, marketInfo.address, marketInfo.programId)?
return await Market.load(
connection,
marketInfo.address,
{},
marketInfo.programId,
);
} catch (e) {
console.log('Failed loading market', marketInfo.name, e);
notify({
message: 'Error loading market',
description: e.message,
type: 'error',
});
return null;
}
};
return (await Promise.all(marketsList.map(getMarket))).filter((x) => x);
};
const [markets] = useAsyncData(
useUnmigratedDeprecatedMarketsInner,
tuple(
'useUnmigratedDeprecatedMarketsInner',
connection,
deps && deps.toString(),
),
{ refreshInterval: _VERY_SLOW_REFRESH_INTERVAL },
);
if (!markets) {
return null;
}
return markets.map((market) => ({
market,
openOrdersList: accounts?.filter(
(openOrders) => market && openOrders.market.equals(market.address),
),
}));
}
export function useGetOpenOrdersForDeprecatedMarkets(): {
openOrders: OrderWithMarketAndMarketName[] | null | undefined;
loaded: boolean;
refreshOpenOrders: () => void;
} {
const { connected, publicKey, wallet } = useWallet();
const { customMarkets } = useCustomMarkets();
const connection = useConnection();
const marketsAndOrders = useUnmigratedDeprecatedMarkets();
const marketsList =
marketsAndOrders && marketsAndOrders.map(({ market }) => market);
// This isn't quite right: open order balances could change
const deps =
marketsList &&
marketsList
.filter((market): market is Market => !!market)
.map((market) => market.address.toBase58());
async function getOpenOrdersForDeprecatedMarkets() {
if (!connected || !publicKey) {
return null;
}
if (!marketsList) {
return null;
}
console.log('refreshing getOpenOrdersForDeprecatedMarkets');
const getOrders = async (market: Market | null) => {
if (!market) {
return null;
}
const { marketName } = getMarketDetails(market, customMarkets);
try {
console.log('Fetching open orders for', marketName);
// Can do better than this, we have the open orders accounts already
return (
await market.loadOrdersForOwner(connection, publicKey)
).map((order) => ({ marketName, market, ...order }));
} catch (e) {
console.log('Failed loading open orders', market.address.toBase58(), e);
notify({
message: `Error loading open orders for deprecated ${marketName}`,
description: e.message,
type: 'error',
});
return null;
}
};
return (await Promise.all(marketsList.map(getOrders)))
.filter((x): x is OrderWithMarketAndMarketName[] => !!x)
.flat();
}
const cacheKey = tuple(
'getOpenOrdersForDeprecatedMarkets',
connected,
connection,
wallet,
deps && deps.toString(),
);
const [openOrders, loaded] = useAsyncData(
getOpenOrdersForDeprecatedMarkets,
cacheKey,
{
refreshInterval: _VERY_SLOW_REFRESH_INTERVAL,
},
);
console.log('openOrders', openOrders);
return {
openOrders,
loaded,
refreshOpenOrders: () => refreshCache(cacheKey),
};
}
export function useBalancesForDeprecatedMarkets() {
const markets = useUnmigratedDeprecatedMarkets();
const [customMarkets] = useLocalStorageState<CustomMarketInfo[]>(
'customMarkets',
[],
);
if (!markets) {
return null;
}
const openOrderAccountBalances: DeprecatedOpenOrdersBalances[] = [];
markets.forEach(({ market, openOrdersList }) => {
const { baseCurrency, quoteCurrency, marketName } = getMarketDetails(
market,
customMarkets,
);
if (!baseCurrency || !quoteCurrency || !market) {
return;
}
(openOrdersList || []).forEach((openOrders) => {
const inOrdersBase =
openOrders?.baseTokenTotal &&
openOrders?.baseTokenFree &&
market.baseSplSizeToNumber(
openOrders.baseTokenTotal.sub(openOrders.baseTokenFree),
);
const inOrdersQuote =
openOrders?.quoteTokenTotal &&
openOrders?.quoteTokenFree &&
market.baseSplSizeToNumber(
openOrders.quoteTokenTotal.sub(openOrders.quoteTokenFree),
);
const unsettledBase =
openOrders?.baseTokenFree &&
market.baseSplSizeToNumber(openOrders.baseTokenFree);
const unsettledQuote =
openOrders?.quoteTokenFree &&
market.baseSplSizeToNumber(openOrders.quoteTokenFree);
openOrderAccountBalances.push({
marketName,
market,
coin: baseCurrency,
key: `${marketName}${baseCurrency}`,
orders: inOrdersBase,
unsettled: unsettledBase,
openOrders,
});
openOrderAccountBalances.push({
marketName,
market,
coin: quoteCurrency,
key: `${marketName}${quoteCurrency}`,
orders: inOrdersQuote,
unsettled: unsettledQuote,
openOrders,
});
});
});
return openOrderAccountBalances;
}
export function getMarketInfos(
customMarkets: CustomMarketInfo[],
): MarketInfo[] {
const customMarketsInfo = customMarkets.map((m) => ({
...m,
address: new PublicKey(m.address),
programId: new PublicKey(m.programId),
deprecated: false,
}));
return [...customMarketsInfo, ...USE_MARKETS];
}
export function useMarketInfos() {
const { customMarkets } = useCustomMarkets();
return getMarketInfos(customMarkets);
}
/**
* If selling, choose min tick size. If buying choose a price
* s.t. given the state of the orderbook, the order will spend
* `cost` cost currency.
*
* @param orderbook serum Orderbook object
* @param cost quantity to spend. Base currency if selling,
* quote currency if buying.
* @param tickSizeDecimals size of price increment of the market
*/
export function getMarketOrderPrice(
orderbook: Orderbook,
cost: number,
tickSizeDecimals?: number,
) {
if (orderbook.isBids) {
return orderbook.market.tickSize;
}
let spentCost = 0;
let price, sizeAtLevel, costAtLevel: number;
const asks = orderbook.getL2(1000);
for ([price, sizeAtLevel] of asks) {
costAtLevel = price * sizeAtLevel;
if (spentCost + costAtLevel > cost) {
break;
}
spentCost += costAtLevel;
}
const sendPrice = Math.min(price * 1.02, asks[0][0] * 1.05);
let formattedPrice;
if (tickSizeDecimals) {
formattedPrice = floorToDecimal(sendPrice, tickSizeDecimals);
} else {
formattedPrice = sendPrice;
}
return formattedPrice;
}
export function getExpectedFillPrice(
orderbook: Orderbook,
cost: number,
tickSizeDecimals?: number,
) {
let spentCost = 0;
let avgPrice = 0;
let price, sizeAtLevel, costAtLevel: number;
for ([price, sizeAtLevel] of orderbook.getL2(1000)) {
costAtLevel = (orderbook.isBids ? 1 : price) * sizeAtLevel;
if (spentCost + costAtLevel > cost) {
avgPrice += (cost - spentCost) * price;
spentCost = cost;
break;
}
avgPrice += costAtLevel * price;
spentCost += costAtLevel;
}
const totalAvgPrice = avgPrice / Math.min(cost, spentCost);
let formattedPrice;
if (tickSizeDecimals) {
formattedPrice = floorToDecimal(totalAvgPrice, tickSizeDecimals);
} else {
formattedPrice = totalAvgPrice;
}
return formattedPrice;
}
export function useCurrentlyAutoSettling(): [
boolean,
(currentlyAutoSettling: boolean) => void,
] {
const [currentlyAutoSettling, setCurrentlyAutosettling] = useState<boolean>(
false,
);
return [currentlyAutoSettling, setCurrentlyAutosettling];
}