mango-v4-ui/utils/governance/listingTools.ts

387 lines
9.9 KiB
TypeScript

import { AnchorProvider, Program } from '@project-serum/anchor'
import { PythHttpClient } from '@pythnetwork/client'
import { notify } from 'utils/notifications'
import { MAINNET_PYTH_PROGRAM } from './constants'
import { OPENBOOK_PROGRAM_ID, toNative } from '@blockworks-foundation/mango-v4'
import { Market } from '@project-serum/serum'
import { Connection, Keypair, PublicKey } from '@solana/web3.js'
import EmptyWallet from 'utils/wallet'
export const getOracle = async ({
baseSymbol,
quoteSymbol,
connection,
pythOnly = false,
}: {
baseSymbol: string
quoteSymbol: string
connection: Connection
pythOnly?: boolean
}) => {
try {
let oraclePk = ''
const pythOracle = await getPythOracle({
baseSymbol,
quoteSymbol,
connection,
})
if (pythOracle) {
oraclePk = pythOracle
} else if (!pythOnly) {
const switchBoardOracle = await getSwitchBoardOracle({
baseSymbol,
quoteSymbol,
connection,
})
oraclePk = switchBoardOracle
}
return oraclePk
} catch (e) {
notify({
title: 'Oracle not found',
description: `${e}`,
type: 'error',
})
}
}
export const getPythOracle = async ({
baseSymbol,
quoteSymbol,
connection,
}: {
baseSymbol: string
quoteSymbol: string
connection: Connection
}) => {
try {
const pythClient = new PythHttpClient(connection, MAINNET_PYTH_PROGRAM)
const pythAccounts = await pythClient.getData()
const product = pythAccounts.products.find(
(x) =>
x.base === baseSymbol.toUpperCase() &&
x.quote_currency === quoteSymbol.toUpperCase()
)
return product?.price_account || ''
} catch (e) {
notify({
title: 'Pyth oracle fetch error',
description: `${e}`,
type: 'error',
})
return ''
}
}
export const getSwitchBoardOracle = async ({
baseSymbol,
quoteSymbol,
connection,
}: {
baseSymbol: string
quoteSymbol: string
connection: Connection
}) => {
try {
const SWITCHBOARD_PROGRAM_ID = 'SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f'
const options = AnchorProvider.defaultOptions()
const provider = new AnchorProvider(
connection,
new EmptyWallet(Keypair.generate()),
options
)
const idl = await Program.fetchIdl(
new PublicKey(SWITCHBOARD_PROGRAM_ID),
provider
)
const switchboardProgram = new Program(
idl!,
new PublicKey(SWITCHBOARD_PROGRAM_ID),
provider
)
const allFeeds =
await switchboardProgram.account.aggregatorAccountData.all()
const feedNames = allFeeds.map((x) =>
String.fromCharCode(...[...(x.account.name as number[])].filter((x) => x))
)
const possibleFeedIndexes = feedNames.reduce(function (r, v, i) {
return r.concat(
v.toLowerCase().includes(baseSymbol.toLowerCase()) &&
v.toLowerCase().includes(quoteSymbol.toLowerCase())
? i
: []
)
}, [] as number[])
const possibleFeeds = allFeeds.filter(
(x, i) => possibleFeedIndexes.includes(i) && x.account.isLocked
)
return possibleFeeds.length ? possibleFeeds[0].publicKey.toBase58() : ''
} catch (e) {
notify({
title: 'Switchboard oracle fetch error',
description: `${e}`,
type: 'error',
})
return ''
}
}
export const getBestMarket = async ({
baseMint,
quoteMint,
cluster,
connection,
}: {
baseMint: string
quoteMint: string
cluster: 'devnet' | 'mainnet-beta'
connection: Connection
}) => {
try {
const dexProgramPk = OPENBOOK_PROGRAM_ID[cluster]
const markets = await Market.findAccountsByMints(
connection,
new PublicKey(baseMint),
new PublicKey(quoteMint),
dexProgramPk
)
if (!markets.length) {
return undefined
}
if (markets.length === 1) {
return markets[0].publicKey
}
const marketsDataJsons = await Promise.all([
...markets.map((x) =>
fetch(`/openSerumApi/market/${x.publicKey.toBase58()}`)
),
])
const marketsData = await Promise.all([
...marketsDataJsons.map((x) => x.json()),
])
const bestMarket = marketsData.sort((a, b) => b.volume24h - a.volume24h)
return bestMarket.length
? new PublicKey(bestMarket[0].id)
: markets[0].publicKey
} catch (e) {
notify({
title: 'Openbook market not found',
description: `${e}`,
type: 'error',
})
}
}
// definitions:
// baseLots = 10 ^ baseLotExponent
// quoteLots = 10 ^ quoteLotExponent
// minOrderSize = 10^(baseLotExponent - baseDecimals)
// minOrderValue = basePrice * minOrderSize
// priceIncrement = 10^(quoteLotExponent + baseDecimals - baseLotExponent - quoteDecimals)
// priceIncrementRelative = priceIncrement * quotePrice / basePrice
// derive: baseLotExponent <= min[ basePrice * minOrderSize > 0.05]
// baseLotExponent = 10
// While (baseLotExponent < 10):
// minOrderSize = 10^(baseLotExponent - baseDecimals)
// minOrderValue = basePrice * minOrderSize
// if minOrderValue > 0.05:
// break;
// Derive: quoteLotExponent <= min[ priceIncrement * quotePrice / basePrice > 0.000025 ]
// quoteLotExponent = 0
// While (quoteLotExponent < 10):
// priceIncrement = 10^(quoteLotExponent + baseDecimals - baseLotExponent - quoteDecimals)
// priceIncrementRelative = priceIncrement * quotePrice / basePrice
// if priceIncrementRelative > 0.000025:
// break;
export function calculateTradingParameters(
basePrice: number,
quotePrice: number,
baseDecimals: number,
quoteDecimals: number
) {
const MAX_MIN_ORDER_VALUE = 0.05
const MIN_PRICE_INCREMENT_RELATIVE = 0.000025
const EXPONENT_THRESHOLD = 10
let minOrderSize = 0
let priceIncrement = 0
let baseLotExponent = 0
let quoteLotExponent = 0
let minOrderValue = 0
let priceIncrementRelative = 0
// Calculate minimum order size
do {
minOrderSize = Math.pow(10, baseLotExponent - baseDecimals)
minOrderValue = basePrice * minOrderSize
if (minOrderValue > MAX_MIN_ORDER_VALUE) {
break
}
baseLotExponent++
} while (baseLotExponent < EXPONENT_THRESHOLD)
// Calculate price increment
do {
priceIncrement = Math.pow(
10,
quoteLotExponent + baseDecimals - baseLotExponent - quoteDecimals
)
priceIncrementRelative = (priceIncrement * quotePrice) / basePrice
if (priceIncrementRelative > MIN_PRICE_INCREMENT_RELATIVE) {
break
}
quoteLotExponent++
} while (quoteLotExponent < EXPONENT_THRESHOLD)
//exception override values in that case example eth/btc market
if (
quoteLotExponent === 0 &&
priceIncrementRelative > 0.001 &&
minOrderSize < 1
) {
baseLotExponent = baseLotExponent + 1
minOrderSize = Math.pow(10, baseLotExponent - baseDecimals)
minOrderValue = basePrice * minOrderSize
priceIncrement = Math.pow(
10,
quoteLotExponent + baseDecimals - baseLotExponent - quoteDecimals
)
priceIncrementRelative = (priceIncrement * quotePrice) / basePrice
}
return {
baseLots: Math.pow(10, baseLotExponent),
quoteLots: Math.pow(10, quoteLotExponent),
minOrderValue: minOrderValue,
baseLotExponent: baseLotExponent,
quoteLotExponent: quoteLotExponent,
minOrderSize: minOrderSize,
priceIncrement: priceIncrement,
priceIncrementRelative: priceIncrementRelative,
}
}
export const getQuoteSymbol = (quoteTokenSymbol: string) => {
if (
quoteTokenSymbol.toLowerCase() === 'usdc' ||
quoteTokenSymbol.toLocaleLowerCase() === 'usdt'
) {
return 'usd'
}
return quoteTokenSymbol
}
const listingBase = {
maxStalenessSlots: 120 as number | null,
oracleConfFilter: 0.1,
adjustmentFactor: 0.004,
util0: 0.5,
rate0: 0.052,
util1: 0.8,
rate1: 0.1446,
maxRate: 1.4456,
loanFeeRate: 0.005,
loanOriginationFeeRate: 0.001,
maintAssetWeight: 0.9,
initAssetWeight: 0.8,
maintLiabWeight: 1.1,
initLiabWeight: 1.2,
liquidationFee: 0.05,
minVaultToDepositsRatio: 0.2,
netBorrowLimitWindowSizeTs: 24 * 60 * 60,
netBorrowLimitPerWindowQuote: toNative(50000, 6).toNumber(),
insuranceFound: true,
borrowWeightScale: toNative(250000, 6).toNumber(),
depositWeightScale: toNative(250000, 6).toNumber(),
}
export type LISTING_PRESETS_KEYS =
| 'PREMIUM'
| 'MID'
| 'MEME'
| 'SHIT'
| 'UNTRUSTED'
export const LISTING_PRESETS: {
[key in LISTING_PRESETS_KEYS]:
| (typeof listingBase & { name: string })
| Record<string, never>
} = {
//Price impact $100,000 < 1%
PREMIUM: {
...listingBase,
name: 'Premium',
},
//Price impact $20,000 < 1%
MID: {
...listingBase,
maintAssetWeight: 0.75,
initAssetWeight: 0.5,
maintLiabWeight: 1.2,
initLiabWeight: 1.4,
liquidationFee: 0.1,
netBorrowLimitPerWindowQuote: toNative(20000, 6).toNumber(),
name: 'Mid',
borrowWeightScale: toNative(50000, 6).toNumber(),
depositWeightScale: toNative(50000, 6).toNumber(),
insuranceFound: false,
},
//Price impact $5,000 < 1%
MEME: {
...listingBase,
maxStalenessSlots: 800,
loanOriginationFeeRate: 0.002,
maintAssetWeight: 0,
initAssetWeight: 0,
maintLiabWeight: 1.25,
initLiabWeight: 1.5,
liquidationFee: 0.125,
netBorrowLimitPerWindowQuote: toNative(5000, 6).toNumber(),
borrowWeightScale: toNative(20000, 6).toNumber(),
depositWeightScale: toNative(20000, 6).toNumber(),
insuranceFound: false,
name: 'Meme',
},
//Price impact $1,000 < 1%
SHIT: {
...listingBase,
maxStalenessSlots: 800,
loanOriginationFeeRate: 0.002,
maintAssetWeight: 0,
initAssetWeight: 0,
maintLiabWeight: 1.4,
initLiabWeight: 1.8,
liquidationFee: 0.2,
netBorrowLimitPerWindowQuote: toNative(1000, 6).toNumber(),
borrowWeightScale: toNative(5000, 6).toNumber(),
depositWeightScale: toNative(5000, 6).toNumber(),
insuranceFound: false,
name: 'Shit',
},
UNTRUSTED: {},
}
export const coinTiersToNames: {
[key in LISTING_PRESETS_KEYS]: string
} = {
PREMIUM: 'Blue Chip',
MID: 'Mid-wit',
MEME: 'Meme',
SHIT: 'Shit Coin',
UNTRUSTED: 'Untrusted',
}