mango-explorer/mango/account.py

384 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# # ⚠ 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 typing
from decimal import Decimal
from solana.publickey import PublicKey
from solana.rpc.types import MemcmpOpts
from .accountinfo import AccountInfo
from .addressableaccount import AddressableAccount
from .context import Context
from .encoding import encode_key
from .group import Group
from .instrumentvalue import InstrumentValue
from .layouts import layouts
from .metadata import Metadata
from .openorders import OpenOrders
from .orders import Side
from .perpaccount import PerpAccount
from .perpopenorders import PerpOpenOrders
from .placedorder import PlacedOrder
from .token import Instrument, Token
from .tokeninfo import TokenInfo
from .version import Version
# # 🥭 AccountSlot class
#
# `AccountSlot` gathers slot items together instead of separate arrays.
#
class AccountSlot:
def __init__(self, index: int, base_instrument: Instrument, base_token_info: typing.Optional[TokenInfo], quote_token_info: TokenInfo, raw_deposit: Decimal, deposit: InstrumentValue, raw_borrow: Decimal, borrow: InstrumentValue, spot_open_orders: typing.Optional[PublicKey], perp_account: typing.Optional[PerpAccount]) -> None:
self.index: int = index
self.base_instrument: Instrument = base_instrument
self.base_token_info: typing.Optional[TokenInfo] = base_token_info
self.quote_token_info: TokenInfo = quote_token_info
self.raw_deposit: Decimal = raw_deposit
self.deposit: InstrumentValue = deposit
self.raw_borrow: Decimal = raw_borrow
self.borrow: InstrumentValue = borrow
self.spot_open_orders: typing.Optional[PublicKey] = spot_open_orders
self.perp_account: typing.Optional[PerpAccount] = perp_account
@property
def net_value(self) -> InstrumentValue:
return self.deposit - self.borrow
@property
def raw_net_value(self) -> Decimal:
return self.raw_deposit - self.raw_borrow
def __str__(self) -> str:
perp_account: str = "None"
if self.perp_account is not None:
perp_account = f"{self.perp_account}".replace("\n", "\n ")
return f"""« 𝙰𝚌𝚌𝚘𝚞𝚗𝚝𝚂𝚕𝚘𝚝 {self.base_instrument.symbol}
Net Value: {self.net_value}
Deposited: {self.deposit} (raw value: {self.raw_deposit})
Borrowed: {self.borrow} (raw value {self.raw_borrow})
Spot OpenOrders: {self.spot_open_orders or "None"}
Perp Account:
{perp_account}
»"""
def __repr__(self) -> str:
return f"{self}"
# # 🥭 Account class
#
# `Account` holds information about the account for a particular user/wallet for a particualr `Group`.
#
class Account(AddressableAccount):
def __init__(self, account_info: AccountInfo, version: Version,
meta_data: Metadata, group_name: str, group_address: PublicKey, owner: PublicKey,
info: str, shared_quote: AccountSlot,
in_margin_basket: typing.Sequence[bool],
slot_indices: typing.Sequence[bool],
base_slots: typing.Sequence[AccountSlot],
msrm_amount: Decimal, being_liquidated: bool, is_bankrupt: bool) -> None:
super().__init__(account_info)
self.version: Version = version
self.meta_data: Metadata = meta_data
self.group_name: str = group_name
self.group_address: PublicKey = group_address
self.owner: PublicKey = owner
self.info: str = info
self.shared_quote: AccountSlot = shared_quote
self.in_margin_basket: typing.Sequence[bool] = in_margin_basket
self.slot_indices: typing.Sequence[bool] = slot_indices
self.base_slots: typing.Sequence[AccountSlot] = base_slots
self.msrm_amount: Decimal = msrm_amount
self.being_liquidated: bool = being_liquidated
self.is_bankrupt: bool = is_bankrupt
@property
def shared_quote_token(self) -> Token:
token_info = self.shared_quote.base_token_info
if token_info is None:
raise Exception(f"Shared quote does not have a token: {self.shared_quote}")
return Token.ensure(token_info.token)
@property
def slots(self) -> typing.Sequence[AccountSlot]:
return [*[slot for slot in self.base_slots], self.shared_quote]
@property
def slots_by_index(self) -> typing.Sequence[typing.Optional[AccountSlot]]:
mapped_items: typing.List[typing.Optional[AccountSlot]] = []
slot_counter = 0
for available in self.slot_indices:
if available:
mapped_items += [self.base_slots[slot_counter]]
slot_counter += 1
else:
mapped_items += [None]
mapped_items += [self.shared_quote]
return mapped_items
@property
def deposits(self) -> typing.Sequence[InstrumentValue]:
return [slot.deposit for slot in self.slots]
@property
def deposits_by_index(self) -> typing.Sequence[typing.Optional[InstrumentValue]]:
return [slot.deposit if slot is not None else None for slot in self.slots_by_index]
@property
def borrows(self) -> typing.Sequence[InstrumentValue]:
return [slot.borrow for slot in self.slots]
@property
def borrows_by_index(self) -> typing.Sequence[typing.Optional[InstrumentValue]]:
return [slot.borrow if slot is not None else None for slot in self.slots_by_index]
@property
def net_values(self) -> typing.Sequence[InstrumentValue]:
return [slot.net_value for slot in self.slots]
@property
def net_values_by_index(self) -> typing.Sequence[typing.Optional[InstrumentValue]]:
return [slot.net_value if slot is not None else None for slot in self.slots_by_index]
@property
def spot_open_orders(self) -> typing.Sequence[PublicKey]:
return [slot.spot_open_orders for slot in self.base_slots if slot.spot_open_orders is not None]
@property
def spot_open_orders_by_index(self) -> typing.Sequence[typing.Optional[PublicKey]]:
return [slot.spot_open_orders if slot is not None else None for slot in self.slots_by_index]
@property
def perp_accounts(self) -> typing.Sequence[PerpAccount]:
return [slot.perp_account for slot in self.base_slots if slot.perp_account is not None]
@property
def perp_accounts_by_index(self) -> typing.Sequence[typing.Optional[PerpAccount]]:
return [slot.perp_account if slot is not None else None for slot in self.slots_by_index]
@staticmethod
def from_layout(layout: typing.Any, account_info: AccountInfo, version: Version, group: Group) -> "Account":
meta_data = Metadata.from_layout(layout.meta_data)
owner: PublicKey = layout.owner
info: str = layout.info
mngo_token = group.liquidity_incentive_token
in_margin_basket: typing.Sequence[bool] = list([bool(in_basket) for in_basket in layout.in_margin_basket])
active_in_basket: typing.List[bool] = []
slots: typing.List[AccountSlot] = []
placed_orders_all_markets: typing.List[typing.List[PlacedOrder]] = [[]
for _ in range(len(group.slot_indices) - 1)]
for index, order_market in enumerate(layout.order_market):
if order_market != 0xFF:
side = Side.from_value(layout.order_side[index])
id = layout.order_ids[index]
client_id = layout.client_order_ids[index]
placed_order = PlacedOrder(id, client_id, side)
placed_orders_all_markets[int(order_market)] += [placed_order]
quote_token_info: TokenInfo = group.shared_quote
quote_token: Token = group.shared_quote_token
for index in range(len(group.slots_by_index)):
group_slot = group.slots_by_index[index]
if group_slot is not None:
instrument = group_slot.base_instrument
token_info = group_slot.base_token_info
raw_deposit: Decimal = Decimal(0)
intrinsic_deposit: Decimal = Decimal(0)
raw_borrow: Decimal = Decimal(0)
intrinsic_borrow: Decimal = Decimal(0)
if token_info is not None:
raw_deposit = layout.deposits[index]
intrinsic_deposit = token_info.root_bank.deposit_index * raw_deposit
raw_borrow = layout.borrows[index]
intrinsic_borrow = token_info.root_bank.borrow_index * raw_borrow
deposit = InstrumentValue(instrument, instrument.shift_to_decimals(intrinsic_deposit))
borrow = InstrumentValue(instrument, instrument.shift_to_decimals(intrinsic_borrow))
perp_open_orders = PerpOpenOrders(placed_orders_all_markets[index])
perp_account = PerpAccount.from_layout(
layout.perp_accounts[index],
instrument,
quote_token,
perp_open_orders,
group_slot.perp_lot_size_converter,
mngo_token)
spot_open_orders = layout.spot_open_orders[index]
account_slot: AccountSlot = AccountSlot(index, instrument, token_info, quote_token_info,
raw_deposit, deposit, raw_borrow, borrow,
spot_open_orders, perp_account)
slots += [account_slot]
active_in_basket += [True]
else:
active_in_basket += [False]
raw_quote_deposit: Decimal = layout.deposits[-1]
intrinsic_quote_deposit = quote_token_info.root_bank.deposit_index * raw_quote_deposit
quote_deposit = InstrumentValue(quote_token, quote_token.shift_to_decimals(intrinsic_quote_deposit))
raw_quote_borrow: Decimal = layout.borrows[-1]
intrinsic_quote_borrow = quote_token_info.root_bank.borrow_index * raw_quote_borrow
quote_borrow = InstrumentValue(quote_token, quote_token.shift_to_decimals(intrinsic_quote_borrow))
quote: AccountSlot = AccountSlot(len(layout.deposits) - 1, quote_token_info.token, quote_token_info,
quote_token_info, raw_quote_deposit, quote_deposit, raw_quote_borrow,
quote_borrow, None, None)
msrm_amount: Decimal = layout.msrm_amount
being_liquidated: bool = bool(layout.being_liquidated)
is_bankrupt: bool = bool(layout.is_bankrupt)
return Account(account_info, version, meta_data, group.name, group.address, owner, info, quote, in_margin_basket, active_in_basket, slots, msrm_amount, being_liquidated, is_bankrupt)
@staticmethod
def parse(account_info: AccountInfo, group: Group) -> "Account":
data = account_info.data
if len(data) != layouts.MANGO_ACCOUNT.sizeof():
raise Exception(
f"Account data length ({len(data)}) does not match expected size ({layouts.MANGO_ACCOUNT.sizeof()})")
layout = layouts.MANGO_ACCOUNT.parse(data)
return Account.from_layout(layout, account_info, Version.V3, group)
@staticmethod
def load(context: Context, address: PublicKey, group: Group) -> "Account":
account_info = AccountInfo.load(context, address)
if account_info is None:
raise Exception(f"Account account not found at address '{address}'")
return Account.parse(account_info, group)
@staticmethod
def load_all(context: Context, group: Group) -> typing.Sequence["Account"]:
# mango_group is just after the METADATA, which is the first entry.
group_offset = layouts.METADATA.sizeof()
# owner is just after mango_group in the layout, and it's a PublicKey which is 32 bytes.
filters = [
MemcmpOpts(
offset=group_offset,
bytes=encode_key(group.address)
)
]
results = context.client.get_program_accounts(
context.mango_program_address, memcmp_opts=filters, data_size=layouts.MANGO_ACCOUNT.sizeof())
accounts = []
for account_data in results:
address = PublicKey(account_data["pubkey"])
account_info = AccountInfo._from_response_values(account_data["account"], address)
account = Account.parse(account_info, group)
accounts += [account]
return accounts
@staticmethod
def load_all_for_owner(context: Context, owner: PublicKey, group: Group) -> typing.Sequence["Account"]:
# mango_group is just after the METADATA, which is the first entry.
group_offset = layouts.METADATA.sizeof()
# owner is just after mango_group in the layout, and it's a PublicKey which is 32 bytes.
owner_offset = group_offset + 32
filters = [
MemcmpOpts(
offset=group_offset,
bytes=encode_key(group.address)
),
MemcmpOpts(
offset=owner_offset,
bytes=encode_key(owner)
)
]
results = context.client.get_program_accounts(
context.mango_program_address, memcmp_opts=filters, data_size=layouts.MANGO_ACCOUNT.sizeof())
accounts = []
for account_data in results:
address = PublicKey(account_data["pubkey"])
account_info = AccountInfo._from_response_values(account_data["account"], address)
account = Account.parse(account_info, group)
accounts += [account]
return accounts
@staticmethod
def load_for_owner_by_address(context: Context, owner: PublicKey, group: Group, account_address: typing.Optional[PublicKey]) -> "Account":
if account_address is not None:
return Account.load(context, account_address, group)
accounts: typing.Sequence[Account] = Account.load_all_for_owner(context, owner, group)
if len(accounts) > 1:
raise Exception(f"More than 1 Mango account for owner '{owner}' and which to choose not specified.")
return accounts[0]
def slot_by_instrument_or_none(self, instrument: Instrument) -> typing.Optional[AccountSlot]:
for slot in self.slots:
if slot.base_instrument == instrument:
return slot
return None
def slot_by_instrument(self, instrument: Instrument) -> AccountSlot:
slot: typing.Optional[AccountSlot] = self.slot_by_instrument_or_none(instrument)
if slot is not None:
return slot
raise Exception(f"Could not find token {instrument} in account {self.address}")
def load_all_spot_open_orders(self, context: Context) -> typing.Dict[str, OpenOrders]:
spot_open_orders_account_infos = AccountInfo.load_multiple(context, self.spot_open_orders)
spot_open_orders_account_infos_by_address = {
str(account_info.address): account_info for account_info in spot_open_orders_account_infos}
spot_open_orders: typing.Dict[str, OpenOrders] = {}
for slot in self.base_slots:
if slot.spot_open_orders is not None:
account_info = spot_open_orders_account_infos_by_address[str(slot.spot_open_orders)]
oo = OpenOrders.parse(account_info, slot.base_instrument.decimals,
self.shared_quote.base_instrument.decimals)
spot_open_orders[str(slot.spot_open_orders)] = oo
return spot_open_orders
def update_spot_open_orders_for_market(self, spot_market_index: int, spot_open_orders: PublicKey) -> None:
item_to_update = self.slots_by_index[spot_market_index]
if item_to_update is None:
raise Exception(f"Could not find AccountBasketItem in Account {self.address} at index {spot_market_index}.")
item_to_update.spot_open_orders = spot_open_orders
def __str__(self) -> str:
info = f"'{self.info}'" if self.info else "(𝑢𝑛-𝑛𝑎𝑚𝑒𝑑)"
shared_quote: str = f"{self.shared_quote}".replace("\n", "\n ")
slot_count = len(self.base_slots)
slots = "\n ".join([f"{item}".replace("\n", "\n ") for item in self.base_slots])
symbols: typing.Sequence[str] = [slot.base_instrument.symbol for slot in self.base_slots]
in_margin_basket = ", ".join(symbols) or "None"
return f"""« 𝙰𝚌𝚌𝚘𝚞𝚗𝚝 {info}, {self.version} [{self.address}]
{self.meta_data}
Owner: {self.owner}
Group: « 𝙶𝚛𝚘𝚞𝚙 '{self.group_name}' [{self.group_address}] »
MSRM: {self.msrm_amount}
Bankrupt? {self.is_bankrupt}
Being Liquidated? {self.being_liquidated}
Shared Quote Token:
{shared_quote}
In Basket: {in_margin_basket}
Basket [{slot_count} in basket]:
{slots}
»"""
def __repr__(self) -> str:
return f"{self}"