{ "cells": [ { "cell_type": "markdown", "id": "informative-necklace", "metadata": {}, "source": [ "# ⚠ Warning\n", "\n", "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.\n", "\n", "[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gl/OpinionatedGeek%2Fmango-explorer/HEAD?filepath=BaseModel.ipynb) _🏃‍♀️ To run this notebook press the ⏩ icon in the toolbar above._\n", "\n", "[🥭 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)" ] }, { "cell_type": "markdown", "id": "extreme-secondary", "metadata": {}, "source": [ "# 🥭 BaseModel\n", "\n", "This notebook contains high-level classes to interact with Mango Markets.\n", "\n", "These often depend on structure layouts in [Layouts](Layouts.ipynb).\n", "\n", "The idea is to have one high-level class, with useful members and methods, that can allow the Solana blobs loaded by the layout to vary. So there could be a layouts.SOMETHING_V1 and a layouts.SOMETHING_V2, and the Something class here can load from both.\n", "\n", "Code, in general, depends only on these classes, abstracting away the version of the layout used to load the data.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "generic-advocacy", "metadata": { "jupyter": { "source_hidden": true } }, "outputs": [], "source": [ "import datetime\n", "import enum\n", "import logging\n", "import typing\n", "\n", "import Layouts as layouts\n", "\n", "from decimal import Decimal\n", "from pyserum.market import Market\n", "from solana.publickey import PublicKey\n", "from solana.rpc.commitment import Single\n", "from solana.rpc.types import MemcmpOpts\n", "\n", "from Constants import NUM_MARKETS, NUM_TOKENS, SYSTEM_PROGRAM_ADDRESS\n", "from Context import AccountInfo, Context\n", "from Decoder import encode_key\n" ] }, { "cell_type": "markdown", "id": "sized-region", "metadata": {}, "source": [ "## Version enum\n", "\n", "This is used to keep track of which version of the layout struct was used to load the data.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "sweet-advancement", "metadata": {}, "outputs": [], "source": [ "class Version(enum.Enum):\n", " UNSPECIFIED = 0\n", " V1 = 1\n", " V2 = 2\n", " V3 = 3\n", " V4 = 4\n", " V5 = 5\n" ] }, { "cell_type": "markdown", "id": "divided-research", "metadata": {}, "source": [ "## SerumAccountFlags class\n", "\n", "The Serum prefix is because there's also `MangoAccountFlags` for the Mango-specific flags." ] }, { "cell_type": "code", "execution_count": null, "id": "neither-automation", "metadata": {}, "outputs": [], "source": [ "class SerumAccountFlags:\n", " def __init__(self, version: Version, initialized: bool, market: bool, open_orders: bool, request_queue: bool,\n", " event_queue: bool, bids: bool, asks: bool, disabled: bool):\n", " self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n", " self.version: Version = version\n", " self.initialized = initialized\n", " self.market = market\n", " self.open_orders = open_orders\n", " self.request_queue = request_queue\n", " self.event_queue = event_queue\n", " self.bids = bids\n", " self.asks = asks\n", " self.disabled = disabled\n", "\n", " @staticmethod\n", " def from_layout(layout: layouts.SERUM_ACCOUNT_FLAGS) -> \"SerumAccountFlags\":\n", " return SerumAccountFlags(Version.UNSPECIFIED, layout.initialized, layout.market, layout.open_orders,\n", " layout.request_queue, layout.event_queue, layout.bids, layout.asks, layout.disabled)\n", "\n", " def __str__(self) -> str:\n", " flags = []\n", " if self.initialized: flags += [\"initialized\"]\n", " if self.market: flags += [\"market\"]\n", " if self.open_orders: flags += [\"open_orders\"]\n", " if self.request_queue: flags += [\"request_queue\"]\n", " if self.event_queue: flags += [\"event_queue\"]\n", " if self.bids: flags += [\"bids\"]\n", " if self.asks: flags += [\"asks\"]\n", " if self.disabled: flags += [\"disabled\"]\n", "\n", " flag_text = \" | \".join(flags) or \"None\"\n", " return f\"« SerumAccountFlags: {flag_text} »\"\n", "\n", " def __repr__(self) -> str:\n", " return f\"{self}\"\n" ] }, { "cell_type": "markdown", "id": "pointed-regular", "metadata": {}, "source": [ "## MangoAccountFlags class\n", "\n", "The Mango prefix is because there's also `SerumAccountFlags` for the standard Serum flags." ] }, { "cell_type": "code", "execution_count": null, "id": "fatal-tuning", "metadata": {}, "outputs": [], "source": [ "class MangoAccountFlags:\n", " def __init__(self, version: Version, initialized: bool, group: bool, margin_account: bool, srm_account: bool):\n", " self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n", " self.version: Version = version\n", " self.initialized = initialized\n", " self.group = group\n", " self.margin_account = margin_account\n", " self.srm_account = srm_account\n", "\n", " @staticmethod\n", " def from_layout(layout: layouts.MANGO_ACCOUNT_FLAGS) -> \"MangoAccountFlags\":\n", " return MangoAccountFlags(Version.UNSPECIFIED, layout.initialized, layout.group, layout.margin_account,\n", " layout.srm_account)\n", "\n", " def __str__(self) -> str:\n", " flags = []\n", " if self.initialized: flags += [\"initialized\"]\n", " if self.group: flags += [\"group\"]\n", " if self.margin_account: flags += [\"margin_account\"]\n", " if self.srm_account: flags += [\"srm_account\"]\n", "\n", " flag_text = \" | \".join(flags) or \"None\"\n", " return f\"« MangoAccountFlags: {flag_text} »\"\n", "\n", " def __repr__(self) -> str:\n", " return f\"{self}\"\n" ] }, { "cell_type": "markdown", "id": "drawn-librarian", "metadata": {}, "source": [ "## Index class" ] }, { "cell_type": "code", "execution_count": null, "id": "fewer-champagne", "metadata": {}, "outputs": [], "source": [ "class Index:\n", " def __init__(self, version: Version, last_update: datetime.datetime, borrow: Decimal, deposit: Decimal):\n", " self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n", " self.version: Version = version\n", " self.last_update: datetime.datetime = last_update\n", " self.borrow: Decimal = borrow\n", " self.deposit: Decimal = deposit\n", "\n", " @staticmethod\n", " def from_layout(layout: layouts.INDEX, decimals: Decimal) -> \"Index\":\n", " borrow = layout.borrow / Decimal(10 ** decimals)\n", " deposit = layout.deposit / Decimal(10 ** decimals)\n", " return Index(Version.UNSPECIFIED, layout.last_update, borrow, deposit)\n", "\n", " def __str__(self) -> str:\n", " return f\"« Index: Borrow: {self.borrow:,.8f}, Deposit: {self.deposit:,.8f} [last update: {self.last_update}] »\"\n", "\n", " def __repr__(self) -> str:\n", " return f\"{self}\"\n" ] }, { "cell_type": "markdown", "id": "stone-topic", "metadata": {}, "source": [ "## AggregatorConfig class" ] }, { "cell_type": "code", "execution_count": null, "id": "nuclear-sensitivity", "metadata": {}, "outputs": [], "source": [ "class AggregatorConfig:\n", " def __init__(self, version: Version, description: str, decimals: Decimal, restart_delay: Decimal,\n", " max_submissions: Decimal, min_submissions: Decimal, reward_amount: Decimal,\n", " reward_token_account: PublicKey):\n", " self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n", " self.version: Version = version\n", " self.description: str = description\n", " self.decimals: Decimal = decimals\n", " self.restart_delay: Decimal = restart_delay\n", " self.max_submissions: Decimal = max_submissions\n", " self.min_submissions: Decimal = min_submissions\n", " self.reward_amount: Decimal = reward_amount\n", " self.reward_token_account: PublicKey = reward_token_account\n", "\n", " @staticmethod\n", " def from_layout(layout: layouts.AGGREGATOR_CONFIG) -> \"AggregatorConfig\":\n", " return AggregatorConfig(Version.UNSPECIFIED, layout.description, layout.decimals,\n", " layout.restart_delay, layout.max_submissions, layout.min_submissions,\n", " layout.reward_amount, layout.reward_token_account)\n", "\n", " def __str__(self) -> str:\n", " return f\"« AggregatorConfig: '{self.description}', Decimals: {self.decimals} [restart delay: {self.restart_delay}], Max: {self.max_submissions}, Min: {self.min_submissions}, Reward: {self.reward_amount}, Reward Account: {self.reward_token_account} »\"\n", "\n", " def __repr__(self) -> str:\n", " return f\"{self}\"\n" ] }, { "cell_type": "markdown", "id": "sublime-gnome", "metadata": {}, "source": [ "## Round class" ] }, { "cell_type": "code", "execution_count": null, "id": "approximate-reviewer", "metadata": {}, "outputs": [], "source": [ "class Round:\n", " def __init__(self, version: Version, id: Decimal, created_at: datetime.datetime, updated_at: datetime.datetime):\n", " self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n", " self.version: Version = version\n", " self.id: Decimal = id\n", " self.created_at: datetime.datetime = created_at\n", " self.updated_at: datetime.datetime = updated_at\n", "\n", " @staticmethod\n", " def from_layout(layout: layouts.ROUND) -> \"Round\":\n", " return Round(Version.UNSPECIFIED, layout.id, layout.created_at, layout.updated_at)\n", "\n", " def __str__(self) -> str:\n", " return f\"« Round[{self.id}], Created: {self.updated_at}, Updated: {self.updated_at} »\"\n", "\n", " def __repr__(self) -> str:\n", " return f\"{self}\"\n" ] }, { "cell_type": "markdown", "id": "minor-infrastructure", "metadata": {}, "source": [ "## Answer class" ] }, { "cell_type": "code", "execution_count": null, "id": "careful-aerospace", "metadata": {}, "outputs": [], "source": [ "class Answer:\n", " def __init__(self, version: Version, round_id: Decimal, median: Decimal, created_at: datetime.datetime, updated_at: datetime.datetime):\n", " self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n", " self.version: Version = version\n", " self.round_id: Decimal = round_id\n", " self.median: Decimal = median\n", " self.created_at: datetime.datetime = created_at\n", " self.updated_at: datetime.datetime = updated_at\n", "\n", " @staticmethod\n", " def from_layout(layout: layouts.ANSWER) -> \"Answer\":\n", " return Answer(Version.UNSPECIFIED, layout.round_id, layout.median, layout.created_at, layout.updated_at)\n", "\n", " def __str__(self) -> str:\n", " return f\"« Answer: Round[{self.round_id}], Median: {self.median:,.8f}, Created: {self.updated_at}, Updated: {self.updated_at} »\"\n", "\n", " def __repr__(self) -> str:\n", " return f\"{self}\"\n" ] }, { "cell_type": "markdown", "id": "informed-agency", "metadata": {}, "source": [ "## Aggregator class" ] }, { "cell_type": "code", "execution_count": null, "id": "comic-formula", "metadata": {}, "outputs": [], "source": [ "class Aggregator:\n", " def __init__(self, version: Version, config: AggregatorConfig, initialized: bool, name: str,\n", " address: PublicKey, owner: PublicKey, round_: Round, round_submissions: PublicKey,\n", " answer: Answer, answer_submissions: PublicKey):\n", " self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n", " self.version: Version = version\n", " self.config: AggregatorConfig = config\n", " self.initialized: bool = initialized\n", " self.name: str = name\n", " self.address: PublicKey = address\n", " self.owner: PublicKey = owner\n", " self.round: Round = round_\n", " self.round_submissions: PublicKey = round_submissions\n", " self.answer: Answer = answer\n", " self.answer_submissions: PublicKey = answer_submissions\n", "\n", " @property\n", " def price(self) -> Decimal:\n", " return self.answer.median / (10 ** self.config.decimals)\n", "\n", " @staticmethod\n", " def from_layout(layout: layouts.AGGREGATOR, context: Context, address: PublicKey) -> \"Aggregator\":\n", " config = AggregatorConfig.from_layout(layout.config)\n", " initialized = bool(layout.initialized)\n", " name = context.lookup_oracle_name(address)\n", " round_ = Round.from_layout(layout.round)\n", " answer = Answer.from_layout(layout.answer)\n", "\n", " return Aggregator(Version.UNSPECIFIED, config, initialized, name, address, layout.owner, round_,\n", " layout.round_submissions, answer, layout.answer_submissions)\n", "\n", " @staticmethod\n", " def parse(data: bytes, context: Context, address: PublicKey) -> \"Aggregator\":\n", " if len(data) != layouts.AGGREGATOR.sizeof():\n", " raise Exception(f\"Data length ({len(data)}) does not match expected size ({layouts.AGGREGATOR.sizeof()})\")\n", "\n", " layout = layouts.AGGREGATOR.parse(data)\n", " return Aggregator.from_layout(layout, context, address)\n", "\n", " @staticmethod\n", " def load(context: Context, account_address: PublicKey):\n", " aggregator_result = context.load_account(account_address)\n", " return Aggregator.parse(aggregator_result.data, context, account_address)\n", "\n", " def __str__(self) -> str:\n", " return f\"\"\"\n", "« Aggregator '{self.name}' [{self.version}]:\n", " Config: {self.config}\n", " Initialized: {self.initialized}\n", " Owner: {self.owner}\n", " Round: {self.round}\n", " Round Submissions: {self.round_submissions}\n", " Answer: {self.answer}\n", " Answer Submissions: {self.answer_submissions}\n", "»\n", "\"\"\"\n", "\n", " def __repr__(self) -> str:\n", " return f\"{self}\"\n" ] }, { "cell_type": "markdown", "id": "unknown-bulgaria", "metadata": {}, "source": [ "## BasketTokenMetadata class\n" ] }, { "cell_type": "code", "execution_count": null, "id": "raised-cancer", "metadata": {}, "outputs": [], "source": [ "class BasketTokenMetadata:\n", " def __init__(self, name: str, mint: PublicKey, decimals: Decimal, vault: PublicKey, index: Index):\n", " self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n", " self.name: str = name\n", " self.mint: PublicKey = mint\n", " self.decimals: Decimal = decimals\n", " self.vault: PublicKey = vault\n", " self.index: Index = index\n", "\n", " def round(self, value: Decimal) -> Decimal:\n", " rounded = round(value, int(self.decimals))\n", " return Decimal(rounded)\n", "\n", " def __str__(self) -> str:\n", " return f\"\"\"« Token '{self.name}' [{self.mint} ({self.decimals} decimals)]:\n", " Vault: {self.vault}\n", " Index: {self.index}\n", "»\"\"\"\n", "\n", " def __repr__(self) -> str:\n", " return f\"{self}\"\n" ] }, { "cell_type": "markdown", "id": "increasing-chile", "metadata": {}, "source": [ "## TokenValue class\n", "\n", "The `TokenValue` class is a simple way of keeping a token and value together, and displaying them nicely consistently." ] }, { "cell_type": "code", "execution_count": null, "id": "necessary-pattern", "metadata": {}, "outputs": [], "source": [ "class TokenValue:\n", " def __init__(self, token: BasketTokenMetadata, value: Decimal):\n", " self.token = token\n", " self.value = value\n", "\n", " def __str__(self) -> str:\n", " return f\"« TokenValue: {self.value:>18,.8f} {self.token.name} »\"\n", "\n", " def __repr__(self) -> str:\n", " return f\"{self}\"\n" ] }, { "cell_type": "markdown", "id": "third-swing", "metadata": {}, "source": [ "## MarketMetadata class" ] }, { "cell_type": "code", "execution_count": null, "id": "printable-hopkins", "metadata": {}, "outputs": [], "source": [ "class MarketMetadata:\n", " def __init__(self, name: str, address: PublicKey, base: BasketTokenMetadata, quote: BasketTokenMetadata,\n", " spot: PublicKey, oracle: PublicKey, decimals: Decimal):\n", " self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n", " self.name: str = name\n", " self.address: PublicKey = address\n", " self.base: BasketTokenMetadata = base\n", " self.quote: BasketTokenMetadata = quote\n", " self.spot: PublicKey = spot\n", " self.oracle: PublicKey = oracle\n", " self.decimals: Decimal = decimals\n", " self._market = None\n", "\n", " def fetch_market(self, context: Context) -> Market:\n", " if self._market is None:\n", " self._market = Market.load(context.client, self.spot)\n", "\n", " return self._market\n", "\n", " def __str__(self) -> str:\n", " return f\"\"\"« Market '{self.name}' [{self.spot}]:\n", " Base: {self.base}\n", " Quote: {self.quote}\n", " Oracle: {self.oracle} ({self.decimals} decimals)\n", "»\"\"\"\n", "\n", " def __repr__(self) -> str:\n", " return f\"{self}\"\n" ] }, { "cell_type": "markdown", "id": "further-pizza", "metadata": {}, "source": [ "## Group class" ] }, { "cell_type": "code", "execution_count": null, "id": "closed-figure", "metadata": {}, "outputs": [], "source": [ "class Group:\n", " def __init__(self, version: Version, context: Context, address: PublicKey,\n", " account_flags: MangoAccountFlags, tokens: typing.List[BasketTokenMetadata],\n", " markets: typing.List[MarketMetadata],\n", " signer_nonce: Decimal, signer_key: PublicKey, dex_program_id: PublicKey,\n", " total_deposits: typing.List[Decimal], total_borrows: typing.List[Decimal],\n", " maint_coll_ratio: Decimal, init_coll_ratio: Decimal, srm_vault: PublicKey,\n", " admin: PublicKey, borrow_limits: typing.List[Decimal]):\n", " self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n", " self.version: Version = version\n", " self.context: Context = context\n", " self.address: PublicKey = address\n", " self.account_flags: MangoAccountFlags = account_flags\n", " self.tokens: typing.List[BasketTokenMetadata] = tokens\n", " self.markets: typing.List[MarketMetadata] = markets\n", " self.signer_nonce: Decimal = signer_nonce\n", " self.signer_key: PublicKey = signer_key\n", " self.dex_program_id: PublicKey = dex_program_id\n", " self.total_deposits: typing.List[Decimal] = total_deposits\n", " self.total_borrows: typing.List[Decimal] = total_borrows\n", " self.maint_coll_ratio: Decimal = maint_coll_ratio\n", " self.init_coll_ratio: Decimal = init_coll_ratio\n", " self.srm_vault: PublicKey = srm_vault\n", " self.admin: PublicKey = admin\n", " self.borrow_limits: typing.List[Decimal] = borrow_limits\n", "\n", " @property\n", " def shared_quote_token(self) -> BasketTokenMetadata:\n", " return self.tokens[NUM_TOKENS - 1]\n", "\n", " @staticmethod\n", " def from_layout(layout: layouts.GROUP, context: Context) -> \"Group\":\n", " account_flags = MangoAccountFlags.from_layout(layout.account_flags)\n", " indexes = list(map(lambda pair: Index.from_layout(pair[0], pair[1]), zip(layout.indexes, layout.mint_decimals)))\n", "\n", " tokens: typing.List[BasketTokenMetadata] = []\n", " for index in range(NUM_TOKENS):\n", " token_address = layout.tokens[index]\n", " token_name = context.lookup_token_name(token_address)\n", " token = BasketTokenMetadata(token_name, token_address,\n", " layout.mint_decimals[index],\n", " layout.vaults[index],\n", " indexes[index])\n", " tokens += [token]\n", "\n", " markets: typing.List[MarketMetadata] = []\n", " for index in range(NUM_MARKETS):\n", " market_address = layout.spot_markets[index]\n", " market_name = context.lookup_market_name(market_address)\n", " base_name, quote_name = market_name.split(\"/\")\n", " base_token = [token for token in tokens if token.name == base_name][0]\n", " quote_token = [token for token in tokens if token.name == quote_name][0]\n", " market = MarketMetadata(market_name, market_address, base_token, quote_token,\n", " layout.spot_markets[index],\n", " layout.oracles[index],\n", " layout.oracle_decimals[index])\n", " markets += [market]\n", "\n", " maint_coll_ratio = layout.maint_coll_ratio.quantize(Decimal('.01'))\n", " init_coll_ratio = layout.init_coll_ratio.quantize(Decimal('.01'))\n", " return Group(Version.UNSPECIFIED, context, context.group_id, account_flags, tokens, markets,\n", " layout.signer_nonce, layout.signer_key, layout.dex_program_id, layout.total_deposits,\n", " layout.total_borrows, maint_coll_ratio, init_coll_ratio, layout.srm_vault,\n", " layout.admin, layout.borrow_limits)\n", "\n", " @staticmethod\n", " def parse(data: bytes, context: Context) -> \"Group\":\n", " if len(data) != layouts.GROUP.sizeof():\n", " raise Exception(f\"Data length ({len(data)}) does not match expected size ({layouts.GROUP.sizeof()})\")\n", "\n", " layout = layouts.GROUP.parse(data)\n", " return Group.from_layout(layout, context)\n", "\n", " @staticmethod\n", " def load(context: Context):\n", " group_account_info = context.load_account(context.group_id);\n", " return Group.parse(group_account_info.data, context)\n", "\n", " def get_prices(self):\n", " oracles = map(lambda market: Aggregator.load(self.context, market.oracle), self.markets)\n", " return list(map(lambda oracle: oracle.price, oracles)) + [Decimal(1)]\n", "\n", " def __str__(self) -> str:\n", " total_deposits = \"\\n \".join(map(str, self.total_deposits))\n", " total_borrows = \"\\n \".join(map(str, self.total_borrows))\n", " borrow_limits = \"\\n \".join(map(str, self.borrow_limits))\n", " return f\"\"\"\n", "« Group [{self.version}] {self.address}:\n", " Flags: {self.account_flags}\n", " Tokens:\n", "{self.tokens}\n", " Markets:\n", "{self.markets}\n", " DEX Program ID: « {self.dex_program_id} »\n", " SRM Vault: « {self.srm_vault} »\n", " Admin: « {self.admin} »\n", " Signer Nonce: {self.signer_nonce}\n", " Signer Key: « {self.signer_key} »\n", " Initial Collateral Ratio: {self.init_coll_ratio}\n", " Maintenance Collateral Ratio: {self.maint_coll_ratio}\n", " Total Deposits:\n", " {total_deposits}\n", " Total Borrows:\n", " {total_borrows}\n", " Borrow Limits:\n", " {borrow_limits}\n", "»\n", "\"\"\"\n", "\n", " def __repr__(self) -> str:\n", " return f\"{self}\"\n" ] }, { "cell_type": "markdown", "id": "forty-young", "metadata": {}, "source": [ "## TokenAccount class" ] }, { "cell_type": "code", "execution_count": null, "id": "collected-helmet", "metadata": {}, "outputs": [], "source": [ "class TokenAccount:\n", " def __init__(self, version: Version, mint: PublicKey, owner: PublicKey, amount: Decimal):\n", " self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n", " self.version: Version = version\n", " self.mint: PublicKey = mint\n", " self.owner: PublicKey = owner\n", " self.amount: Decimal = amount\n", "\n", " @staticmethod\n", " def from_layout(layout: layouts.TOKEN_ACCOUNT) -> \"TokenAccount\":\n", " return TokenAccount(Version.UNSPECIFIED, layout.mint, layout.owner, layout.amount)\n", "\n", " @staticmethod\n", " def parse(data) -> \"TokenAccount\":\n", " if len(data) != layouts.TOKEN_ACCOUNT.sizeof():\n", " raise Exception(f\"Data length ({len(data)}) does not match expected size ({layouts.TOKEN_ACCOUNT.sizeof()})\")\n", "\n", " layout = layouts.TOKEN_ACCOUNT.parse(data)\n", " return TokenAccount.from_layout(layout)\n", "\n", " def __str__(self) -> str:\n", " return f\"« Token: Mint: {self.mint}, Owner: {self.owner}, Amount: {self.amount} »\"\n", "\n", " def __repr__(self) -> str:\n", " return f\"{self}\"\n" ] }, { "cell_type": "markdown", "id": "suburban-panama", "metadata": {}, "source": [ "## OpenOrders class\n" ] }, { "cell_type": "code", "execution_count": null, "id": "suburban-cycling", "metadata": {}, "outputs": [], "source": [ "class OpenOrders:\n", " def __init__(self, version: Version, address: PublicKey, program_id: PublicKey,\n", " account_flags: SerumAccountFlags, market: PublicKey, owner: PublicKey,\n", " base_token_free: Decimal, base_token_total: Decimal, quote_token_free: Decimal,\n", " quote_token_total: Decimal, free_slot_bits: Decimal, is_bid_bits: Decimal,\n", " orders: typing.List[Decimal], client_ids: typing.List[Decimal],\n", " referrer_rebate_accrued: Decimal):\n", " self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n", " self.version: Version = version\n", " self.address: PublicKey = address\n", " self.program_id: PublicKey = program_id\n", " self.account_flags: SerumAccountFlags = account_flags\n", " self.market: PublicKey = market\n", " self.owner: PublicKey = owner\n", " self.base_token_free: Decimal = base_token_free\n", " self.base_token_total: Decimal = base_token_total\n", " self.quote_token_free: Decimal = quote_token_free\n", " self.quote_token_total: Decimal = quote_token_total\n", " self.free_slot_bits: Decimal = free_slot_bits\n", " self.is_bid_bits: Decimal = is_bid_bits\n", " self.orders: typing.List[Decimal] = orders\n", " self.client_ids: typing.List[Decimal] = client_ids\n", " self.referrer_rebate_accrued: Decimal = referrer_rebate_accrued\n", "\n", " @staticmethod\n", " def from_layout(layout: layouts.OPEN_ORDERS, address: PublicKey, program_id: PublicKey,\n", " base_decimals: Decimal, quote_decimals: Decimal) -> \"OpenOrders\":\n", " account_flags = SerumAccountFlags.from_layout(layout.account_flags)\n", "\n", " base_divisor = 10 ** base_decimals\n", " quote_divisor = 10 ** quote_decimals\n", " base_token_free: Decimal = layout.base_token_free / base_divisor\n", " base_token_total: Decimal = layout.base_token_total / base_divisor\n", " quote_token_free: Decimal = layout.quote_token_free / quote_divisor\n", " quote_token_total: Decimal = layout.quote_token_total / quote_divisor\n", " nonzero_orders: typing.List[Decimal] = list([order for order in layout.orders if order != 0])\n", " nonzero_client_ids: typing.List[Decimal] = list([client_id for client_id in layout.client_ids if client_id != 0])\n", "\n", " return OpenOrders(Version.UNSPECIFIED, address, program_id, account_flags, layout.market,\n", " layout.owner, base_token_free, base_token_total, quote_token_free, quote_token_total,\n", " layout.free_slot_bits, layout.is_bid_bits, nonzero_orders, nonzero_client_ids,\n", " layout.referrer_rebate_accrued)\n", "\n", " @staticmethod\n", " def parse(data: bytes, address: PublicKey, program_id: PublicKey, base_decimals: Decimal, quote_decimals: Decimal) -> \"OpenOrders\":\n", " if len(data) != layouts.OPEN_ORDERS.sizeof():\n", " raise Exception(f\"Data length ({len(data)}) does not match expected size ({layouts.OPEN_ORDERS.sizeof()})\")\n", "\n", " layout = layouts.OPEN_ORDERS.parse(data)\n", " return OpenOrders.from_layout(layout, address, program_id, base_decimals, quote_decimals)\n", "\n", " @staticmethod\n", " def load_raw_open_orders_accounts(context: Context, group: Group) -> typing.List[typing.Tuple[PublicKey, typing.Any]]: # Return type should be typing.Tuple[PublicKey, layouts.OPEN_ORDERS]\n", " filters = [\n", " MemcmpOpts(\n", " offset=layouts.SERUM_ACCOUNT_FLAGS.sizeof() + 37,\n", " bytes=encode_key(group.signer_key)\n", " )\n", " ]\n", "\n", " response = context.client.get_program_accounts(group.dex_program_id, data_size=layouts.OPEN_ORDERS.sizeof(), memcmp_opts=filters, commitment=Single, encoding=\"base64\")\n", " accounts = list(map(lambda pair: AccountInfo._from_response_values(pair[0], pair[1]), [(result[\"account\"], PublicKey(result[\"pubkey\"])) for result in response[\"result\"]]))\n", " structs = list(map(lambda acc: (acc.address, layouts.OPEN_ORDERS.parse(acc.data)), accounts))\n", " return structs\n", "\n", " @staticmethod\n", " def load(context: Context, address: PublicKey, base_decimals: Decimal, quote_decimals: Decimal):\n", " acc = context.load_account(address)\n", " return OpenOrders.parse(acc.data, acc.address, acc.owner, base_decimals, quote_decimals)\n", "\n", " @staticmethod\n", " def load_for_market_and_owner(context: Context, market: PublicKey, owner: PublicKey, program_id: PublicKey, base_decimals: Decimal, quote_decimals: Decimal):\n", " filters = [\n", " MemcmpOpts(\n", " offset=layouts.SERUM_ACCOUNT_FLAGS.sizeof() + 5,\n", " bytes=encode_key(market)\n", " ),\n", " MemcmpOpts(\n", " offset=layouts.SERUM_ACCOUNT_FLAGS.sizeof() + 37,\n", " bytes=encode_key(owner)\n", " )\n", " ]\n", "\n", " response = context.client.get_program_accounts(context.dex_program_id, data_size=layouts.OPEN_ORDERS.sizeof(), memcmp_opts=filters, commitment=Single, encoding=\"base64\")\n", " accounts = list(map(lambda pair: AccountInfo._from_response_values(pair[0], pair[1]), [(result[\"account\"], PublicKey(result[\"pubkey\"])) for result in response[\"result\"]]))\n", " return list(map(lambda acc: OpenOrders.parse(acc.data, acc.address, acc.owner, base_decimals, quote_decimals), accounts))\n", "\n", " def __str__(self) -> str:\n", " orders = \", \".join(map(str, self.orders)) or \"None\"\n", " client_ids = \", \".join(map(str, self.client_ids)) or \"None\"\n", " \n", " return f\"\"\"« OpenOrders:\n", " Flags: {self.account_flags}\n", " Program ID: {self.program_id}\n", " Address: {self.address}\n", " Market: {self.market}\n", " Owner: {self.owner}\n", " Base Token: {self.base_token_free:,.8f} of {self.base_token_total:,.8f}\n", " Quote Token: {self.quote_token_free:,.8f} of {self.quote_token_total:,.8f}\n", " Referrer Rebate Accrued: {self.referrer_rebate_accrued}\n", " Orders:\n", " {orders}\n", " Client IDs:\n", " {client_ids}\n", "»\"\"\"\n", "\n", " def __repr__(self) -> str:\n", " return f\"{self}\"\n" ] }, { "cell_type": "markdown", "id": "explicit-oxford", "metadata": {}, "source": [ "## BalanceSheet class" ] }, { "cell_type": "code", "execution_count": null, "id": "joined-helmet", "metadata": {}, "outputs": [], "source": [ "class BalanceSheet:\n", " def __init__(self, token: typing.Optional[BasketTokenMetadata], liabilities: Decimal, settled_assets: Decimal, unsettled_assets: Decimal):\n", " self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n", " self.token: typing.Optional[BasketTokenMetadata] = token\n", " self.liabilities: Decimal = liabilities\n", " self.settled_assets: Decimal = settled_assets\n", " self.unsettled_assets: Decimal = unsettled_assets\n", "\n", " @property\n", " def assets(self) -> Decimal:\n", " return self.settled_assets + self.unsettled_assets\n", "\n", " @property\n", " def value(self) -> Decimal:\n", " return self.assets - self.liabilities\n", "\n", " @property\n", " def collateral_ratio(self) -> Decimal:\n", " if self.liabilities == Decimal(0):\n", " return Decimal(0)\n", " return self.assets / self.liabilities\n", "\n", " def __str__(self) -> str:\n", " name = \"«Unspecified»\"\n", " if self.token is not None:\n", " name = self.token.name\n", "\n", " return f\"\"\"« BalanceSheet [{name}]:\n", " Assets : {self.assets:>18,.8f}\n", " Settled Assets : {self.settled_assets:>18,.8f}\n", " Unsettled Assets : {self.unsettled_assets:>18,.8f}\n", " Liabilities : {self.liabilities:>18,.8f}\n", " Value : {self.value:>18,.8f}\n", " Collateral Ratio : {self.collateral_ratio:>18,.2%}\n", "»\n", "\"\"\"\n", "\n", " def __repr__(self) -> str:\n", " return f\"{self}\"\n" ] }, { "cell_type": "markdown", "id": "blond-intro", "metadata": {}, "source": [ "## MarginAccount class\n" ] }, { "cell_type": "code", "execution_count": null, "id": "starting-fence", "metadata": {}, "outputs": [], "source": [ "class MarginAccount:\n", " def __init__(self, version: Version, address: PublicKey, account_flags: MangoAccountFlags,\n", " mango_group: PublicKey, owner: PublicKey, deposits: typing.List[Decimal],\n", " borrows: typing.List[Decimal], open_orders: typing.List[PublicKey]):\n", " self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n", " self.version: Version = version\n", " self.address: PublicKey = address\n", " self.account_flags: MangoAccountFlags = account_flags\n", " self.mango_group: PublicKey = mango_group\n", " self.owner: PublicKey = owner\n", " self.deposits: typing.List[Decimal] = deposits\n", " self.borrows: typing.List[Decimal] = borrows\n", " self.open_orders: typing.List[PublicKey] = open_orders\n", " self.open_orders_accounts: typing.List[typing.Optional[OpenOrders]] = [None] * NUM_MARKETS\n", "\n", " @staticmethod\n", " def from_layout(layout: layouts.MARGIN_ACCOUNT, address: PublicKey) -> \"MarginAccount\":\n", " account_flags: MangoAccountFlags = MangoAccountFlags.from_layout(layout.account_flags)\n", " deposits: typing.List[Decimal] = []\n", " for index, deposit in enumerate(layout.deposits):\n", " deposits += [deposit]\n", "\n", " borrows: typing.List[Decimal] = []\n", " for index, borrow in enumerate(layout.borrows):\n", " borrows += [borrow]\n", "\n", " return MarginAccount(Version.UNSPECIFIED, address, account_flags, layout.mango_group,\n", " layout.owner, deposits, borrows, list(layout.open_orders))\n", "\n", " @staticmethod\n", " def parse(data: bytes, address: PublicKey) -> \"MarginAccount\":\n", " if len(data) != layouts.MARGIN_ACCOUNT.sizeof():\n", " raise Exception(f\"Data length ({len(data)}) does not match expected size ({layouts.MARGIN_ACCOUNT.sizeof()})\")\n", "\n", " layout = layouts.MARGIN_ACCOUNT.parse(data)\n", " return MarginAccount.from_layout(layout, address)\n", "\n", " @staticmethod\n", " def load(context: Context, margin_account_address: PublicKey, group: typing.Optional[Group] = None) -> \"MarginAccount\":\n", " account = context.load_account(margin_account_address)\n", " margin_account = MarginAccount.parse(account.data, margin_account_address)\n", " if group is None:\n", " group = Group.load(context)\n", " margin_account.load_open_orders_accounts(context, group)\n", " return margin_account\n", "\n", " @staticmethod\n", " def load_all_for_group(context: Context, program_id: PublicKey, group: Group) -> typing.List[\"MarginAccount\"]:\n", " filters = [\n", " MemcmpOpts(\n", " offset=layouts.MANGO_ACCOUNT_FLAGS.sizeof(), # mango_group is just after the MangoAccountFlags, which is the first entry\n", " bytes=encode_key(group.address)\n", " )\n", " ]\n", " response = context.client.get_program_accounts(program_id, data_size=layouts.MARGIN_ACCOUNT.sizeof(), memcmp_opts=filters, commitment=Single, encoding=\"base64\")\n", " margin_accounts = []\n", " for margin_account_data in response[\"result\"]:\n", " address = PublicKey(margin_account_data[\"pubkey\"])\n", " account = AccountInfo._from_response_values(margin_account_data[\"account\"], address)\n", " margin_account = MarginAccount.parse(account.data, address)\n", " margin_accounts += [margin_account]\n", " return margin_accounts\n", "\n", " @staticmethod\n", " def load_all_for_owner(context: Context, owner: PublicKey, group: typing.Optional[Group] = None) -> typing.List[\"MarginAccount\"]:\n", " if group is None:\n", " group = Group.load(context)\n", "\n", " mango_group_offset = layouts.MANGO_ACCOUNT_FLAGS.sizeof() # mango_group is just after the MangoAccountFlags, which is the first entry.\n", " owner_offset = mango_group_offset + 32 # owner is just after mango_group in the layout, and it's a PublicKey which is 32 bytes.\n", " filters = [\n", " MemcmpOpts(\n", " offset=mango_group_offset,\n", " bytes=encode_key(group.address)\n", " ),\n", " MemcmpOpts(\n", " offset=owner_offset,\n", " bytes=encode_key(owner)\n", " )\n", " ]\n", "\n", " response = context.client.get_program_accounts(context.program_id, data_size=layouts.MARGIN_ACCOUNT.sizeof(), memcmp_opts=filters, commitment=Single, encoding=\"base64\")\n", " margin_accounts = []\n", " for margin_account_data in response[\"result\"]:\n", " address = PublicKey(margin_account_data[\"pubkey\"])\n", " account = AccountInfo._from_response_values(margin_account_data[\"account\"], address)\n", " margin_account = MarginAccount.parse(account.data, address)\n", " margin_account.load_open_orders_accounts(context, group)\n", " margin_accounts += [margin_account]\n", " return margin_accounts\n", "\n", " def load_open_orders_accounts(self, context: Context, group: Group) -> None:\n", " for index, oo in enumerate(self.open_orders):\n", " key = oo\n", " if key != SYSTEM_PROGRAM_ADDRESS:\n", " self.open_orders_accounts[index] = OpenOrders.load(context, key, group.tokens[index].decimals, group.shared_quote_token.decimals)\n", "\n", " def install_open_orders_accounts(self, group: Group, all_open_orders_by_address: typing.Dict[str, typing.Any]) -> None: # Dict value type should be layouts.OPEN_ORDERS\n", " for index, oo in enumerate(self.open_orders):\n", " key = str(oo)\n", " if key in all_open_orders_by_address:\n", " open_orders_layout = all_open_orders_by_address[key]\n", " open_orders = OpenOrders.from_layout(open_orders_layout, oo, group.context.dex_program_id,\n", " group.tokens[index].decimals,\n", " group.shared_quote_token.decimals)\n", " self.open_orders_accounts[index] = open_orders\n", "\n", " def get_intrinsic_balance_sheets(self, group: Group) -> typing.List[BalanceSheet]:\n", " settled_assets: typing.List[Decimal] = [Decimal(0)] * NUM_TOKENS\n", " liabilities: typing.List[Decimal] = [Decimal(0)] * NUM_TOKENS\n", " for index in range(NUM_TOKENS):\n", " settled_assets[index] = group.tokens[index].index.deposit * self.deposits[index]\n", " liabilities[index] = group.tokens[index].index.borrow * self.borrows[index]\n", "\n", " unsettled_assets: typing.List[Decimal] = [Decimal(0)] * NUM_TOKENS\n", " for index in range(NUM_MARKETS):\n", " open_orders_account = self.open_orders_accounts[index]\n", " if open_orders_account is not None:\n", " unsettled_assets[index] += open_orders_account.base_token_total\n", " unsettled_assets[NUM_TOKENS - 1] += open_orders_account.quote_token_total\n", "\n", " balance_sheets: typing.List[BalanceSheet] = []\n", " for index in range(NUM_TOKENS):\n", " balance_sheets += [BalanceSheet(group.tokens[index], liabilities[index],\n", " settled_assets[index], unsettled_assets[index])]\n", "\n", " return balance_sheets\n", "\n", " def get_priced_balance_sheets(self, group: Group, prices: typing.List[Decimal]) -> typing.List[typing.Optional[BalanceSheet]]:\n", " priced: typing.List[typing.Optional[BalanceSheet]] = [None] * NUM_TOKENS\n", " balance_sheets = self.get_intrinsic_balance_sheets(group)\n", " for index, balance_sheet in enumerate(balance_sheets):\n", " if balance_sheet is not None:\n", " liabilities = balance_sheet.liabilities * prices[index]\n", " settled_assets = balance_sheet.settled_assets * prices[index]\n", " unsettled_assets = balance_sheet.unsettled_assets * prices[index]\n", " token_metadata = group.tokens[index]\n", " priced[index] = BalanceSheet(\n", " token_metadata,\n", " token_metadata.round(liabilities),\n", " token_metadata.round(settled_assets),\n", " token_metadata.round(unsettled_assets)\n", " )\n", "\n", " return priced\n", "\n", " def get_balance_sheet_totals(self, group: Group, prices: typing.List[Decimal]) -> BalanceSheet:\n", " liabilities = Decimal(0)\n", " settled_assets = Decimal(0)\n", " unsettled_assets = Decimal(0)\n", "\n", " balance_sheets = self.get_priced_balance_sheets(group, prices)\n", " for balance_sheet in balance_sheets:\n", " if balance_sheet is not None:\n", " liabilities += balance_sheet.liabilities\n", " settled_assets += balance_sheet.settled_assets\n", " unsettled_assets += balance_sheet.unsettled_assets\n", "\n", " return BalanceSheet(None, liabilities, settled_assets, unsettled_assets)\n", "\n", " def get_intrinsic_balances(self, group: Group) -> typing.List[TokenValue]:\n", " balance_sheets = self.get_intrinsic_balance_sheets(group)\n", " balances: typing.List[TokenValue] = []\n", " for index, balance_sheet in enumerate(balance_sheets):\n", " if balance_sheet.token is None:\n", " raise Exception(f\"Intrinsic balance sheet with index [{index}] has no token.\")\n", " balances += [TokenValue(balance_sheet.token, balance_sheet.value)]\n", "\n", " return balances\n", "\n", " def __str__(self) -> str:\n", " deposits = \", \".join([f\"{item:,.8f}\" for item in self.deposits])\n", " borrows = \", \".join([f\"{item:,.8f}\" for item in self.borrows])\n", " if all(oo is None for oo in self.open_orders_accounts):\n", " open_orders = f\"{self.open_orders}\"\n", " else:\n", " open_orders_unindented = f\"{self.open_orders_accounts}\"\n", " open_orders = open_orders_unindented.replace(\"\\n\", \"\\n \")\n", " return f\"\"\"« MarginAccount: {self.address}\n", " Flags: {self.account_flags}\n", " Owner: {self.owner}\n", " Mango Group: {self.mango_group}\n", " Deposits: [{deposits}]\n", " Borrows: [{borrows}]\n", " Mango Open Orders: {open_orders}\n", "»\"\"\"\n", "\n", " def __repr__(self) -> str:\n", " return f\"{self}\"\n" ] }, { "cell_type": "markdown", "id": "documented-jefferson", "metadata": {}, "source": [ "# 🏃 Running" ] }, { "cell_type": "code", "execution_count": null, "id": "supposed-connection", "metadata": {}, "outputs": [], "source": [ "if __name__ == \"__main__\":\n", " logging.getLogger().setLevel(logging.INFO)\n", "\n", " import base64\n", " from Context import default_context\n", "\n", " encoded = \"AwAAAAAAAACCaOmpoURMK6XHelGTaFawcuQ/78/15LAemWI8jrt3SRKLy2R9i60eclDjuDS8+p/ZhvTUd9G7uQVOYCsR6+BhmqGCiO6EPYP2PQkf/VRTvw7JjXvIjPFJy06QR1Cq1WfTonHl0OjCkyEf60SD07+MFJu5pVWNFGGEO/8AiAYfduaKdnFTaZEHPcK5Eq72WWHeHg2yIbBF09kyeOhlCJwOoG8O5SgpPV8QOA64ZNV4aKroFfADg6kEy/wWCdp3fv0O4GJgAAAAAPH6Ud6jtjwAAQAAAAAAAADiDkkCi9UOAAEAAAAAAAAADuBiYAAAAACNS5bSy7soAAEAAAAAAAAACMvgO+2jCwABAAAAAAAAAA7gYmAAAAAAZFeDUBNVhwABAAAAAAAAABtRNytozC8AAQAAAAAAAABIBGiCcyaEZdNhrTyeqUY692vOzzPdHaxAxguht3JQGlkzjtd05dX9LENHkl2z1XvUbTNKZlweypNRetmH0lmQ9VYQAHqylxZVK65gEg85g27YuSyvOBZAjJyRmYU9KdCO1D+4ehdPu9dQB1yI1uh75wShdAaFn2o4qrMYwq3SQQEAAAAAAAAAAiH1PPJKAuh6oGiE35aGhUQhFi/bxgKOudpFv8HEHNCFDy1uAqR6+CTQmradxC1wyyjL+iSft+5XudJWwSdi7wvphsxb96x7Obj/AgAAAAAKlV4LL5ow6r9LMhIAAAAADvsOtqcVFmChDPzPnwAAAE33lx1h8hPFD04AAAAAAAA8YRV3Oa309B2wGwAAAAAA+yPBZRlZz7b605n+AQAAAACgmZmZmZkZAQAAAAAAAAAAMDMzMzMzMwEAAAAAAAAA25D1XcAtRzSuuyx3U+X7aE9vM1EJySU9KprgL0LMJ/vat9+SEEUZuga7O5tTUrcMDYWDg+LYaAWhSQiN2fYk7aCGAQAAAAAAgIQeAAAAAAAA8gUqAQAAAAYGBgICAAAA\"\n", " decoded = base64.b64decode(encoded)\n", "\n", " group = Group.parse(decoded, default_context)\n", " print(\"\\n\\nThis is hard-coded, not live information!\")\n", " print(group)\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.6" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": true, "sideBar": true, "skip_h1_title": false, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": false, "toc_position": {}, "toc_section_display": true, "toc_window_display": true }, "varInspector": { "cols": { "lenName": 16, "lenType": 16, "lenVar": 40 }, "kernels_config": { "python": { "delete_cmd_postfix": "", "delete_cmd_prefix": "del ", "library": "var_list.py", "varRefreshCmd": "print(var_dic_list())" }, "r": { "delete_cmd_postfix": ") ", "delete_cmd_prefix": "rm(", "library": "var_list.r", "varRefreshCmd": "cat(var_dic_list()) " } }, "types_to_exclude": [ "module", "function", "builtin_function_or_method", "instance", "_Feature" ], "window_display": false } }, "nbformat": 4, "nbformat_minor": 5 }