mango-explorer/mango/accountscout.py

188 lines
6.5 KiB
Python

# # ⚠ Warning
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# [🥭 Mango Markets](https://mango.markets/) support is available at:
# [Docs](https://docs.mango.markets/)
# [Discord](https://discord.gg/67jySBhxrg)
# [Twitter](https://twitter.com/mangomarkets)
# [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation)
import typing
from solana.publickey import PublicKey
from .account import Account
from .accountinfo import AccountInfo
from .constants import SYSTEM_PROGRAM_ADDRESS
from .context import Context
from .group import Group
from .tokens import Token
from .tokenaccount import TokenAccount
from .wallet import Wallet
# # 🥭 AccountScout
#
# Required Accounts
#
# Mango Markets code expects some accounts to be present, and if they're not present some
# actions can fail.
#
# From [Daffy on Discord](https://discord.com/channels/791995070613159966/820390560085835786/834024719958147073):
#
# > You need an open orders account for each of the spot markets Mango. And you need a
# token account for each of the tokens.
#
# 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.
#
# (There's no reason not to write the code to fix problems and create any missing accounts.
# It just hasn't been done yet.)
#
# # 🥭 ScoutReport class
#
# The `ScoutReport` class is built up by the `AccountScout` to report errors, warnings and
# details pertaining to a user account.
#
class ScoutReport:
def __init__(self, address: PublicKey):
self.address = address
self.errors: typing.List[str] = []
self.warnings: typing.List[str] = []
self.details: typing.List[str] = []
@property
def has_errors(self) -> bool:
return len(self.errors) > 0
@property
def has_warnings(self) -> bool:
return len(self.warnings) > 0
def add_error(self, error: str) -> None:
self.errors += [error]
def add_warning(self, warning: str) -> None:
self.warnings += [warning]
def add_detail(self, detail: str) -> None:
self.details += [detail]
def __str__(self) -> str:
def _pad(text_list: typing.List[str]) -> str:
if len(text_list) == 0:
return "None"
padding = "\n "
return padding.join(
map(lambda text: text.replace("\n", padding), text_list)
)
error_text = _pad(self.errors)
warning_text = _pad(self.warnings)
detail_text = _pad(self.details)
if len(self.errors) > 0 or len(self.warnings) > 0:
summary = f"Found {len(self.errors)} error(s) and {len(self.warnings)} warning(s)."
else:
summary = "No problems found"
return f"""« ScoutReport [{self.address}]:
Summary:
{summary}
Errors:
{error_text}
Warnings:
{warning_text}
Details:
{detail_text}
»"""
def __repr__(self) -> str:
return f"{self}"
# # 🥭 AccountScout class
#
# The `AccountScout` class aims to run various checks against a user account to make sure
# it is in a position to run the liquidator.
#
# Passing all checks here with no errors will be a precondition on liquidator startup.
#
class AccountScout:
def __init__(self) -> None:
pass
def require_account_prepared_for_group(
self, context: Context, group: Group, account_address: PublicKey
) -> None:
report = self.verify_account_prepared_for_group(context, group, account_address)
if report.has_errors:
raise Exception(
f"Account '{account_address}' is not prepared for group '{group.address}':\n\n{report}"
)
def verify_account_prepared_for_group(
self, context: Context, group: Group, account_address: PublicKey
) -> ScoutReport:
report = ScoutReport(account_address)
# First of all, the account must actually exist. If it doesn't, just return early.
root_account = AccountInfo.load(context, account_address)
if root_account is None:
report.add_error(f"Root account '{account_address}' does not exist.")
return report
# For this to be a user/wallet account, it must be owned by 11111111111111111111111111111111.
if root_account.owner != SYSTEM_PROGRAM_ADDRESS:
report.add_error(f"Account '{account_address}' is not a root user account.")
return report
# Must have token accounts for each of the tokens in the group's basket.
for basket_token in group.tokens:
if isinstance(basket_token.token, Token):
token_accounts = TokenAccount.fetch_all_for_owner_and_token(
context, account_address, basket_token.token
)
if len(token_accounts) == 0:
report.add_error(
f"Account '{account_address}' has no account for token '{basket_token.token.name}'."
)
else:
report.add_detail(
f"Account '{account_address}' has {len(token_accounts)} {basket_token.token.name} token account(s): {[ta.address for ta in token_accounts]}"
)
# May have one or more Mango Markets margin account, but it's optional for liquidating
accounts = Account.load_all_for_owner(context, account_address, group)
if len(accounts) == 0:
report.add_detail(
f"Account '{account_address}' has no Mango Markets margin accounts."
)
else:
for account in accounts:
report.add_detail(f"Margin account: {account}")
return report
# It would be good to be able to fix an account automatically, which should
# be possible if a Wallet is passed.
def prepare_wallet_for_group(self, wallet: Wallet, group: Group) -> ScoutReport:
report = ScoutReport(wallet.address)
report.add_error("AccountScout can't yet prepare wallets.")
return report