import create, { GetState, SetState, Mutate, StoreApi } from 'zustand' import { subscribeWithSelector } from 'zustand/middleware' import produce from 'immer' import { Market } from '@project-serum/serum' import { IDS, Config, MangoClient, MangoGroup, MangoAccount, MarketConfig, getMarketByBaseSymbolAndKind, GroupConfig, TokenConfig, getTokenAccountsByOwnerWithWrappedSol, getTokenByMint, TokenAccount, nativeToUi, MangoCache, PerpMarket, getAllMarkets, getMultipleAccounts, PerpMarketLayout, msrmMints, MangoAccountLayout, BlockhashTimes, MarketMode, } from '@blockworks-foundation/mango-client' import { AccountInfo, Commitment, Connection, PublicKey } from '@solana/web3.js' import { EndpointInfo } from '../@types/types' import { isDefined, zipDict } from '../utils' import { Notification, notify } from '../utils/notifications' import { LAST_ACCOUNT_KEY } from '../components/AccountsModal' import { DEFAULT_MARKET_KEY, initialMarket, NODE_URL_KEY, } from '../components/SettingsModal' import { MSRM_DECIMALS } from '@project-serum/serum/lib/token-instructions' import { getProfilePicture, ProfilePicture } from '@solflare-wallet/pfp' import { decodeBook } from '../hooks/useHydrateStore' import { IOrderLineAdapter } from '../public/charting_library/charting_library' import { Wallet } from '@solana/wallet-adapter-react' import { coingeckoIds } from 'utils/tokens' import { getTokenAccountsByMint } from 'utils/tokens' import { getParsedNftAccountsByOwner } from 'utils/getParsedNftAccountsByOwner' export const ENDPOINTS: EndpointInfo[] = [ { name: 'mainnet', url: process.env.NEXT_PUBLIC_ENDPOINT || 'https://mango.rpcpool.com', websocket: process.env.NEXT_PUBLIC_ENDPOINT || 'https://mango.rpcpool.com', custom: false, }, { name: 'devnet', // url: 'https://mango.devnet.rpcpool.com', // websocket: 'https://mango.devnet.rpcpool.com', url: 'https://api.devnet.solana.com', websocket: 'https://api.devnet.solana.com', custom: false, }, ] type ClusterType = 'mainnet' | 'devnet' const DEFAULT_MANGO_GROUP_NAME = process.env.NEXT_PUBLIC_GROUP || 'mainnet.1' export const CLUSTER = DEFAULT_MANGO_GROUP_NAME.split('.')[0] as ClusterType const ENDPOINT = ENDPOINTS.find((e) => e.name === CLUSTER) as EndpointInfo export const WEBSOCKET_CONNECTION = new Connection( ENDPOINT.websocket, 'processed' as Commitment ) export const DEFAULT_MANGO_GROUP_CONFIG = Config.ids().getGroup( CLUSTER, DEFAULT_MANGO_GROUP_NAME ) as GroupConfig const defaultMangoGroupIds = IDS['groups'].find( (group) => group.name === DEFAULT_MANGO_GROUP_NAME ) export const MNGO_INDEX = defaultMangoGroupIds!.oracles.findIndex( (t) => t.symbol === 'MNGO' ) export const programId = new PublicKey(defaultMangoGroupIds!.mangoProgramId) export const serumProgramId = new PublicKey( defaultMangoGroupIds!.serumProgramId ) const mangoGroupPk = new PublicKey(defaultMangoGroupIds!.publicKey) export const SECONDS = 1000 export const CLIENT_TX_TIMEOUT = 90000 // Used to retry loading the MangoGroup and MangoAccount if an rpc node error occurs let mangoGroupRetryAttempt = 0 let mangoAccountRetryAttempt = 0 // an object with keys of Solana account addresses that we are // subscribing to with connection.onAccountChange() in the // useHydrateStore hook interface AccountInfoList { [key: string]: AccountInfo } export interface WalletToken { account: TokenAccount config: TokenConfig uiBalance: number } export interface Orderbook { bids: number[][] asks: number[][] } export interface Alert { acc: PublicKey alertProvider: 'mail' health: number _id: string open: boolean timestamp: number triggeredTimestamp: number | undefined } export interface AlertRequest { alertProvider: 'mail' health: number mangoGroupPk: string mangoAccountPk: string email: string | undefined } interface NFTFiles { type: string uri: string } interface NFTProperties { category: string files: NFTFiles[] } interface NFTData { image: string name: string description: string properties: NFTProperties collection: { family: string name: string } } interface NFTWithMint { val: NFTData mint: string tokenAddress: string } export type MangoStore = { notificationIdCounter: number notifications: Array accountInfos: AccountInfoList connection: { cluster: ClusterType current: Connection websocket: Connection endpoint: string client: MangoClient slot: number blockhashTimes: BlockhashTimes[] } selectedMarket: { config: MarketConfig current: Market | PerpMarket | null markPrice: number kind: string orderBook: Orderbook fills: any[] } mangoGroups: Array selectedMangoGroup: { config: GroupConfig name: string current: MangoGroup | null markets: { [address: string]: Market | PerpMarket | undefined } cache: MangoCache | null } mangoAccounts: MangoAccount[] referrals: { total: number history: any[] } referrerPk: PublicKey | null selectedMangoAccount: { current: MangoAccount | null initialLoad: boolean lastUpdatedAt: string lastSlot: number openOrders: any[] totalOpenOrders: number perpAccounts: any[] openPerpPositions: any[] totalOpenPerpPositions: number unsettledPerpPositions: any[] } tradeForm: { side: 'buy' | 'sell' price: number | '' baseSize: number | '' quoteSize: number | '' tradeType: | 'Market' | 'Limit' | 'Stop Loss' | 'Take Profit' | 'Stop Limit' | 'Take Profit Limit' triggerPrice: number | '' triggerCondition: 'above' | 'below' } wallet: { tokens: WalletToken[] | any[] pfp: ProfilePicture | undefined nfts: { data: NFTWithMint[] | [] initialLoad: boolean loading: boolean accounts: any[] loadingTransaction: boolean } } settings: { uiLocked: boolean } tradeHistory: { initialLoad: boolean spot: any[] perp: any[] parsed: any[] } set: (x: (x: MangoStore) => void) => void actions: { fetchWalletTokens: (wallet: Wallet) => void fetchProfilePicture: (wallet: Wallet) => void fetchNfts: ( connection: Connection, walletPk: PublicKey | null, offset?: number ) => void fetchNftAccounts: (connection: Connection, walletPk: PublicKey) => void fetchAllMangoAccounts: (wallet: Wallet) => Promise fetchMangoGroup: () => Promise fetchTradeHistory: () => void reloadMangoAccount: () => void reloadOrders: () => void updateOpenOrders: () => void loadMarketFills: () => void loadReferralData: () => void fetchMangoGroupCache: () => void updateConnection: (url: string) => void createAlert: (alert: AlertRequest) => void deleteAlert: (id: string) => void loadAlerts: (pk: PublicKey) => void fetchMarketsInfo: () => void fetchCoingeckoPrices: () => void } alerts: { activeAlerts: Array triggeredAlerts: Array loading: boolean error: string submitting: boolean success: string } marketsInfo: any[] tradingView: { orderLines: Map } coingeckoPrices: { data: any[]; loading: boolean } } const useMangoStore = create< MangoStore, SetState, GetState, Mutate, [['zustand/subscribeWithSelector', never]]> >( subscribeWithSelector((set, get) => { let rpcUrl = ENDPOINT?.url if (typeof window !== 'undefined' && CLUSTER === 'mainnet') { const urlFromLocalStorage = localStorage.getItem(NODE_URL_KEY) rpcUrl = urlFromLocalStorage ? JSON.parse(urlFromLocalStorage) : ENDPOINT?.url } let defaultMarket = initialMarket if (typeof window !== 'undefined') { const marketFromLocalStorage = localStorage.getItem(DEFAULT_MARKET_KEY) defaultMarket = marketFromLocalStorage ? JSON.parse(marketFromLocalStorage) : initialMarket } const connection = new Connection(rpcUrl, 'processed' as Commitment) const client = new MangoClient(connection, programId, { timeout: CLIENT_TX_TIMEOUT, prioritizationFee: 2, postSendTxCallback: ({ txid }: { txid: string }) => { notify({ title: 'Transaction sent', description: 'Waiting for confirmation', type: 'confirm', txid: txid, }) }, blockhashCommitment: 'confirmed', }) return { marketsInfo: [], notificationIdCounter: 0, notifications: [], accountInfos: {}, connection: { cluster: CLUSTER, current: connection, websocket: WEBSOCKET_CONNECTION, client, endpoint: ENDPOINT.url, slot: 0, blockhashTimes: [], }, selectedMangoGroup: { config: DEFAULT_MANGO_GROUP_CONFIG, name: DEFAULT_MANGO_GROUP_NAME, current: null, markets: {}, rootBanks: [], cache: null, }, selectedMarket: { config: getMarketByBaseSymbolAndKind( DEFAULT_MANGO_GROUP_CONFIG, defaultMarket.base, defaultMarket.kind ) as MarketConfig, kind: defaultMarket.kind, current: null, markPrice: 0, orderBook: { bids: [], asks: [] }, fills: [], }, mangoGroups: [], mangoAccounts: [], referrals: { total: 0, history: [], }, referrerPk: null, selectedMangoAccount: { current: null, initialLoad: true, lastUpdatedAt: '0', lastSlot: 0, openOrders: [], totalOpenOrders: 0, perpAccounts: [], openPerpPositions: [], totalOpenPerpPositions: 0, unsettledPerpPositions: [], }, tradeForm: { side: 'buy', baseSize: '', quoteSize: '', tradeType: 'Limit', price: '', triggerPrice: '', triggerCondition: 'above', }, wallet: { tokens: [], pfp: undefined, nfts: { data: [], initialLoad: false, loading: false, accounts: [], loadingTransaction: false, }, }, settings: { uiLocked: true, }, alerts: { activeAlerts: [], triggeredAlerts: [], loading: false, error: '', submitting: false, success: '', }, tradeHistory: { initialLoad: false, spot: [], perp: [], parsed: [], }, tradingView: { orderLines: new Map(), }, coingeckoPrices: { data: [], loading: false }, set: (fn) => set(produce(fn)), actions: { async fetchWalletTokens(wallet: Wallet) { const groupConfig = get().selectedMangoGroup.config const connected = wallet?.adapter?.connected const connection = get().connection.current const cluster = get().connection.cluster const set = get().set if (wallet?.adapter?.publicKey && connected) { const ownedTokenAccounts = await getTokenAccountsByOwnerWithWrappedSol( connection, wallet.adapter.publicKey ) const tokens: any = [] ownedTokenAccounts?.forEach((account) => { const config = getTokenByMint(groupConfig, account.mint) if (config) { const uiBalance = nativeToUi(account.amount, config.decimals) tokens.push({ account, config, uiBalance }) } else if (msrmMints[cluster].equals(account.mint)) { const uiBalance = nativeToUi(account.amount, 6) tokens.push({ account, config: { symbol: 'MSRM', mintKey: msrmMints[cluster], decimals: MSRM_DECIMALS, }, uiBalance, }) } }) set((state) => { state.wallet.tokens = tokens }) } else { set((state) => { state.wallet.tokens = [] }) } }, async fetchProfilePicture(wallet: Wallet) { const set = get().set const walletPk = wallet?.adapter?.publicKey const connection = get().connection.current if (!walletPk) return try { const result = await getProfilePicture(connection, walletPk) set((state) => { state.wallet.pfp = result }) } catch (e) { console.log('Could not get profile picture', e) } }, async fetchNftAccounts(connection: Connection, walletPk: PublicKey) { const set = get().set try { const nfts = await getParsedNftAccountsByOwner({ publicAddress: walletPk.toBase58(), connection: connection, }) const data = Object.keys(nfts) .map((key) => nfts[key]) .filter((data) => data.primarySaleHappened) set((state) => { state.wallet.nfts.accounts = data }) } catch (error) { console.log(error) } }, async fetchNfts( connection: Connection, walletPk: PublicKey, offset = 0 ) { const set = get().set set((state) => { state.wallet.nfts.loading = true }) try { const nftAccounts = get().wallet.nfts.accounts const loadedNfts = get().wallet.nfts.data const arr: NFTWithMint[] = [] if (loadedNfts.length < nftAccounts.length) { for (let i = offset; i < offset + 9; i++) { try { const [nftAccountsResp, tokenAcctsByMint] = await Promise.all( [ fetch(nftAccounts[i].data.uri), getTokenAccountsByMint(connection, nftAccounts[i].mint), ] ) const val = await nftAccountsResp.json() arr.push({ val, mint: nftAccounts[i].mint, tokenAddress: tokenAcctsByMint .find( (x) => x.account.owner.toBase58() === walletPk.toBase58() )! .publicKey.toBase58(), }) } catch (e) { console.log(e) } } } if (loadedNfts.length === 0) { set((state) => { state.wallet.nfts.data = arr state.wallet.nfts.initialLoad = true state.wallet.nfts.loading = false }) } else { set((state) => { state.wallet.nfts.data = [...loadedNfts, ...arr] state.wallet.nfts.loading = false }) } } catch (error) { notify({ title: 'Unable to fetch nfts', description: '', }) set((state) => { state.wallet.nfts.loading = false }) } }, async fetchAllMangoAccounts(wallet) { const set = get().set const mangoGroup = get().selectedMangoGroup.current const mangoClient = get().connection.client const actions = get().actions if (!wallet?.adapter?.publicKey || !mangoGroup) return const delegateFilter = [ { memcmp: { offset: MangoAccountLayout.offsetOf('delegate'), bytes: wallet.adapter.publicKey?.toBase58(), }, }, ] const accountSorter = (a, b) => a.publicKey.toBase58() > b.publicKey.toBase58() ? 1 : -1 return Promise.all([ mangoClient.getMangoAccountsForOwner( mangoGroup, wallet.adapter.publicKey, true ), mangoClient.getAllMangoAccounts(mangoGroup, delegateFilter, false), ]) .then((values) => { const [mangoAccounts, delegatedAccounts] = values if (mangoAccounts.length + delegatedAccounts.length > 0) { const sortedAccounts = mangoAccounts .slice() .sort(accountSorter) .concat(delegatedAccounts.sort(accountSorter)) set((state) => { state.selectedMangoAccount.initialLoad = false state.mangoAccounts = sortedAccounts if (!state.selectedMangoAccount.current) { const lastAccount = localStorage.getItem(LAST_ACCOUNT_KEY) let currentAcct = sortedAccounts[0] if (lastAccount) { currentAcct = mangoAccounts.find( (ma) => ma.publicKey.toString() === JSON.parse(lastAccount) ) || sortedAccounts[0] } state.selectedMangoAccount.current = currentAcct } }) } else { set((state) => { state.selectedMangoAccount.initialLoad = false }) } }) .catch((err) => { if (mangoAccountRetryAttempt < 2) { actions.fetchAllMangoAccounts(wallet) mangoAccountRetryAttempt++ } else { notify({ type: 'error', title: 'Unable to load mango account', description: err.message, }) console.log('Could not get margin accounts for wallet', err) } }) }, async fetchMangoGroup() { const set = get().set const mangoGroupConfig = get().selectedMangoGroup.config const selectedMarketConfig = get().selectedMarket.config const mangoClient = get().connection.client const connection = get().connection.current const actions = get().actions return mangoClient .getMangoGroup(mangoGroupPk) .then(async (mangoGroup) => { mangoGroup.loadCache(connection).then((mangoCache) => { set((state) => { state.selectedMangoGroup.cache = mangoCache }) }) mangoGroup.loadRootBanks(connection).then(() => { set((state) => { state.selectedMangoGroup.current = mangoGroup }) }) const allMarketConfigs = getAllMarkets(mangoGroupConfig) const allMarketPks = allMarketConfigs.map((m) => m.publicKey) const allBidsAndAsksPks = allMarketConfigs .map((m) => [m.bidsKey, m.asksKey]) .flat() let allMarketAccountInfos, allBidsAndAsksAccountInfos try { const resp = await Promise.all([ getMultipleAccounts(connection, allMarketPks), getMultipleAccounts(connection, allBidsAndAsksPks), ]) allMarketAccountInfos = resp[0] allBidsAndAsksAccountInfos = resp[1] } catch { notify({ type: 'error', title: 'Failed to load the mango group. Please refresh.', }) } const allMarketAccounts = allMarketConfigs.map((config, i) => { if (config.kind == 'spot') { const decoded = Market.getLayout(programId).decode( allMarketAccountInfos[i].accountInfo.data ) return new Market( decoded, config.baseDecimals, config.quoteDecimals, undefined, mangoGroupConfig.serumProgramId ) } if (config.kind == 'perp') { const decoded = PerpMarketLayout.decode( allMarketAccountInfos[i].accountInfo.data ) return new PerpMarket( config.publicKey, config.baseDecimals, config.quoteDecimals, decoded ) } }) const allMarkets = zipDict( allMarketPks.map((pk) => pk.toBase58()), allMarketAccounts ) const currentSelectedMarket = allMarketAccounts.find((mkt) => mkt?.publicKey.equals(selectedMarketConfig.publicKey) ) set((state) => { state.selectedMangoGroup.markets = allMarkets state.selectedMarket.current = currentSelectedMarket ? currentSelectedMarket : null allBidsAndAsksAccountInfos.forEach( ({ publicKey, context, accountInfo }) => { if (context.slot >= state.connection.slot) { state.connection.slot = context.slot const perpMarket = allMarketAccounts.find((mkt) => { if (mkt instanceof PerpMarket) { return ( mkt.bids.equals(publicKey) || mkt.asks.equals(publicKey) ) } }) if (perpMarket) { accountInfo['parsed'] = decodeBook( perpMarket, accountInfo ) } state.accountInfos[publicKey.toBase58()] = accountInfo } } ) }) }) .catch((err) => { if (mangoGroupRetryAttempt < 2) { actions.fetchMangoGroup() mangoGroupRetryAttempt++ } else { notify({ title: 'Failed to load mango group. Please refresh', description: `${err}`, type: 'error', }) console.log('Could not get mango group: ', err) } }) }, async fetchTradeHistory() { const selectedMangoAccount = get().selectedMangoAccount.current const set = get().set if (!selectedMangoAccount) return fetch( `https://trade-history-api-v3.onrender.com/perp_trades/${selectedMangoAccount.publicKey.toString()}` ) .then((response) => response.json()) .then((jsonPerpHistory) => { const perpHistory = jsonPerpHistory?.data || [] if (perpHistory.length === 5000) { fetch( `https://trade-history-api-v3.onrender.com/perp_trades/${selectedMangoAccount.publicKey.toString()}?page=2` ) .then((response) => response.json()) .then((jsonPerpHistory) => { const perpHistory2 = jsonPerpHistory?.data || [] set((state) => { state.tradeHistory.perp = perpHistory.concat(perpHistory2) }) }) .catch((e) => { console.error('Error fetching trade history', e) }) } else { set((state) => { state.tradeHistory.perp = perpHistory }) } }) .catch((e) => { console.error('Error fetching trade history', e) }) if (selectedMangoAccount.spotOpenOrdersAccounts.length) { const openOrdersAccounts = selectedMangoAccount.spotOpenOrdersAccounts.filter(isDefined) const publicKeys = openOrdersAccounts.map((act) => act.publicKey.toString() ) Promise.all( publicKeys.map(async (pk) => { const response = await fetch( `https://trade-history-api-v3.onrender.com/trades/open_orders/${pk.toString()}` ) const parsedResponse = await response.json() return parsedResponse?.data ? parsedResponse.data : [] }) ) .then((serumTradeHistory) => { set((state) => { state.tradeHistory.spot = serumTradeHistory }) }) .catch((e) => { console.error('Error fetching trade history', e) }) } set((state) => { state.tradeHistory.initialLoad = true }) }, async reloadMangoAccount() { const set = get().set const mangoAccount = get().selectedMangoAccount.current const connection = get().connection.current const mangoClient = get().connection.client if (!mangoAccount) return const [reloadedMangoAccount, lastSlot] = await mangoAccount.reloadFromSlot(connection, mangoClient.lastSlot) const lastSeenSlot = get().selectedMangoAccount.lastSlot if (lastSlot > lastSeenSlot) { set((state) => { state.selectedMangoAccount.current = reloadedMangoAccount state.selectedMangoAccount.lastUpdatedAt = new Date().toISOString() state.selectedMangoAccount.lastSlot = lastSlot }) } }, async reloadOrders() { const set = get().set const mangoAccount = get().selectedMangoAccount.current const connection = get().connection.current if (mangoAccount) { const [spotOpenOrdersAccounts, advancedOrders] = await Promise.all([ mangoAccount.loadOpenOrders( connection, new PublicKey(serumProgramId) ), mangoAccount.loadAdvancedOrders(connection), ]) mangoAccount.spotOpenOrdersAccounts = spotOpenOrdersAccounts mangoAccount.advancedOrders = advancedOrders set((state) => { state.selectedMangoAccount.current = mangoAccount state.selectedMangoAccount.lastUpdatedAt = new Date().toISOString() }) } }, async updateOpenOrders() { const set = get().set const connection = get().connection.current const bidAskAccounts = Object.keys(get().accountInfos).map( (pk) => new PublicKey(pk) ) const markets = Object.values( useMangoStore.getState().selectedMangoGroup.markets ) const allBidsAndAsksAccountInfos = await getMultipleAccounts( connection, bidAskAccounts ) set((state) => { allBidsAndAsksAccountInfos.forEach( ({ publicKey, context, accountInfo }) => { state.connection.slot = context.slot const perpMarket = markets.find((mkt) => { if (mkt instanceof PerpMarket) { return ( mkt.bids.equals(publicKey) || mkt.asks.equals(publicKey) ) } }) if (perpMarket) { accountInfo['parsed'] = decodeBook(perpMarket, accountInfo) } state.accountInfos[publicKey.toBase58()] = accountInfo } ) }) }, async loadMarketFills() { const set = get().set const selectedMarket = get().selectedMarket.current const connection = get().connection.current if (!selectedMarket) { return null } try { const loadedFills = await selectedMarket.loadFills( connection, 10000 ) set((state) => { state.selectedMarket.fills = loadedFills }) } catch (err) { console.log('Error fetching fills:', err) } }, async loadReferralData() { const set = get().set const mangoAccount = get().selectedMangoAccount.current const pk = mangoAccount?.publicKey.toString() if (!pk) { return } const getData = async (type: 'history' | 'total') => { const res = await fetch( `https://mango-transaction-log.herokuapp.com/v3/stats/referral-fees-${type}?referrer-account=${pk}` ) const data = type === 'history' ? await res.json() : await res.text() return data } const data = await getData('history') const totalBalance = await getData('total') set((state) => { state.referrals.total = parseFloat(totalBalance) state.referrals.history = data }) }, async fetchMangoGroupCache() { const set = get().set const mangoGroup = get().selectedMangoGroup.current const connection = get().connection.current if (mangoGroup) { try { const mangoCache = await mangoGroup.loadCache(connection) set((state) => { state.selectedMangoGroup.cache = mangoCache }) } catch (e) { console.warn('Error fetching mango group cache:', e) } } }, updateConnection(endpointUrl) { const set = get().set const newConnection = new Connection(endpointUrl, 'processed') const newClient = new MangoClient(newConnection, programId) set((state) => { state.connection.endpoint = endpointUrl state.connection.current = newConnection state.connection.client = newClient }) }, async createAlert(req: AlertRequest) { const set = get().set const alert = { acc: new PublicKey(req.mangoAccountPk), alertProvider: req.alertProvider, health: req.health, open: true, timestamp: Date.now(), } set((state) => { state.alerts.submitting = true state.alerts.error = '' state.alerts.success = '' }) const mangoAccount = get().selectedMangoAccount.current const mangoGroup = get().selectedMangoGroup.current const mangoCache = get().selectedMangoGroup.cache if (!mangoGroup || !mangoAccount || !mangoCache) return const currentAccHealth = await mangoAccount.getHealthRatio( mangoGroup, mangoCache, 'Maint' ) if (currentAccHealth && currentAccHealth.toNumber() <= req.health) { set((state) => { state.alerts.submitting = false state.alerts.error = `Current account health is already below ${req.health}%` }) return false } const fetchUrl = `https://mango-alerts-v3.herokuapp.com/alerts` const headers = { 'Content-Type': 'application/json' } const response = await fetch(fetchUrl, { method: 'POST', headers: headers, body: JSON.stringify(req), }) if (response.ok) { const alerts = get().alerts.activeAlerts set((state) => { state.alerts.activeAlerts = [alert as Alert].concat(alerts) state.alerts.submitting = false state.alerts.success = 'Alert saved' }) notify({ title: 'Alert saved', type: 'success', }) return true } else { set((state) => { state.alerts.error = 'Something went wrong' state.alerts.submitting = false }) notify({ title: 'Something went wrong', type: 'error', }) return false } }, async deleteAlert(id: string) { const set = get().set set((state) => { state.alerts.submitting = true state.alerts.error = '' state.alerts.success = '' }) const fetchUrl = `https://mango-alerts-v3.herokuapp.com/delete-alert` const headers = { 'Content-Type': 'application/json' } const response = await fetch(fetchUrl, { method: 'POST', headers: headers, body: JSON.stringify({ id }), }) if (response.ok) { const alerts = get().alerts.activeAlerts set((state) => { state.alerts.activeAlerts = alerts.filter( (alert) => alert._id !== id ) state.alerts.submitting = false state.alerts.success = 'Alert deleted' }) notify({ title: 'Alert deleted', type: 'success', }) } else { set((state) => { state.alerts.error = 'Something went wrong' state.alerts.submitting = false }) notify({ title: 'Something went wrong', type: 'error', }) } }, async loadAlerts(mangoAccountPk: PublicKey) { const set = get().set set((state) => { state.alerts.error = '' state.alerts.loading = true }) const headers = { 'Content-Type': 'application/json' } const response = await fetch( `https://mango-alerts-v3.herokuapp.com/alerts/${mangoAccountPk}`, { method: 'GET', headers: headers, } ) if (response.ok) { const parsedResponse = await response.json() // sort active by latest creation time first const activeAlerts = parsedResponse.alerts .filter((alert) => alert.open) .sort((a, b) => { return b.timestamp - a.timestamp }) // sort triggered by latest trigger time first const triggeredAlerts = parsedResponse.alerts .filter((alert) => !alert.open) .sort((a, b) => { return b.triggeredTimestamp - a.triggeredTimestamp }) set((state) => { state.alerts.activeAlerts = activeAlerts state.alerts.triggeredAlerts = triggeredAlerts state.alerts.loading = false }) } else { set((state) => { state.alerts.error = 'Error loading alerts' state.alerts.loading = false }) } }, async fetchMarketsInfo() { const set = get().set const groupConfig = get().selectedMangoGroup.config const mangoGroup = get().selectedMangoGroup.current try { const data = await fetch( `https://mango-all-markets-api.herokuapp.com/markets/` ) if (data?.status === 200) { const parsedMarketsInfo = (await data.json()).filter((market) => { const marketKind = market.name.includes('PERP') ? 'perp' : 'spot' const marketConfig = getMarketByBaseSymbolAndKind( groupConfig, market.baseSymbol, marketKind ) if (!marketConfig || !marketConfig.publicKey) return false const marketMode: MarketMode = mangoGroup?.tokens[marketConfig.marketIndex][ marketKind + 'MarketMode' ] const isInactive = marketMode == MarketMode.ForceCloseOnly || marketMode == MarketMode.Inactive return !isInactive }) set((state) => { state.marketsInfo = parsedMarketsInfo }) } } catch (e) { console.log('ERORR: Unable to load all market info') } }, async fetchCoingeckoPrices() { const set = get().set set((state) => { state.coingeckoPrices.loading = true }) try { const promises: any = [] for (const asset of coingeckoIds) { promises.push( fetch( `https://api.coingecko.com/api/v3/coins/${asset.id}/market_chart?vs_currency=usd&days=1` ).then((res) => res.json()) ) } const data = await Promise.all(promises) for (let i = 0; i < data.length; i++) { data[i].symbol = coingeckoIds[i].symbol } set((state) => { state.coingeckoPrices.data = data state.coingeckoPrices.loading = false }) } catch (e) { console.log('ERORR: Unable to load Coingecko prices') set((state) => { state.coingeckoPrices.loading = false }) } }, }, } }) ) export default useMangoStore