mango-explorer/AccountScout.ipynb

333 lines
12 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": "juvenile-attempt",
"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=AccountScout.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": "proud-grace",
"metadata": {},
"source": [
"# 🥭 AccountScout\n",
"\n",
"If you want to run this code to check a user account, skip down to the **Running** section below and follow the instructions there.\n",
"\n",
"(A 'user account' here is the root SOL account for a user - the one you have the private key for, and the one that owns sub-accounts.)\n"
]
},
{
"cell_type": "markdown",
"id": "disturbed-vector",
"metadata": {},
"source": [
"## Required Accounts\n",
"\n",
"Mango Markets code expects some accounts to be present, and if they're not present some actions can fail.\n",
"\n",
"From [Daffy on Discord](https://discord.com/channels/791995070613159966/820390560085835786/834024719958147073):\n",
"\n",
"> You need an open orders account for each of the spot markets Mango. And you need a token account for each of the tokens.\n",
"\n",
"This notebook (and the `AccountScout` class) can be used to check the required accounts are present and maybe in future set up all the required accounts.\n",
"\n",
"(There's no reason not to write the code to fix problems and create any missing accounts. It just hasn't been done yet.)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "regulation-jaguar",
"metadata": {
"jupyter": {
"source_hidden": true
}
},
"outputs": [],
"source": [
"import logging\n",
"import typing\n",
"\n",
"from solana.publickey import PublicKey\n",
"\n",
"from BaseModel import Group, MarginAccount, OpenOrders\n",
"from Constants import SYSTEM_PROGRAM_ADDRESS\n",
"from Context import Context\n",
"from Wallet import Wallet\n"
]
},
{
"cell_type": "markdown",
"id": "noted-modeling",
"metadata": {},
"source": [
"# ScoutReport class\n",
"\n",
"The `ScoutReport` class is built up by the `AccountScout` to report errors, warnings and details pertaining to a user account."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "figured-drove",
"metadata": {},
"outputs": [],
"source": [
"class ScoutReport:\n",
" def __init__(self, address: PublicKey):\n",
" self.address = address\n",
" self.errors: typing.List[str] = []\n",
" self.warnings: typing.List[str] = []\n",
" self.details: typing.List[str] = []\n",
"\n",
" @property\n",
" def has_errors(self) -> bool:\n",
" return len(self.errors) > 0\n",
"\n",
" @property\n",
" def has_warnings(self) -> bool:\n",
" return len(self.warnings) > 0\n",
"\n",
" def add_error(self, error) -> None:\n",
" self.errors += [error]\n",
"\n",
" def add_warning(self, warning) -> None:\n",
" self.warnings += [warning]\n",
"\n",
" def add_detail(self, detail) -> None:\n",
" self.details += [detail]\n",
"\n",
" def __str__(self) -> str:\n",
" def _pad(text_list: typing.List[str]) -> str:\n",
" if len(text_list) == 0:\n",
" return \"None\"\n",
" padding = \"\\n \"\n",
" return padding.join(map(lambda text: text.replace(\"\\n\", padding), text_list))\n",
"\n",
" error_text = _pad(self.errors)\n",
" warning_text = _pad(self.warnings)\n",
" detail_text = _pad(self.details)\n",
" if len(self.errors) > 0 or len(self.warnings) > 0:\n",
" summary = f\"Found {len(self.errors)} error(s) and {len(self.warnings)} warning(s).\"\n",
" else:\n",
" summary = \"No problems found\"\n",
"\n",
" return f\"\"\"« ScoutReport [{self.address}]:\n",
" Summary:\n",
" {summary}\n",
"\n",
" Errors:\n",
" {error_text}\n",
"\n",
" Warnings:\n",
" {warning_text}\n",
"\n",
" Details:\n",
" {detail_text}\n",
"»\"\"\"\n",
"\n",
" def __repr__(self) -> str:\n",
" return f\"{self}\"\n"
]
},
{
"cell_type": "markdown",
"id": "greenhouse-colombia",
"metadata": {},
"source": [
"# AccountScout class\n",
"\n",
"The `AccountScout` class aims to run various checks against a user account to make sure it is in a position to run the liquidator.\n",
"\n",
"Passing all checks here with no errors will be a precondition on liquidator startup."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "favorite-church",
"metadata": {},
"outputs": [],
"source": [
"class AccountScout:\n",
" def __init__(self):\n",
" pass\n",
"\n",
" def require_account_prepared_for_group(self, context: Context, group: Group, account_address: PublicKey) -> None:\n",
" report = self.verify_account_prepared_for_group(context, group, account_address)\n",
" if report.has_errors:\n",
" raise Exception(f\"Account '{account_address}' is not prepared for group '{group.address}':\\n\\n{report}\")\n",
"\n",
" def verify_account_prepared_for_group(self, context: Context, group: Group, account_address: PublicKey) -> ScoutReport:\n",
" report = ScoutReport(account_address)\n",
"\n",
" # First of all, the account must actually exist. If it doesn't, just return early.\n",
" root_account = context.load_account(account_address)\n",
" if root_account is None:\n",
" report.add_error(f\"Root account '{account_address}' does not exist.\")\n",
" return report\n",
"\n",
" # For this to be a user/wallet account, it must be owned by 11111111111111111111111111111111.\n",
" if root_account.owner != SYSTEM_PROGRAM_ADDRESS:\n",
" report.add_error(f\"Account '{account_address}' is not a root user account.\")\n",
" return report\n",
"\n",
" # Must have token accounts for each of the tokens in the group's basket.\n",
" for basket_token in group.basket_tokens:\n",
" token_account = context.fetch_token_balance(account_address, basket_token.token.mint)\n",
" if token_account is None:\n",
" report.add_error(f\"Account '{account_address}' has no account for token '{basket_token.token.name}', mint '{basket_token.token.mint}'.\")\n",
" else:\n",
" report.add_detail(f\"Token account with mint '{basket_token.token.mint}' has balance: {token_account} {basket_token.token.name}\")\n",
"\n",
" # Should have an open orders account for each market in the group. (Only required for re-balancing via Serum, which isn't implemented here yet.)\n",
" for market in group.markets:\n",
" open_orders = OpenOrders.load_for_market_and_owner(context, market.address, account_address, context.dex_program_id, market.base.token.decimals, market.quote.token.decimals)\n",
" if len(open_orders) == 0:\n",
" report.add_warning(f\"No Serum open orders account for market '{market.base.token.name}/{market.quote.token.name}' [{market.address}]'.\")\n",
" else:\n",
" for open_orders_account in open_orders:\n",
" report.add_detail(f\"Serum open orders account for market '{market.base.token.name}/{market.quote.token.name}': {open_orders_account}\")\n",
"\n",
" # May have one or more Mango Markets margin account, but it's optional for liquidating\n",
" margin_accounts = MarginAccount.load_all_for_owner(context, account_address, group)\n",
" if len(margin_accounts) == 0:\n",
" report.add_detail(f\"Account '{account_address}' has no Mango Markets margin accounts.\")\n",
" else:\n",
" for margin_account in margin_accounts:\n",
" report.add_detail(f\"Margin account: {margin_account}\")\n",
"\n",
" return report\n",
"\n",
" # It would be good to be able to fix an account automatically, which should\n",
" # be possible if a Wallet is passed.\n",
" def prepare_wallet_for_group(self, wallet: Wallet, group: Group) -> ScoutReport:\n",
" report = ScoutReport(wallet.address)\n",
" report.add_error(\"AccountScout can't yet prepare wallets.\")\n",
" return report\n"
]
},
{
"cell_type": "markdown",
"id": "interim-header",
"metadata": {},
"source": [
"# 🏃 Running\n",
"\n",
"You can run the following cell to check any user account to make sure it has the proper sub-accounts set up and available.\n",
"\n",
"Enter the public key of the account you want to verify in the value for `ACCOUNT_TO_VERIFY` 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": "subsequent-straight",
"metadata": {},
"outputs": [],
"source": [
"ACCOUNT_TO_VERIFY = \"\""
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "contained-particular",
"metadata": {},
"outputs": [],
"source": [
"if __name__ == \"__main__\":\n",
" logging.getLogger().setLevel(logging.INFO)\n",
"\n",
" if ACCOUNT_TO_VERIFY == \"\":\n",
" raise Exception(\"No account to look up - try setting the variable ACCOUNT_TO_LOOK_UP to an account public key.\")\n",
"\n",
" from Context import default_context\n",
"\n",
" print(\"Context:\", default_context)\n",
"\n",
" root_account_key = PublicKey(ACCOUNT_TO_VERIFY)\n",
" group = Group.load(default_context)\n",
"\n",
" scout = AccountScout()\n",
" report = scout.verify_account_prepared_for_group(default_context, group, root_account_key)\n",
"\n",
" print(report)\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
}