2021-06-07 07:10:18 -07:00
|
|
|
# # ⚠ 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
|
|
|
|
|
2021-06-25 07:50:37 -07:00
|
|
|
from .account import Account
|
2021-06-07 07:10:18 -07:00
|
|
|
from .accountinfo import AccountInfo
|
|
|
|
from .constants import SYSTEM_PROGRAM_ADDRESS
|
|
|
|
from .context import Context
|
|
|
|
from .group import Group
|
2021-11-08 03:39:09 -08:00
|
|
|
from .token import Token
|
2021-06-07 07:10:18 -07:00
|
|
|
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
|
|
|
|
|
2021-11-09 05:23:36 -08:00
|
|
|
def add_error(self, error: str) -> None:
|
2021-06-07 07:10:18 -07:00
|
|
|
self.errors += [error]
|
|
|
|
|
2021-11-09 05:23:36 -08:00
|
|
|
def add_warning(self, warning: str) -> None:
|
2021-06-07 07:10:18 -07:00
|
|
|
self.warnings += [warning]
|
|
|
|
|
2021-11-09 05:23:36 -08:00
|
|
|
def add_detail(self, detail: str) -> None:
|
2021-06-07 07:10:18 -07:00
|
|
|
self.details += [detail]
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
def _pad(text_list: typing.List[str]) -> str:
|
|
|
|
if len(text_list) == 0:
|
|
|
|
return "None"
|
|
|
|
padding = "\n "
|
2022-02-09 11:31:50 -08:00
|
|
|
return padding.join(
|
|
|
|
map(lambda text: text.replace("\n", padding), text_list)
|
|
|
|
)
|
2021-06-07 07:10:18 -07:00
|
|
|
|
|
|
|
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.
|
|
|
|
#
|
|
|
|
|
2022-02-09 11:31:50 -08:00
|
|
|
|
2021-06-07 07:10:18 -07:00
|
|
|
class AccountScout:
|
2021-11-09 05:23:36 -08:00
|
|
|
def __init__(self) -> None:
|
2021-06-07 07:10:18 -07:00
|
|
|
pass
|
|
|
|
|
2022-02-09 11:31:50 -08:00
|
|
|
def require_account_prepared_for_group(
|
|
|
|
self, context: Context, group: Group, account_address: PublicKey
|
|
|
|
) -> None:
|
2021-06-07 07:10:18 -07:00
|
|
|
report = self.verify_account_prepared_for_group(context, group, account_address)
|
|
|
|
if report.has_errors:
|
2022-02-09 11:31:50 -08:00
|
|
|
raise Exception(
|
|
|
|
f"Account '{account_address}' is not prepared for group '{group.address}':\n\n{report}"
|
|
|
|
)
|
2021-06-07 07:10:18 -07:00
|
|
|
|
2022-02-09 11:31:50 -08:00
|
|
|
def verify_account_prepared_for_group(
|
|
|
|
self, context: Context, group: Group, account_address: PublicKey
|
|
|
|
) -> ScoutReport:
|
2021-06-07 07:10:18 -07:00
|
|
|
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.
|
2021-06-25 07:50:37 -07:00
|
|
|
for basket_token in group.tokens:
|
2021-11-08 03:39:09 -08:00
|
|
|
if isinstance(basket_token.token, Token):
|
2021-06-25 07:50:37 -07:00
|
|
|
token_accounts = TokenAccount.fetch_all_for_owner_and_token(
|
2022-02-09 11:31:50 -08:00
|
|
|
context, account_address, basket_token.token
|
|
|
|
)
|
2021-06-25 07:50:37 -07:00
|
|
|
if len(token_accounts) == 0:
|
|
|
|
report.add_error(
|
2022-02-09 11:31:50 -08:00
|
|
|
f"Account '{account_address}' has no account for token '{basket_token.token.name}'."
|
|
|
|
)
|
2021-06-25 07:50:37 -07:00
|
|
|
else:
|
2021-06-07 07:10:18 -07:00
|
|
|
report.add_detail(
|
2022-02-09 11:31:50 -08:00
|
|
|
f"Account '{account_address}' has {len(token_accounts)} {basket_token.token.name} token account(s): {[ta.address for ta in token_accounts]}"
|
|
|
|
)
|
2021-06-07 07:10:18 -07:00
|
|
|
|
|
|
|
# May have one or more Mango Markets margin account, but it's optional for liquidating
|
2021-07-12 10:26:35 -07:00
|
|
|
accounts = Account.load_all_for_owner(context, account_address, group)
|
|
|
|
if len(accounts) == 0:
|
2022-02-09 11:31:50 -08:00
|
|
|
report.add_detail(
|
|
|
|
f"Account '{account_address}' has no Mango Markets margin accounts."
|
|
|
|
)
|
2021-06-07 07:10:18 -07:00
|
|
|
else:
|
2021-07-12 10:26:35 -07:00
|
|
|
for account in accounts:
|
|
|
|
report.add_detail(f"Margin account: {account}")
|
2021-06-07 07:10:18 -07:00
|
|
|
|
|
|
|
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
|