import create, { State } from 'zustand' import produce from 'immer' import { Market } from '@project-serum/serum' import { IDS, MangoClient, MangoGroup, MarginAccount, nativeToUi, } from '@blockworks-foundation/mango-client' import { SRM_DECIMALS } from '@project-serum/serum/lib/token-instructions' import { AccountInfo, Connection, PublicKey } from '@solana/web3.js' import { EndpointInfo, WalletAdapter } from '../@types/types' import { getWalletTokenInfo } from '../utils/tokens' import { isDefined } from '../utils/index' import { notify } from '../utils/notifications' export const ENDPOINTS: EndpointInfo[] = [ { name: 'mainnet-beta', url: 'https://mango.rpcpool.com/', websocket: 'https://mango.rpcpool.com/', custom: false, }, { name: 'devnet', url: 'https://api.devnet.solana.com', websocket: 'https://api.devnet.solana.com', custom: false, }, ] type ClusterType = 'mainnet-beta' | 'devnet' const CLUSTER = (process.env.NEXT_PUBLIC_CLUSTER as ClusterType) || 'mainnet-beta' const ENDPOINT = ENDPOINTS.find((e) => e.name === CLUSTER) const DEFAULT_CONNECTION = new Connection(ENDPOINT.url, 'recent') const WEBSOCKET_CONNECTION = new Connection(ENDPOINT.websocket, 'recent') const DEFAULT_MANGO_GROUP_NAME = 'BTC_ETH_SOL_SRM_USDC' export const INITIAL_STATE = { WALLET: { providerUrl: null, connected: false, current: null, balances: [], srmAccountsForOwner: [], contributedSrm: 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 } interface MangoStore extends State { notifications: Array<{ type: string message: string description?: string txid?: string }> accountInfos: AccountInfoList connection: { cluster: ClusterType current: Connection websocket: Connection endpoint: string srmMint: string } selectedMarket: { name: string address: string current: Market | null mangoProgramId: number | null markPrice: number orderBook: any[] } mangoClient: MangoClient mangoGroups: Array selectedMangoGroup: { name: string current: MangoGroup | null srmAccount: AccountInfo | null markets: { [address: string]: Market } mintDecimals: number[] prices: number[] } marginAccounts: MarginAccount[] selectedMarginAccount: { current: MarginAccount | null initialLoad: boolean } tradeForm: { side: 'buy' | 'sell' price: number | '' baseSize: number | '' quoteSize: number | '' tradeType: 'Market' | 'Limit' } wallet: { providerUrl: string connected: boolean current: WalletAdapter | undefined balances: Array<{ account: any; publicKey: PublicKey }> srmAccountsForOwner: any[] contributedSrm: number } settings: { uiLocked: boolean } tradeHistory: any[] set: (x: any) => void actions: { [key: string]: () => void } } const useMangoStore = create((set, get) => ({ notifications: [], accountInfos: {}, connection: { cluster: CLUSTER, current: DEFAULT_CONNECTION, websocket: WEBSOCKET_CONNECTION, endpoint: ENDPOINT.url, srmMint: IDS[CLUSTER].symbols['SRM'], }, selectedMangoGroup: { name: DEFAULT_MANGO_GROUP_NAME, current: null, markets: {}, srmAccount: null, mintDecimals: [], prices: [], }, selectedMarket: { name: Object.entries( IDS[CLUSTER].mango_groups[DEFAULT_MANGO_GROUP_NAME].spot_market_symbols )[0][0], address: Object.entries( IDS[CLUSTER].mango_groups[DEFAULT_MANGO_GROUP_NAME].spot_market_symbols )[0][1], current: null, mangoProgramId: null, markPrice: 0, orderBook: [], }, mangoClient: new MangoClient(), mangoGroups: [], marginAccounts: [], selectedMarginAccount: { current: null, initialLoad: false, }, tradeForm: { side: 'buy', baseSize: '', quoteSize: '', tradeType: 'Limit', price: '', }, wallet: INITIAL_STATE.WALLET, settings: { uiLocked: true, }, tradeHistory: [], set: (fn) => set(produce(fn)), actions: { async fetchWalletBalances() { const connection = get().connection.current const wallet = get().wallet.current const connected = get().wallet.connected const set = get().set if (wallet?.publicKey && connected) { const ownerAddress = wallet.publicKey const ownedTokenAccounts = await getWalletTokenInfo( connection, ownerAddress ) set((state) => { state.wallet.balances = ownedTokenAccounts }) } else { set((state) => { state.wallet.balances = [] }) } }, async fetchMangoSrmAccounts() { const connection = get().connection.current const wallet = get().wallet.current const connected = get().wallet.connected const selectedMangoGroup = get().selectedMangoGroup.current const cluster = get().connection.cluster const mangoClient = get().mangoClient const set = get().set if (wallet?.publicKey && connected && selectedMangoGroup) { const usersMangoSrmAccounts = await mangoClient.getMangoSrmAccountsForOwner( connection, new PublicKey(IDS[cluster].mango_program_id), selectedMangoGroup, wallet ) if (usersMangoSrmAccounts.length) { set((state) => { state.wallet.srmAccountsForOwner = usersMangoSrmAccounts const totalSrmDeposits = usersMangoSrmAccounts.reduce( (prev, cur) => prev + nativeToUi(cur.amount, SRM_DECIMALS), 0 ) state.wallet.contributedSrm = totalSrmDeposits }) } } }, async fetchMarginAccounts() { const connection = get().connection.current const mangoGroup = get().selectedMangoGroup.current const selectedMarginAcount = get().selectedMarginAccount.current const wallet = get().wallet.current const cluster = get().connection.cluster const mangoClient = get().mangoClient const programId = IDS[cluster].mango_program_id const set = get().set if (!wallet?.publicKey || !wallet.publicKey) return if (!selectedMarginAcount) { set((state) => { state.selectedMarginAccount.initialLoad = true }) } return mangoClient .getMarginAccountsForOwner( connection, new PublicKey(programId), mangoGroup, wallet ) .then((marginAccounts) => { if (marginAccounts.length > 0) { const sortedAccounts = marginAccounts .slice() .sort( (a, b) => (a.publicKey.toBase58() > b.publicKey.toBase58() && 1) || -1 ) set((state) => { state.marginAccounts = sortedAccounts if (state.selectedMarginAccount.current) { state.selectedMarginAccount.current = marginAccounts.find( (ma) => ma.publicKey.equals( state.selectedMarginAccount.current.publicKey ) ) } else { const lastAccount = localStorage.getItem('lastAccountViewed') state.selectedMarginAccount.current = marginAccounts.find( (ma) => ma.publicKey.toString() === JSON.parse(lastAccount) ) || sortedAccounts[0] } }) } set((state) => { state.selectedMarginAccount.initialLoad = false }) }) .catch((err) => { console.error('Could not get margin accounts for wallet', err) }) }, async fetchAllMangoGroups() { const connection = get().connection.current const cluster = get().connection.cluster const mangoClient = get().mangoClient const set = get().set const mangoGroups = Object.keys(IDS[cluster].mango_groups) const allMangoGroups = await Promise.all( mangoGroups.map((mangoGroupName) => { const mangoGroupIds = IDS[cluster].mango_groups[mangoGroupName] const mangoGroupPk = new PublicKey(mangoGroupIds.mango_group_pk) const srmVaultPk = new PublicKey(mangoGroupIds.srm_vault_pk) return mangoClient.getMangoGroup(connection, mangoGroupPk, srmVaultPk) }) ) set((state) => { state.mangoGroups = allMangoGroups }) }, async fetchMangoGroup() { const connection = get().connection.current const mangoGroupName = get().selectedMangoGroup.name const cluster = get().connection.cluster const mangoClient = get().mangoClient const set = get().set const mangoGroupIds = IDS[cluster].mango_groups[mangoGroupName] if (!mangoClient) return const mangoGroupPk = new PublicKey(mangoGroupIds.mango_group_pk) const srmVaultPk = new PublicKey(mangoGroupIds.srm_vault_pk) return mangoClient .getMangoGroup(connection, mangoGroupPk, srmVaultPk) .then(async (mangoGroup) => { const srmAccountInfoPromise = connection.getAccountInfo( mangoGroup.srmVault ) const pricesPromise = mangoGroup.getPrices(connection) const [srmAccountInfo, prices] = await Promise.all([ srmAccountInfoPromise, pricesPromise, ]) // Set the mango group set((state) => { state.selectedMangoGroup.current = mangoGroup state.selectedMangoGroup.srmAccount = srmAccountInfo state.selectedMangoGroup.mintDecimals = mangoGroup.mintDecimals state.selectedMangoGroup.prices = prices }) }) .catch((err) => { notify({ message: 'Could not get mango group: ', description: `${err}`, type: 'error', }) console.log('Could not get mango group: ', err) }) }, async fetchTradeHistory(marginAccount = null) { const selectedMarginAccount = marginAccount || get().selectedMarginAccount.current const set = get().set if (!selectedMarginAccount) return if (selectedMarginAccount.openOrdersAccounts.length === 0) return const openOrdersAccounts = selectedMarginAccount.openOrdersAccounts.filter(isDefined) const publicKeys = openOrdersAccounts.map((act) => act.publicKey.toString() ) const results = await Promise.all( publicKeys.map(async (pk) => { const response = await fetch( `https://stark-fjord-45757.herokuapp.com/trades/open_orders/${pk.toString()}` ) const parsedResponse = await response.json() return parsedResponse?.data ? parsedResponse.data : [] }) ) set((state) => { state.tradeHistory = results }) }, }, })) export default useMangoStore