mango-explorer/BaseModel.ipynb

2274 lines
106 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"cells": [
{
"cell_type": "markdown",
"id": "interesting-continuity",
"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": "chemical-paper",
"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": "herbal-bracket",
"metadata": {
"jupyter": {
"source_hidden": true
}
},
"outputs": [],
"source": [
"import abc\n",
"import construct\n",
"import datetime\n",
"import enum\n",
"import json\n",
"import logging\n",
"import time\n",
"import typing\n",
"\n",
"import Layouts as layouts\n",
"\n",
"from decimal import Decimal\n",
"from pyserum.market import Market\n",
"from pyserum.open_orders_account import OpenOrdersAccount\n",
"from solana.account import Account\n",
"from solana.publickey import PublicKey\n",
"from solana.rpc.commitment import Single\n",
"from solana.rpc.types import MemcmpOpts, TokenAccountOpts, RPCMethod, RPCResponse\n",
"from spl.token.client import Token as SplToken\n",
"from spl.token.constants import TOKEN_PROGRAM_ID\n",
"\n",
"from Constants import SOL_DECIMALS, SOL_MINT_ADDRESS, SYSTEM_PROGRAM_ADDRESS\n",
"from Context import Context\n",
"from Decoder import decode_binary, encode_int, encode_binary, encode_key\n"
]
},
{
"cell_type": "markdown",
"id": "aboriginal-active",
"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": "stretch-analyst",
"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": "cooked-india",
"metadata": {},
"source": [
"## InstructionType enum\n",
"\n",
"This `enum` encapsulates all current Mango Market instruction variants.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "composite-scotland",
"metadata": {},
"outputs": [],
"source": [
"class InstructionType(enum.IntEnum):\n",
" InitMangoGroup = 0\n",
" InitMarginAccount = 1\n",
" Deposit = 2\n",
" Withdraw = 3\n",
" Borrow = 4\n",
" SettleBorrow = 5\n",
" Liquidate = 6\n",
" DepositSrm = 7\n",
" WithdrawSrm = 8\n",
" PlaceOrder = 9\n",
" SettleFunds = 10\n",
" CancelOrder = 11\n",
" CancelOrderByClientId = 12\n",
" ChangeBorrowLimit = 13\n",
" PlaceAndSettle = 14\n",
" ForceCancelOrders = 15\n",
" PartialLiquidate = 16\n",
"\n",
" def __str__(self):\n",
" return self.name\n"
]
},
{
"cell_type": "markdown",
"id": "rubber-wisconsin",
"metadata": {},
"source": [
"## Internal functions\n",
"\n",
"These aren't published, they're really just for internal use. If they become more widely used we can move them to a better place."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "certain-particular",
"metadata": {},
"outputs": [],
"source": [
"def _split_list_into_chunks(to_chunk: typing.List, chunk_size: int = 100) -> typing.List[typing.List]:\n",
" chunks = []\n",
" start = 0\n",
" while start < len(to_chunk):\n",
" chunk = to_chunk[start:start + chunk_size]\n",
" chunks += [chunk]\n",
" start += chunk_size\n",
" return chunks\n"
]
},
{
"cell_type": "markdown",
"id": "innovative-conditioning",
"metadata": {},
"source": [
"## AccountInfo class\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "beneficial-weekly",
"metadata": {},
"outputs": [],
"source": [
"class AccountInfo:\n",
" def __init__(self, address: PublicKey, executable: bool, lamports: Decimal, owner: PublicKey, rent_epoch: Decimal, data: bytes):\n",
" self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n",
" self.address: PublicKey = address\n",
" self.executable: bool = executable\n",
" self.lamports: Decimal = lamports\n",
" self.owner: PublicKey = owner\n",
" self.rent_epoch: Decimal = rent_epoch\n",
" self.data: bytes = data\n",
"\n",
" def encoded_data(self) -> typing.List:\n",
" return encode_binary(self.data)\n",
"\n",
" def __str__(self) -> str:\n",
" return f\"\"\"« AccountInfo [{self.address}]:\n",
" Owner: {self.owner}\n",
" Executable: {self.executable}\n",
" Lamports: {self.lamports}\n",
" Rent Epoch: {self.rent_epoch}\n",
"»\"\"\"\n",
"\n",
" def __repr__(self) -> str:\n",
" return f\"{self}\"\n",
"\n",
" @staticmethod\n",
" def load(context: Context, address: PublicKey) -> typing.Optional[\"AccountInfo\"]:\n",
" response: RPCResponse = context.client.get_account_info(address)\n",
" result = context.unwrap_or_raise_exception(response)\n",
" if result[\"value\"] is None:\n",
" return None\n",
"\n",
" return AccountInfo._from_response_values(result[\"value\"], address)\n",
"\n",
" @staticmethod\n",
" def load_multiple(context: Context, addresses: typing.List[PublicKey], chunk_size: int = 100, sleep_between_calls: float = 0.0) -> typing.List[\"AccountInfo\"]:\n",
" # This is a tricky one to get right.\n",
" # Some errors this can generate:\n",
" # 413 Client Error: Payload Too Large for url\n",
" # Error response from server: 'Too many inputs provided; max 100', code: -32602\n",
" address_strings: typing.List[str] = list(map(PublicKey.__str__, addresses))\n",
" multiple: typing.List[AccountInfo] = []\n",
" chunks = _split_list_into_chunks(address_strings, chunk_size)\n",
" for counter, chunk in enumerate(chunks):\n",
" response = context.client._provider.make_request(RPCMethod(\"getMultipleAccounts\"), chunk)\n",
" result = context.unwrap_or_raise_exception(response)\n",
" response_value_list = zip(result[\"value\"], addresses)\n",
" multiple += list(map(lambda pair: AccountInfo._from_response_values(pair[0], pair[1]), response_value_list))\n",
" if (sleep_between_calls > 0.0) and (counter < (len(chunks) - 1)):\n",
" time.sleep(sleep_between_calls)\n",
"\n",
" return multiple\n",
"\n",
" @staticmethod\n",
" def _from_response_values(response_values: typing.Dict[str, typing.Any], address: PublicKey) -> \"AccountInfo\":\n",
" executable = bool(response_values[\"executable\"])\n",
" lamports = Decimal(response_values[\"lamports\"])\n",
" owner = PublicKey(response_values[\"owner\"])\n",
" rent_epoch = Decimal(response_values[\"rentEpoch\"])\n",
" data = decode_binary(response_values[\"data\"])\n",
" return AccountInfo(address, executable, lamports, owner, rent_epoch, data)\n",
"\n",
" @staticmethod\n",
" def from_response(response: RPCResponse, address: PublicKey) -> \"AccountInfo\":\n",
" return AccountInfo._from_response_values(response[\"result\"][\"value\"], address)\n"
]
},
{
"cell_type": "markdown",
"id": "planned-referral",
"metadata": {},
"source": [
"## AddressableAccount class\n",
"\n",
"Some of our most-used objects (like `Group` or `MarginAccount`) are accounts on Solana with packed data. When these are loaded, they're typically loaded by loading the `AccountInfo` and parsing it in an object-specific way.\n",
"\n",
"It's sometimes useful to be able to treat these in a common fashion so we use `AddressableAccount` as a way of sharing common features and providing a common base."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "expired-stopping",
"metadata": {},
"outputs": [],
"source": [
"class AddressableAccount(metaclass=abc.ABCMeta):\n",
" def __init__(self, account_info: AccountInfo):\n",
" self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n",
" self.account_info = account_info\n",
"\n",
" @property\n",
" def address(self) -> PublicKey:\n",
" return self.account_info.address\n",
"\n",
" def __repr__(self) -> str:\n",
" return f\"{self}\"\n"
]
},
{
"cell_type": "markdown",
"id": "unsigned-circus",
"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": "spare-large",
"metadata": {},
"outputs": [],
"source": [
"class SerumAccountFlags:\n",
" def __init__(self, version: Version, initialized: bool, market: bool, open_orders: bool,\n",
" request_queue: bool, 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,\n",
" layout.open_orders, layout.request_queue, layout.event_queue,\n",
" layout.bids, layout.asks, layout.disabled)\n",
"\n",
" def __str__(self) -> str:\n",
" flags: typing.List[typing.Optional[str]] = []\n",
" flags += [\"initialized\" if self.initialized else None]\n",
" flags += [\"market\" if self.market else None]\n",
" flags += [\"open_orders\" if self.open_orders else None]\n",
" flags += [\"request_queue\" if self.request_queue else None]\n",
" flags += [\"event_queue\" if self.event_queue else None]\n",
" flags += [\"bids\" if self.bids else None]\n",
" flags += [\"asks\" if self.asks else None]\n",
" flags += [\"disabled\" if self.disabled else None]\n",
" flag_text = \" | \".join(flag for flag in flags if flag is not None) or \"None\"\n",
" return f\"« SerumAccountFlags: {flag_text} »\"\n",
"\n",
" def __repr__(self) -> str:\n",
" return f\"{self}\"\n"
]
},
{
"cell_type": "markdown",
"id": "substantial-worcester",
"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": "integral-uniform",
"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,\n",
" layout.margin_account, layout.srm_account)\n",
"\n",
" def __str__(self) -> str:\n",
" flags: typing.List[typing.Optional[str]] = []\n",
" flags += [\"initialized\" if self.initialized else None]\n",
" flags += [\"group\" if self.group else None]\n",
" flags += [\"margin_account\" if self.margin_account else None]\n",
" flags += [\"srm_account\" if self.srm_account else None]\n",
" flag_text = \" | \".join(flag for flag in flags if flag is not None) or \"None\"\n",
" return f\"« MangoAccountFlags: {flag_text} »\"\n",
"\n",
" def __repr__(self) -> str:\n",
" return f\"{self}\"\n"
]
},
{
"cell_type": "markdown",
"id": "tired-collins",
"metadata": {},
"source": [
"## Index class"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "designed-syndicate",
"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 [{self.last_update}]: Borrow: {self.borrow:,.8f}, Deposit: {self.deposit:,.8f} »\"\n",
"\n",
" def __repr__(self) -> str:\n",
" return f\"{self}\"\n"
]
},
{
"cell_type": "markdown",
"id": "democratic-thread",
"metadata": {},
"source": [
"## AggregatorConfig class"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "coordinate-headquarters",
"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": "therapeutic-audit",
"metadata": {},
"source": [
"## Round class"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "liquid-solution",
"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": "rural-candidate",
"metadata": {},
"source": [
"## Answer class"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "graphic-release",
"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": "established-exemption",
"metadata": {},
"source": [
"## Aggregator class"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "refined-alexander",
"metadata": {},
"outputs": [],
"source": [
"class Aggregator(AddressableAccount):\n",
" def __init__(self, account_info: AccountInfo, version: Version, config: AggregatorConfig,\n",
" initialized: bool, name: str, owner: PublicKey, round_: Round,\n",
" round_submissions: PublicKey, answer: Answer, answer_submissions: PublicKey):\n",
" super().__init__(account_info)\n",
" self.version: Version = version\n",
" self.config: AggregatorConfig = config\n",
" self.initialized: bool = initialized\n",
" self.name: str = name\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, account_info: AccountInfo, name: str) -> \"Aggregator\":\n",
" config = AggregatorConfig.from_layout(layout.config)\n",
" initialized = bool(layout.initialized)\n",
" round_ = Round.from_layout(layout.round)\n",
" answer = Answer.from_layout(layout.answer)\n",
"\n",
" return Aggregator(account_info, Version.UNSPECIFIED, config, initialized, name, layout.owner,\n",
" round_, layout.round_submissions, answer, layout.answer_submissions)\n",
"\n",
" @staticmethod\n",
" def parse(context: Context, account_info: AccountInfo) -> \"Aggregator\":\n",
" data = account_info.data\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",
" name = context.lookup_oracle_name(account_info.address)\n",
" layout = layouts.AGGREGATOR.parse(data)\n",
" return Aggregator.from_layout(layout, account_info, name)\n",
"\n",
" @staticmethod\n",
" def load(context: Context, account_address: PublicKey):\n",
" account_info = AccountInfo.load(context, account_address)\n",
" if account_info is None:\n",
" raise Exception(f\"Aggregator account not found at address '{account_address}'\")\n",
" return Aggregator.parse(context, account_info)\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"
]
},
{
"cell_type": "markdown",
"id": "varied-batch",
"metadata": {},
"source": [
"## Token class\n",
"\n",
"`Token` defines aspects common to every token."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "three-legislature",
"metadata": {},
"outputs": [],
"source": [
"class Token:\n",
" def __init__(self, symbol: str, name: str, mint: PublicKey, decimals: Decimal):\n",
" self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n",
" self.symbol: str = symbol.upper()\n",
" self.name: str = name\n",
" self.mint: PublicKey = mint\n",
" self.decimals: Decimal = decimals\n",
"\n",
" def round(self, value: Decimal) -> Decimal:\n",
" return round(value, int(self.decimals))\n",
"\n",
" def symbol_matches(self, symbol: str) -> bool:\n",
" return self.symbol.upper() == symbol.upper()\n",
"\n",
" @staticmethod\n",
" def find_by_symbol(values: typing.List[\"Token\"], symbol: str) -> \"Token\":\n",
" found = [value for value in values if value.symbol_matches(symbol)]\n",
" if len(found) == 0:\n",
" raise Exception(f\"Token '{symbol}' not found in token values: {values}\")\n",
"\n",
" if len(found) > 1:\n",
" raise Exception(f\"Token '{symbol}' matched multiple tokens in values: {values}\")\n",
"\n",
" return found[0]\n",
"\n",
" @staticmethod\n",
" def find_by_mint(values: typing.List[\"Token\"], mint: PublicKey) -> \"Token\":\n",
" found = [value for value in values if value.mint == mint]\n",
" if len(found) == 0:\n",
" raise Exception(f\"Token '{mint}' not found in token values: {values}\")\n",
"\n",
" if len(found) > 1:\n",
" raise Exception(f\"Token '{mint}' matched multiple tokens in values: {values}\")\n",
"\n",
" return found[0]\n",
"\n",
" # TokenMetadatas are equal if they have the same mint address.\n",
" def __eq__(self, other):\n",
" if hasattr(other, 'mint'):\n",
" return self.mint == other.mint\n",
" return False\n",
"\n",
" def __str__(self) -> str:\n",
" return f\"« Token '{self.name}' [{self.mint} ({self.decimals} decimals)] »\"\n",
"\n",
" def __repr__(self) -> str:\n",
" return f\"{self}\"\n"
]
},
{
"cell_type": "markdown",
"id": "optimum-packaging",
"metadata": {},
"source": [
"## SolToken object\n",
"\n",
"It's sometimes handy to have a `Token` for SOL, but SOL isn't actually a token and can't appear in baskets. This object defines a special case for SOL.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "resistant-lawsuit",
"metadata": {},
"outputs": [],
"source": [
"SolToken = Token(\"SOL\", \"Pure SOL\", SOL_MINT_ADDRESS, SOL_DECIMALS)"
]
},
{
"cell_type": "markdown",
"id": "dimensional-interference",
"metadata": {},
"source": [
"## TokenLookup class\n",
"\n",
"This class allows us to look up token symbols, names, mint addresses and decimals, all from our Solana static data.\n",
"\n",
"The `_find_data_by_symbol()` is used here and later in the `SpotMarketLookup` class.\n",
"\n",
"The static data is the [Solana token list](https://raw.githubusercontent.com/solana-labs/token-list/main/src/tokens/solana.tokenlist.json) provided by Serum.\n",
"\n",
"You can load a `TokenLookup` class by something like:\n",
"```\n",
"with open(\"solana.tokenlist.json\") as json_file:\n",
" token_data = json.load(json_file)\n",
" token_lookup = TokenLookup(token_data)\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "attached-stocks",
"metadata": {},
"outputs": [],
"source": [
"def _find_data_by_symbol(symbol: str, token_data: typing.Dict) -> typing.Optional[typing.Dict]:\n",
" for token in token_data[\"tokens\"]:\n",
" if token[\"symbol\"] == symbol:\n",
" return token\n",
" return None\n",
"\n",
"\n",
"class TokenLookup:\n",
" def __init__(self, token_data: typing.Dict) -> None:\n",
" self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n",
" self.token_data = token_data\n",
"\n",
" def find_by_symbol(self, symbol: str):\n",
" found = _find_data_by_symbol(symbol, self.token_data)\n",
" if found is not None:\n",
" return Token(found[\"symbol\"], found[\"name\"], PublicKey(found[\"address\"]), Decimal(found[\"decimals\"]))\n",
"\n",
" return None\n",
"\n",
" def find_by_mint(self, mint: PublicKey):\n",
" mint_string: str = str(mint)\n",
" for token in self.token_data[\"tokens\"]:\n",
" if token[\"address\"] == mint_string:\n",
" return Token(token[\"symbol\"], token[\"name\"], PublicKey(token[\"address\"]), Decimal(token[\"decimals\"]))\n",
"\n",
" return None\n",
"\n",
" @staticmethod\n",
" def default_lookups() -> \"TokenLookup\":\n",
" with open(\"solana.tokenlist.json\") as json_file:\n",
" token_data = json.load(json_file)\n",
" return TokenLookup(token_data)"
]
},
{
"cell_type": "markdown",
"id": "informative-fortune",
"metadata": {},
"source": [
"## SpotMarket class"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "rational-drain",
"metadata": {},
"outputs": [],
"source": [
"class SpotMarket:\n",
" def __init__(self, address: PublicKey, base: Token, quote: Token):\n",
" self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n",
" self.address: PublicKey = address\n",
" self.base: Token = base\n",
" self.quote: Token = quote\n",
"\n",
" @property\n",
" def name(self) -> str:\n",
" return f\"{self.base.symbol}/{self.quote.symbol}\"\n",
"\n",
" def __str__(self) -> str:\n",
" return f\"« Market {self.name}: {self.address} »\"\n",
"\n",
" def __repr__(self) -> str:\n",
" return f\"{self}\"\n"
]
},
{
"cell_type": "markdown",
"id": "cordless-diversity",
"metadata": {},
"source": [
"## SpotMarketLookup class\n",
"\n",
"This class allows us to look up Serum market addresses and tokens, all from our Solana static data.\n",
"\n",
"The static data is the [Solana token list](https://raw.githubusercontent.com/solana-labs/token-list/main/src/tokens/solana.tokenlist.json) provided by Serum.\n",
"\n",
"You can load a `SpotMarketLookup` class by something like:\n",
"```\n",
"with open(\"solana.tokenlist.json\") as json_file:\n",
" token_data = json.load(json_file)\n",
" spot_market_lookup = SpotMarketLookup(token_data)\n",
"```\n",
"This uses the same data file as `TokenLookup` but it looks a lot more complicated. The main reason for this is that tokens are described in a list, whereas markets are optional child attributes of tokens.\n",
"\n",
"To find a token, we can just go through the list.\n",
"\n",
"To find a market, we need to split the market symbol into the two token symbols, go through the list, check if the item has the optional `extensions` attribute, and in there see if there is a name-value pair for the particular market we're interested in. Also, the current file only lists USDC and USDT markets, so that's all we can support this way."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "civic-sunset",
"metadata": {},
"outputs": [],
"source": [
"class SpotMarketLookup:\n",
" def __init__(self, token_data: typing.Dict) -> None:\n",
" self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n",
" self.token_data: typing.Dict = token_data\n",
"\n",
" def find_by_symbol(self, symbol: str) -> typing.Optional[SpotMarket]:\n",
" base_symbol, quote_symbol = symbol.split(\"/\")\n",
" base_data = _find_data_by_symbol(base_symbol, self.token_data)\n",
" if base_data is None:\n",
" self.logger.warning(f\"Could not find data for base token '{base_symbol}'\")\n",
" return None\n",
" base = Token(base_data[\"symbol\"], base_data[\"name\"], PublicKey(base_data[\"address\"]), Decimal(base_data[\"decimals\"]))\n",
"\n",
" quote_data = _find_data_by_symbol(quote_symbol, self.token_data)\n",
" if quote_data is None:\n",
" self.logger.warning(f\"Could not find data for quote token '{quote_symbol}'\")\n",
" return None\n",
" quote = Token(quote_data[\"symbol\"], quote_data[\"name\"], PublicKey(quote_data[\"address\"]), Decimal(quote_data[\"decimals\"]))\n",
"\n",
" if \"extensions\" not in base_data:\n",
" self.logger.warning(f\"No markets found for base token '{base.symbol}'.\")\n",
" return None\n",
"\n",
" if quote.symbol == \"USDC\":\n",
" if \"serumV3Usdc\" not in base_data[\"extensions\"]:\n",
" self.logger.warning(f\"No USDC market found for base token '{base.symbol}'.\")\n",
" return None\n",
"\n",
" market_address_string = base_data[\"extensions\"][\"serumV3Usdc\"]\n",
" market_address = PublicKey(market_address_string)\n",
" elif quote.symbol == \"USDT\":\n",
" if \"serumV3Usdt\" not in base_data[\"extensions\"]:\n",
" self.logger.warning(f\"No USDT market found for base token '{base.symbol}'.\")\n",
" return None\n",
"\n",
" market_address_string = base_data[\"extensions\"][\"serumV3Usdt\"]\n",
" market_address = PublicKey(market_address_string)\n",
" else:\n",
" self.logger.warning(f\"Could not find market with quote token '{quote.symbol}'. Only markets based on USDC or USDT are supported.\")\n",
" return None\n",
"\n",
" return SpotMarket(market_address, base, quote)\n",
"\n",
" def find_by_address(self, address: PublicKey) -> typing.Optional[SpotMarket]:\n",
" address_string: str = str(address)\n",
" for token_data in self.token_data[\"tokens\"]:\n",
" if \"extensions\" in token_data:\n",
" if \"serumV3Usdc\" in token_data[\"extensions\"]:\n",
" if token_data[\"extensions\"][\"serumV3Usdc\"] == address_string:\n",
" market_address_string = token_data[\"extensions\"][\"serumV3Usdc\"]\n",
" market_address = PublicKey(market_address_string)\n",
" base = Token(token_data[\"symbol\"], token_data[\"name\"], PublicKey(token_data[\"address\"]), Decimal(token_data[\"decimals\"]))\n",
" quote_data = _find_data_by_symbol(\"USDC\", self.token_data)\n",
" if quote_data is None:\n",
" raise Exception(\"Could not load token data for USDC (which should always be present).\")\n",
" quote = Token(quote_data[\"symbol\"], quote_data[\"name\"], PublicKey(quote_data[\"address\"]), Decimal(quote_data[\"decimals\"]))\n",
" return SpotMarket(market_address, base, quote)\n",
" if \"serumV3Usdt\" in token_data[\"extensions\"]:\n",
" if token_data[\"extensions\"][\"serumV3Usdt\"] == address_string:\n",
" market_address_string = token_data[\"extensions\"][\"serumV3Usdt\"]\n",
" market_address = PublicKey(market_address_string)\n",
" base = Token(token_data[\"symbol\"], token_data[\"name\"], PublicKey(token_data[\"address\"]), Decimal(token_data[\"decimals\"]))\n",
" quote_data = _find_data_by_symbol(\"USDT\", self.token_data)\n",
" if quote_data is None:\n",
" raise Exception(\"Could not load token data for USDT (which should always be present).\")\n",
" quote = Token(quote_data[\"symbol\"], quote_data[\"name\"], PublicKey(quote_data[\"address\"]), Decimal(quote_data[\"decimals\"]))\n",
" return SpotMarket(market_address, base, quote)\n",
" return None\n",
"\n",
" @staticmethod\n",
" def default_lookups() -> \"SpotMarketLookup\":\n",
" with open(\"solana.tokenlist.json\") as json_file:\n",
" token_data = json.load(json_file)\n",
" return SpotMarketLookup(token_data)"
]
},
{
"cell_type": "markdown",
"id": "satellite-shadow",
"metadata": {},
"source": [
"## BasketToken class\n",
"\n",
"`BasketToken` defines aspects of `Token`s that are part of a `Group` basket."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "breathing-drove",
"metadata": {},
"outputs": [],
"source": [
"class BasketToken:\n",
" def __init__(self, token: Token, vault: PublicKey, index: Index):\n",
" self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n",
" self.token: Token = token\n",
" self.vault: PublicKey = vault\n",
" self.index: Index = index\n",
"\n",
" @staticmethod\n",
" def find_by_symbol(values: typing.List[\"BasketToken\"], symbol: str) -> \"BasketToken\":\n",
" found = [value for value in values if value.token.symbol_matches(symbol)]\n",
" if len(found) == 0:\n",
" raise Exception(f\"Token '{symbol}' not found in token values: {values}\")\n",
"\n",
" if len(found) > 1:\n",
" raise Exception(f\"Token '{symbol}' matched multiple tokens in values: {values}\")\n",
"\n",
" return found[0]\n",
"\n",
" @staticmethod\n",
" def find_by_mint(values: typing.List[\"BasketToken\"], mint: PublicKey) -> \"BasketToken\":\n",
" found = [value for value in values if value.token.mint == mint]\n",
" if len(found) == 0:\n",
" raise Exception(f\"Token '{mint}' not found in token values: {values}\")\n",
"\n",
" if len(found) > 1:\n",
" raise Exception(f\"Token '{mint}' matched multiple tokens in values: {values}\")\n",
"\n",
" return found[0]\n",
"\n",
" @staticmethod\n",
" def find_by_token(values: typing.List[\"BasketToken\"], token: Token) -> \"BasketToken\":\n",
" return BasketToken.find_by_mint(values, token.mint)\n",
"\n",
" # BasketTokens are equal if they have the same underlying token.\n",
" def __eq__(self, other):\n",
" if hasattr(other, 'token'):\n",
" return self.token == other.token\n",
" return False\n",
"\n",
" def __str__(self) -> str:\n",
" return f\"\"\"« BasketToken:\n",
" {self.token}\n",
" Vault: {self.vault}\n",
" Index: {self.index}\n",
"»\"\"\"\n",
"\n",
" def __repr__(self) -> str:\n",
" return f\"{self}\"\n"
]
},
{
"cell_type": "markdown",
"id": "centered-locking",
"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": "limited-wrapping",
"metadata": {},
"outputs": [],
"source": [
"class TokenValue:\n",
" def __init__(self, token: Token, value: Decimal):\n",
" self.token = token\n",
" self.value = value\n",
"\n",
" @staticmethod\n",
" def fetch_total_value_or_none(context: Context, account_public_key: PublicKey, token: Token) -> typing.Optional[\"TokenValue\"]:\n",
" opts = TokenAccountOpts(mint=token.mint)\n",
"\n",
" token_accounts_response = context.client.get_token_accounts_by_owner(account_public_key, opts, commitment=context.commitment)\n",
" token_accounts = token_accounts_response[\"result\"][\"value\"]\n",
" if len(token_accounts) == 0:\n",
" return None\n",
"\n",
" total_value = Decimal(0)\n",
" for token_account in token_accounts:\n",
" result = context.client.get_token_account_balance(token_account[\"pubkey\"], commitment=context.commitment)\n",
" value = Decimal(result[\"result\"][\"value\"][\"amount\"])\n",
" decimal_places = result[\"result\"][\"value\"][\"decimals\"]\n",
" divisor = Decimal(10 ** decimal_places)\n",
" total_value += value / divisor\n",
"\n",
" return TokenValue(token, total_value)\n",
"\n",
" @staticmethod\n",
" def fetch_total_value(context: Context, account_public_key: PublicKey, token: Token) -> \"TokenValue\":\n",
" value = TokenValue.fetch_total_value_or_none(context, account_public_key, token)\n",
" if value is None:\n",
" return TokenValue(token, Decimal(0))\n",
" return value\n",
"\n",
" @staticmethod\n",
" def report(reporter: typing.Callable[[str], None], values: typing.List[\"TokenValue\"]) -> None:\n",
" for value in values:\n",
" reporter(f\"{value.value:>18,.8f} {value.token.name}\")\n",
"\n",
" @staticmethod\n",
" def find_by_symbol(values: typing.List[\"TokenValue\"], symbol: str) -> \"TokenValue\":\n",
" found = [value for value in values if value.token.symbol_matches(symbol)]\n",
" if len(found) == 0:\n",
" raise Exception(f\"Token '{symbol}' not found in token values: {values}\")\n",
"\n",
" if len(found) > 1:\n",
" raise Exception(f\"Token '{symbol}' matched multiple tokens in values: {values}\")\n",
"\n",
" return found[0]\n",
"\n",
" @staticmethod\n",
" def find_by_mint(values: typing.List[\"TokenValue\"], mint: PublicKey) -> \"TokenValue\":\n",
" found = [value for value in values if value.token.mint == mint]\n",
" if len(found) == 0:\n",
" raise Exception(f\"Token '{mint}' not found in token values: {values}\")\n",
"\n",
" if len(found) > 1:\n",
" raise Exception(f\"Token '{mint}' matched multiple tokens in values: {values}\")\n",
"\n",
" return found[0]\n",
"\n",
" @staticmethod\n",
" def find_by_token(values: typing.List[\"TokenValue\"], token: Token) -> \"TokenValue\":\n",
" return TokenValue.find_by_mint(values, token.mint)\n",
"\n",
" @staticmethod\n",
" def changes(before: typing.List[\"TokenValue\"], after: typing.List[\"TokenValue\"]) -> typing.List[\"TokenValue\"]:\n",
" changes: typing.List[TokenValue] = []\n",
" for before_balance in before:\n",
" after_balance = TokenValue.find_by_token(after, before_balance.token)\n",
" result = TokenValue(before_balance.token, after_balance.value - before_balance.value)\n",
" changes += [result]\n",
"\n",
" return changes\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": "metallic-director",
"metadata": {},
"source": [
"## OwnedTokenValue class\n",
"\n",
"Ties an owner and `TokenValue` together. This is useful in the `TransactionScout`, where token mints and values are given separate from the owner `PublicKey` - we can package them together in this `OwnedTokenValue` class."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "honey-longitude",
"metadata": {},
"outputs": [],
"source": [
"class OwnedTokenValue:\n",
" def __init__(self, owner: PublicKey, token_value: TokenValue):\n",
" self.owner = owner\n",
" self.token_value = token_value\n",
"\n",
" @staticmethod\n",
" def find_by_owner(values: typing.List[\"OwnedTokenValue\"], owner: PublicKey) -> \"OwnedTokenValue\":\n",
" found = [value for value in values if value.owner == owner]\n",
" if len(found) == 0:\n",
" raise Exception(f\"Owner '{owner}' not found in: {values}\")\n",
"\n",
" if len(found) > 1:\n",
" raise Exception(f\"Owner '{owner}' matched multiple tokens in: {values}\")\n",
"\n",
" return found[0]\n",
"\n",
" @staticmethod\n",
" def changes(before: typing.List[\"OwnedTokenValue\"], after: typing.List[\"OwnedTokenValue\"]) -> typing.List[\"OwnedTokenValue\"]:\n",
" changes: typing.List[OwnedTokenValue] = []\n",
" for before_value in before:\n",
" after_value = OwnedTokenValue.find_by_owner(after, before_value.owner)\n",
" token_value = TokenValue(before_value.token_value.token, after_value.token_value.value - before_value.token_value.value)\n",
" result = OwnedTokenValue(before_value.owner, token_value)\n",
" changes += [result]\n",
"\n",
" return changes\n",
"\n",
" def __str__(self) -> str:\n",
" return f\"[{self.owner}]: {self.token_value}\"\n",
"\n",
" def __repr__(self) -> str:\n",
" return f\"{self}\"\n"
]
},
{
"cell_type": "markdown",
"id": "capable-sauce",
"metadata": {},
"source": [
"## MarketMetadata class"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "continent-bookmark",
"metadata": {},
"outputs": [],
"source": [
"class MarketMetadata:\n",
" def __init__(self, name: str, address: PublicKey, base: BasketToken, quote: BasketToken,\n",
" spot: SpotMarket, 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: BasketToken = base\n",
" self.quote: BasketToken = quote\n",
" self.spot: SpotMarket = 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.address)\n",
"\n",
" return self._market\n",
"\n",
" def __str__(self) -> str:\n",
" base = f\"{self.base}\".replace(\"\\n\", \"\\n \")\n",
" quote = f\"{self.quote}\".replace(\"\\n\", \"\\n \")\n",
" return f\"\"\"« Market '{self.name}' [{self.address}/{self.spot.address}]:\n",
" Base: {base}\n",
" Quote: {quote}\n",
" Oracle: {self.oracle} ({self.decimals} decimals)\n",
"»\"\"\"\n",
"\n",
" def __repr__(self) -> str:\n",
" return f\"{self}\"\n"
]
},
{
"cell_type": "markdown",
"id": "elect-principal",
"metadata": {},
"source": [
"## Group class\n",
"\n",
"The `Group` class encapsulates the data for the Mango Group - the cross-margined basket of tokens with lending."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "premier-vaccine",
"metadata": {},
"outputs": [],
"source": [
"class Group(AddressableAccount):\n",
" def __init__(self, account_info: AccountInfo, version: Version, context: Context,\n",
" account_flags: MangoAccountFlags, basket_tokens: typing.List[BasketToken],\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",
" super().__init__(account_info)\n",
" self.version: Version = version\n",
" self.context: Context = context\n",
" self.account_flags: MangoAccountFlags = account_flags\n",
" self.basket_tokens: typing.List[BasketToken] = basket_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) -> BasketToken:\n",
" return self.basket_tokens[-1]\n",
"\n",
" @property\n",
" def base_tokens(self) -> typing.List[BasketToken]:\n",
" return self.basket_tokens[:-1]\n",
"\n",
" @staticmethod\n",
" def from_layout(layout: construct.Struct, context: Context, account_info: AccountInfo, version: Version, token_lookup: TokenLookup = TokenLookup.default_lookups(), spot_market_lookup: SpotMarketLookup = SpotMarketLookup.default_lookups()) -> \"Group\":\n",
" account_flags: MangoAccountFlags = 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",
" basket_tokens: typing.List[BasketToken] = []\n",
" for index, token_address in enumerate(layout.tokens):\n",
" static_token_data = token_lookup.find_by_mint(token_address)\n",
" if static_token_data is None:\n",
" raise Exception(f\"Could not find token with mint '{token_address}'.\")\n",
"\n",
" # We create a new Token object here specifically to force the use of our own decimals\n",
" token = Token(static_token_data.symbol, static_token_data.name, token_address, layout.mint_decimals[index])\n",
" basket_token = BasketToken(token, layout.vaults[index], indexes[index])\n",
" basket_tokens += [basket_token]\n",
"\n",
" markets: typing.List[MarketMetadata] = []\n",
" for index, market_address in enumerate(layout.spot_markets):\n",
" spot_market = spot_market_lookup.find_by_address(market_address)\n",
" if spot_market is None:\n",
" raise Exception(f\"Could not find spot market with address '{market_address}'.\")\n",
"\n",
" base_token = BasketToken.find_by_mint(basket_tokens, spot_market.base.mint)\n",
" quote_token = BasketToken.find_by_mint(basket_tokens, spot_market.quote.mint)\n",
"\n",
" market = MarketMetadata(spot_market.name, market_address, base_token, quote_token,\n",
" spot_market, layout.oracles[index], 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",
"\n",
" return Group(account_info, version, context, account_flags, basket_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(context: Context, account_info: AccountInfo) -> \"Group\":\n",
" data = account_info.data\n",
" if len(data) == layouts.GROUP_V1.sizeof():\n",
" layout = layouts.GROUP_V1.parse(data)\n",
" version: Version = Version.V1\n",
" elif len(data) == layouts.GROUP_V2.sizeof():\n",
" version = Version.V2\n",
" layout = layouts.GROUP_V2.parse(data)\n",
" else:\n",
" raise Exception(f\"Group data length ({len(data)}) does not match expected size ({layouts.GROUP_V1.sizeof()} or {layouts.GROUP_V2.sizeof()})\")\n",
"\n",
" return Group.from_layout(layout, context, account_info, version)\n",
"\n",
" @staticmethod\n",
" def load(context: Context):\n",
" account_info = AccountInfo.load(context, context.group_id)\n",
" if account_info is None:\n",
" raise Exception(f\"Group account not found at address '{context.group_id}'\")\n",
" return Group.parse(context, account_info)\n",
"\n",
" def price_index_of_token(self, token: Token) -> int:\n",
" for index, existing in enumerate(self.basket_tokens):\n",
" if existing.token == token:\n",
" return index\n",
" return -1\n",
"\n",
" def fetch_token_prices(self) -> typing.List[TokenValue]:\n",
" started_at = time.time()\n",
"\n",
" # Note: we can just load the oracle data in a simpler way, with:\n",
" # oracles = map(lambda market: Aggregator.load(self.context, market.oracle), self.markets)\n",
" # but that makes a network request for every oracle. We can reduce that to just one request\n",
" # if we use AccountInfo.load_multiple() and parse the data ourselves.\n",
" #\n",
" # This seems to halve the time this function takes.\n",
" oracle_addresses = list([market.oracle for market in self.markets])\n",
" oracle_account_infos = AccountInfo.load_multiple(self.context, oracle_addresses)\n",
" oracles = map(lambda oracle_account_info: Aggregator.parse(self.context, oracle_account_info), oracle_account_infos)\n",
" prices = list(map(lambda oracle: oracle.price, oracles)) + [Decimal(1)]\n",
" token_prices = []\n",
" for index, price in enumerate(prices):\n",
" token_prices += [TokenValue(self.basket_tokens[index].token, price)]\n",
"\n",
" time_taken = time.time() - started_at\n",
" self.logger.info(f\"Fetching prices complete. Time taken: {time_taken:.2f} seconds.\")\n",
" return token_prices\n",
"\n",
" @staticmethod\n",
" def load_with_prices(context: Context) -> typing.Tuple[\"Group\", typing.List[TokenValue]]:\n",
" group = Group.load(context)\n",
" prices = group.fetch_token_prices()\n",
" return group, prices\n",
"\n",
" def fetch_balances(self, root_address: PublicKey) -> typing.List[TokenValue]:\n",
" balances: typing.List[TokenValue] = []\n",
" sol_balance = self.context.fetch_sol_balance(root_address)\n",
" balances += [TokenValue(SolToken, sol_balance)]\n",
"\n",
" for basket_token in self.basket_tokens:\n",
" balance = TokenValue.fetch_total_value(self.context, root_address, basket_token.token)\n",
" balances += [balance]\n",
" return balances\n",
"\n",
" # The old way of fetching ripe margin accounts was to fetch them all then inspect them to see\n",
" # if they were ripe. That was a big performance problem - fetching all groups was quite a penalty.\n",
" #\n",
" # This is still how it's done in load_ripe_margin_accounts_v1().\n",
" #\n",
" # The newer mechanism is to look for the has_borrows flag in the MangoAccount. That should\n",
" # mean fewer MarginAccounts need to be fetched.\n",
" #\n",
" # This newer method is implemented in load_ripe_margin_accounts_v2()\n",
" def load_ripe_margin_accounts(self) -> typing.List[\"MarginAccount\"]:\n",
" if self.version == Version.V1:\n",
" return self.load_ripe_margin_accounts_v1()\n",
" else:\n",
" return self.load_ripe_margin_accounts_v2()\n",
"\n",
" def load_ripe_margin_accounts_v2(self) -> typing.List[\"MarginAccount\"]:\n",
" started_at = time.time()\n",
"\n",
" filters = [\n",
" # 'has_borrows' offset is: 8 + 32 + 32 + (5 * 16) + (5 * 16) + (4 * 32) + 1\n",
" # = 361\n",
" MemcmpOpts(\n",
" offset=361,\n",
" bytes=encode_int(1)\n",
" ),\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(self.address)\n",
" )\n",
" ]\n",
"\n",
" response = self.context.client.get_program_accounts(self.context.program_id, data_size=layouts.MARGIN_ACCOUNT_V2.sizeof(), memcmp_opts=filters, commitment=Single, encoding=\"base64\")\n",
" result = self.context.unwrap_or_raise_exception(response)\n",
" margin_accounts = []\n",
" open_orders_addresses = []\n",
" for margin_account_data in 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)\n",
" open_orders_addresses += margin_account.open_orders\n",
" margin_accounts += [margin_account]\n",
"\n",
" self.logger.info(f\"Fetched {len(margin_accounts)} V2 margin accounts to process.\")\n",
"\n",
" # It looks like this will be more efficient - just specify only the addresses we\n",
" # need, and install them.\n",
" #\n",
" # Unfortunately there's a limit of 100 for the getMultipleAccounts() RPC call,\n",
" # and doing it repeatedly requires some pauses because of rate limits.\n",
" #\n",
" # It's quicker (so far) to bring back every openorders account for the group.\n",
" #\n",
" # open_orders_addresses = [oo for oo in open_orders_addresses if oo is not None]\n",
"\n",
" # open_orders_account_infos = AccountInfo.load_multiple(self.context, open_orders_addresses)\n",
" # open_orders_account_infos_by_address = {key: value for key, value in [(str(account_info.address), account_info) for account_info in open_orders_account_infos]}\n",
"\n",
" # for margin_account in margin_accounts:\n",
" # margin_account.install_open_orders_accounts(self, open_orders_account_infos_by_address)\n",
"\n",
" # This just fetches every openorder account for the group.\n",
" open_orders = OpenOrders.load_raw_open_orders_account_infos(self.context, self)\n",
" self.logger.info(f\"Fetched {len(open_orders)} openorders accounts.\")\n",
" for margin_account in margin_accounts:\n",
" margin_account.install_open_orders_accounts(self, open_orders)\n",
"\n",
" prices = self.fetch_token_prices()\n",
" ripe_accounts = MarginAccount.filter_out_unripe(margin_accounts, self, prices)\n",
"\n",
" time_taken = time.time() - started_at\n",
" self.logger.info(f\"Loading ripe 🥭 accounts complete. Time taken: {time_taken:.2f} seconds.\")\n",
" return ripe_accounts\n",
"\n",
" def load_ripe_margin_accounts_v1(self) -> typing.List[\"MarginAccount\"]:\n",
" started_at = time.time()\n",
"\n",
" margin_accounts = MarginAccount.load_all_for_group_with_open_orders(self.context, self.context.program_id, self)\n",
" self.logger.info(f\"Fetched {len(margin_accounts)} V1 margin accounts to process.\")\n",
"\n",
" prices = self.fetch_token_prices()\n",
" ripe_accounts = MarginAccount.filter_out_unripe(margin_accounts, self, prices)\n",
"\n",
" time_taken = time.time() - started_at\n",
" self.logger.info(f\"Loading ripe 🥭 accounts complete. Time taken: {time_taken:.2f} seconds.\")\n",
" return ripe_accounts\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",
" shared_quote_token = str(self.shared_quote_token).replace(\"\\n\", \"\\n \")\n",
" base_tokens = \"\\n \".join([f\"{tok}\".replace(\"\\n\", \"\\n \") for tok in self.base_tokens])\n",
" markets = \"\\n \".join([f\"{mkt}\".replace(\"\\n\", \"\\n \") for mkt in self.markets])\n",
" return f\"\"\"\n",
"« Group [{self.version}] {self.address}:\n",
" Flags: {self.account_flags}\n",
" Base Tokens:\n",
" {base_tokens}\n",
" Quote Token:\n",
" {shared_quote_token}\n",
" Markets:\n",
" {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"
]
},
{
"cell_type": "markdown",
"id": "rotary-therapist",
"metadata": {},
"source": [
"## TokenAccount class"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "little-portfolio",
"metadata": {},
"outputs": [],
"source": [
"class TokenAccount(AddressableAccount):\n",
" def __init__(self, account_info: AccountInfo, version: Version, mint: PublicKey, owner: PublicKey, amount: Decimal):\n",
" super().__init__(account_info)\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 create(context: Context, account: Account, token: Token):\n",
" spl_token = SplToken(context.client, token.mint, TOKEN_PROGRAM_ID, account)\n",
" owner = account.public_key()\n",
" new_account_address = spl_token.create_account(owner)\n",
" return TokenAccount.load(context, new_account_address)\n",
"\n",
" @staticmethod\n",
" def fetch_all_for_owner_and_token(context: Context, owner_public_key: PublicKey, token: Token) -> typing.List[\"TokenAccount\"]:\n",
" opts = TokenAccountOpts(mint=token.mint)\n",
"\n",
" token_accounts_response = context.client.get_token_accounts_by_owner(owner_public_key, opts, commitment=context.commitment)\n",
"\n",
" all_accounts: typing.List[TokenAccount] = []\n",
" for token_account_response in token_accounts_response[\"result\"][\"value\"]:\n",
" account_info = AccountInfo._from_response_values(token_account_response[\"account\"], PublicKey(token_account_response[\"pubkey\"]))\n",
" token_account = TokenAccount.parse(account_info)\n",
" all_accounts += [token_account]\n",
"\n",
" return all_accounts\n",
"\n",
" @staticmethod\n",
" def fetch_largest_for_owner_and_token(context: Context, owner_public_key: PublicKey, token: Token) -> typing.Optional[\"TokenAccount\"]:\n",
" all_accounts = TokenAccount.fetch_all_for_owner_and_token(context, owner_public_key, token)\n",
"\n",
" largest_account: typing.Optional[TokenAccount] = None\n",
" for token_account in all_accounts:\n",
" if largest_account is None or token_account.amount > largest_account.amount:\n",
" largest_account = token_account\n",
"\n",
" return largest_account\n",
"\n",
" @staticmethod\n",
" def fetch_or_create_largest_for_owner_and_token(context: Context, account: Account, token: Token) -> \"TokenAccount\":\n",
" all_accounts = TokenAccount.fetch_all_for_owner_and_token(context, account.public_key(), token)\n",
"\n",
" largest_account: typing.Optional[TokenAccount] = None\n",
" for token_account in all_accounts:\n",
" if largest_account is None or token_account.amount > largest_account.amount:\n",
" largest_account = token_account\n",
"\n",
" if largest_account is None:\n",
" return TokenAccount.create(context, account, token)\n",
"\n",
" return largest_account\n",
"\n",
" @staticmethod\n",
" def from_layout(layout: layouts.TOKEN_ACCOUNT, account_info: AccountInfo) -> \"TokenAccount\":\n",
" return TokenAccount(account_info, Version.UNSPECIFIED, layout.mint, layout.owner, layout.amount)\n",
"\n",
" @staticmethod\n",
" def parse(account_info: AccountInfo) -> \"TokenAccount\":\n",
" data = account_info.data\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, account_info)\n",
"\n",
" @staticmethod\n",
" def load(context: Context, address: PublicKey) -> typing.Optional[\"TokenAccount\"]:\n",
" account_info = AccountInfo.load(context, address)\n",
" if account_info is None or (len(account_info.data) != layouts.TOKEN_ACCOUNT.sizeof()):\n",
" return None\n",
" return TokenAccount.parse(account_info)\n",
"\n",
" def __str__(self) -> str:\n",
" return f\"« Token: Address: {self.address}, Mint: {self.mint}, Owner: {self.owner}, Amount: {self.amount} »\"\n"
]
},
{
"cell_type": "markdown",
"id": "exact-emergency",
"metadata": {},
"source": [
"## OpenOrders class\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "spanish-promise",
"metadata": {},
"outputs": [],
"source": [
"class OpenOrders(AddressableAccount):\n",
" def __init__(self, account_info: AccountInfo, version: Version, 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",
" super().__init__(account_info)\n",
" self.version: Version = version\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",
" # Sometimes pyserum wants to take its own OpenOrdersAccount as a parameter (e.g. in settle_funds())\n",
" def to_pyserum(self) -> OpenOrdersAccount:\n",
" return OpenOrdersAccount.from_bytes(self.address, self.account_info.data)\n",
"\n",
" @staticmethod\n",
" def from_layout(layout: layouts.OPEN_ORDERS, account_info: AccountInfo,\n",
" base_decimals: Decimal, quote_decimals: Decimal) -> \"OpenOrders\":\n",
" account_flags = SerumAccountFlags.from_layout(layout.account_flags)\n",
" program_id = account_info.owner\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(account_info, Version.UNSPECIFIED, 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(account_info: AccountInfo, base_decimals: Decimal, quote_decimals: Decimal) -> \"OpenOrders\":\n",
" data = account_info.data\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, account_info, base_decimals, quote_decimals)\n",
"\n",
" @staticmethod\n",
" def load_raw_open_orders_account_infos(context: Context, group: Group) -> typing.Dict[str, AccountInfo]:\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",
" account_infos = list(map(lambda pair: AccountInfo._from_response_values(pair[0], pair[1]), [(result[\"account\"], PublicKey(result[\"pubkey\"])) for result in response[\"result\"]]))\n",
" account_infos_by_address = {key: value for key, value in [(str(account_info.address), account_info) for account_info in account_infos]}\n",
" return account_infos_by_address\n",
"\n",
" @staticmethod\n",
" def load(context: Context, address: PublicKey, base_decimals: Decimal, quote_decimals: Decimal) -> \"OpenOrders\":\n",
" open_orders_account = AccountInfo.load(context, address)\n",
" if open_orders_account is None:\n",
" raise Exception(f\"OpenOrders account not found at address '{address}'\")\n",
" return OpenOrders.parse(open_orders_account, 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, 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"
]
},
{
"cell_type": "markdown",
"id": "listed-error",
"metadata": {},
"source": [
"## BalanceSheet class"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "vulnerable-conjunction",
"metadata": {},
"outputs": [],
"source": [
"class BalanceSheet:\n",
" def __init__(self, token: Token, liabilities: Decimal, settled_assets: Decimal, unsettled_assets: Decimal):\n",
" self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n",
" self.token: Token = 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": "seeing-victorian",
"metadata": {},
"source": [
"## MarginAccount class\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "sunset-chase",
"metadata": {},
"outputs": [],
"source": [
"class MarginAccount(AddressableAccount):\n",
" def __init__(self, account_info: AccountInfo, version: Version, account_flags: MangoAccountFlags,\n",
" has_borrows: bool, mango_group: PublicKey, owner: PublicKey,\n",
" deposits: typing.List[Decimal], borrows: typing.List[Decimal],\n",
" open_orders: typing.List[PublicKey]):\n",
" super().__init__(account_info)\n",
" self.version: Version = version\n",
" self.account_flags: MangoAccountFlags = account_flags\n",
" self.has_borrows: bool = has_borrows\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] * len(open_orders)\n",
"\n",
" @staticmethod\n",
" def from_layout(layout: construct.Struct, account_info: AccountInfo, version: Version) -> \"MarginAccount\":\n",
" if version == Version.V1:\n",
" # This is an old-style margin account, with no borrows flag\n",
" has_borrows = False\n",
" else:\n",
" # This is a new-style margin account where we can depend on the presence of the borrows flag\n",
" has_borrows = bool(layout.has_borrows)\n",
"\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(account_info, version, account_flags, has_borrows, layout.mango_group,\n",
" layout.owner, deposits, borrows, list(layout.open_orders))\n",
"\n",
" @staticmethod\n",
" def parse(account_info: AccountInfo) -> \"MarginAccount\":\n",
" data = account_info.data\n",
" if len(data) == layouts.MARGIN_ACCOUNT_V1.sizeof():\n",
" layout = layouts.MARGIN_ACCOUNT_V1.parse(data)\n",
" version: Version = Version.V1\n",
" elif len(data) == layouts.MARGIN_ACCOUNT_V2.sizeof():\n",
" version = Version.V2\n",
" layout = layouts.MARGIN_ACCOUNT_V2.parse(data)\n",
" else:\n",
" raise Exception(f\"Data length ({len(data)}) does not match expected size ({layouts.MARGIN_ACCOUNT_V1.sizeof()} or {layouts.MARGIN_ACCOUNT_V2.sizeof()})\")\n",
"\n",
" return MarginAccount.from_layout(layout, account_info, version)\n",
"\n",
" @staticmethod\n",
" def load(context: Context, margin_account_address: PublicKey, group: typing.Optional[Group] = None) -> \"MarginAccount\":\n",
" account_info = AccountInfo.load(context, margin_account_address)\n",
" if account_info is None:\n",
" raise Exception(f\"MarginAccount account not found at address '{margin_account_address}'\")\n",
"\n",
" margin_account = MarginAccount.parse(account_info)\n",
" if group is None:\n",
" group_context = context.new_from_group_id(margin_account.mango_group)\n",
" group = Group.load(group_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",
"\n",
" if group.version == Version.V1:\n",
" parser = layouts.MARGIN_ACCOUNT_V1\n",
" else:\n",
" parser = layouts.MARGIN_ACCOUNT_V2\n",
"\n",
" response = context.client.get_program_accounts(program_id, data_size=parser.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)\n",
" margin_accounts += [margin_account]\n",
" return margin_accounts\n",
"\n",
" @staticmethod\n",
" def load_all_for_group_with_open_orders(context: Context, program_id: PublicKey, group: Group) -> typing.List[\"MarginAccount\"]:\n",
" margin_accounts = MarginAccount.load_all_for_group(context, context.program_id, group)\n",
" open_orders = OpenOrders.load_raw_open_orders_account_infos(context, group)\n",
" for margin_account in margin_accounts:\n",
" margin_account.install_open_orders_accounts(group, open_orders)\n",
"\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, 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)\n",
" margin_account.load_open_orders_accounts(context, group)\n",
" margin_accounts += [margin_account]\n",
" return margin_accounts\n",
"\n",
" @classmethod\n",
" def filter_out_unripe(cls, margin_accounts: typing.List[\"MarginAccount\"], group: Group, prices: typing.List[TokenValue]) -> typing.List[\"MarginAccount\"]:\n",
" logger: logging.Logger = logging.getLogger(cls.__name__)\n",
"\n",
" nonzero: typing.List[MarginAccountMetadata] = []\n",
" for margin_account in margin_accounts:\n",
" balance_sheet = margin_account.get_balance_sheet_totals(group, prices)\n",
" if balance_sheet.collateral_ratio > 0:\n",
" balances = margin_account.get_intrinsic_balances(group)\n",
" nonzero += [MarginAccountMetadata(margin_account, balance_sheet, balances)]\n",
" logger.info(f\"Of those {len(margin_accounts)}, {len(nonzero)} have a nonzero collateral ratio.\")\n",
"\n",
" ripe_metadata = filter(lambda mam: mam.balance_sheet.collateral_ratio <= group.init_coll_ratio, nonzero)\n",
" ripe_accounts = list(map(lambda mam: mam.margin_account, ripe_metadata))\n",
" logger.info(f\"Of those {len(nonzero)}, {len(ripe_accounts)} are ripe 🥭.\")\n",
" return ripe_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 is not None:\n",
" self.open_orders_accounts[index] = OpenOrders.load(context, key, group.basket_tokens[index].token.decimals, group.shared_quote_token.token.decimals)\n",
"\n",
" def install_open_orders_accounts(self, group: Group, all_open_orders_by_address: typing.Dict[str, AccountInfo]) -> None:\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_account_info = all_open_orders_by_address[key]\n",
" open_orders = OpenOrders.parse(open_orders_account_info,\n",
" group.basket_tokens[index].token.decimals,\n",
" group.shared_quote_token.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)] * len(group.basket_tokens)\n",
" liabilities: typing.List[Decimal] = [Decimal(0)] * len(group.basket_tokens)\n",
" for index, token in enumerate(group.basket_tokens):\n",
" settled_assets[index] = token.index.deposit * self.deposits[index]\n",
" liabilities[index] = token.index.borrow * self.borrows[index]\n",
"\n",
" unsettled_assets: typing.List[Decimal] = [Decimal(0)] * len(group.basket_tokens)\n",
" for index, open_orders_account in enumerate(self.open_orders_accounts):\n",
" if open_orders_account is not None:\n",
" unsettled_assets[index] += open_orders_account.base_token_total\n",
" unsettled_assets[-1] += open_orders_account.quote_token_total\n",
"\n",
" balance_sheets: typing.List[BalanceSheet] = []\n",
" for index, token in enumerate(group.basket_tokens):\n",
" balance_sheets += [BalanceSheet(token.token, 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[TokenValue]) -> typing.List[BalanceSheet]:\n",
" priced: typing.List[BalanceSheet] = []\n",
" balance_sheets = self.get_intrinsic_balance_sheets(group)\n",
" for balance_sheet in balance_sheets:\n",
" price = TokenValue.find_by_token(prices, balance_sheet.token)\n",
" liabilities = balance_sheet.liabilities * price.value\n",
" settled_assets = balance_sheet.settled_assets * price.value\n",
" unsettled_assets = balance_sheet.unsettled_assets * price.value\n",
" priced += [BalanceSheet(\n",
" price.token,\n",
" price.token.round(liabilities),\n",
" price.token.round(settled_assets),\n",
" price.token.round(unsettled_assets)\n",
" )]\n",
"\n",
" return priced\n",
"\n",
" def get_balance_sheet_totals(self, group: Group, prices: typing.List[TokenValue]) -> 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",
" # A BalanceSheet must have a token - it's a pain to make it a typing.Optional[Token].\n",
" # So in this one case, we produce a 'fake' token whose symbol is a summary of all token\n",
" # symbols that went into it.\n",
" #\n",
" # If this becomes more painful than typing.Optional[Token], we can go with making\n",
" # Token optional.\n",
" summary_name = \"-\".join([bal.token.name for bal in balance_sheets])\n",
" summary_token = Token(summary_name, f\"{summary_name} Summary\", SYSTEM_PROGRAM_ADDRESS, Decimal(0))\n",
" return BalanceSheet(summary_token, 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",
" Has Borrows: {self.has_borrows}\n",
" Owner: {self.owner}\n",
" Mango Group: {self.mango_group}\n",
" Deposits: [{deposits}]\n",
" Borrows: [{borrows}]\n",
" Mango Open Orders: {open_orders}\n",
"»\"\"\"\n"
]
},
{
"cell_type": "markdown",
"id": "extraordinary-mozambique",
"metadata": {},
"source": [
"## MarginAccountMetadata class"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "connected-cattle",
"metadata": {},
"outputs": [],
"source": [
"class MarginAccountMetadata:\n",
" def __init__(self, margin_account: MarginAccount, balance_sheet: BalanceSheet, balances: typing.List[TokenValue]):\n",
" self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n",
" self.margin_account = margin_account\n",
" self.balance_sheet = balance_sheet\n",
" self.balances = balances\n",
"\n",
" @property\n",
" def assets(self):\n",
" return self.balance_sheet.assets\n",
"\n",
" @property\n",
" def liabilities(self):\n",
" return self.balance_sheet.liabilities\n",
"\n",
" @property\n",
" def collateral_ratio(self):\n",
" return self.balance_sheet.collateral_ratio\n"
]
},
{
"cell_type": "markdown",
"id": "green-boxing",
"metadata": {},
"source": [
"# Events"
]
},
{
"cell_type": "markdown",
"id": "biological-penny",
"metadata": {},
"source": [
"## LiquidationEvent"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "affecting-hydrogen",
"metadata": {},
"outputs": [],
"source": [
"class LiquidationEvent:\n",
" def __init__(self, timestamp: datetime.datetime, liquidator_name: str, group_name: str, succeeded: bool, signature: str, wallet_address: PublicKey, margin_account_address: PublicKey, balances_before: typing.List[TokenValue], balances_after: typing.List[TokenValue]):\n",
" self.timestamp: datetime.datetime = timestamp\n",
" self.liquidator_name: str = liquidator_name\n",
" self.group_name: str = group_name\n",
" self.succeeded: bool = succeeded\n",
" self.signature: str = signature\n",
" self.wallet_address: PublicKey = wallet_address\n",
" self.margin_account_address: PublicKey = margin_account_address\n",
" self.balances_before: typing.List[TokenValue] = balances_before\n",
" self.balances_after: typing.List[TokenValue] = balances_after\n",
" self.changes: typing.List[TokenValue] = TokenValue.changes(balances_before, balances_after)\n",
"\n",
" def __str__(self) -> str:\n",
" result = \"✅\" if self.succeeded else \"❌\"\n",
" changes_text = \"\\n \".join([f\"{change.value:>15,.8f} {change.token.symbol}\" for change in self.changes])\n",
" return f\"\"\"« 🥭 Liqudation Event {result} at {self.timestamp}\n",
" 💧 Liquidator: {self.liquidator_name}\n",
" 🗃️ Group: {self.group_name}\n",
" 📇 Signature: {self.signature}\n",
" 👛 Wallet: {self.wallet_address}\n",
" 💳 Margin Account: {self.margin_account_address}\n",
" 💸 Changes:\n",
" {changes_text}\n",
"»\"\"\"\n",
"\n",
" def __repr__(self) -> str:\n",
" return f\"{self}\"\n"
]
},
{
"cell_type": "markdown",
"id": "geological-pantyhose",
"metadata": {},
"source": [
"# ✅ Testing"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "saving-lending",
"metadata": {},
"outputs": [],
"source": [
"def _notebook_tests():\n",
" log_level = logging.getLogger().level\n",
" try:\n",
" logging.getLogger().setLevel(logging.CRITICAL)\n",
"\n",
" list_to_split = [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\"]\n",
" split_3 = _split_list_into_chunks(list_to_split, 3)\n",
" assert(len(split_3) == 4)\n",
" assert(split_3[0] == [\"a\", \"b\", \"c\"])\n",
" assert(split_3[1] == [\"d\", \"e\", \"f\"])\n",
" assert(split_3[2] == [\"g\", \"h\", \"i\"])\n",
" assert(split_3[3] == [\"j\"])\n",
" split_2 = _split_list_into_chunks(list_to_split, 2)\n",
" assert(len(split_2) == 5)\n",
" assert(split_2[0] == [\"a\", \"b\"])\n",
" assert(split_2[1] == [\"c\", \"d\"])\n",
" assert(split_2[2] == [\"e\", \"f\"])\n",
" assert(split_2[3] == [\"g\", \"h\"])\n",
" assert(split_2[4] == [\"i\", \"j\"])\n",
" split_20 = _split_list_into_chunks(list_to_split, 20)\n",
" assert(len(split_20) == 1)\n",
" assert(split_20[0] == [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\"])\n",
"\n",
" token_lookup = TokenLookup.default_lookups()\n",
"\n",
" assert(token_lookup.find_by_symbol(\"ETH\").mint == PublicKey(\"2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk\"))\n",
" assert(token_lookup.find_by_mint(\"AKJHspCwDhABucCxNLXUSfEzb7Ny62RqFtC9uNjJi4fq\").symbol == \"SRM-SOL\")\n",
"\n",
" market_lookup = SpotMarketLookup.default_lookups()\n",
" assert(market_lookup.find_by_symbol(\"ETH/USDT\").base.symbol == \"ETH\")\n",
" assert(market_lookup.find_by_symbol(\"ETH/USDT\").quote.symbol == \"USDT\")\n",
" assert(market_lookup.find_by_symbol(\"ETH/USDT\").address == PublicKey(\"7dLVkUfBVfCGkFhSXDCq1ukM9usathSgS716t643iFGF\"))\n",
" assert(market_lookup.find_by_symbol(\"BTC/USDC\").base.symbol == \"BTC\")\n",
" assert(market_lookup.find_by_symbol(\"BTC/USDC\").quote.symbol == \"USDC\")\n",
" assert(market_lookup.find_by_symbol(\"BTC/USDC\").address == PublicKey(\"A8YFbxQYFVqKZaoYJLLUVcQiWP7G2MeEgW5wsAQgMvFw\"))\n",
" assert(market_lookup.find_by_symbol(\"ETH/BTC\") is None) # No such market\n",
" assert(market_lookup.find_by_address(\"ByRys5tuUWDgL73G8JBAEfkdFf8JWBzPBDHsBVQ5vbQA\").base.symbol == \"SRM\")\n",
" assert(market_lookup.find_by_address(\"ByRys5tuUWDgL73G8JBAEfkdFf8JWBzPBDHsBVQ5vbQA\").quote.symbol == \"USDC\")\n",
" assert(market_lookup.find_by_address(\"ByRys5tuUWDgL73G8JBAEfkdFf8JWBzPBDHsBVQ5vbQA\").address == PublicKey(\"ByRys5tuUWDgL73G8JBAEfkdFf8JWBzPBDHsBVQ5vbQA\"))\n",
"\n",
" balances_before = [\n",
" TokenValue(token_lookup.find_by_symbol(\"ETH\"), Decimal(1)),\n",
" TokenValue(token_lookup.find_by_symbol(\"BTC\"), Decimal(\"0.1\")),\n",
" TokenValue(token_lookup.find_by_symbol(\"USDT\"), Decimal(1000))\n",
" ]\n",
" balances_after = [\n",
" TokenValue(token_lookup.find_by_symbol(\"ETH\"), Decimal(1)),\n",
" TokenValue(token_lookup.find_by_symbol(\"BTC\"), Decimal(\"0.05\")),\n",
" TokenValue(token_lookup.find_by_symbol(\"USDT\"), Decimal(2000))\n",
" ]\n",
" timestamp = datetime.datetime(2021, 5, 17, 12, 20, 56)\n",
" event = LiquidationEvent(timestamp, \"Liquidator\", \"Group\", True, \"signature\",\n",
" SYSTEM_PROGRAM_ADDRESS, SYSTEM_PROGRAM_ADDRESS,\n",
" balances_before, balances_after)\n",
" assert(str(event) == \"\"\"« 🥭 Liqudation Event ✅ at 2021-05-17 12:20:56\n",
" 💧 Liquidator: Liquidator\n",
" 🗃️ Group: Group\n",
" 📇 Signature: signature\n",
" 👛 Wallet: 11111111111111111111111111111111\n",
" 💳 Margin Account: 11111111111111111111111111111111\n",
" 💸 Changes:\n",
" 0.00000000 ETH\n",
" -0.05000000 BTC\n",
" 1,000.00000000 USDT\n",
"»\"\"\")\n",
" finally:\n",
" logging.getLogger().setLevel(log_level)\n",
"\n",
"\n",
"_notebook_tests()\n",
"del _notebook_tests"
]
},
{
"cell_type": "markdown",
"id": "insured-tackle",
"metadata": {},
"source": [
"# 🏃 Running"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "identified-margin",
"metadata": {},
"outputs": [],
"source": [
"if __name__ == \"__main__\":\n",
" def _notebook_main():\n",
" log_level = logging.getLogger().level\n",
" try:\n",
" logging.getLogger().setLevel(logging.CRITICAL)\n",
"\n",
" import base64\n",
" from Context import default_context\n",
"\n",
" # Data for group BTC_ETH_USDT\n",
" group_3_public_key = PublicKey(\"7pVYhpKUHw88neQHxgExSH6cerMZ1Axx1ALQP9sxtvQV\")\n",
" owner_3_public_key = PublicKey(\"JD3bq9hGdy38PuWQ4h2YJpELmHVGPPfFSuFkpzAd9zfu\")\n",
" encoded_3 = \"AwAAAAAAAACCaOmpoURMK6XHelGTaFawcuQ/78/15LAemWI8jrt3SRKLy2R9i60eclDjuDS8+p/ZhvTUd9G7uQVOYCsR6+BhzgEOYK/tsicXvWMZL1QUWj+WWjO7gtLHAp6yzh4ggmR5TYxOe+df4LNiUJGSedvZ1K+r6GIzQEosNxNHhh2V7yAW8uStEyfEUTbEEkKgyDlUOVRWgGFbsiOC/Uzmn5ghfd2vMNvykHBB4JNMAUG0WhTyCizezFE3eOWvscJG7VWUUa5gAAAAAO1Ih8hkuwwAAQAAAAAAAADCuVoJcm8AAAEAAAAAAAAAlFGuYAAAAAAq17B5bBMDAAEAAAAAAAAAmLhxEVn+//8AAAAAAAAAAJRRrmAAAAAA+TEN0IhNFAIBAAAAAAAAAOiJg4cDQlkAAQAAAAAAAACjgEchygfXnwdEo4sw2++jvroovFb2BReD7MwO2ycvyWJ1ExP6tUyAXZHgwFt800+q+x6ZXzsCtRysH3ay+vKU9VYQAHqylxZVK65gEg85g27YuSyvOBZAjJyRmYU9KdCO1D+4ehdPu9dQB1yI1uh75wShdAaFn2o4qrMYwq3SQQIAAAAAAAAAT1DEK0hxpSv5VHH5kTlWeJePlGQpPYZ+uqcUELR4sLKFDy1uAqR6+CTQmradxC1wyyjL+iSft+5XudJWwSdi78PjPEJTOBGbyhEgBwAAAAD1dEYfuWMjjZ/rgK0AAAAAnKsRD43mpS3XCLztBAUAAKp2XItMhJ/N4sYBAAAAAABTEKVFyYfPc+mmjQAAAAAAWCug5qta1bqvFvY5uAAAAACgmZmZmZkZAQAAAAAAAAAAMDMzMzMzMwEAAAAAAAAAS153X9szDlbg9dv9VWFE+e6Hzhj8N5Of9zJLVkbx/U3at9+SEEUZuga7O5tTUrcMDYWDg+LYaAWhSQiN2fYk7UBCDwAAAAAAgI1bAAAAAAAAdDukCwAAAAYGBgICAAAA\"\n",
" decoded_3 = base64.b64decode(encoded_3)\n",
" group_3_account_info = AccountInfo(group_3_public_key, False, Decimal(6069120), owner_3_public_key, Decimal(185), decoded_3)\n",
" group_3 = Group.parse(default_context, group_3_account_info)\n",
" print(\"\\n\\nThis is hard-coded, not live information!\")\n",
" print(\"3-token group\", group_3)\n",
"\n",
" # Data for group BTC_ETH_SOL_SRM_USDC\n",
" group_5_context = default_context.new_from_group_name(\"BTC_ETH_SOL_SRM_USDC\")\n",
" group_5_public_key = PublicKey(\"79rqTHePsNnGEX6Re8Xkgf4QEkBfsXncXzyRbyMhHPMU\")\n",
" owner_5_public_key = PublicKey(\"3ZhWpsT19EuBZhG2BvcTVHBtFW4vFacGgLYpXZxotg1P\")\n",
" encoded_5 = \"AwAAAAAAAAAI9f3BfgBs05rqiAwecBatsPMhUbtwy/lssY5IHfz1as3WedpQtXtJeKZndHMtJOx7NoURsW+VQN5PR7WCMJr+BpuIV/6rgYT7aH9jRhjANdrEOdwa6ztVmKDwAAAAAAF6mkC+0kyNlxFPeKUyBpYL11A4a/pUcOYunlw92EFaYe8xN4hyxY/VqgKDw3gpT/JSVB/N/r9zVgAMlmL27VP2E9HOlNA8BU4JrUJL9JQ30DUtHJTFIXaQN1pfDgz/KVX+5r0fIkHxn8CfTzJ6xolktpxI5iPcYED3iDjXX10LTbrYnfWNWX0g3ojdBQP55TgRaxKmM2rKPCR9ZVj9U6ugOKB1oHM+iXvpy89ra32TRE5LVPCRCxDKQW+HAixGJRL+zsGa6VtOWqJYWqNCV5ef3grP0G7ATNeVtXpsms96wQVUrmAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAFVK5gAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAABVSuYAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAVUrmAAAAAAPzGbhwwAAAABAAAAAAAAAFtLagYAAAAAAQAAAAAAAAAFVK5gAAAAADi95MmtAAAAAQAAAAAAAADw2EQGAAAAAAEAAAAAAAAAl5zHWv/y5wWxpcFtmFSbBTWMi2Bd8NV+Sr1XVxD132WPgK2ybPoMrgyN4s5T+6J30kS9BrEGnwtaAp4GCD27TVgDGoaPituuWI3sfe9LKjbMNihp3zYxLPyNBlB3cZswVvPrJ9M2SuDyQR+Cyo4YHlucZh37xO6TuuhSDaaRq4hSNqBWdfB5O0wXcsXSAxviIZnpzZb8yVPAJhqxFNViEC+Y1L1+ULx6KHTSRIwn5wXTktPuZEsRtSpUJpqZD9V5y+8i6KVSbQNBUDzf0xeAFH85lOlowgaoBW/MjycjsrXlD0s7jbPTK2SvlK0fgerysq1O8y7bk5yp6001Zr/sLAEAAAAAAAAAAZbGvC2Uwmchkn8Xc2PeT4aBGReM7Kjuqc1DuN5ufbG1vZ7LZ502Gt8wb/WL0dyrOS4eySbPnOiFlVUBjsVtHwAAAAAAAAAA9Mq1OgAAAAAAAAAAAAAAAIDw+gIAAAAAAAAAAAAAAADwf4dkHAAAAAEAAAAAAAAAAMqaOwAAAAAUOu5om0z+/+/n31+gWwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZwqohV9gfgEAAAAAAAAAAOuuBPiBhIG6j5hOWgMAAAAAoJmZmZmZGQEAAAAAAAAAADAzMzMzMzMBAAAAAAAAAGtqUa92w4LGfIDkG7RkvDMOZX+ssXFAKsKq/DKs+TSjBpU3RtcibIXhdSaNSvboTLAKz91Es3OeaoUjHgSxfAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgYJBgkCAgICAAAAAAAAAA==\"\n",
" decoded_5 = base64.b64decode(encoded_5)\n",
" group_5_account_info = AccountInfo(group_5_public_key, False, Decimal(9020160), owner_5_public_key, Decimal(135), decoded_5)\n",
" group_5 = Group.parse(group_5_context, group_5_account_info)\n",
" print(\"\\n\\nThis is hard-coded, not live information!\")\n",
" print(\"5-token group\", group_5)\n",
"\n",
" token_lookup = TokenLookup.default_lookups()\n",
" print(token_lookup.find_by_symbol(\"ETH\"))\n",
" print(token_lookup.find_by_symbol(\"BTC\"))\n",
"\n",
" # USDT\n",
" print(token_lookup.find_by_mint(PublicKey(\"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB\")))\n",
"\n",
" single_account_info = AccountInfo.load(default_context, default_context.dex_program_id)\n",
" print(\"DEX account info\", single_account_info)\n",
"\n",
" multiple_account_info = AccountInfo.load_multiple(default_context, [default_context.program_id, default_context.dex_program_id])\n",
" print(\"Mango program and DEX account info\", multiple_account_info)\n",
"\n",
" balances_before = [\n",
" TokenValue(token_lookup.find_by_symbol(\"ETH\"), Decimal(1)),\n",
" TokenValue(token_lookup.find_by_symbol(\"BTC\"), Decimal(\"0.1\")),\n",
" TokenValue(token_lookup.find_by_symbol(\"USDT\"), Decimal(1000))\n",
" ]\n",
" balances_after = [\n",
" TokenValue(token_lookup.find_by_symbol(\"ETH\"), Decimal(1)),\n",
" TokenValue(token_lookup.find_by_symbol(\"BTC\"), Decimal(\"0.05\")),\n",
" TokenValue(token_lookup.find_by_symbol(\"USDT\"), Decimal(2000))\n",
" ]\n",
" timestamp = datetime.datetime(2021, 5, 17, 12, 20, 56)\n",
" event = LiquidationEvent(timestamp, \"Liquidator\", \"Group\", True, \"signature\",\n",
" SYSTEM_PROGRAM_ADDRESS, SYSTEM_PROGRAM_ADDRESS,\n",
" balances_before, balances_after)\n",
" print(event)\n",
" finally:\n",
" logging.getLogger().setLevel(log_level)\n",
"\n",
" _notebook_main()\n",
" del _notebook_main\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
}