mango-ui-v3/stores/useMangoStore.tsx

490 lines
14 KiB
TypeScript
Raw Normal View History

2021-04-02 11:26:21 -07:00
import create, { State } from 'zustand'
import produce from 'immer'
import { Market } from '@project-serum/serum'
import {
2021-04-06 15:11:42 -07:00
IDS,
Config,
2021-06-23 08:32:33 -07:00
MangoClient,
MangoGroup,
MangoAccount,
MarketConfig,
getMarketByBaseSymbolAndKind,
GroupConfig,
2021-06-17 11:03:47 -07:00
TokenConfig,
getTokenAccountsByOwnerWithWrappedSol,
getTokenByMint,
TokenAccount,
nativeToUi,
2021-06-23 08:32:33 -07:00
MangoCache,
2021-06-17 14:07:10 -07:00
PerpMarket,
2021-06-18 20:07:57 -07:00
getAllMarkets,
getMultipleAccounts,
2021-06-18 20:07:57 -07:00
PerpMarketLayout,
2021-04-02 11:26:21 -07:00
} from '@blockworks-foundation/mango-client'
import { AccountInfo, Commitment, Connection, PublicKey } from '@solana/web3.js'
2021-04-13 22:23:50 -07:00
import { EndpointInfo, WalletAdapter } from '../@types/types'
2021-08-03 10:59:16 -07:00
import { isDefined, zipDict } from '../utils'
2021-04-20 07:09:25 -07:00
import { notify } from '../utils/notifications'
import { LAST_ACCOUNT_KEY } from '../components/AccountsModal'
export const ENDPOINTS: EndpointInfo[] = [
{
2021-08-19 10:49:44 -07:00
name: 'mainnet',
2021-08-20 03:10:47 -07:00
url: process.env.NEXT_PUBLIC_ENDPOINT || 'https://mango.rpcpool.com',
websocket: process.env.NEXT_PUBLIC_ENDPOINT || 'https://mango.rpcpool.com',
custom: false,
},
{
name: 'devnet',
2021-08-17 13:36:31 -07:00
// url: "https://mango.devnet.rpcpool.com",
// websocket: "https://mango.devnet.rpcpool.com",
2021-08-17 10:35:30 -07:00
url: 'https://api.devnet.solana.com',
2021-06-12 12:10:56 -07:00
websocket: 'https://api.devnet.solana.com',
custom: false,
},
]
2021-08-19 10:49:44 -07:00
type ClusterType = 'mainnet' | 'devnet'
2021-04-25 09:22:28 -07:00
2021-08-20 03:10:47 -07:00
const CLUSTER = (process.env.NEXT_PUBLIC_CLUSTER as ClusterType) || 'mainnet'
const ENDPOINT = ENDPOINTS.find((e) => e.name === CLUSTER)
export const DEFAULT_CONNECTION = new Connection(
ENDPOINT.url,
'processed' as Commitment
)
export const WEBSOCKET_CONNECTION = new Connection(
ENDPOINT.websocket,
'processed' as Commitment
)
2021-06-16 18:55:46 -07:00
2021-08-25 10:12:43 -07:00
const DEFAULT_MANGO_GROUP_NAME = process.env.NEXT_PUBLIC_GROUP || 'mainnet.1'
2021-06-17 11:38:53 -07:00
const DEFAULT_MANGO_GROUP_CONFIG = Config.ids().getGroup(
CLUSTER,
DEFAULT_MANGO_GROUP_NAME
)
const defaultMangoGroupIds = IDS['groups'].find(
(group) => group.name === DEFAULT_MANGO_GROUP_NAME
)
export const MNGO_INDEX = defaultMangoGroupIds.oracles.findIndex(
(t) => t.symbol === 'MNGO'
)
2021-06-23 08:32:33 -07:00
export const programId = new PublicKey(defaultMangoGroupIds.mangoProgramId)
2021-06-18 16:56:53 -07:00
export const serumProgramId = new PublicKey(defaultMangoGroupIds.serumProgramId)
2021-06-23 08:32:33 -07:00
const mangoGroupPk = new PublicKey(defaultMangoGroupIds.publicKey)
export const mangoClient = new MangoClient(DEFAULT_CONNECTION, programId)
2021-04-13 16:41:04 -07:00
export const INITIAL_STATE = {
WALLET: {
2021-04-13 22:23:50 -07:00
providerUrl: null,
2021-04-13 16:41:04 -07:00
connected: false,
current: null,
2021-06-17 11:03:47 -07:00
tokens: [],
2021-04-13 16:41:04 -07:00
},
}
2021-04-10 14:12:15 -07:00
// 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<Buffer>
}
2021-04-02 11:26:21 -07:00
2021-06-17 11:03:47 -07:00
export interface WalletToken {
2021-06-17 15:38:17 -07:00
account: TokenAccount
config: TokenConfig
2021-06-17 11:03:47 -07:00
uiBalance: number
}
export interface Orderbook {
bids: number[][]
asks: number[][]
}
2021-04-02 11:26:21 -07:00
interface MangoStore extends State {
2021-04-11 21:17:23 -07:00
notifications: Array<{
type: string
title: string
2021-04-11 21:17:23 -07:00
description?: string
txid?: string
}>
accountInfos: AccountInfoList
connection: {
2021-04-25 09:22:28 -07:00
cluster: ClusterType
current: Connection
websocket: Connection
endpoint: string
slot: number
}
2021-04-29 07:38:28 -07:00
selectedMarket: {
2021-06-17 11:38:53 -07:00
config: MarketConfig
2021-06-17 14:07:10 -07:00
current: Market | PerpMarket | null
2021-04-02 11:26:21 -07:00
markPrice: number
kind: string
2021-06-17 14:07:10 -07:00
askInfo: AccountInfo<Buffer> | null
bidInfo: AccountInfo<Buffer> | null
orderBook: Orderbook
fills: any[]
2021-04-02 11:26:21 -07:00
}
mangoGroups: Array<MangoGroup>
selectedMangoGroup: {
2021-06-17 11:38:53 -07:00
config: GroupConfig
name: string
current: MangoGroup | null
2021-04-07 08:44:22 -07:00
markets: {
2021-06-18 20:07:57 -07:00
[address: string]: Market | PerpMarket
2021-04-07 08:44:22 -07:00
}
2021-06-23 08:32:33 -07:00
cache: MangoCache | null
}
2021-06-23 08:32:33 -07:00
mangoAccounts: MangoAccount[]
selectedMangoAccount: {
current: MangoAccount | null
initialLoad: boolean
}
2021-04-02 11:26:21 -07:00
tradeForm: {
side: 'buy' | 'sell'
price: number | ''
baseSize: number | ''
quoteSize: number | ''
tradeType: 'Market' | 'Limit'
2021-04-02 11:26:21 -07:00
}
wallet: {
2021-04-13 22:23:50 -07:00
providerUrl: string
connected: boolean
2021-04-13 22:23:50 -07:00
current: WalletAdapter | undefined
2021-06-17 15:38:17 -07:00
tokens: WalletToken[]
}
settings: {
uiLocked: boolean
}
2021-04-14 15:46:36 -07:00
tradeHistory: any[]
2021-04-02 11:26:21 -07:00
set: (x: any) => void
2021-04-25 09:22:28 -07:00
actions: {
[key: string]: (args?) => void
2021-04-25 09:22:28 -07:00
}
2021-04-02 11:26:21 -07:00
}
2021-04-12 20:39:08 -07:00
const useMangoStore = create<MangoStore>((set, get) => ({
notifications: [],
accountInfos: {},
connection: {
cluster: CLUSTER,
current: DEFAULT_CONNECTION,
websocket: WEBSOCKET_CONNECTION,
endpoint: ENDPOINT.url,
slot: 0,
2021-04-12 20:39:08 -07:00
},
selectedMangoGroup: {
config: DEFAULT_MANGO_GROUP_CONFIG,
2021-04-12 20:39:08 -07:00
name: DEFAULT_MANGO_GROUP_NAME,
current: null,
markets: {},
2021-06-16 18:55:46 -07:00
rootBanks: [],
2021-06-17 11:38:53 -07:00
cache: null,
2021-04-12 20:39:08 -07:00
},
selectedMarket: {
2021-06-17 11:38:53 -07:00
config: getMarketByBaseSymbolAndKind(
DEFAULT_MANGO_GROUP_CONFIG,
'BTC',
2021-08-25 11:30:16 -07:00
'perp'
2021-06-17 11:38:53 -07:00
) as MarketConfig,
2021-08-25 11:30:16 -07:00
kind: 'perp',
2021-04-12 20:39:08 -07:00
current: null,
markPrice: 0,
askInfo: null,
bidInfo: null,
2021-06-18 08:29:52 -07:00
orderBook: { bids: [], asks: [] },
fills: [],
2021-04-12 20:39:08 -07:00
},
mangoGroups: [],
2021-06-23 08:32:33 -07:00
mangoAccounts: [],
selectedMangoAccount: {
2021-04-12 20:39:08 -07:00
current: null,
2021-06-24 13:57:11 -07:00
initialLoad: true,
2021-04-12 20:39:08 -07:00
},
tradeForm: {
side: 'buy',
baseSize: '',
quoteSize: '',
tradeType: 'Limit',
price: '',
},
2021-04-13 16:41:04 -07:00
wallet: INITIAL_STATE.WALLET,
settings: {
uiLocked: true,
},
2021-04-14 15:46:36 -07:00
tradeHistory: [],
2021-04-12 20:39:08 -07:00
set: (fn) => set(produce(fn)),
actions: {
2021-06-17 11:03:47 -07:00
async fetchWalletTokens() {
const groupConfig = get().selectedMangoGroup.config
2021-04-12 20:39:08 -07:00
const wallet = get().wallet.current
const connected = get().wallet.connected
const set = get().set
2021-04-14 15:46:36 -07:00
if (wallet?.publicKey && connected) {
2021-06-17 11:03:47 -07:00
const ownedTokenAccounts = await getTokenAccountsByOwnerWithWrappedSol(
DEFAULT_CONNECTION,
2021-07-06 17:13:17 -07:00
wallet.publicKey
2021-04-12 20:39:08 -07:00
)
2021-06-17 11:03:47 -07:00
const tokens = []
2021-06-17 15:38:17 -07:00
ownedTokenAccounts.forEach((account) => {
const config = getTokenByMint(groupConfig, account.mint)
2021-06-17 11:03:47 -07:00
if (config) {
2021-06-17 15:38:17 -07:00
const uiBalance = nativeToUi(account.amount, config.decimals)
tokens.push({ account, config, uiBalance })
2021-06-17 11:03:47 -07:00
}
2021-06-17 15:38:17 -07:00
})
2021-06-17 11:03:47 -07:00
2021-04-12 20:39:08 -07:00
set((state) => {
2021-06-17 15:38:17 -07:00
state.wallet.tokens = tokens
2021-04-12 20:39:08 -07:00
})
} else {
set((state) => {
2021-06-17 11:03:47 -07:00
state.wallet.tokens = []
2021-04-12 20:39:08 -07:00
})
}
},
2021-06-23 08:32:33 -07:00
async fetchMangoAccounts() {
const set = get().set
2021-06-17 11:03:47 -07:00
const mangoGroup = get().selectedMangoGroup.current
const wallet = get().wallet.current
const walletPk = wallet?.publicKey
2021-04-12 20:39:08 -07:00
if (!walletPk) return
2021-06-17 11:03:47 -07:00
return mangoClient
.getMangoAccountsForOwner(mangoGroup, walletPk, true)
2021-06-23 08:32:33 -07:00
.then((mangoAccounts) => {
if (mangoAccounts.length > 0) {
const sortedAccounts = mangoAccounts
2021-06-17 11:03:47 -07:00
.slice()
.sort((a, b) =>
a.publicKey.toBase58() > b.publicKey.toBase58() ? 1 : -1
2021-06-17 11:03:47 -07:00
)
2021-06-18 16:56:53 -07:00
2021-06-17 11:03:47 -07:00
set((state) => {
state.selectedMangoAccount.initialLoad = false
2021-06-23 08:32:33 -07:00
state.mangoAccounts = sortedAccounts
if (state.selectedMangoAccount.current) {
state.selectedMangoAccount.current = mangoAccounts.find((ma) =>
ma.publicKey.equals(
state.selectedMangoAccount.current.publicKey
)
)
} else {
const lastAccount = localStorage.getItem(LAST_ACCOUNT_KEY)
state.selectedMangoAccount.current =
mangoAccounts.find(
(ma) => ma.publicKey.toString() === JSON.parse(lastAccount)
) || sortedAccounts[0]
}
2021-06-17 11:03:47 -07:00
})
} else {
set((state) => {
state.selectedMangoAccount.initialLoad = false
})
2021-06-17 11:03:47 -07:00
}
})
.catch((err) => {
notify({
type: 'error',
title: 'Unable to load mango account',
description: err.message,
})
console.log('Could not get margin accounts for wallet', err)
2021-06-17 11:03:47 -07:00
})
},
2021-04-12 20:39:08 -07:00
async fetchMangoGroup() {
const set = get().set
const mangoGroupConfig = get().selectedMangoGroup.config
const selectedMarketConfig = get().selectedMarket.config
return mangoClient
2021-06-23 08:32:33 -07:00
.getMangoGroup(mangoGroupPk)
2021-04-12 20:39:08 -07:00
.then(async (mangoGroup) => {
const allMarketConfigs = getAllMarkets(mangoGroupConfig)
const allMarketPks = allMarketConfigs.map((m) => m.publicKey)
2021-08-25 07:22:59 -07:00
let allMarketAccountInfos, mangoCache
try {
const resp = await Promise.all([
getMultipleAccounts(DEFAULT_CONNECTION, allMarketPks),
mangoGroup.loadCache(DEFAULT_CONNECTION),
mangoGroup.loadRootBanks(DEFAULT_CONNECTION),
])
allMarketAccountInfos = resp[0]
mangoCache = resp[1]
} catch {
notify({
type: 'error',
title: 'Failed to load the mango group. Please refresh.',
})
}
2021-06-18 20:07:57 -07:00
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
)
}
2021-06-18 20:07:57 -07:00
})
const allBidsAndAsksPks = allMarketConfigs
.map((m) => [m.bidsKey, m.asksKey])
.flat()
const allBidsAndAsksAccountInfos = await getMultipleAccounts(
DEFAULT_CONNECTION,
allBidsAndAsksPks
)
2021-06-18 20:07:57 -07:00
const allMarkets = zipDict(
allMarketPks.map((pk) => pk.toBase58()),
allMarketAccounts
)
2021-04-09 17:01:00 -07:00
set((state) => {
2021-04-12 20:39:08 -07:00
state.selectedMangoGroup.current = mangoGroup
2021-06-23 08:32:33 -07:00
state.selectedMangoGroup.cache = mangoCache
2021-06-18 20:07:57 -07:00
state.selectedMangoGroup.markets = allMarkets
state.selectedMarket.current =
allMarkets[selectedMarketConfig.publicKey.toBase58()]
2021-06-18 20:07:57 -07:00
allMarketAccountInfos
.concat(allBidsAndAsksAccountInfos)
.forEach(({ publicKey, context, accountInfo }) => {
2021-08-24 10:19:34 -07:00
if (context.slot >= state.connection.slot) {
state.connection.slot = context.slot
state.accountInfos[publicKey.toBase58()] = accountInfo
}
})
2021-04-09 17:01:00 -07:00
})
2021-04-12 20:39:08 -07:00
})
.catch((err) => {
2021-04-20 07:09:25 -07:00
notify({
title: 'Could not get mango group',
2021-04-20 07:09:25 -07:00
description: `${err}`,
type: 'error',
})
console.log('Could not get mango group: ', err)
2021-04-12 20:39:08 -07:00
})
2021-04-09 17:01:00 -07:00
},
async fetchTradeHistory(mangoAccount = null) {
2021-08-03 10:59:16 -07:00
const selectedMangoAccount =
mangoAccount || get().selectedMangoAccount.current
const set = get().set
if (!selectedMangoAccount) return
2021-07-19 10:13:03 -07:00
2021-08-03 10:59:16 -07:00
if (selectedMangoAccount.spotOpenOrdersAccounts.length === 0) return
const openOrdersAccounts =
selectedMangoAccount.spotOpenOrdersAccounts.filter(isDefined)
const publicKeys = openOrdersAccounts.map((act) =>
act.publicKey.toString()
)
2021-08-13 08:05:49 -07:00
const perpHistory = await fetch(
`https://event-history-api.herokuapp.com/perp_trades/${selectedMangoAccount.publicKey.toString()}`
)
let parsedPerpHistory = await perpHistory.json()
parsedPerpHistory = parsedPerpHistory?.data || []
const serumHistory = await Promise.all(
2021-08-03 10:59:16 -07:00
publicKeys.map(async (pk) => {
const response = await fetch(
2021-08-23 15:37:48 -07:00
`https://event-history-api.herokuapp.com/trades/open_orders/${pk.toString()}`
2021-08-03 10:59:16 -07:00
)
const parsedResponse = await response.json()
return parsedResponse?.data ? parsedResponse.data : []
})
)
set((state) => {
2021-08-13 08:05:49 -07:00
state.tradeHistory = [...serumHistory, ...parsedPerpHistory]
2021-08-03 10:59:16 -07:00
})
},
2021-08-31 10:47:03 -07:00
async reloadMangoAccount() {
const set = get().set
const mangoAccount = get().selectedMangoAccount.current
const [reloadedMangoAccount, reloadedOpenOrders] = await Promise.all([
mangoAccount.reload(DEFAULT_CONNECTION),
mangoAccount.loadOpenOrders(
DEFAULT_CONNECTION,
new PublicKey(serumProgramId)
),
])
reloadedMangoAccount.spotOpenOrdersAccounts = reloadedOpenOrders
set((state) => {
state.selectedMangoAccount.current = reloadedMangoAccount
})
},
async updateOpenOrders() {
const set = get().set
2021-09-09 17:23:02 -07:00
const bidAskAccounts = Object.keys(get().accountInfos).map(
(pk) => new PublicKey(pk)
)
const allBidsAndAsksAccountInfos = await getMultipleAccounts(
DEFAULT_CONNECTION,
2021-09-09 17:23:02 -07:00
bidAskAccounts
)
set((state) => {
allBidsAndAsksAccountInfos.forEach(
({ publicKey, context, accountInfo }) => {
2021-09-09 17:23:02 -07:00
state.connection.slot = context.slot
state.accountInfos[publicKey.toBase58()] = accountInfo
}
)
})
},
async loadMarketFills() {
const set = get().set
const selectedMarket = get().selectedMarket.current
if (!selectedMarket) {
return null
}
try {
const loadedFills = await selectedMarket.loadFills(
DEFAULT_CONNECTION,
10000
)
set((state) => {
state.selectedMarket.fills = loadedFills
})
} catch (err) {
console.log('Error fetching fills:', err)
}
},
2021-09-07 16:27:14 -07:00
async fetchMangoGroupCache() {
const set = get().set
const mangoGroup = get().selectedMangoGroup.current
if (mangoGroup) {
const mangoCache = await mangoGroup.loadCache(DEFAULT_CONNECTION)
set((state) => {
state.selectedMangoGroup.cache = mangoCache
})
}
},
2021-04-12 20:39:08 -07:00
},
}))
2021-04-02 11:26:21 -07:00
export default useMangoStore