mango-explorer/mango/group.py

651 lines
23 KiB
Python

# # ⚠ Warning
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# [🥭 Mango Markets](https://mango.markets/) support is available at:
# [Docs](https://docs.mango.markets/)
# [Discord](https://discord.gg/67jySBhxrg)
# [Twitter](https://twitter.com/mangomarkets)
# [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation)
import logging
import typing
from decimal import Decimal
from solana.publickey import PublicKey
from .accountinfo import AccountInfo
from .addressableaccount import AddressableAccount
from .cache import Cache, PerpMarketCache, MarketCache
from .constants import SYSTEM_PROGRAM_ADDRESS
from .context import Context
from .instrumentlookup import InstrumentLookup
from .instrumentvalue import InstrumentValue
from .layouts import layouts
from .lotsizeconverter import LotSizeConverter, RaisingLotSizeConverter
from .marketlookup import MarketLookup
from .metadata import Metadata
from .tokens import Instrument, Token
from .tokenbank import TokenBank
from .version import Version
# # 🥭 GroupSlotSpotMarket class
#
class GroupSlotSpotMarket:
def __init__(
self,
address: PublicKey,
maint_asset_weight: Decimal,
init_asset_weight: Decimal,
maint_liab_weight: Decimal,
init_liab_weight: Decimal,
) -> None:
self.address: PublicKey = address
self.maint_asset_weight: Decimal = maint_asset_weight
self.init_asset_weight: Decimal = init_asset_weight
self.maint_liab_weight: Decimal = maint_liab_weight
self.init_liab_weight: Decimal = init_liab_weight
@staticmethod
def from_layout(layout: typing.Any) -> "GroupSlotSpotMarket":
spot_market: PublicKey = layout.spot_market
maint_asset_weight: Decimal = round(layout.maint_asset_weight, 8)
init_asset_weight: Decimal = round(layout.init_asset_weight, 8)
maint_liab_weight: Decimal = round(layout.maint_liab_weight, 8)
init_liab_weight: Decimal = round(layout.init_liab_weight, 8)
return GroupSlotSpotMarket(
spot_market,
maint_asset_weight,
init_asset_weight,
maint_liab_weight,
init_liab_weight,
)
@staticmethod
def from_layout_or_none(
layout: typing.Any,
) -> typing.Optional["GroupSlotSpotMarket"]:
if (layout.spot_market is None) or (
layout.spot_market == SYSTEM_PROGRAM_ADDRESS
):
return None
return GroupSlotSpotMarket.from_layout(layout)
def __str__(self) -> str:
return f"""« GroupSlotSpotMarket [{self.address}]
Asset Weights:
Initial: {self.init_asset_weight}
Maintenance: {self.maint_asset_weight}
Liability Weights:
Initial: {self.init_liab_weight}
Maintenance: {self.maint_liab_weight}
»"""
def __repr__(self) -> str:
return f"{self}"
# # 🥭 GroupSlotPerpMarket class
#
class GroupSlotPerpMarket:
def __init__(
self,
address: PublicKey,
maint_asset_weight: Decimal,
init_asset_weight: Decimal,
maint_liab_weight: Decimal,
init_liab_weight: Decimal,
liquidation_fee: Decimal,
base_lot_size: Decimal,
quote_lot_size: Decimal,
) -> None:
self.address: PublicKey = address
self.maint_asset_weight: Decimal = maint_asset_weight
self.init_asset_weight: Decimal = init_asset_weight
self.maint_liab_weight: Decimal = maint_liab_weight
self.init_liab_weight: Decimal = init_liab_weight
self.liquidation_fee: Decimal = liquidation_fee
self.base_lot_size: Decimal = base_lot_size
self.quote_lot_size: Decimal = quote_lot_size
@staticmethod
def from_layout(layout: typing.Any) -> "GroupSlotPerpMarket":
perp_market: PublicKey = layout.perp_market
maint_asset_weight: Decimal = round(layout.maint_asset_weight, 8)
init_asset_weight: Decimal = round(layout.init_asset_weight, 8)
maint_liab_weight: Decimal = round(layout.maint_liab_weight, 8)
init_liab_weight: Decimal = round(layout.init_liab_weight, 8)
liquidation_fee: Decimal = round(layout.liquidation_fee, 8)
base_lot_size: Decimal = layout.base_lot_size
quote_lot_size: Decimal = layout.quote_lot_size
return GroupSlotPerpMarket(
perp_market,
maint_asset_weight,
init_asset_weight,
maint_liab_weight,
init_liab_weight,
liquidation_fee,
base_lot_size,
quote_lot_size,
)
@staticmethod
def from_layout_or_none(
layout: typing.Any,
) -> typing.Optional["GroupSlotPerpMarket"]:
if (layout.perp_market is None) or (
layout.perp_market == SYSTEM_PROGRAM_ADDRESS
):
return None
return GroupSlotPerpMarket.from_layout(layout)
def __str__(self) -> str:
return f"""« GroupSlotPerpMarket [{self.address}]
Asset Weights:
Initial: {self.init_asset_weight}
Maintenance: {self.maint_asset_weight}
Liability Weights:
Initial: {self.init_liab_weight}
Maintenance: {self.maint_liab_weight}
Liquidation Fee: {self.liquidation_fee}
Base Lot Size: {self.base_lot_size}
Quote Lot Size: {self.quote_lot_size}
»"""
def __repr__(self) -> str:
return f"{self}"
# # 🥭 GroupSlot class
#
# `GroupSlot` gathers indexed slot items together instead of separate arrays.
#
class GroupSlot:
def __init__(
self,
index: int,
base_instrument: Instrument,
base_token_bank: typing.Optional[TokenBank],
quote_token_bank: TokenBank,
spot_market_info: typing.Optional[GroupSlotSpotMarket],
perp_market_info: typing.Optional[GroupSlotPerpMarket],
perp_lot_size_converter: LotSizeConverter,
oracle: PublicKey,
) -> None:
self.index: int = index
self.base_instrument: Instrument = base_instrument
self.base_token_bank: typing.Optional[TokenBank] = base_token_bank
self.quote_token_bank: TokenBank = quote_token_bank
self.spot_market: typing.Optional[GroupSlotSpotMarket] = spot_market_info
self.perp_market: typing.Optional[GroupSlotPerpMarket] = perp_market_info
self.perp_lot_size_converter: LotSizeConverter = perp_lot_size_converter
self.oracle: PublicKey = oracle
@property
def spot_market_symbol(self) -> str:
return f"{self.base_instrument.symbol}/{self.quote_token_bank.token.symbol}"
@property
def perp_market_symbol(self) -> str:
return f"{self.base_instrument.symbol}-PERP"
def __str__(self) -> str:
base_token_bank = f"{self.base_token_bank}".replace("\n", "\n ")
quote_token_bank = f"{self.quote_token_bank}".replace("\n", "\n ")
spot_market_info = f"{self.spot_market}".replace("\n", "\n ")
perp_market_info = f"{self.perp_market}".replace("\n", "\n ")
return f"""« GroupSlot[{self.index}] {self.base_instrument}
Base Token Info:
{base_token_bank}
Quote Token Info:
{quote_token_bank}
Oracle: {self.oracle}
Spot Market:
{spot_market_info}
Perp Market:
{perp_market_info}
»"""
def __repr__(self) -> str:
return f"{self}"
# # 🥭 Group class
#
# `Group` defines root functionality for Mango Markets.
#
class Group(AddressableAccount):
def __init__(
self,
account_info: AccountInfo,
version: Version,
name: str,
meta_data: Metadata,
shared_quote: TokenBank,
slot_indices: typing.Sequence[bool],
slots: typing.Sequence[GroupSlot],
signer_nonce: Decimal,
signer_key: PublicKey,
admin: PublicKey,
serum_program_address: PublicKey,
cache: PublicKey,
valid_interval: Decimal,
insurance_vault: PublicKey,
srm_vault: PublicKey,
msrm_vault: PublicKey,
fees_vault: PublicKey,
max_mango_accounts: Decimal,
num_mango_accounts: Decimal,
referral_surcharge_centibps: Decimal,
referral_share_centibps: Decimal,
referral_mngo_required: Decimal,
) -> None:
super().__init__(account_info)
self.version: Version = version
self.name: str = name
self.meta_data: Metadata = meta_data
self.shared_quote: TokenBank = shared_quote
self.slot_indices: typing.Sequence[bool] = slot_indices
self.slots: typing.Sequence[GroupSlot] = slots
self.signer_nonce: Decimal = signer_nonce
self.signer_key: PublicKey = signer_key
self.admin: PublicKey = admin
self.serum_program_address: PublicKey = serum_program_address
self.cache: PublicKey = cache
self.valid_interval: Decimal = valid_interval
self.insurance_vault: PublicKey = insurance_vault
self.srm_vault: PublicKey = srm_vault
self.msrm_vault: PublicKey = msrm_vault
self.fees_vault: PublicKey = fees_vault
self.max_mango_accounts: Decimal = max_mango_accounts
self.num_mango_accounts: Decimal = num_mango_accounts
self.referral_surcharge_centibps: Decimal = referral_surcharge_centibps
self.referral_share_centibps: Decimal = referral_share_centibps
self.referral_mngo_required: Decimal = referral_mngo_required
@property
def shared_quote_token(self) -> Token:
return Token.ensure(self.shared_quote.token)
@property
def liquidity_incentive_token_bank(self) -> TokenBank:
for token_bank in self.tokens:
if token_bank.token.symbol_matches("MNGO"):
return token_bank
raise Exception(
f"Could not find token info for symbol 'MNGO' in group {self.address}"
)
@property
def liquidity_incentive_token(self) -> Token:
return Token.ensure(self.liquidity_incentive_token_bank.token)
@property
def tokens(self) -> typing.Sequence[TokenBank]:
return [*self.base_tokens, self.shared_quote]
@property
def tokens_by_index(self) -> typing.Sequence[typing.Optional[TokenBank]]:
return [*self.base_tokens_by_index, self.shared_quote]
@property
def slots_by_index(self) -> typing.Sequence[typing.Optional[GroupSlot]]:
mapped_items: typing.List[typing.Optional[GroupSlot]] = []
slot_counter = 0
for available in self.slot_indices:
if available:
mapped_items += [self.slots[slot_counter]]
slot_counter += 1
else:
mapped_items += [None]
return mapped_items
@property
def base_tokens(self) -> typing.Sequence[TokenBank]:
return [
slot.base_token_bank
for slot in self.slots
if slot.base_token_bank is not None
]
@property
def base_tokens_by_index(self) -> typing.Sequence[typing.Optional[TokenBank]]:
return [
slot.base_token_bank if slot is not None else None
for slot in self.slots_by_index
]
@property
def oracles(self) -> typing.Sequence[PublicKey]:
return [slot.oracle for slot in self.slots if slot.oracle is not None]
@property
def oracles_by_index(self) -> typing.Sequence[typing.Optional[PublicKey]]:
return [
slot.oracle if slot is not None else None for slot in self.slots_by_index
]
@property
def spot_markets(self) -> typing.Sequence[GroupSlotSpotMarket]:
return [slot.spot_market for slot in self.slots if slot.spot_market is not None]
@property
def spot_markets_by_index(
self,
) -> typing.Sequence[typing.Optional[GroupSlotSpotMarket]]:
return [
slot.spot_market if slot is not None else None
for slot in self.slots_by_index
]
@property
def perp_markets(self) -> typing.Sequence[GroupSlotPerpMarket]:
return [slot.perp_market for slot in self.slots if slot.perp_market is not None]
@property
def perp_markets_by_index(
self,
) -> typing.Sequence[typing.Optional[GroupSlotPerpMarket]]:
return [
slot.perp_market if slot is not None else None
for slot in self.slots_by_index
]
@staticmethod
def from_layout(
layout: typing.Any,
name: str,
account_info: AccountInfo,
version: Version,
instrument_lookup: InstrumentLookup,
market_lookup: MarketLookup,
) -> "Group":
meta_data: Metadata = Metadata.from_layout(layout.meta_data)
tokens: typing.List[typing.Optional[TokenBank]] = [
TokenBank.from_layout_or_none(t, instrument_lookup) for t in layout.tokens
]
# By convention, the shared quote token is always at the end.
quote_token_bank: typing.Optional[TokenBank] = tokens[-1]
if quote_token_bank is None:
raise Exception("Could not find quote token info at end of group tokens.")
slots: typing.List[GroupSlot] = []
in_slots: typing.List[bool] = []
for index in range(len(tokens) - 1):
spot_market_info: typing.Optional[
GroupSlotSpotMarket
] = GroupSlotSpotMarket.from_layout_or_none(layout.spot_markets[index])
perp_market_info: typing.Optional[
GroupSlotPerpMarket
] = GroupSlotPerpMarket.from_layout_or_none(layout.perp_markets[index])
if (spot_market_info is None) and (perp_market_info is None):
in_slots += [False]
else:
perp_lot_size_converter: LotSizeConverter = RaisingLotSizeConverter()
base_token_bank: typing.Optional[TokenBank] = tokens[index]
base_instrument: Instrument
if base_token_bank is not None:
base_instrument = base_token_bank.token
else:
# It's possible there's no underlying SPL token and we have a pure PERP market.
if perp_market_info is None:
raise Exception(
f"Cannot find base token or perp market info for index {index}"
)
perp_market = market_lookup.find_by_address(
perp_market_info.address
)
if perp_market is None:
in_slots += [False]
logging.warning(
f"Group cannot find base token or perp market for index {index} - {perp_market_info}"
)
continue
base_instrument = perp_market.base
if perp_market_info is not None:
perp_lot_size_converter = LotSizeConverter(
base_instrument,
perp_market_info.base_lot_size,
quote_token_bank.token,
perp_market_info.quote_lot_size,
)
oracle: PublicKey = layout.oracles[index]
slot: GroupSlot = GroupSlot(
index,
base_instrument,
base_token_bank,
quote_token_bank,
spot_market_info,
perp_market_info,
perp_lot_size_converter,
oracle,
)
slots += [slot]
in_slots += [True]
signer_nonce: Decimal = layout.signer_nonce
signer_key: PublicKey = layout.signer_key
admin: PublicKey = layout.admin
serum_program_address: PublicKey = layout.serum_program_address
cache_address: PublicKey = layout.cache
valid_interval: Decimal = layout.valid_interval
insurance_vault: PublicKey = layout.insurance_vault
srm_vault: PublicKey = layout.srm_vault
msrm_vault: PublicKey = layout.msrm_vault
fees_vault: PublicKey = layout.fees_vault
max_mango_accounts: Decimal = layout.max_mango_accounts
num_mango_accounts: Decimal = layout.num_mango_accounts
referral_surcharge_centibps: Decimal = layout.referral_surcharge_centibps
referral_share_centibps: Decimal = layout.referral_share_centibps
referral_mngo_required: Decimal = layout.referral_mngo_required
return Group(
account_info,
version,
name,
meta_data,
quote_token_bank,
in_slots,
slots,
signer_nonce,
signer_key,
admin,
serum_program_address,
cache_address,
valid_interval,
insurance_vault,
srm_vault,
msrm_vault,
fees_vault,
max_mango_accounts,
num_mango_accounts,
referral_surcharge_centibps,
referral_share_centibps,
referral_mngo_required,
)
@staticmethod
def parse(
account_info: AccountInfo,
name: str,
instrument_lookup: InstrumentLookup,
market_lookup: MarketLookup,
) -> "Group":
data = account_info.data
if len(data) != layouts.GROUP.sizeof():
raise Exception(
f"Group data length ({len(data)}) does not match expected size ({layouts.GROUP.sizeof()})"
)
layout = layouts.GROUP.parse(data)
return Group.from_layout(
layout, name, account_info, Version.V3, instrument_lookup, market_lookup
)
@staticmethod
def parse_with_context(context: Context, account_info: AccountInfo) -> "Group":
name = context.lookup_group_name(account_info.address)
return Group.parse(
account_info, name, context.instrument_lookup, context.market_lookup
)
@staticmethod
def load(context: Context, address: typing.Optional[PublicKey] = None) -> "Group":
group_address: PublicKey = address or context.group_address
account_info = AccountInfo.load(context, group_address)
if account_info is None:
raise Exception(f"Group account not found at address '{group_address}'")
name = context.lookup_group_name(account_info.address)
return Group.parse(
account_info, name, context.instrument_lookup, context.market_lookup
)
def slot_by_spot_market_address(self, spot_market_address: PublicKey) -> GroupSlot:
for slot in self.slots:
if (
slot.spot_market is not None
and slot.spot_market.address == spot_market_address
):
return slot
raise Exception(
f"Could not find spot market {spot_market_address} in group {self.address}"
)
def slot_by_perp_market_address(self, perp_market_address: PublicKey) -> GroupSlot:
for slot in self.slots:
if (
slot.perp_market is not None
and slot.perp_market.address == perp_market_address
):
return slot
raise Exception(
f"Could not find perp market {perp_market_address} in group {self.address}"
)
def slot_by_instrument_or_none(
self, instrument: Instrument
) -> typing.Optional[GroupSlot]:
for slot in self.slots:
if slot.base_instrument == instrument:
return slot
return None
def slot_by_instrument(self, instrument: Instrument) -> GroupSlot:
slot: typing.Optional[GroupSlot] = self.slot_by_instrument_or_none(instrument)
if slot is not None:
return slot
raise Exception(f"Could not find slot for {instrument} in group {self.address}")
def token_bank_by_instrument(self, instrument: Instrument) -> TokenBank:
for token_bank in self.tokens:
if token_bank.token == instrument:
return token_bank
raise Exception(f"Could not find token {instrument} in group {self.address}")
def token_price_from_cache(
self, cache: Cache, token: Instrument
) -> InstrumentValue:
if token == self.shared_quote_token:
# 1 USDC is always worth 1 USDC
return InstrumentValue(self.shared_quote_token, Decimal(1))
else:
market_cache: MarketCache = self.market_cache_from_cache(cache, token)
return market_cache.adjusted_price(token, self.shared_quote_token)
def perp_market_cache_from_cache(
self, cache: Cache, token: Instrument
) -> typing.Optional[PerpMarketCache]:
market_cache: MarketCache = self.market_cache_from_cache(cache, token)
return market_cache.perp_market
def market_cache_from_cache_or_none(
self, cache: Cache, instrument: Instrument
) -> typing.Optional[MarketCache]:
slot: typing.Optional[GroupSlot] = self.slot_by_instrument_or_none(instrument)
if slot is None:
return None
instrument_index: int = slot.index
return cache.market_cache_for_index(instrument_index)
def market_cache_from_cache(
self, cache: Cache, instrument: Instrument
) -> MarketCache:
market_cache: typing.Optional[
MarketCache
] = self.market_cache_from_cache_or_none(cache, instrument)
if market_cache is not None:
return market_cache
raise Exception(
f"Could not find market cache for instrument {instrument.symbol}"
)
def fetch_cache(self, context: Context) -> Cache:
return Cache.load(context, self.cache)
def derive_referrer_record_address(self, context: Context, id: str) -> PublicKey:
if not isinstance(id, str):
raise Exception(f"Referrer ID '{id}' is not a string")
id_bytes = id.encode("utf-8")
if len(id_bytes) > 32:
raise Exception(f"Referrer ID '{id}' is too long - maximum is 32 bytes")
id_bytes_padded = id_bytes.ljust(32, b"\0")
referrer_record_address_and_nonce: typing.Tuple[
PublicKey, int
] = PublicKey.find_program_address(
[bytes(self.address), b"ReferrerIdRecord", id_bytes_padded],
context.mango_program_address,
)
return referrer_record_address_and_nonce[0]
def __str__(self) -> str:
slot_count = len(self.slots)
slots = "\n ".join(
[f"{item}".replace("\n", "\n ") for item in self.slots]
)
return f"""« Group {self.version} [{self.address}]
{self.meta_data}
Name: {self.name}
Signer [Nonce: {self.signer_nonce}]: {self.signer_key}
Admin: {self.admin}
DEX Program ID: {self.serum_program_address}
Cache: {self.cache}
Insurance Vault: {self.insurance_vault}
SRM Vault: {self.srm_vault}
MSRM Vault: {self.msrm_vault}
Fees Vault: {self.fees_vault}
Num Accounts: {self.num_mango_accounts:,} out of {self.max_mango_accounts:,}
Valid Interval: {self.valid_interval}
Referral:
Surcharge: {self.referral_surcharge_centibps}
Share: {self.referral_share_centibps}
MNGO Required: {self.referral_mngo_required}
Basket [{slot_count} markets]:
{slots}
»"""