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
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 ) - > None :
self . errors + = [ error ]
def add_warning ( self , warning ) - > None :
self . warnings + = [ warning ]
def add_detail ( self , detail ) - > 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 ) :
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.
2021-06-25 07:50:37 -07:00
for basket_token in group . tokens :
if basket_token is not None :
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 } ' , mint ' { basket_token . token . mint } ' . " )
else :
2021-06-07 07:10:18 -07:00
report . add_detail (
2021-06-25 07:50:37 -07:00
f " Account ' { account_address } ' has { len ( token_accounts ) } { basket_token . token . name } token account(s) with mint ' { basket_token . token . mint } ' : { [ 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 :
2021-06-07 07:10:18 -07:00
report . add_detail ( f " Account ' { account_address } ' has no Mango Markets margin accounts. " )
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