Initial import.

This commit is contained in:
Geoff Taylor 2021-04-14 16:51:39 +01:00
commit 2cc5135517
23 changed files with 3371 additions and 0 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
.git
wallet.json

4
.envrc Normal file
View File

@ -0,0 +1,4 @@
CURRENT_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
export PATH=$PATH:$CURRENT_DIRECTORY/bin

17
.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
# Mac OS X files
.DS_Store
# Don't store any data in git
data/
# Ignore tmp/ directories
tmp/
# Ignore any Python autogenerated nonsense.
.ipynb_checkpoints
__pycache__
.mypy_cache
# Don't check in anything that might have credentials.
configuration.json
wallet.json

1071
Classes.ipynb Normal file

File diff suppressed because it is too large Load Diff

177
Constants.ipynb Normal file
View File

@ -0,0 +1,177 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "periodic-education",
"metadata": {},
"source": [
"# 🥭 Constants\n",
"\n",
"Some hard-coded values, all kept in one place."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "recreational-fellowship",
"metadata": {},
"outputs": [],
"source": [
"import decimal\n",
"import json\n",
"\n",
"from solana.publickey import PublicKey\n"
]
},
{
"cell_type": "markdown",
"id": "marine-skiing",
"metadata": {},
"source": [
"## SYSTEM_PROGRAM_ADDRESS\n",
"\n",
"The Solana system program address is always 11111111111111111111111111111111."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "matched-variance",
"metadata": {},
"outputs": [],
"source": [
"SYSTEM_PROGRAM_ADDRESS = PublicKey(\"11111111111111111111111111111111\")"
]
},
{
"cell_type": "markdown",
"id": "champion-advance",
"metadata": {},
"source": [
"## SOL_DECIMAL_DIVISOR decimal\n",
"\n",
"The divisor to use to turn an integer value of SOLs from an account's `balance` into a value with the correct number of decimal places."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "eastern-broadway",
"metadata": {},
"outputs": [],
"source": [
"SOL_DECIMAL_DIVISOR = decimal.Decimal(1000000000)"
]
},
{
"cell_type": "markdown",
"id": "completed-saturn",
"metadata": {},
"source": [
"## NUM_TOKENS\n",
"\n",
"This is currently hard-coded to 3."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "rocky-adjustment",
"metadata": {},
"outputs": [],
"source": [
"NUM_TOKENS = 3\n"
]
},
{
"cell_type": "markdown",
"id": "suburban-negative",
"metadata": {},
"source": [
"## NUM_MARKETS\n",
"\n",
"There is one fewer market than tokens."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "bound-handling",
"metadata": {},
"outputs": [],
"source": [
"NUM_MARKETS = NUM_TOKENS - 1\n"
]
},
{
"cell_type": "markdown",
"id": "tough-difference",
"metadata": {},
"source": [
"## MangoConstants\n",
"\n",
"Load all Mango Market's constants from its own `ids.json` file (retrieved from [GitHub](https://raw.githubusercontent.com/blockworks-foundation/mango-client-ts/main/src/ids.json)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "blank-swaziland",
"metadata": {},
"outputs": [],
"source": [
"with open(\"ids.json\") as json_file:\n",
" MangoConstants = json.load(json_file)\n"
]
},
{
"cell_type": "markdown",
"id": "precious-climate",
"metadata": {},
"source": [
"# Running\n",
"\n",
"Just try to access some things and print them out to make sure we have loaded properly."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "forward-google",
"metadata": {},
"outputs": [],
"source": [
"if __name__ == \"__main__\":\n",
" print(\"System program address:\", SYSTEM_PROGRAM_ADDRESS)\n",
" print(\"SOL decimal divisor:\", SOL_DECIMAL_DIVISOR)\n",
" print(\"Number of tokens:\", NUM_TOKENS)\n",
" print(\"Number of markets:\", NUM_MARKETS)\n",
" mango_group = MangoConstants[\"mainnet-beta\"]\n",
" print(f\"Mango program ID: {mango_group['mango_program_id']}\")\n",
" for oracle in mango_group[\"oracles\"]:\n",
" print(f\"Oracle [{oracle}]: {mango_group['oracles'][oracle]}\")\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"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

195
Context.ipynb Normal file
View File

@ -0,0 +1,195 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "ceramic-logic",
"metadata": {},
"source": [
"# 🥭 Context\n",
"\n",
"A `Context` object to manage Solana connection configuration and Mango groups."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "roman-arrival",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"import typing\n",
"\n",
"from decimal import Decimal\n",
"from solana.publickey import PublicKey\n",
"from solana.rpc.api import Client\n",
"from solana.rpc.types import MemcmpOpts, TokenAccountOpts\n",
"from solana.rpc.commitment import Single\n",
"\n",
"from Constants import MangoConstants, SOL_DECIMAL_DIVISOR\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "educational-investment",
"metadata": {},
"outputs": [],
"source": [
"class Context:\n",
" def __init__(self, cluster: str, cluster_url: str, program_id: PublicKey, dex_program_id: PublicKey,\n",
" group_name: str, group_id: PublicKey):\n",
" self.cluster: str = cluster\n",
" self.cluster_url: str = cluster_url\n",
" self.client: Client = Client(cluster_url)\n",
" self.program_id: PublicKey = program_id\n",
" self.dex_program_id: PublicKey = dex_program_id\n",
" self.group_name: str = group_name\n",
" self.group_id: PublicKey = group_id\n",
" self.commitment = Single\n",
" self.encoding:str = \"base64\"\n",
"\n",
"\n",
" def fetch_sol_balance(self, account_public_key: PublicKey):\n",
" result = self.client.get_balance(account_public_key, commitment=self.commitment)\n",
" value = Decimal(result[\"result\"][\"value\"])\n",
" return value / SOL_DECIMAL_DIVISOR\n",
"\n",
"\n",
" def fetch_token_balance(self, account_public_key: PublicKey, token_public_key: PublicKey):\n",
" opts = TokenAccountOpts(mint = token_public_key)\n",
"\n",
" token_account = self.client.get_token_accounts_by_owner(account_public_key, opts, commitment=self.commitment)\n",
" result = self.client.get_token_account_balance(token_account[\"result\"][\"value\"][0][\"pubkey\"], commitment=self.commitment)\n",
" value = Decimal(result[\"result\"][\"value\"][\"amount\"])\n",
" decimal_places = result[\"result\"][\"value\"][\"decimals\"]\n",
" divisor = Decimal(10 ** decimal_places)\n",
" return value / divisor\n",
"\n",
"\n",
" # This should be easier to call and should probably be on the Client object, but we don't\n",
" # control that.\n",
" def fetch_program_accounts_for_owner(self, program_id: PublicKey, owner: PublicKey):\n",
" memcmp_opts = [\n",
" MemcmpOpts(offset=40, bytes=str(owner)),\n",
" ]\n",
"\n",
" return self.client.get_program_accounts(program_id, memcmp_opts=memcmp_opts, commitment=self.commitment, encoding=self.encoding)\n",
"\n",
"\n",
" @staticmethod\n",
" def _lookup_name_by_address(address: PublicKey, collection: typing.Dict[str, str]) -> typing.Optional[str]:\n",
" address_string = str(address)\n",
" for stored_name, stored_address in collection.items():\n",
" if address_string == stored_address:\n",
" return stored_name\n",
" return None\n",
"\n",
" def lookup_market_name(self, market_address: PublicKey) -> str:\n",
" return Context._lookup_name_by_address(market_address, MangoConstants[self.cluster][\"spot_markets\"]) or \"« Unknown Market »\"\n",
"\n",
" def lookup_oracle_name(self, token_address: PublicKey) -> str:\n",
" return Context._lookup_name_by_address(token_address, MangoConstants[self.cluster][\"oracles\"]) or \"« Unknown Oracle »\"\n",
"\n",
" def lookup_token_name(self, token_address: PublicKey) -> str:\n",
" return Context._lookup_name_by_address(token_address, MangoConstants[self.cluster][\"symbols\"]) or \"« Unknown Token »\"\n",
"\n",
" def __str__(self) -> str:\n",
" return f\"\"\"« Context:\n",
" Cluster: {self.cluster}\n",
" Cluster URL: {self.cluster_url}\n",
" Program ID: {self.program_id}\n",
" DEX Program ID: {self.dex_program_id}\n",
" Group Name: {self.group_name}\n",
" Group ID: {self.group_id}\n",
"\"\"\"\n",
"\n",
" def __repr__(self) -> str:\n",
" return f\"{self}\"\n"
]
},
{
"cell_type": "markdown",
"id": "pointed-minimum",
"metadata": {},
"source": [
"# default_context object\n",
"\n",
"A default `Context` object that connects to mainnet, to save having to create one all over the place."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "advisory-workstation",
"metadata": {},
"outputs": [],
"source": [
"default_cluster = os.environ.get(\"CLUSTER\") or 'mainnet-beta'\n",
"default_cluster_url = os.environ.get(\"CLUSTER_URL\") or MangoConstants[\"cluster_urls\"][default_cluster]\n",
"\n",
"default_program_id = PublicKey(MangoConstants[default_cluster][\"mango_program_id\"])\n",
"default_dex_program_id = PublicKey(MangoConstants[default_cluster][\"dex_program_id\"])\n",
"\n",
"default_group_name = os.environ.get(\"GROUP_NAME\") or 'BTC_ETH_USDT'\n",
"default_group_id = PublicKey(MangoConstants[default_cluster][\"mango_groups\"][default_group_name][\"mango_group_pk\"])\n",
"\n",
"default_context = Context(default_cluster, default_cluster_url, default_program_id,\n",
" default_dex_program_id, default_group_name, default_group_id)"
]
},
{
"cell_type": "markdown",
"id": "disciplinary-shield",
"metadata": {},
"source": [
"# Running\n",
"\n",
"If running interactively, just print out the default Context object."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "welcome-hearing",
"metadata": {},
"outputs": [],
"source": [
"if __name__ == \"__main__\":\n",
" print(default_context)\n",
"\n",
" print(\"Lookup ETH token name result:\", default_context.lookup_token_name(PublicKey(\"2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk\")))\n",
" print(\"Lookup BTC/USDC market name result:\", default_context.lookup_market_name(PublicKey(\"CVfYa8RGXnuDBeGmniCcdkBwoLqVxh92xB1JqgRQx3F\")))\n",
" # Fill out your account address between the quotes below\n",
" MY_ACCOUNT_ADDRESS = \"\"\n",
" # Don't edit anything beyond here.\n",
"\n",
" if MY_ACCOUNT_ADDRESS != \"\":\n",
" account_key = PublicKey(MY_ACCOUNT_ADDRESS)\n",
" print(\"SOL balance:\", default_context.fetch_sol_balance(account_key))\n",
" print(\"SRM balance:\", default_context.fetch_token_balance(account_key, PublicKey(\"SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt\")))\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"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

147
Decoder.ipynb Normal file
View File

@ -0,0 +1,147 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "helpful-command",
"metadata": {},
"source": [
"# 🥭 Decoder\n",
"\n",
"Some useful functions for decoding base64 and base58 data."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "likely-gender",
"metadata": {},
"outputs": [],
"source": [
"import base64\n",
"import base58\n",
"import typing\n",
"\n",
"from solana.publickey import PublicKey\n"
]
},
{
"cell_type": "markdown",
"id": "ruled-percentage",
"metadata": {},
"source": [
"## decode_binary() function\n",
"\n",
"A Solana binary data structure may come back as an array with the base64 or base58 encoded data, and a text moniker saying which encoding was used.\n",
"\n",
"For example:\n",
"```\n",
"['AwAAAAAAAACCaOmpoURMK6XHelGTaFawcuQ/78/15LAemWI8jrt3SRKLy2R9i60eclDjuDS8+p/ZhvTUd9G7uQVOYCsR6+BhmqGCiO6EPYP2PQkf/VRTvw7JjXvIjPFJy06QR1Cq1WfTonHl0OjCkyEf60SD07+MFJu5pVWNFGGEO/8AiAYfduaKdnFTaZEHPcK5Eq72WWHeHg2yIbBF09kyeOhlCJwOoG8O5SgpPV8QOA64ZNV4aKroFfADg6kEy/wWCdp3fv2B8WJgAAAAANVfH3HGtjwAAQAAAAAAAADr8cwFi9UOAAEAAAAAAAAAgfFiYAAAAABo3Dbz0L0oAAEAAAAAAAAAr8K+TvCjCwABAAAAAAAAAIHxYmAAAAAA49t5tVNZhwABAAAAAAAAAAmPtcB1zC8AAQAAAAAAAABIBGiCcyaEZdNhrTyeqUY692vOzzPdHaxAxguht3JQGlkzjtd05dX9LENHkl2z1XvUbTNKZlweypNRetmH0lmQ9VYQAHqylxZVK65gEg85g27YuSyvOBZAjJyRmYU9KdCO1D+4ehdPu9dQB1yI1uh75wShdAaFn2o4qrMYwq3SQQEAAAAAAAAAAiH1PPJKAuh6oGiE35aGhUQhFi/bxgKOudpFv8HEHNCFDy1uAqR6+CTQmradxC1wyyjL+iSft+5XudJWwSdi72NJGmyK96x7Obj/AgAAAAB8RjOEdJow6r9LMhIAAAAAGkNK4CXHh5M2st7PnwAAAE33lx1h8hPFD04AAAAAAAA8YRV3Oa309B2wGwAAAAAAOIlOLmkr6+r605n+AQAAAACgmZmZmZkZAQAAAAAAAAAAMDMzMzMzMwEAAAAAAAAA25D1XcAtRzSuuyx3U+X7aE9vM1EJySU9KprgL0LMJ/vat9+SEEUZuga7O5tTUrcMDYWDg+LYaAWhSQiN2fYk7aCGAQAAAAAAgIQeAAAAAAAA8gUqAQAAAAYGBgICAAAA', 'base64']\n",
"```\n",
"Alternatively, it may just be a base58-encoded string.\n",
"\n",
"`decode_binary()` decodes the data properly based on which encoding was used."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "available-latvia",
"metadata": {},
"outputs": [],
"source": [
"def decode_binary(encoded: typing.List) -> bytes:\n",
" if isinstance(encoded, str):\n",
" return base58.b58decode(encoded)\n",
" elif encoded[1] == \"base64\":\n",
" return base64.b64decode(encoded[0])\n",
" else:\n",
" return base58.b58decode(encoded[0])\n"
]
},
{
"cell_type": "markdown",
"id": "integrated-appendix",
"metadata": {},
"source": [
"## encode_binary() function\n",
"\n",
"Inverse of `decode_binary()`, this takes a binary list and encodes it (using base 64), then returns the encoded string and the string \"base64\" in an array.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "seven-buying",
"metadata": {},
"outputs": [],
"source": [
"def encode_binary(decoded: bytes) -> typing.List:\n",
" return [base64.b64encode(decoded), \"base64\"]\n"
]
},
{
"cell_type": "markdown",
"id": "automotive-eating",
"metadata": {},
"source": [
"## encode_key() function\n",
"\n",
"Encodes a `PublicKey` in the proper way for RPC calls."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "minute-titanium",
"metadata": {},
"outputs": [],
"source": [
"def encode_key(key: PublicKey) -> str:\n",
" return str(key)\n"
]
},
{
"cell_type": "markdown",
"id": "above-graduate",
"metadata": {},
"source": [
"# Running\n",
"\n",
"A simple harness to run the above code.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "deadly-norwegian",
"metadata": {},
"outputs": [],
"source": [
"if __name__ == \"__main__\":\n",
" data = decode_binary(['AwAAAAAAAACCaOmpoURMK6XHelGTaFawcuQ/78/15LAemWI8jrt3SRKLy2R9i60eclDjuDS8+p/ZhvTUd9G7uQVOYCsR6+BhmqGCiO6EPYP2PQkf/VRTvw7JjXvIjPFJy06QR1Cq1WfTonHl0OjCkyEf60SD07+MFJu5pVWNFGGEO/8AiAYfduaKdnFTaZEHPcK5Eq72WWHeHg2yIbBF09kyeOhlCJwOoG8O5SgpPV8QOA64ZNV4aKroFfADg6kEy/wWCdp3fv2B8WJgAAAAANVfH3HGtjwAAQAAAAAAAADr8cwFi9UOAAEAAAAAAAAAgfFiYAAAAABo3Dbz0L0oAAEAAAAAAAAAr8K+TvCjCwABAAAAAAAAAIHxYmAAAAAA49t5tVNZhwABAAAAAAAAAAmPtcB1zC8AAQAAAAAAAABIBGiCcyaEZdNhrTyeqUY692vOzzPdHaxAxguht3JQGlkzjtd05dX9LENHkl2z1XvUbTNKZlweypNRetmH0lmQ9VYQAHqylxZVK65gEg85g27YuSyvOBZAjJyRmYU9KdCO1D+4ehdPu9dQB1yI1uh75wShdAaFn2o4qrMYwq3SQQEAAAAAAAAAAiH1PPJKAuh6oGiE35aGhUQhFi/bxgKOudpFv8HEHNCFDy1uAqR6+CTQmradxC1wyyjL+iSft+5XudJWwSdi72NJGmyK96x7Obj/AgAAAAB8RjOEdJow6r9LMhIAAAAAGkNK4CXHh5M2st7PnwAAAE33lx1h8hPFD04AAAAAAAA8YRV3Oa309B2wGwAAAAAAOIlOLmkr6+r605n+AQAAAACgmZmZmZkZAQAAAAAAAAAAMDMzMzMzMwEAAAAAAAAA25D1XcAtRzSuuyx3U+X7aE9vM1EJySU9KprgL0LMJ/vat9+SEEUZuga7O5tTUrcMDYWDg+LYaAWhSQiN2fYk7aCGAQAAAAAAgIQeAAAAAAAA8gUqAQAAAAYGBgICAAAA', 'base64'])\n",
" print(f\"Data length (should be 744): {len(data)}\")\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"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM jupyter/scipy-notebook:latest
COPY requirements.txt /tmp/
RUN pip install --requirement /tmp/requirements.txt && \
fix-permissions $CONDA_DIR && \
fix-permissions /home/$NB_USER
# Create our profile directory.
RUN ipython profile create
# Copy across our magic/startup scripts.
COPY meta/startup /home/jovyan/.ipython/profile_default/startup
ENV PATH="/home/jovyan/work/bin:${PATH}"
ADD . /home/jovyan/work
WORKDIR /home/jovyan/work

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Geoff Taylor
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.

687
Layouts.ipynb Normal file
View File

@ -0,0 +1,687 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "tight-particular",
"metadata": {},
"source": [
"# 🥭 Layouts\n",
"\n",
"Structure layouts to load the sometimes-opaque data blobs from Solana accounts.\n",
"\n",
"The idea is to have one data-encapsulating class (in [Classes](Classes.ipynb)) for each type, as well as one or more LAYOUT structures. So for example `Group` loads `GROUP` but in future will also be able to load `GROUP_V2`.\n",
"\n",
"The general approach is:\n",
"* Define one (or more) layouts to read in the data blob\n",
"* Use the data from the data blob to construct a more useful strongly-typed object\n",
"\n",
"So for example `GROUP` is defined below but it's a low-level dependency. In general, code should depend and work with the `Group` class, not the `GROUP` structure.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "featured-exploration",
"metadata": {},
"outputs": [],
"source": [
"import construct\n",
"import datetime\n",
"\n",
"from decimal import Decimal\n",
"from solana.publickey import PublicKey\n",
"\n",
"from Constants import NUM_MARKETS, NUM_TOKENS\n"
]
},
{
"cell_type": "markdown",
"id": "painted-citizenship",
"metadata": {},
"source": [
"# Adapters\n",
"\n",
"These are adapters for the construct package to simplify our struct declarations."
]
},
{
"cell_type": "markdown",
"id": "fuzzy-cambridge",
"metadata": {},
"source": [
"## DecimalAdapter class\n",
"\n",
"A simple construct `Adapter` that lets us use `Decimal`s directly in our structs."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "pretty-twenty",
"metadata": {},
"outputs": [],
"source": [
"class DecimalAdapter(construct.Adapter):\n",
" def __init__(self, size: int = 8):\n",
" construct.Adapter.__init__(self, construct.BytesInteger(size, swapped=True))\n",
"\n",
" def _decode(self, obj, context, path) -> Decimal:\n",
" return Decimal(obj)\n",
"\n",
" def _encode(self, obj, context, path) -> bytes:\n",
" return bytes(obj)\n"
]
},
{
"cell_type": "markdown",
"id": "alternate-disposition",
"metadata": {},
"source": [
"## Float64Adapter class\n",
"\n",
"Some numbers are packaged as 16-bytes to represent a `float`. The way to get the `float` is to take the 16-byte int value and divide it by 2 to the power 64. In Javascript this would be:\n",
"```\n",
"return intValue / Math.pow(2, 64);\n",
"```\n",
"\n",
"This is a simple construct `Adapter` that lets us use these float values directly in our structs."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "orange-router",
"metadata": {},
"outputs": [],
"source": [
"class Float64Adapter(construct.Adapter):\n",
" _divisor = Decimal(2 ** 64)\n",
"\n",
" def __init__(self, size: int = 16):\n",
" construct.Adapter.__init__(self, construct.BytesInteger(size, swapped=True))\n",
"\n",
" def _decode(self, obj, context, path) -> Decimal:\n",
" return Decimal(obj) / Float64Adapter._divisor\n",
"\n",
" def _encode(self, obj, context, path) -> bytes:\n",
" return bytes(obj)\n"
]
},
{
"cell_type": "markdown",
"id": "liberal-trout",
"metadata": {},
"source": [
"## PublicKeyAdapter\n",
"\n",
"A simple construct `Adapter` that lets us use `PublicKey`s directly in our structs."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "tested-shame",
"metadata": {},
"outputs": [],
"source": [
"class PublicKeyAdapter(construct.Adapter):\n",
" def __init__(self):\n",
" construct.Adapter.__init__(self, construct.Bytes(32))\n",
"\n",
" def _decode(self, obj, context, path) -> PublicKey:\n",
" return PublicKey(obj)\n",
"\n",
" def _encode(self, obj, context, path) -> bytes:\n",
" return bytes(obj)\n"
]
},
{
"cell_type": "markdown",
"id": "superb-consumer",
"metadata": {},
"source": [
"## DatetimeAdapter\n",
"\n",
"A simple construct `Adapter` that lets us load `datetime`s directly in our structs."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "express-duration",
"metadata": {},
"outputs": [],
"source": [
"class DatetimeAdapter(construct.Adapter):\n",
" def __init__(self):\n",
" construct.Adapter.__init__(self, construct.BytesInteger(8, swapped=True))\n",
"\n",
" def _decode(self, obj, context, path) -> datetime.datetime:\n",
" return datetime.datetime.fromtimestamp(obj)\n",
"\n",
" def _encode(self, obj, context, path) -> bytes:\n",
" return bytes(obj)\n",
"\n",
" "
]
},
{
"cell_type": "markdown",
"id": "several-child",
"metadata": {},
"source": [
"# SERUM_ACCOUNT_FLAGS\n",
"\n",
"The SERUM_ prefix is because there's also `MANGO_ACCOUNT_FLAGS`.\n",
"\n",
"Here's the [Serum Rust structure](https://github.com/project-serum/serum-dex/blob/master/dex/src/state.rs):\n",
"```\n",
"#[derive(Copy, Clone, BitFlags, Debug, Eq, PartialEq)]\n",
"#[repr(u64)]\n",
"pub enum AccountFlag {\n",
" Initialized = 1u64 << 0,\n",
" Market = 1u64 << 1,\n",
" OpenOrders = 1u64 << 2,\n",
" RequestQueue = 1u64 << 3,\n",
" EventQueue = 1u64 << 4,\n",
" Bids = 1u64 << 5,\n",
" Asks = 1u64 << 6,\n",
" Disabled = 1u64 << 7,\n",
"}\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "indie-surgeon",
"metadata": {},
"outputs": [],
"source": [
"SERUM_ACCOUNT_FLAGS = construct.BitsSwapped(\n",
" construct.BitStruct(\n",
" \"initialized\" / construct.Flag,\n",
" \"market\" / construct.Flag,\n",
" \"open_orders\" / construct.Flag,\n",
" \"request_queue\" / construct.Flag,\n",
" \"event_queue\" / construct.Flag,\n",
" \"bids\" / construct.Flag,\n",
" \"asks\" / construct.Flag,\n",
" \"disabled\" / construct.Flag,\n",
" construct.Padding(7 * 8)\n",
" )\n",
")\n"
]
},
{
"cell_type": "markdown",
"id": "acquired-republic",
"metadata": {},
"source": [
"# MANGO_ACCOUNT_FLAGS\n",
"\n",
"The MANGO_ prefix is because there's also `SERUM_ACCOUNT_FLAGS`.\n",
"\n",
"The MANGO_ACCOUNT_FLAGS should be exactly 8 bytes.\n",
"\n",
"Here's the [Mango Rust structure](https://github.com/blockworks-foundation/mango/blob/master/program/src/state.rs):\n",
"```\n",
"#[derive(Copy, Clone, BitFlags, Debug, Eq, PartialEq)]\n",
"#[repr(u64)]\n",
"pub enum AccountFlag {\n",
" Initialized = 1u64 << 0,\n",
" MangoGroup = 1u64 << 1,\n",
" MarginAccount = 1u64 << 2,\n",
" MangoSrmAccount = 1u64 << 3\n",
"}\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "curious-thursday",
"metadata": {},
"outputs": [],
"source": [
"MANGO_ACCOUNT_FLAGS = construct.BitsSwapped(\n",
" construct.BitStruct(\n",
" \"initialized\" / construct.Flag,\n",
" \"group\" / construct.Flag,\n",
" \"margin_account\" / construct.Flag,\n",
" \"srm_account\" / construct.Flag,\n",
" construct.Padding(4 + (7 * 8))\n",
" )\n",
")\n"
]
},
{
"cell_type": "markdown",
"id": "saved-conference",
"metadata": {},
"source": [
"# INDEX\n",
"\n",
"Here's the [Mango Rust structure](https://github.com/blockworks-foundation/mango/blob/master/program/src/state.rs):\n",
"```\n",
"#[derive(Copy, Clone)]\n",
"#[repr(C)]\n",
"pub struct MangoIndex {\n",
" pub last_update: u64,\n",
" pub borrow: U64F64,\n",
" pub deposit: U64F64\n",
"}\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "acting-annotation",
"metadata": {},
"outputs": [],
"source": [
"INDEX = construct.Struct(\n",
" \"last_update\" / DatetimeAdapter(),\n",
" \"borrow\" / Float64Adapter(),\n",
" \"deposit\" / Float64Adapter()\n",
");\n"
]
},
{
"cell_type": "markdown",
"id": "crazy-market",
"metadata": {},
"source": [
"# AGGREGATOR_CONFIG\n",
"\n",
"Here's the [Flux Rust structure](https://github.com/blockworks-foundation/solana-flux-aggregator/blob/master/program/src/state.rs):\n",
"```\n",
"#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Default, PartialEq)]\n",
"pub struct AggregatorConfig {\n",
" /// description\n",
" pub description: [u8; 32],\n",
"\n",
" /// decimals for this feed\n",
" pub decimals: u8,\n",
"\n",
" /// oracle cannot start a new round until after `restart_relay` rounds\n",
" pub restart_delay: u8,\n",
"\n",
" /// max number of submissions in a round\n",
" pub max_submissions: u8,\n",
"\n",
" /// min number of submissions in a round to resolve an answer\n",
" pub min_submissions: u8,\n",
"\n",
" /// amount of tokens oracles are reward per submission\n",
" pub reward_amount: u64,\n",
"\n",
" /// SPL token account from which to withdraw rewards\n",
" pub reward_token_account: PublicKey,\n",
"}\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "atomic-senator",
"metadata": {},
"outputs": [],
"source": [
"AGGREGATOR_CONFIG = construct.Struct(\n",
" \"description\" / construct.PaddedString(32, \"utf8\"),\n",
" \"decimals\" / DecimalAdapter(1),\n",
" \"restart_delay\" / DecimalAdapter(1),\n",
" \"max_submissions\" / DecimalAdapter(1),\n",
" \"min_submissions\" / DecimalAdapter(1),\n",
" \"reward_amount\" / DecimalAdapter(),\n",
" \"reward_token_account\" / PublicKeyAdapter()\n",
");\n"
]
},
{
"cell_type": "markdown",
"id": "utility-accountability",
"metadata": {},
"source": [
"# ROUND\n",
"\n",
"Here's the [Flux Rust structure](https://github.com/blockworks-foundation/solana-flux-aggregator/blob/master/program/src/state.rs):\n",
"```\n",
"#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Default, PartialEq)]\n",
"pub struct Round {\n",
" pub id: u64,\n",
" pub created_at: u64,\n",
" pub updated_at: u64,\n",
"}\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "unlike-passage",
"metadata": {},
"outputs": [],
"source": [
"ROUND = construct.Struct(\n",
" \"id\" / DecimalAdapter(),\n",
" \"created_at\" / DecimalAdapter(),\n",
" \"updated_at\" / DecimalAdapter()\n",
");\n"
]
},
{
"cell_type": "markdown",
"id": "essential-steal",
"metadata": {},
"source": [
"# ANSWER\n",
"\n",
"Here's the [Flux Rust structure](https://github.com/blockworks-foundation/solana-flux-aggregator/blob/master/program/src/state.rs):\n",
"```\n",
"#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Default, PartialEq)]\n",
"pub struct Answer {\n",
" pub round_id: u64,\n",
" pub median: u64,\n",
" pub created_at: u64,\n",
" pub updated_at: u64,\n",
"}\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "similar-result",
"metadata": {},
"outputs": [],
"source": [
"ANSWER = construct.Struct(\n",
" \"round_id\" / DecimalAdapter(),\n",
" \"median\" / DecimalAdapter(),\n",
" \"created_at\" / DatetimeAdapter(),\n",
" \"updated_at\" / DatetimeAdapter()\n",
");\n"
]
},
{
"cell_type": "markdown",
"id": "macro-invention",
"metadata": {},
"source": [
"# AGGREGATOR\n",
"\n",
"Here's the [Flux Rust structure](https://github.com/blockworks-foundation/solana-flux-aggregator/blob/master/program/src/state.rs):\n",
"```\n",
"#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Default, PartialEq)]\n",
"pub struct Aggregator {\n",
" pub config: AggregatorConfig,\n",
" /// is initialized\n",
" pub is_initialized: bool,\n",
" /// authority\n",
" pub owner: PublicKey,\n",
" /// current round accepting oracle submissions\n",
" pub round: Round,\n",
" pub round_submissions: PublicKey, // has_one: Submissions\n",
" /// the latest answer resolved\n",
" pub answer: Answer,\n",
" pub answer_submissions: PublicKey, // has_one: Submissions\n",
"}\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "delayed-malpractice",
"metadata": {},
"outputs": [],
"source": [
"AGGREGATOR = construct.Struct(\n",
" \"config\" / AGGREGATOR_CONFIG,\n",
" \"initialized\" / DecimalAdapter(1),\n",
" \"owner\" / PublicKeyAdapter(),\n",
" \"round\" / ROUND,\n",
" \"round_submissions\" / PublicKeyAdapter(),\n",
" \"answer\" / ANSWER,\n",
" \"answer_submissions\" / PublicKeyAdapter()\n",
");\n"
]
},
{
"cell_type": "markdown",
"id": "protecting-finland",
"metadata": {},
"source": [
"# GROUP\n",
"\n",
"Here's the [Mango Rust structure](https://github.com/blockworks-foundation/mango/blob/master/program/src/state.rs):\n",
"```\n",
"#[derive(Copy, Clone)]\n",
"#[repr(C)]\n",
"pub struct MangoGroup {\n",
" pub account_flags: u64,\n",
" pub tokens: [Pubkey; NUM_TOKENS], // Last token is shared quote currency\n",
" pub vaults: [Pubkey; NUM_TOKENS], // where funds are stored\n",
" pub indexes: [MangoIndex; NUM_TOKENS], // to keep track of interest\n",
" pub spot_markets: [Pubkey; NUM_MARKETS], // pubkeys to MarketState of serum dex\n",
" pub oracles: [Pubkey; NUM_MARKETS], // oracles that give price of each base currency in quote currency\n",
" pub signer_nonce: u64,\n",
" pub signer_key: Pubkey,\n",
" pub dex_program_id: Pubkey, // serum dex program id\n",
"\n",
" // denominated in Mango index adjusted terms\n",
" pub total_deposits: [U64F64; NUM_TOKENS],\n",
" pub total_borrows: [U64F64; NUM_TOKENS],\n",
"\n",
" pub maint_coll_ratio: U64F64, // 1.10\n",
" pub init_coll_ratio: U64F64, // 1.20\n",
"\n",
" pub srm_vault: Pubkey, // holds users SRM for fee reduction\n",
"\n",
" /// This admin key is only for alpha release and the only power it has is to amend borrow limits\n",
" /// If users borrow too much too quickly before liquidators are able to handle the volume,\n",
" /// lender funds will be at risk. Hence these borrow limits will be raised slowly\n",
" pub admin: Pubkey,\n",
" pub borrow_limits: [u64; NUM_TOKENS],\n",
"\n",
" pub mint_decimals: [u8; NUM_TOKENS],\n",
" pub oracle_decimals: [u8; NUM_MARKETS],\n",
" pub padding: [u8; MANGO_GROUP_PADDING]\n",
"}\n",
"impl_loadable!(MangoGr\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "alpha-telescope",
"metadata": {},
"outputs": [],
"source": [
"GROUP_PADDING = 8 - (NUM_TOKENS + NUM_MARKETS) % 8\n",
"\n",
"GROUP = construct.Struct(\n",
" \"account_flags\" / MANGO_ACCOUNT_FLAGS,\n",
" \"tokens\" / construct.Array(NUM_TOKENS, PublicKeyAdapter()),\n",
" \"vaults\" / construct.Array(NUM_TOKENS, PublicKeyAdapter()),\n",
" \"indexes\" / construct.Array(NUM_TOKENS, INDEX),\n",
" \"spot_markets\" / construct.Array(NUM_MARKETS, PublicKeyAdapter()),\n",
" \"oracles\" / construct.Array(NUM_MARKETS, PublicKeyAdapter()),\n",
" \"signer_nonce\" / DecimalAdapter(),\n",
" \"signer_key\" / PublicKeyAdapter(),\n",
" \"dex_program_id\" / PublicKeyAdapter(),\n",
" \"total_deposits\" / construct.Array(NUM_TOKENS, Float64Adapter()),\n",
" \"total_borrows\" / construct.Array(NUM_TOKENS, Float64Adapter()),\n",
" \"maint_coll_ratio\" / Float64Adapter(),\n",
" \"init_coll_ratio\" / Float64Adapter(),\n",
" \"srm_vault\" / PublicKeyAdapter(),\n",
" \"admin\" / PublicKeyAdapter(),\n",
" \"borrow_limits\" / construct.Array(NUM_TOKENS, DecimalAdapter()),\n",
" \"mint_decimals\" / construct.Array(NUM_TOKENS, DecimalAdapter(1)),\n",
" \"oracle_decimals\" / construct.Array(NUM_MARKETS, DecimalAdapter(1)),\n",
" \"padding\" / construct.Array(GROUP_PADDING, construct.Padding(1))\n",
");\n"
]
},
{
"cell_type": "markdown",
"id": "expired-japan",
"metadata": {},
"source": [
"# TOKEN_ACCOUNT"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cheap-birthday",
"metadata": {},
"outputs": [],
"source": [
"TOKEN_ACCOUNT = construct.Struct(\n",
" \"mint\" / PublicKeyAdapter(),\n",
" \"owner\" / PublicKeyAdapter(),\n",
" \"amount\" / DecimalAdapter(),\n",
" \"padding\" / construct.Padding(93)\n",
");\n"
]
},
{
"cell_type": "markdown",
"id": "induced-latitude",
"metadata": {},
"source": [
"# OPEN_ORDERS\n",
"\n",
"Trying to use the `OPEN_ORDERS_LAYOUT` and `OpenOrdersAccount` from `pyserum` just proved too probelmatic. (`OpenOrdersAccount` doesn't expose `referrer_rebate_accrued`, for instance.)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "animal-definition",
"metadata": {},
"outputs": [],
"source": [
"OPEN_ORDERS = construct.Struct(\n",
" construct.Padding(5),\n",
" \"account_flags\" / SERUM_ACCOUNT_FLAGS,\n",
" \"market\" / PublicKeyAdapter(),\n",
" \"owner\" / PublicKeyAdapter(),\n",
" \"base_token_free\" / DecimalAdapter(),\n",
" \"base_token_total\" / DecimalAdapter(),\n",
" \"quote_token_free\" / DecimalAdapter(),\n",
" \"quote_token_total\" / DecimalAdapter(),\n",
" \"free_slot_bits\" / DecimalAdapter(16),\n",
" \"is_bid_bits\" / DecimalAdapter(16),\n",
" \"orders\" / construct.Array(128, DecimalAdapter(16)),\n",
" \"client_ids\" / construct.Array(128, DecimalAdapter()),\n",
" \"referrer_rebate_accrued\" / DecimalAdapter(),\n",
" \"padding\" / construct.Padding(7)\n",
");\n"
]
},
{
"cell_type": "markdown",
"id": "mature-banana",
"metadata": {},
"source": [
"# MARGIN_ACCOUNT\n",
"\n",
"Here's the [Mango Rust structure](https://github.com/blockworks-foundation/mango/blob/master/program/src/state.rs):\n",
"```\n",
"#[derive(Copy, Clone)]\n",
"#[repr(C)]\n",
"pub struct MarginAccount {\n",
" pub account_flags: u64,\n",
" pub mango_group: Pubkey,\n",
" pub owner: Pubkey, // solana pubkey of owner\n",
"\n",
" // assets and borrows are denominated in Mango adjusted terms\n",
" pub deposits: [U64F64; NUM_TOKENS], // assets being lent out and gaining interest, including collateral\n",
"\n",
" // this will be incremented every time an order is opened and decremented when order is closed\n",
" pub borrows: [U64F64; NUM_TOKENS], // multiply by current index to get actual value\n",
"\n",
" pub open_orders: [Pubkey; NUM_MARKETS], // owned by Mango\n",
"\n",
" pub being_liquidated: bool,\n",
" pub padding: [u8; 7] // padding to make compatible with previous MarginAccount size\n",
" // TODO add has_borrows field for easy memcmp fetching\n",
"}\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "lucky-wealth",
"metadata": {},
"outputs": [],
"source": [
"MARGIN_ACCOUNT = construct.Struct(\n",
" \"account_flags\" / MANGO_ACCOUNT_FLAGS,\n",
" \"mango_group\" / PublicKeyAdapter(),\n",
" \"owner\" / PublicKeyAdapter(),\n",
" \"deposits\" / construct.Array(NUM_TOKENS, Float64Adapter()),\n",
" \"borrows\" / construct.Array(NUM_TOKENS, Float64Adapter()),\n",
" \"open_orders\" / construct.Array(NUM_MARKETS, PublicKeyAdapter()),\n",
" \"padding\" / construct.Padding(8)\n",
");\n"
]
},
{
"cell_type": "markdown",
"id": "clinical-subject",
"metadata": {},
"source": [
"# Running"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "soviet-information",
"metadata": {},
"outputs": [],
"source": [
"if __name__ == \"__main__\":\n",
" import base64\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)\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-showcode": false
},
"nbformat": 4,
"nbformat_minor": 5
}

168
Pandas.ipynb Normal file
View File

@ -0,0 +1,168 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "boring-principle",
"metadata": {},
"source": [
"# 🥭 Mango + Pandas 🐼🐼"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "expressed-creek",
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import time\n",
"\n",
"from Context import default_context\n",
"from Classes import Group, MarginAccount, OpenOrders\n",
"\n",
"start_time = time.time()\n",
"\n",
"print(\"Loading group...\")\n",
"group = Group.load(default_context)\n",
"print(f\"Done. Time taken: {time.time() - start_time}\")\n",
"\n",
"print(\"Loading prices...\")\n",
"prices = group.get_prices()\n",
"print(f\"Done. Time taken: {time.time() - start_time}\")\n",
"\n",
"print(\"Loading margin accounts...\")\n",
"margin_accounts = MarginAccount.load_all_for_group(default_context, default_context.program_id, group)\n",
"print(f\"Done. Time taken: {time.time() - start_time}\")\n",
"\n",
"print(\"Loading open orders accounts...\")\n",
"open_orders = OpenOrders.load_raw_open_orders_accounts(default_context, group)\n",
"print(f\"Done. Time taken: {time.time() - start_time}\")\n",
"\n",
"print(\"Installing open orders accounts...\")\n",
"open_orders_by_address = {key: value for key, value in [(str(address), open_orders_account) for address, open_orders_account in open_orders]}\n",
"for margin_account in margin_accounts:\n",
" margin_account.install_open_orders_accounts(group, open_orders_by_address)\n",
"print(f\"Done. Time taken: {time.time() - start_time}\")\n",
"\n",
"print(\"Loading pandas dataframe...\")\n",
"data = []\n",
"df_index = []\n",
"for index, margin_account in enumerate(margin_accounts):\n",
" balance_sheet = margin_account.get_balance_sheet(group, prices)\n",
" df_index += [str(margin_account.address)]\n",
" data += [{\"Owner\": margin_account.owner, \"Liabilities\": balance_sheet.liabilities, \"Assets\": balance_sheet.assets, \"Settled Assets\": balance_sheet.settled_assets, \"Unsettled Assets\": balance_sheet.unsettled_assets, \"Collateral Ratio\": balance_sheet.collateral_ratio}]\n",
"df = pd.DataFrame(data, index=df_index)\n",
"\n",
"print(f\"Done. Time taken: {time.time() - start_time}\")\n",
"\n",
"def render_styled(df: pd.DataFrame):\n",
" return df.style.format({\n",
" \"Liabilities\": \"${:,.2f}\",\n",
" \"Assets\": \"${:,.2f}\",\n",
" \"Settled Assets\": \"${:,.2f}\",\n",
" \"Unsettled Assets\": \"${:,.2f}\",\n",
" \"Collateral Ratio\": \"{:,.2%}\"\n",
"})\n"
]
},
{
"cell_type": "markdown",
"id": "rural-number",
"metadata": {},
"source": [
"# Total assets and liabilities"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "optical-protocol",
"metadata": {},
"outputs": [],
"source": [
"print(f\"\"\"\n",
"Total Assets: ${df['Assets'].sum():>15,.2f}\n",
"Total Liabilities: ${df['Liabilities'].sum():>15,.2f}\n",
"\"\"\")"
]
},
{
"cell_type": "markdown",
"id": "hollow-smile",
"metadata": {},
"source": [
"# Top 10 margin accounts with most assets"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "adopted-corrections",
"metadata": {},
"outputs": [],
"source": [
"render_styled(df.sort_values(\"Assets\", ascending=False).head(10))"
]
},
{
"cell_type": "markdown",
"id": "executive-malawi",
"metadata": {},
"source": [
"# Top 10 margin accounts with most liabilities"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "comparable-davis",
"metadata": {},
"outputs": [],
"source": [
"render_styled(df.sort_values(\"Liabilities\", ascending=False).head(10))"
]
},
{
"cell_type": "markdown",
"id": "welsh-blake",
"metadata": {},
"source": [
"# Top 10 least collateralised margin accounts\n",
"\n",
"Collect all margin accounts that have a non-zero Collateral Ratio (so have some liabilities). Then sort them from least-collateralised to most-collateralised."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "gothic-example",
"metadata": {},
"outputs": [],
"source": [
"render_styled(df[df[\"Collateral Ratio\"] != 0].sort_values(\"Collateral Ratio\", ascending=True).head(10))"
]
}
],
"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"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

65
README.md Normal file
View File

@ -0,0 +1,65 @@
# Mango Explorer
**I am not yet confident in the figures and calculations. Please don't rely on this code yet!**
This is the start of a project to explore and provide useful code for [Mango Markets](https://mango.markets/).
There are some notebook pages to explore Mango account structures for your own accounts.
The aim is also to have, at some point, a fully-functioning liquidator.
The goal is not to be easy to use (although that would be nice!). The goal is to show you how the system works.
## Show your Mango margin accounts
To try this out, go to the [Show My Accounts](ShowMyAccounts.ipynb) page and enter your public key. (Note: I know you're running untrusted code, but it can't do much if you only give it your public key.)
## Show all Mango margin accounts
To try this out, go to the [Show All Accounts](ShowAllAccounts.ipynb) page and run the code.
## Load all margin accounts into a Pandas `DataFrame`
To try this out, go to the [Pandas](Pandas.ipynb) page and run the code.
[Pandas](https://pandas.pydata.org/) is a data analysis and manipulation tool and it's useful for sorting and slicing large data sets.
The [Pandas](Pandas.ipynb) page can currently show you:
* The total assets and liabilities currently in [Mango Markets](https://mango.markets/) margin accounts.
* The top ten margin accounts with the most assets.
* The top ten margin accounts with the most liabilities.
* The top ten margin accounts with the lowest collateralisation.
## Structure of this project
The code is (nearly) all Python in Jupyter Notebooks.
Some notebooks are more code files than useful notebooks themselves (although being able to easily run the code is still a boon):
* The [Layouts](Layouts.ipynb) notebook contains the low-level Python data structures for interpreting raw Solana program data.
* The [Classes](Classes.ipynb) notebook contains higher-level classes for loading and working with that data.
Other notebooks are more user-oriented:
* [Show My Accounts](ShowMyAccounts.ipynb) to show data pertaining to a single Mango Markets margin account.
* [Show All Accounts](ShowAllAccounts.ipynb) to show data for all Mango Markets margin accounts.
* [Pandas](Pandas.ipynb) to load data into a [Pandas](https://pandas.pydata.org/) `DataFrame` to allow for further manipulation and analysis.
## TODO
Still to come:
* More work on margin accounts and valuation
* Finding and showing liquidatable margin accounts
* A command-line tool to run a full liquidator
* Performance of code that loads all margin accounts is much too slow and needs to be improved.
* The notebooks use [hard-coded data from the Mango Client](https://raw.githubusercontent.com/blockworks-foundation/mango-client-ts/main/src/ids.json) - it would be good if it could do without that.
Don't hold your breath waiting for these!
# Support
You can contact me [@OpinionatedGeek on Twitter](https://twitter.com/OpinionatedGeek).

105
ShowAllAccounts.ipynb Normal file
View File

@ -0,0 +1,105 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "sonic-somewhere",
"metadata": {},
"source": [
"# 🥭 Show All Accounts\n",
"\n",
"This notebook tries to display information about all Mango margin accounts.\n",
"\n",
"It fetches the data from Solana, parses it, and then prints it.\n"
]
},
{
"cell_type": "markdown",
"id": "addressed-society",
"metadata": {},
"source": [
"## How To Use This Page\n",
"\n",
"Theo code should be runnable as-is. Just click the >> button in the toolbar above, and you should see output appear below the code."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "naked-notion",
"metadata": {},
"outputs": [],
"source": [
"from pyserum.market import Market\n",
"\n",
"from Context import default_context\n",
"from Classes import AccountInfo, Group, MarginAccount, OpenOrders, TokenAccount\n",
"\n",
"def show_all_accounts():\n",
" print(\"Context:\", default_context);\n",
"\n",
" group = Group.load(default_context)\n",
" print(\"Group:\", group)\n",
"\n",
" markets = list(map(lambda market: Market.load(default_context.client, market.spot), group.markets))\n",
" print(\"Markets:\", markets)\n",
"\n",
" prices = group.get_prices()\n",
" for price in prices:\n",
" print(f\"Price: {price:,.8f}\")\n",
"\n",
" vaults = AccountInfo.load_multiple(default_context, [token.vault for token in group.tokens])\n",
" print(\"Vaults:\", vaults)\n",
"\n",
" for index, vault in enumerate(vaults):\n",
" token = TokenAccount.parse(vault.data)\n",
" decimals = group.tokens[index].decimals\n",
" amount = token.amount / (10 ** decimals)\n",
" print(f\"Vault token amount[{index}]: {amount:,.8f}\")\n",
"\n",
" import time\n",
" start_time = time.time()\n",
" print(\"Loading margin accounts...\")\n",
" margin_accounts = MarginAccount.load_all_for_group(default_context, default_context.program_id, group)\n",
" print(f\"Done. Time taken: {time.time() - start_time}\")\n",
"\n",
" print(\"Loading open orders accounts...\")\n",
" # Calling margin_account.load_open_orders() in a loop for each margin account would take a long time.\n",
" # We can do better. Load all the relevant OpenOrders accounts, and then just call each margin account\n",
" # to map its own OpenOrders from the loaded OpenOrders dictionary.\n",
" open_orders = OpenOrders.load_raw_open_orders_accounts(default_context, group)\n",
" open_orders_by_address = {key: value for key, value in [(str(address), open_orders_account) for address, open_orders_account in open_orders]}\n",
" for margin_account in margin_accounts:\n",
" margin_account.install_open_orders_accounts(group, open_orders_by_address)\n",
" print(f\"Done. Time taken: {time.time() - start_time}\")\n",
"\n",
" # print(margin_accounts)\n",
"\n",
"# show_all_accounts()\n",
"import cProfile\n",
"import pstats\n",
"cProfile.run(\"show_all_accounts()\", sort=pstats.SortKey.TIME)\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"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

85
ShowMyAccounts.ipynb Normal file
View File

@ -0,0 +1,85 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "acute-worker",
"metadata": {},
"source": [
"# 🥭 Show My Accounts\n",
"\n",
"This notebook tries to display information about all Mango margin accounts that belong to a specified account.\n",
"\n",
"It fetches the data from Solana, parses it, and then prints it.\n"
]
},
{
"cell_type": "markdown",
"id": "blessed-concentration",
"metadata": {},
"source": [
"## How To Use This Page\n",
"\n",
"Enter the public key of the account you want to check in the value for `ACCOUNT_TO_LOOK_UP` in the box below, between the double-quote marks. Then run the notebook by choosing 'Run > Run All Cells' from the notebook menu at the top of the page."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "lonely-graham",
"metadata": {},
"outputs": [],
"source": [
"ACCOUNT_TO_LOOK_UP = \"\""
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "prescription-basics",
"metadata": {},
"outputs": [],
"source": [
"from solana.publickey import PublicKey\n",
"from Context import default_context\n",
"from Classes import AccountInfo, Group, MarginAccount\n",
"\n",
"print(\"Context:\", default_context);\n",
"\n",
"root_account_key = PublicKey(ACCOUNT_TO_LOOK_UP)\n",
"root_account = AccountInfo.load(default_context, root_account_key)\n",
"print(\"My account:\", root_account)\n",
"\n",
"group = Group.load(default_context)\n",
"# print(\"Group:\", group)\n",
"\n",
"prices = group.get_prices()\n",
"\n",
"my_margin_accounts = MarginAccount.load_all_for_owner(default_context, default_context.program_id, group, root_account_key)\n",
"for my_margin_account in my_margin_accounts:\n",
" print(\"My margin account:\", my_margin_account)\n",
" print(my_margin_account.get_balance_sheet(group, prices))\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"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

7
bin/build-container Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
CURRENT_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "${CURRENT_DIRECTORY}/.."
docker build . -t mango-markets

11
bin/run-jupyter Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
CURRENT_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
docker run -it --rm \
-e GRANT_SUDO=yes --user root \
-p 8888:8888 \
--name mango-markets \
-v ${CURRENT_DIRECTORY}/..:/home/jovyan/work \
mango-markets:latest \
start.sh jupyter lab --NotebookApp.iopub_data_rate_limit=9e9

36
bin/run-lint.py Executable file
View File

@ -0,0 +1,36 @@
#!/usr/bin/env python3
import os
import sys
from pathlib import Path
from os.path import isfile, join
# Get the full path to this script.
script_path = Path(os.path.realpath(__file__))
# The parent of the script is the bin directory, the parent of that is the
# notebook directory. It's this notebook directory we want.
notebook_directory = script_path.parent.parent
print(f"Linting notebook files in: {notebook_directory}")
# But we want to lint every notebook just to make sure the code in it is OK.
all_notebook_files = [f for f in os.listdir(notebook_directory) if isfile(
notebook_directory / f) and f.endswith(".ipynb")]
all_notebook_files.sort()
try:
# Now lint each notebook in turn.
for notebook_name in all_notebook_files:
print(f"Linting {notebook_name}...", flush=True)
command = f'nblint --linter pyflakes {notebook_name} | grep -v -E "Code Cell [0-9]+ that starts with" | grep -v "may be undefined" | grep -v "projectsetup" | grep -v "unable to detect undefined names" | sed "/^[[:space:]]*$/d"'
os.system(command)
except Exception as ex:
print(f"Caught exception: {ex}")
tmpfile = notebook_directory / "tmp.py"
if os.path.exists(tmpfile):
os.unlink(tmpfile)
print("All linting complete.")

82
bin/run-mypy.py Executable file
View File

@ -0,0 +1,82 @@
#!/usr/bin/env python3
import argparse
import importlib
import nbformat
import os
import re
import sys
from pathlib import Path
from os.path import isfile, join
from mypy import api
from nbconvert.exporters import PythonExporter
# Get the full path to this script.
script_path = Path(os.path.realpath(__file__))
# The parent of the script is the bin directory, the parent of that is the
# notebook directory. It's this notebook directory we want.
notebook_directory = script_path.parent.parent
print(f"Running notebook files in: {notebook_directory}")
# Add the notebook directory to our import path.
sys.path.append(str(notebook_directory))
sys.path.append(str(notebook_directory / "meta" / "startup"))
# Change into the notebook directory as well so relative locations work properly.
os.chdir(notebook_directory)
# Tell the importer how to import .ipynb files.
import projectsetup
parser = argparse.ArgumentParser(description='Run MyPy for static checks.')
parser.add_argument('--filename',
metavar='FILENAME',
help="(optional) name of file to be checked")
args = parser.parse_args()
all_notebook_files = []
if args.filename:
all_notebook_files = [args.filename]
else:
# We want to load every notebook just to make sure the code in it is OK.
all_notebook_files = [f for f in os.listdir(notebook_directory) if isfile(notebook_directory / f) and f.endswith(".ipynb")]
all_notebook_files.sort()
line_pattern = re.compile('(<[a-z]+>:)\s*(\d+)\s*:\s*(.*?)\s*:\s*(.*?)$')
def print_pattern_result(message: str):
pattern_result = line_pattern.findall(message)
if pattern_result:
_, line_number, category, text = pattern_result[0]
print(f"[{category}] line {line_number}: {text}")
elif message:
print(message)
# Now import each notebook in turn. If there are code problems, this should show them.
# This should also run tests, if the module has any.
for module_name in all_notebook_files:
print(f"Checking: {module_name}")
with open(module_name, 'r') as notebook_file:
body = notebook_file.read()
if module_name.endswith(".ipynb"):
notebook = nbformat.reads(body, as_version=4)
python_exporter = PythonExporter()
(body, resources) = python_exporter.from_notebook_node(notebook)
warnings, errors, exit_code = api.run(['--ignore-missing-imports', '-c', body])
if exit_code != 0:
print(f"MyPy exited with code {exit_code} on {module_name}")
if warnings:
print("MyPy Issues:")
for warning in warnings.split("\n"):
print_pattern_result(warning)
if errors:
print("MyPy Errors:")
for error in errors.split("\n"):
print_pattern_result(error)
break
print("All files checked using MyPy.")

270
ids.json Normal file
View File

@ -0,0 +1,270 @@
{
"cluster_urls": {
"devnet": "https://devnet.solana.com",
"localnet": "http://127.0.0.1:8899",
"mainnet-beta": "https://solana-api.projectserum.com",
"testnet": "https://testnet.solana.com"
},
"devnet": {
"dex_program_id": "DESVgJVGajEgKGXhb6XmqDHGz3VjdgP7rEVESBgxmroY",
"faucets": {
"BTC": "97z3NzcDxqRMyE7F73PuHEmAbA72S7eDopjhe7GTymTk",
"ETH": "CvRouhBrimBuSyLd8zHxduNJDtV4LWtoowoF62FCwK7V",
"MSRM": "9ysywkpvyvxaaezq2Dapj1p1gHPP3U3D3ccTTecVfYHe",
"SRM": "9NzrM7CZ1jq46mX2JGcqyUxBupQEn614A5sZrvg3TrCU",
"USDC": "",
"USDT": "AS1EfwXvpejkkLrEPdz9J84By9kPvVBzYaD6Ks8ya1A6",
"WUSDT": "DV8YAUHc4CiadQoFCHriTjNXbtwCw1Rt834EBYeCyvGt"
},
"fee_symbol": "SRM",
"mango_groups": {
"BTC_ETH_USDC": {
"mango_group_pk": "C9ZtsC1wmqMzbyCUTeBppZSKH82FsKrGnaWjv5BtWvvo",
"mint_pks": [
"C6kYXcaRUMqeBF5fhg165RWU7AnpT9z92fvKNoMqjmz6",
"8p968u9m7jZzKSsqxFDqki69MjqdFkwPM9FN4AN8hvHR",
"Fq939Y5hycK62ZGwBjftLY2VyxqAQ8f1MxRqBMdAaBS7"
],
"oracle_pks": [
"3iQqi9nBREjVvKtVWd44Jcbvs39CDAe6zSd613QzxuPE",
"5qxMJFJXB42j3kKo3FbTughREjziottXHcgLnjCNwjEs"
],
"spot_market_pks": [
"FKysSZkCCh41G1SCxpE7Cb7eaLofYBhEneLzHFz6JvjH",
"BYz5dJegg11x94jS2R7ZTCgaJwimvupmkjeYDm9Y3UwP"
],
"spot_market_symbols": {
"BTC/USDC": "FKysSZkCCh41G1SCxpE7Cb7eaLofYBhEneLzHFz6JvjH",
"ETH/USDC": "BYz5dJegg11x94jS2R7ZTCgaJwimvupmkjeYDm9Y3UwP"
},
"srm_vault_pk": "7YBghCMgSnvs3cg9taiwwKySiwDvby4USL3bmg8JQXF2",
"symbols": {
"BTC": "C6kYXcaRUMqeBF5fhg165RWU7AnpT9z92fvKNoMqjmz6",
"ETH": "8p968u9m7jZzKSsqxFDqki69MjqdFkwPM9FN4AN8hvHR",
"USDC": "Fq939Y5hycK62ZGwBjftLY2VyxqAQ8f1MxRqBMdAaBS7"
},
"vault_pks": [
"Avpn6H3Tu2kJqCouYVpJgqU17iKRR9e2eEjVydQpPuyM",
"GRPxtA2TdS8PaGK4E9utnFo7QGf4jLt7RRqbkQXR5fsU",
"6gmXb2hRKeSRD1vvGwVtAVJSH8fBH7SitseyMnC3cat3"
]
},
"BTC_ETH_USDT": {
"mango_group_pk": "H7T5VdS3x68VTFak9h34Q6qQzECeF9PcPL82qyfiUrcH",
"mint_pks": [
"C6kYXcaRUMqeBF5fhg165RWU7AnpT9z92fvKNoMqjmz6",
"8p968u9m7jZzKSsqxFDqki69MjqdFkwPM9FN4AN8hvHR",
"7KBVenLz5WNH4PA5MdGkJNpDDyNKnBQTwnz1UqJv9GUm"
],
"oracle_pks": [
"EHasrbBk5mFnTYPjmdNzDoa7cEBH3yN8D4DLJn7Q41hY",
"3ynBi9nQyKoEJwC47cmXviLSgHaaXQBKRxukatYYLN1Y"
],
"spot_market_pks": [
"6Cpt7EYmzUcHLBQzZuYNqnyKQKieofZXw6bpCWtmwZM1",
"4UQq7c8FdwGkb2TghHVgJShHMJwS4YzjvA3yiF6zArJD"
],
"spot_market_symbols": {
"BTC/USDT": "6Cpt7EYmzUcHLBQzZuYNqnyKQKieofZXw6bpCWtmwZM1",
"ETH/USDT": "4UQq7c8FdwGkb2TghHVgJShHMJwS4YzjvA3yiF6zArJD"
},
"srm_vault_pk": "7Q85DzBw92oyXxQ5e5K2XnNHw9doRQaZKqvhxCef9uN8",
"symbols": {
"BTC": "C6kYXcaRUMqeBF5fhg165RWU7AnpT9z92fvKNoMqjmz6",
"ETH": "8p968u9m7jZzKSsqxFDqki69MjqdFkwPM9FN4AN8hvHR",
"USDT": "7KBVenLz5WNH4PA5MdGkJNpDDyNKnBQTwnz1UqJv9GUm"
},
"vault_pks": [
"4ntkC6t3h75T5SWTKcgHHy9NQeengcydyn4NQtrb5aSZ",
"6kdcJ3fTNmFRozBn3Yi6S9WgjzUPo6ZQg6LXQzomTjmj",
"7LTGV36ixZBp3gPDowvFQx3HKdVgcLX3TDzGVeWsTiy8"
]
},
"BTC_ETH_WUSDT": {
"mango_group_pk": "6WZGjqRi9XKgkqZdsYwit4ASVDQ1iiHqkiDvVgY1n2uW",
"mint_pks": [
"C6kYXcaRUMqeBF5fhg165RWU7AnpT9z92fvKNoMqjmz6",
"8p968u9m7jZzKSsqxFDqki69MjqdFkwPM9FN4AN8hvHR",
"7tSPGVhneTBWZjLGJGZb9V2UntC7T98cwtSLtgcXjeSs"
],
"oracle_pks": [
"EHasrbBk5mFnTYPjmdNzDoa7cEBH3yN8D4DLJn7Q41hY",
"3ynBi9nQyKoEJwC47cmXviLSgHaaXQBKRxukatYYLN1Y"
],
"spot_market_pks": [
"ELXP9wTE4apvK9sxAqtCtMidbAvJJDrNVg4wL6jqQEBA",
"97mbLfi4S56y5Vg2LCF4Z7ru8jD1QjHa5SH3eyFYrMdg"
],
"spot_market_symbols": {
"BTC/WUSDT": "ELXP9wTE4apvK9sxAqtCtMidbAvJJDrNVg4wL6jqQEBA",
"ETH/WUSDT": "97mbLfi4S56y5Vg2LCF4Z7ru8jD1QjHa5SH3eyFYrMdg"
},
"srm_vault_pk": "3TgeEz19ycH7dH6FvR5rCjxD1sJzAq8Esj8BC98CaeEN",
"symbols": {
"BTC": "C6kYXcaRUMqeBF5fhg165RWU7AnpT9z92fvKNoMqjmz6",
"ETH": "8p968u9m7jZzKSsqxFDqki69MjqdFkwPM9FN4AN8hvHR",
"WUSDT": "7tSPGVhneTBWZjLGJGZb9V2UntC7T98cwtSLtgcXjeSs"
},
"vault_pks": [
"Gf4Z19ygbzmBVSXcyvwZQPdrGs8k5TLPPSikU3hZzq8k",
"5AUNwSv8hdwJVhUuaBtsFadtJQfiudSZcY6U9fNoRd2A",
"41r2LvZZDECzm3sdxuP9AcyXZ9FPXqoTjbmnPfBG7ofn"
]
}
},
"mango_program_id": "32YaLZeyUHhdFaGSemTncUEcZEEGjKart8NX1XG2k3fs",
"oracles": {
"BTC/USDC": "EHasrbBk5mFnTYPjmdNzDoa7cEBH3yN8D4DLJn7Q41hY",
"BTC/USDT": "EHasrbBk5mFnTYPjmdNzDoa7cEBH3yN8D4DLJn7Q41hY",
"BTC/WUSDT": "EHasrbBk5mFnTYPjmdNzDoa7cEBH3yN8D4DLJn7Q41hY",
"ETH/USDC": "3ynBi9nQyKoEJwC47cmXviLSgHaaXQBKRxukatYYLN1Y",
"ETH/USDT": "3ynBi9nQyKoEJwC47cmXviLSgHaaXQBKRxukatYYLN1Y",
"ETH/WUSDT": "3ynBi9nQyKoEJwC47cmXviLSgHaaXQBKRxukatYYLN1Y"
},
"spot_markets": {
"BTC/USDC": "FKysSZkCCh41G1SCxpE7Cb7eaLofYBhEneLzHFz6JvjH",
"BTC/USDT": "6Cpt7EYmzUcHLBQzZuYNqnyKQKieofZXw6bpCWtmwZM1",
"BTC/WUSDT": "ELXP9wTE4apvK9sxAqtCtMidbAvJJDrNVg4wL6jqQEBA",
"ETH/USDC": "BYz5dJegg11x94jS2R7ZTCgaJwimvupmkjeYDm9Y3UwP",
"ETH/USDT": "4UQq7c8FdwGkb2TghHVgJShHMJwS4YzjvA3yiF6zArJD",
"ETH/WUSDT": "97mbLfi4S56y5Vg2LCF4Z7ru8jD1QjHa5SH3eyFYrMdg"
},
"symbols": {
"BTC": "C6kYXcaRUMqeBF5fhg165RWU7AnpT9z92fvKNoMqjmz6",
"ETH": "8p968u9m7jZzKSsqxFDqki69MjqdFkwPM9FN4AN8hvHR",
"MSRM": "934bNdNw9QfE8dXD4mKQiKajYURfSkPhxfYZzpvmygca",
"SRM": "9FbAMDvXqNjPqZSYt4EWTguJuDrGkfvwr3gSFpiSbX9S",
"USDC": "Fq939Y5hycK62ZGwBjftLY2VyxqAQ8f1MxRqBMdAaBS7",
"USDT": "7KBVenLz5WNH4PA5MdGkJNpDDyNKnBQTwnz1UqJv9GUm",
"WUSDT": "7tSPGVhneTBWZjLGJGZb9V2UntC7T98cwtSLtgcXjeSs"
}
},
"localnet": {
"dex_program_id": "",
"mango_groups": {
"BTC_ETH_USDC": {}
},
"mango_program_id": "",
"spot_markets": {
"BTC_USDC": "",
"ETH_USDC": ""
},
"symbols": {
"BTC": "",
"ETH": "",
"USDC": ""
}
},
"mainnet-beta": {
"dex_program_id": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin",
"dex_program_id_v2": "EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o",
"fee_token": "SRM",
"mango_groups": {
"BTC_ETH_USDT": {
"mango_group_pk": "7pVYhpKUHw88neQHxgExSH6cerMZ1Axx1ALQP9sxtvQV",
"mint_pks": [
"9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E",
"2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk",
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
],
"oracle_pks": [
"HWh11EWkVHHZoRV6D6WzfRSna4yFv8ZvwcqDk74oDnSs",
"AcYcDG74nxeFHxuqeD5RRWTMWKi77QVx7t9bEy8Y4eyN"
],
"spot_market_pks": [
"C1EuT9VokAKLiW7i2ASnZUvxDoKuKkCpDDeNxAptuNe4",
"7dLVkUfBVfCGkFhSXDCq1ukM9usathSgS716t643iFGF"
],
"spot_market_symbols": {
"BTC/USDT": "C1EuT9VokAKLiW7i2ASnZUvxDoKuKkCpDDeNxAptuNe4",
"ETH/USDT": "7dLVkUfBVfCGkFhSXDCq1ukM9usathSgS716t643iFGF"
},
"srm_vault_pk": "65D8BWH5Bx4jstkZAkc5a3CdYvZjUAehJde3TTUTruQY",
"symbols": {
"BTC": "9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E",
"ETH": "2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk",
"USDT": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
},
"vault_pks": [
"9AWv6CMSUwppXV1VW8ueMvevtPsyDEPFjeJ7UYHhyxMk",
"3AGLriXSkujXN3TT2HrmfdLhMR9ApoYSMdPUiuXW95Kn",
"9UL2DZCskV2m7zFsA7h7igzEw4HMtTjRdQr2X6sTCn1i"
]
},
"BTC_ETH_WUSDT": {
"mango_group_pk": "6NsLVpG2pdxn2rEFkmHHYjkY5QP5qG4RN1fRcSqDVxPC",
"mint_pks": [
"9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E",
"2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk",
"BQcdHdAQW1hczDbBi9hiegXAR7A98Q9jx3X3iBBBDiq4"
],
"oracle_pks": [
"HWh11EWkVHHZoRV6D6WzfRSna4yFv8ZvwcqDk74oDnSs",
"AcYcDG74nxeFHxuqeD5RRWTMWKi77QVx7t9bEy8Y4eyN"
],
"spot_market_pks": [
"5r8FfnbNYcQbS1m4CYmoHYGjBtu6bxfo6UJHNRfzPiYH",
"71CtEComq2XdhGNbXBuYPmosAjMCPSedcgbNi5jDaGbR"
],
"spot_market_symbols": {
"BTC/WUSDT": "5r8FfnbNYcQbS1m4CYmoHYGjBtu6bxfo6UJHNRfzPiYH",
"ETH/WUSDT": "71CtEComq2XdhGNbXBuYPmosAjMCPSedcgbNi5jDaGbR"
},
"srm_vault_pk": "Fn6VHzE2PkJBeJAtCd6T5PP4ni79SqAdnFozFewgh8i6",
"symbols": {
"BTC": "9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E",
"ETH": "2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk",
"WUSDT": "BQcdHdAQW1hczDbBi9hiegXAR7A98Q9jx3X3iBBBDiq4"
},
"vault_pks": [
"FF8h6eCSqoGQyuYmh3BzxdaTfomCBPrCctRq9Yo6nCcd",
"GWwECYXmTUumcUsjbwdJDc9ws4KDWYBJ1GGmckZr2hTK",
"BoGTDjtbEtK8HPCu2VPNJfA7bTLuVDPETDoHvztm6Mqe"
]
}
},
"mango_program_id": "JD3bq9hGdy38PuWQ4h2YJpELmHVGPPfFSuFkpzAd9zfu",
"oracles": {
"BTC/USDT": "HWh11EWkVHHZoRV6D6WzfRSna4yFv8ZvwcqDk74oDnSs",
"BTC/WUSDT": "HWh11EWkVHHZoRV6D6WzfRSna4yFv8ZvwcqDk74oDnSs",
"ETH/USDT": "AcYcDG74nxeFHxuqeD5RRWTMWKi77QVx7t9bEy8Y4eyN",
"ETH/WUSDT": "AcYcDG74nxeFHxuqeD5RRWTMWKi77QVx7t9bEy8Y4eyN"
},
"spot_markets": {
"BTC/USDC": "CVfYa8RGXnuDBeGmniCcdkBwoLqVxh92xB1JqgRQx3F",
"BTC/USDT": "C1EuT9VokAKLiW7i2ASnZUvxDoKuKkCpDDeNxAptuNe4",
"BTC/WUSDT": "5r8FfnbNYcQbS1m4CYmoHYGjBtu6bxfo6UJHNRfzPiYH",
"ETH/USDC": "H5uzEytiByuXt964KampmuNCurNDwkVVypkym75J2DQW",
"ETH/USDT": "7dLVkUfBVfCGkFhSXDCq1ukM9usathSgS716t643iFGF",
"ETH/WUSDT": "71CtEComq2XdhGNbXBuYPmosAjMCPSedcgbNi5jDaGbR",
"SOL/USDT": "HWHvQhFmJB3NUcu1aihKmrKegfVxBEHzwVX6yZCKEsi1",
"SOL/WUSDT": "7xLk17EQQ5KLDLDe44wCmupJKJjTGd8hs3eSVVhCx932",
"SRM/USDC": "CDdR97S8y96v3To93aKvi3nCnjUrbuVSuumw8FLvbVeg"
},
"symbols": {
"BTC": "9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E",
"ETH": "2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk",
"MSRM": "MSRMcoVyrFxnSgo5uXwone5SKcGhT1KEJMFEkMEWf9L",
"SRM": "SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt",
"USDC": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"USDT": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
"WUSDT": "BQcdHdAQW1hczDbBi9hiegXAR7A98Q9jx3X3iBBBDiq4"
}
},
"testnet": {
"dex_program_id": "",
"mango_groups": {
"BTC_ETH_USDC": {}
},
"mango_program_id": "",
"spot_markets": {
"BTC_USDC": "",
"ETH_USDC": ""
},
"symbols": {
"BTC": "BahhjqQVqw3LELEVfyhHyEp7Db1TbFUNVrMiSsmTaBR2",
"ETH": "Dw91oaC5UGxzSDGaFontF1JS3XoViQV3EaTAgdtRPprM",
"USDC": "9q4p8UFxphSipGL3TGku8byTijgk4koTMwhBMV4QKvjw"
}
}
}

63
meta/startup/mypymagic.py Normal file
View File

@ -0,0 +1,63 @@
"""
From: https://github.com/knowsuchagency/jupyter-mypy/blob/master/main.py
Add mypy type-checking cell magic to jupyter/ipython.
"""
from IPython.core.magic import register_cell_magic
@register_cell_magic
def mypy(line, cell):
"""
Run the following cell though mypy.
Any parameters that would normally be passed to the mypy cli
can be passed on the first line, with the exception of the
-c flag we use to pass the code from the cell we want to execute
i.e.
%%mypy --something
...
...
...
If mypy returns a 0 exit code nothing will be printed.
"""
from IPython import get_ipython
from IPython.display import display, HTML
from mypy import api
result = api.run(line.split() + ['--ignore-missing-imports', '-c', cell])
# Result is a tuple with three parts:
# 0 - mypy warnings/problems/issues.
# 1 - mypy's stderr, for mypy problems.
# 2 - mypy's exit code.
if result[2] != 0:
html = ""
if result[0]:
html += f"<div style='color: darkorange'>Warnings:<ul>"
for message in result[0].split("<string>"):
if message:
html += f"<li>{message}</li>"
html += f"</ul></div>"
if result[1]:
html += f"<div style='color: red'>Errors:<ul>"
for message in result[1].split("<string>"):
if message:
html += f"<li>{message}</li>"
html += f"</ul></div>"
display(HTML(html))
shell = get_ipython()
shell.run_cell(cell)
# Delete these to avoid name conflicts.
del mypy

View File

@ -0,0 +1,115 @@
# This is all taken from:
# https://jupyter-notebook.readthedocs.io/en/stable/examples/Notebook/Importing%20Notebooks.html
import io
import os
import sys
import types
from importlib.abc import MetaPathFinder
from importlib.util import spec_from_loader
from nbformat import read
from IPython import get_ipython
from IPython.core.interactiveshell import InteractiveShell
from IPython.display import display, HTML
ALWAYS_IMPORT = """
import datetime
import decimal
import logging
import logging.handlers
import numbers
import pandas as pd
import traceback
import typing
from IPython.display import display, HTML
"""
def find_notebook(fullname, path=None):
"""Find a notebook, given its fully qualified name and an optional path
This turns "foo.bar" into "foo/bar.ipynb"
and tries turning "Foo_Bar" into "Foo Bar" if Foo_Bar
does not exist.
"""
name = fullname.rsplit('.', 1)[-1]
if not path:
path = ['']
for d in path:
nb_path = os.path.join(d, name + ".ipynb")
if os.path.isfile(nb_path):
return nb_path
# let import Notebook_Name find "Notebook Name.ipynb"
nb_path = nb_path.replace("_", " ")
if os.path.isfile(nb_path):
return nb_path
class NotebookLoader(object):
"""Module Loader for Jupyter Notebooks"""
def __init__(self, path=None):
self.shell = InteractiveShell.instance()
self.path = path
def load_module(self, fullname):
"""import a notebook as a module"""
path = find_notebook(fullname, self.path)
# load the notebook object
with io.open(path, 'r', encoding='utf-8') as f:
nb = read(f, 4)
# create the module and add it to sys.modules
# if name in sys.modules:
# return sys.modules[name]
mod = types.ModuleType(fullname)
mod.__file__ = path
mod.__loader__ = self
mod.__dict__['get_ipython'] = get_ipython
sys.modules[fullname] = mod
# extra work to ensure that magics that would affect the user_ns
# actually affect the notebook module's ns
save_user_ns = self.shell.user_ns
self.shell.user_ns = mod.__dict__
try:
exec(ALWAYS_IMPORT, mod.__dict__)
for cell in nb.cells:
if cell.cell_type == 'code':
# transform the input to executable Python
code = self.shell.input_transformer_manager.transform_cell(
cell.source)
# run the code in themodule
exec(code, mod.__dict__)
finally:
self.shell.user_ns = save_user_ns
return mod
class NotebookFinder(MetaPathFinder):
"""Module finder that locates Jupyter Notebooks"""
def __init__(self):
super().__init__()
def find_spec(self, fullname, path, target=None):
found = False
try:
nb_path = find_notebook(fullname, path)
if nb_path:
found = True
except Exception:
pass
if not found:
return
notebookloader = NotebookLoader(path)
return spec_from_loader(fullname, notebookloader)
sys.meta_path.append(NotebookFinder())

View File

@ -0,0 +1,19 @@
import datetime
import decimal
import logging
import logging.handlers
import numbers
import pandas as pd
import traceback
import typing
from IPython.display import display, HTML
# Perform some magic around importing notebooks.
import notebookimporter
pd.options.display.float_format = '{:,.8f}'.format
decimal.getcontext().prec = 18
# Make logging a little more verbose than the default.
logging.basicConfig(level=logging.INFO)

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
ipython
mypy
nblint
pandas
pyserum
solana
web3