import create, { State } from 'zustand' import produce from 'immer' import { Market } from '@project-serum/serum' import { IDS, Config, MarketKind, MerpsClient as MangoClient, MerpsGroup as MangoGroup, MerpsAccount as MarginAccount, MarketConfig, getMarketByBaseSymbolAndKind, GroupConfig, TokenConfig, getTokenAccountsByOwnerWithWrappedSol, getTokenByMint, TokenAccount, nativeToUi, MerpsCache, PerpMarket, getMultipleAccounts, } from '@blockworks-foundation/mango-client' // import { SRM_DECIMALS } from '@project-serum/serum/lib/token-instructions' import { AccountInfo, Commitment, Connection, PublicKey, TokenAmount, } from '@solana/web3.js' import { EndpointInfo, WalletAdapter } from '../@types/types' import { getWalletTokenInfo } from '../utils/tokens' import { decodeAndLoadMarkets, getOrderBookAccountInfos, isDefined, } from '../utils' import { notify } from '../utils/notifications' import useAllMarkets from '../hooks/useAllMarkets' 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) || 'devnet' const ENDPOINT = ENDPOINTS.find((e) => e.name === CLUSTER) export const DEFAULT_CONNECTION = new Connection( ENDPOINT.url, 'processed' as Commitment ) const WEBSOCKET_CONNECTION = new Connection( ENDPOINT.websocket, 'processed' as Commitment ) const DEFAULT_MANGO_GROUP_NAME = 'merps_test_v1' const DEFAULT_MANGO_GROUP_CONFIG = Config.ids().getGroup( CLUSTER, DEFAULT_MANGO_GROUP_NAME ) console.log(DEFAULT_MANGO_GROUP_CONFIG) const defaultMangoGroupIds = IDS['groups'].find( (group) => group.name === DEFAULT_MANGO_GROUP_NAME ) export const programId = new PublicKey(defaultMangoGroupIds.merps_program_id) export const serumProgramId = new PublicKey( defaultMangoGroupIds.serum_program_id ) const merpsGroupPk = new PublicKey(defaultMangoGroupIds.key) export const mangoClient = new MangoClient(DEFAULT_CONNECTION, programId) export const INITIAL_STATE = { WALLET: { providerUrl: null, connected: false, current: null, tokens: [], 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 } export interface WalletToken { account: TokenAccount config: TokenConfig uiBalance: number } export interface Orderbook { bids: number[][] asks: number[][] } 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 } selectedMarket: { config: MarketConfig name: string address: string current: Market | PerpMarket | null mangoProgramId: number | null markPrice: number askInfo: AccountInfo | null bidInfo: AccountInfo | null orderBook: Orderbook } mangoGroups: Array selectedMangoGroup: { config: GroupConfig name: string current: MangoGroup | null markets: { [address: string]: Market } rootBanks: any[] cache: MerpsCache | null } 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 tokens: WalletToken[] 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, }, selectedMangoGroup: { config: DEFAULT_MANGO_GROUP_CONFIG, name: DEFAULT_MANGO_GROUP_NAME, current: null, markets: {}, rootBanks: [], cache: null, }, selectedMarket: { config: getMarketByBaseSymbolAndKind( DEFAULT_MANGO_GROUP_CONFIG, 'BTC', 'spot' ) as MarketConfig, kind: 'spot', name: 'BTC/USDC', address: defaultMangoGroupIds.spot_markets[0].key, current: null, mangoProgramId: null, markPrice: 0, askInfo: null, bidInfo: null, orderBook: { bids: [], asks: [] }, }, 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 fetchWalletTokens() { const groupConfig = get().selectedMangoGroup.config 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 getTokenAccountsByOwnerWithWrappedSol( DEFAULT_CONNECTION, ownerAddress ) const tokens = [] ownedTokenAccounts.forEach((account) => { const config = getTokenByMint(groupConfig, account.mint) if (config) { const uiBalance = nativeToUi(account.amount, config.decimals) tokens.push({ account, config, uiBalance }) } }) set((state) => { state.wallet.tokens = tokens }) } else { set((state) => { state.wallet.tokens = [] }) } }, async fetchMarginAccounts() { const mangoGroup = get().selectedMangoGroup.current const selectedMarginAcount = get().selectedMarginAccount.current const wallet = get().wallet.current const set = get().set if (!wallet?.publicKey || !wallet.publicKey) return if (!selectedMarginAcount) { set((state) => { state.selectedMarginAccount.initialLoad = true }) } return mangoClient .getMarginAccountsForOwner(mangoGroup, wallet.publicKey, true) .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 state.selectedMarginAccount.current = sortedAccounts[0] // 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 fetchMangoGroup() { const set = get().set const mangoGroupPk = merpsGroupPk const mangoGroupConfig = get().selectedMangoGroup.config console.log('CONFIG:', mangoGroupConfig) return mangoClient .getMerpsGroup(mangoGroupPk) .then(async (mangoGroup) => { const rootBanks = await mangoGroup.loadRootBanks(DEFAULT_CONNECTION) const merpsCache = await mangoGroup.loadCache(DEFAULT_CONNECTION) const spotMarketAccountInfos = await getMultipleAccounts( DEFAULT_CONNECTION, mangoGroupConfig.spot_markets.map((mkt) => mkt.key) ) const spotOrderBookAccountInfos = await getOrderBookAccountInfos( mangoGroup.dexProgramId, spotMarketAccountInfos.map(({ accountInfo }) => accountInfo) ) const spotMarkets = await decodeAndLoadMarkets( mangoGroup.dexProgramId, spotMarketAccountInfos ) // 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.rootBanks = rootBanks state.selectedMangoGroup.cache = merpsCache state.selectedMangoGroup.markets = spotMarkets spotMarketAccountInfos .concat(spotOrderBookAccountInfos) .forEach(({ publicKey, accountInfo }) => { state.accountInfos[publicKey.toString()] = accountInfo }) // state.selectedMangoGroup.srmAccount = srmAccountInfo // state.selectedMangoGroup.mintDecimals = mangoGroup.mintDecimals // TODO store "tokens" from merps group ids // 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 fetchAllMarkets() {}, // 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 // console.log('spot-history', results); // }) // }, }, })) export default useMangoStore