Big Rename - removal of old Group and MarginAccount, deletion of a lot of code that is no longer relevant.

* Added some TODO comments where work needs to be done but pieces are currently missing.
This commit is contained in:
Geoff Taylor 2021-06-25 15:50:37 +01:00
parent b8dc12a3e6
commit 5b71ffbd18
75 changed files with 1133 additions and 5564 deletions

File diff suppressed because it is too large Load Diff

View File

@ -21,11 +21,11 @@ mypy:
for file in bin/* ; do \ for file in bin/* ; do \
cp $${file} .tmplintdir/$${file##*/}.py ; \ cp $${file} .tmplintdir/$${file##*/}.py ; \
done done
-mypy mango tests .tmplintdir -mypy --no-incremental --cache-dir=/dev/null mango tests .tmplintdir
rm -rf .tmplintdir rm -rf .tmplintdir
flake8: flake8:
flake8 --extend-ignore E402,E501,E722,W291,W391 . tests/* bin/* flake8 --extend-ignore E402,E501,E722,W291,W391 . bin/*
lint: flake8 mypy lint: flake8 mypy

View File

@ -34,7 +34,7 @@ if (token_account is None) or (token_account.value.token.mint != wrapped_sol.min
raise Exception(f"Account {args.address} is not a {wrapped_sol.name} account.") raise Exception(f"Account {args.address} is not a {wrapped_sol.name} account.")
transaction = Transaction() transaction = Transaction()
signers: typing.List[Account] = [wallet.account] signers: typing.Sequence[Account] = [wallet.account]
payer = wallet.address payer = wallet.address
close_instruction = mango.CloseSplAccountInstructionBuilder(context, wallet, args.address) close_instruction = mango.CloseSplAccountInstructionBuilder(context, wallet, args.address)

View File

@ -6,6 +6,7 @@ import os
import os.path import os.path
import sys import sys
import traceback import traceback
import typing
from decimal import Decimal from decimal import Decimal
@ -42,13 +43,15 @@ try:
logging.info(f"Wallet address: {wallet.address}") logging.info(f"Wallet address: {wallet.address}")
group = mango.Group.load(context) group = mango.Group.load(context)
tokens = [basket_token.token for basket_token in group.basket_tokens] tokens = [token_info.token for token_info in group.tokens if token_info is not None]
balance_parser = mango.TargetBalanceParser(tokens) balance_parser = mango.TargetBalanceParser(tokens)
targets = list(map(balance_parser.parse, args.target)) targets = list(map(balance_parser.parse, args.target))
logging.info(f"Targets: {targets}") logging.info(f"Targets: {targets}")
prices = group.fetch_token_prices(context) # TODO - fetch prices when available for V3.
# prices = group.fetch_token_prices(context)
prices: typing.Sequence[mango.TokenValue] = []
logging.info(f"Prices: {prices}") logging.info(f"Prices: {prices}")
if args.dry_run: if args.dry_run:

View File

@ -36,7 +36,7 @@ try:
logging.info(f"Context: {context}") logging.info(f"Context: {context}")
logging.info(f"Address: {address}") logging.info(f"Address: {address}")
group = mango.MangoGroup.load(context) group = mango.Group.load(context)
balances = group.fetch_balances(context, address) balances = group.fetch_balances(context, address)
print("Balances:") print("Balances:")
mango.TokenValue.report(print, balances) mango.TokenValue.report(print, balances)

View File

@ -24,12 +24,12 @@ logging.getLogger().setLevel(args.log_level)
context = mango.Context.from_command_line_parameters(args).new_from_cluster("devnet") context = mango.Context.from_command_line_parameters(args).new_from_cluster("devnet")
wallet = mango.Wallet.from_command_line_parameters_or_raise(args) wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.MangoGroup.load(context, context.group_id) group = mango.Group.load(context, context.group_id)
mango_account = Account() mango_account = Account()
init = mango.build_create_margin_account_instructions(context, wallet, group, mango_account) init = mango.build_create_account_instructions(context, wallet, group, mango_account)
signers: typing.List[Account] = [wallet.account, mango_account] signers: typing.Sequence[Account] = [wallet.account, mango_account]
transaction = Transaction() transaction = Transaction()
transaction.instructions.extend(init) transaction.instructions.extend(init)

View File

@ -78,20 +78,17 @@ try:
item, mango.LiquidationEvent) and not item.succeeded) item, mango.LiquidationEvent) and not item.succeeded)
liquidations_publisher.subscribe(on_next=filtering.send) liquidations_publisher.subscribe(on_next=filtering.send)
# TODO: Add proper liquidator classes here when they're written for V3
if args.dry_run: if args.dry_run:
account_liquidator: mango.AccountLiquidator = mango.NullAccountLiquidator() account_liquidator: mango.AccountLiquidator = mango.NullAccountLiquidator()
else: else:
intermediate = mango.ForceCancelOrdersAccountLiquidator(context, wallet) account_liquidator = mango.NullAccountLiquidator()
account_liquidator = mango.ReportingAccountLiquidator(intermediate,
context,
wallet,
liquidations_publisher,
liquidator_name)
prices = group.fetch_token_prices(context) # TODO - fetch prices when available for V3.
margin_account = mango.MarginAccount.load(context, margin_account_address, group) # prices = group.fetch_token_prices(context)
margin_account = mango.Account.load(context, margin_account_address)
worthwhile_threshold = Decimal(0) # No threshold - don't take this into account. worthwhile_threshold = Decimal(0) # No threshold - don't take this into account.
liquidatable_report = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold) liquidatable_report = mango.LiquidatableReport.build(group, [], margin_account, worthwhile_threshold)
transaction_id = account_liquidator.liquidate(liquidatable_report) transaction_id = account_liquidator.liquidate(liquidatable_report)
if transaction_id is None: if transaction_id is None:
print("No transaction sent.") print("No transaction sent.")

View File

@ -96,7 +96,7 @@ try:
logging.info(f"Wallet address: {wallet.address}") logging.info(f"Wallet address: {wallet.address}")
group = mango.Group.load(context) group = mango.Group.load(context)
tokens = [basket_token.token for basket_token in group.basket_tokens] tokens = [token_info.token for token_info in group.tokens if token_info is not None]
logging.info("Checking wallet accounts.") logging.info("Checking wallet accounts.")
scout = mango.AccountScout() scout = mango.AccountScout()
@ -123,16 +123,11 @@ try:
captured_failed_notification_target, lambda item: isinstance(item, mango.LiquidationEvent) and not item.succeeded) captured_failed_notification_target, lambda item: isinstance(item, mango.LiquidationEvent) and not item.succeeded)
liquidations_publisher.subscribe(on_next=filtering_failed.send) liquidations_publisher.subscribe(on_next=filtering_failed.send)
# TODO: Add proper liquidator classes here when they're written for V3
if args.dry_run: if args.dry_run:
intermediate: mango.AccountLiquidator = mango.NullAccountLiquidator() account_liquidator: mango.AccountLiquidator = mango.NullAccountLiquidator()
else: else:
intermediate = mango.ForceCancelOrdersAccountLiquidator(context, wallet) account_liquidator = mango.NullAccountLiquidator()
account_liquidator: mango.AccountLiquidator = mango.ReportingAccountLiquidator(intermediate,
context,
wallet,
liquidations_publisher,
liquidator_name)
if args.dry_run or (args.target is None) or (len(args.target) == 0): if args.dry_run or (args.target is None) or (len(args.target) == 0):
wallet_balancer: mango.WalletBalancer = mango.NullWalletBalancer() wallet_balancer: mango.WalletBalancer = mango.NullWalletBalancer()
@ -145,23 +140,16 @@ try:
# These (along with `context`) are captured and read by `load_updated_price_details()`. # These (along with `context`) are captured and read by `load_updated_price_details()`.
group_address = group.address group_address = group.address
oracle_addresses = list([market.oracle for market in group.markets]) oracle_addresses = group.oracles
def load_updated_price_details() -> typing.Tuple[mango.Group, typing.List[mango.TokenValue]]: def load_updated_price_details() -> typing.Tuple[mango.Group, typing.Sequence[mango.TokenValue]]:
all_addresses = [group_address, *oracle_addresses] all_addresses = [group_address, *oracle_addresses]
all_account_infos = mango.AccountInfo.load_multiple(context, all_addresses) all_account_infos = mango.AccountInfo.load_multiple(context, all_addresses)
group_account_info = all_account_infos[0] group_account_info = all_account_infos[0]
oracle_account_infos = all_account_infos[1:]
group = mango.Group.parse(context, group_account_info) group = mango.Group.parse(context, group_account_info)
oracles = map(lambda oracle_account_info: mango.Aggregator.parse( # TODO - fetch prices when code available in V3.
context, oracle_account_info), oracle_account_infos) return group, []
prices = list(map(lambda oracle: oracle.price, oracles)) + [Decimal(1)]
token_prices = []
for index, price in enumerate(prices):
token_prices += [mango.TokenValue(group.basket_tokens[index].token, price)]
return group, token_prices
def fetch_prices(context): def fetch_prices(context):
def _fetch_prices(_): def _fetch_prices(_):
@ -175,7 +163,7 @@ try:
def fetch_margin_accounts(context): def fetch_margin_accounts(context):
def _actual_fetch(): def _actual_fetch():
group = mango.Group.load(context) group = mango.Group.load(context)
return mango.MarginAccount.load_ripe(context, group) return mango.Account.load_ripe(context, group)
def _fetch_margin_accounts(_): def _fetch_margin_accounts(_):
with mango.retry_context("Margin Account Fetch", with mango.retry_context("Margin Account Fetch",

View File

@ -73,27 +73,23 @@ try:
item, mango.LiquidationEvent) and not item.succeeded) item, mango.LiquidationEvent) and not item.succeeded)
liquidations_publisher.subscribe(on_next=filtering.send) liquidations_publisher.subscribe(on_next=filtering.send)
# TODO: Add proper liquidator classes here when they're written for V3
if args.dry_run: if args.dry_run:
account_liquidator: mango.AccountLiquidator = mango.NullAccountLiquidator() account_liquidator: mango.AccountLiquidator = mango.NullAccountLiquidator()
else: else:
intermediate = mango.ForceCancelOrdersAccountLiquidator(context, wallet) account_liquidator = mango.NullAccountLiquidator()
account_liquidator = mango.ReportingAccountLiquidator(intermediate,
context,
wallet,
liquidations_publisher,
liquidator_name)
wallet_balancer = mango.NullWalletBalancer() wallet_balancer = mango.NullWalletBalancer()
liquidation_processor = mango.LiquidationProcessor(context, liquidator_name, account_liquidator, wallet_balancer) liquidation_processor = mango.LiquidationProcessor(context, liquidator_name, account_liquidator, wallet_balancer)
started_at = time.time() started_at = time.time()
ripe = group.load_ripe_margin_accounts() # ripe = group.load_ripe_margin_accounts()
liquidation_processor.update_margin_accounts(ripe) liquidation_processor.update_margin_accounts([])
group = mango.Group.load(context) # Refresh group data group = mango.Group.load(context) # Refresh group data
prices = group.fetch_token_prices(context) # prices = group.fetch_token_prices(context)
liquidation_processor.update_prices(group, prices) liquidation_processor.update_prices(group, [])
time_taken = time.time() - started_at time_taken = time.time() - started_at
logging.info(f"Check of all margin accounts complete. Time taken: {time_taken:.2f} seconds.") logging.info(f"Check of all margin accounts complete. Time taken: {time_taken:.2f} seconds.")

View File

@ -22,7 +22,7 @@ import mango # nopep8
parser = argparse.ArgumentParser(description="Sends an SPL tokens to a different address.") parser = argparse.ArgumentParser(description="Sends an SPL tokens to a different address.")
mango.Context.add_command_line_parameters(parser) mango.Context.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser) mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--token-symbol", type=str, required=True, help="token symbol to send (e.g. ETH)") parser.add_argument("--symbol", type=str, required=True, help="token symbol to send (e.g. ETH)")
parser.add_argument("--address", type=PublicKey, parser.add_argument("--address", type=PublicKey,
help="Destination address for the SPL token - can be either the actual token address or the address of the owner of the token address") help="Destination address for the SPL token - can be either the actual token address or the address of the owner of the token address")
parser.add_argument("--quantity", type=Decimal, required=True, help="quantity of token to send") parser.add_argument("--quantity", type=Decimal, required=True, help="quantity of token to send")
@ -40,18 +40,18 @@ try:
logging.info(f"Context: {context}") logging.info(f"Context: {context}")
logging.info(f"Wallet address: {wallet.address}") logging.info(f"Wallet address: {wallet.address}")
group = mango.Group.load(context) token = context.token_lookup.find_by_symbol(args.symbol)
group_basket_token = mango.BasketToken.find_by_symbol(group.basket_tokens, args.token_symbol) if token is None:
group_token = group_basket_token.token raise Exception(f"Could not find details of token with symbol {args.symbol}.")
spl_token = Token(context.client, group_token.mint, TOKEN_PROGRAM_ID, wallet.account) spl_token = Token(context.client, token.mint, TOKEN_PROGRAM_ID, wallet.account)
source_accounts = spl_token.get_accounts(wallet.address) source_accounts = spl_token.get_accounts(wallet.address)
source_account = source_accounts["result"]["value"][0] source_account = source_accounts["result"]["value"][0]
source = PublicKey(source_account["pubkey"]) source = PublicKey(source_account["pubkey"])
# Is the address an actual token account? Or is it the SOL address of the owner? # Is the address an actual token account? Or is it the SOL address of the owner?
possible_dest: typing.Optional[mango.TokenAccount] = mango.TokenAccount.load(context, args.address) possible_dest: typing.Optional[mango.TokenAccount] = mango.TokenAccount.load(context, args.address)
if (possible_dest is not None) and (possible_dest.value.token.mint == group_token.mint): if (possible_dest is not None) and (possible_dest.value.token.mint == token.mint):
# We successfully loaded the token account. # We successfully loaded the token account.
destination: PublicKey = args.address destination: PublicKey = args.address
else: else:
@ -63,11 +63,11 @@ try:
destination = PublicKey(destination_account["pubkey"]) destination = PublicKey(destination_account["pubkey"])
owner = wallet.account owner = wallet.account
amount = int(args.quantity * Decimal(10 ** group_token.decimals)) amount = int(args.quantity * Decimal(10 ** token.decimals))
print("Balance:", source_account["account"]["data"]["parsed"] print("Balance:", source_account["account"]["data"]["parsed"]
["info"]["tokenAmount"]["uiAmountString"], group_token.name) ["info"]["tokenAmount"]["uiAmountString"], token.name)
text_amount = f"{amount} {group_token.name} (@ {group_token.decimals} decimal places)" text_amount = f"{amount} {token.name} (@ {token.decimals} decimal places)"
print(f"Sending {text_amount}") print(f"Sending {text_amount}")
print(f" From: {source}") print(f" From: {source}")
print(f" To: {destination}") print(f" To: {destination}")
@ -82,7 +82,7 @@ try:
updated_balance = spl_token.get_balance(source) updated_balance = spl_token.get_balance(source)
updated_balance_text = updated_balance["result"]["value"]["uiAmountString"] updated_balance_text = updated_balance["result"]["value"]["uiAmountString"]
print(f"{text_amount} sent. Balance now: {updated_balance_text} {group_token.name}") print(f"{text_amount} sent. Balance now: {updated_balance_text} {token.name}")
except Exception as exception: except Exception as exception:
logging.critical(f"send-token stopped because of exception: {exception} - {traceback.format_exc()}") logging.critical(f"send-token stopped because of exception: {exception} - {traceback.format_exc()}")
except: except:

View File

@ -30,6 +30,6 @@ if address is None:
wallet = mango.Wallet.from_command_line_parameters_or_raise(args) wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
address = wallet.address address = wallet.address
group = mango.MangoGroup.load(context, context.group_id) group = mango.Group.load(context, context.group_id)
mango_accounts = mango.MangoAccount.load_all_for_owner(context, address, group) mango_accounts = mango.Account.load_all_for_owner(context, address, group)
print(mango_accounts) print(mango_accounts)

View File

@ -11,8 +11,6 @@ sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), ".."))) os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8 import mango # nopep8
# We explicitly want argument parsing to be outside the main try-except block because some arguments
# (like --help) will cause an exit, which our except: block traps.
parser = argparse.ArgumentParser(description="Shows the on-chain data of a Mango Markets Group.") parser = argparse.ArgumentParser(description="Shows the on-chain data of a Mango Markets Group.")
mango.Context.add_command_line_parameters(parser) mango.Context.add_command_line_parameters(parser)
args = parser.parse_args() args = parser.parse_args()
@ -23,7 +21,7 @@ logging.warning(mango.WARNING_DISCLAIMER_TEXT)
try: try:
context = mango.Context.from_command_line_parameters(args) context = mango.Context.from_command_line_parameters(args)
group = mango.MangoGroup.load(context) group = mango.Group.load(context)
print(group) print(group)
except Exception as exception: except Exception as exception:
logging.critical(f"show-group stopped because of exception: {exception} - {traceback.format_exc()}") logging.critical(f"show-group stopped because of exception: {exception} - {traceback.format_exc()}")

View File

@ -1,37 +0,0 @@
#!/usr/bin/env pyston3
import argparse
import logging
import os
import os.path
import sys
import traceback
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
# We explicitly want argument parsing to be outside the main try-except block because some arguments
# (like --help) will cause an exit, which our except: block traps.
parser = argparse.ArgumentParser(description="Shows the on-chain data of a Mango Markets Margin Account.")
mango.Context.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey, required=True,
help="Solana address of the Mango Markets Margin Account")
args = parser.parse_args()
logging.getLogger().setLevel(args.log_level)
logging.warning(mango.WARNING_DISCLAIMER_TEXT)
try:
context = mango.Context.from_command_line_parameters(args)
group = mango.Group.load(context)
margin_account = mango.MarginAccount.load(context, args.address, group)
print(margin_account)
except Exception as exception:
logging.critical(f"show-margin-account stopped because of exception: {exception} - {traceback.format_exc()}")
except:
logging.critical(f"show-margin-account stopped because of uncatchable error: {traceback.format_exc()}")

View File

@ -34,10 +34,8 @@ if largest_token_account is None:
raise Exception(f"No {wrapped_sol.name} accounts found for owner {wallet.address}.") raise Exception(f"No {wrapped_sol.name} accounts found for owner {wallet.address}.")
transaction = Transaction() transaction = Transaction()
signers: typing.List[Account] = [wallet.account]
wrapped_sol_account = Account() wrapped_sol_account = Account()
signers.append(wrapped_sol_account) signers: typing.Sequence[Account] = [wallet.account, wrapped_sol_account]
create_instruction = mango.CreateSplAccountInstructionBuilder( create_instruction = mango.CreateSplAccountInstructionBuilder(
context, wallet, wrapped_sol_account.public_key()) context, wallet, wrapped_sol_account.public_key())

View File

@ -29,11 +29,11 @@ logging.getLogger().setLevel(args.log_level)
context = mango.Context.from_command_line_parameters(args) context = mango.Context.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args) wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.MangoGroup.load(context, context.group_id) group = mango.Group.load(context, context.group_id)
margin_accounts = mango.MangoAccount.load_all_for_owner(context, wallet.address, group) accounts = mango.Account.load_all_for_owner(context, wallet.address, group)
if len(margin_accounts) == 0: if len(accounts) == 0:
raise Exception(f"Could not find any margin accounts for '{wallet.address}'.") raise Exception(f"Could not find any margin accounts for '{wallet.address}'.")
margin_account = margin_accounts[0] margin_account = accounts[0]
token = context.token_lookup.find_by_symbol(args.symbol) token = context.token_lookup.find_by_symbol(args.symbol)
print(token) print(token)
@ -58,7 +58,7 @@ node_bank = root_bank.pick_node_bank(context)
withdraw = mango.build_withdraw_instructions( withdraw = mango.build_withdraw_instructions(
context, wallet, group, margin_account, root_bank, node_bank, withdrawal_token_account, args.allow_borrow) context, wallet, group, margin_account, root_bank, node_bank, withdrawal_token_account, args.allow_borrow)
print(withdraw) print(withdraw)
signers: typing.List[Account] = [wallet.account] signers: typing.Sequence[Account] = [wallet.account]
transaction = Transaction() transaction = Transaction()
transaction.instructions.extend(withdraw) transaction.instructions.extend(withdraw)

View File

@ -31,9 +31,8 @@ wrapped_sol = context.token_lookup.find_by_symbol_or_raise("SOL")
amount_to_transfer = int(args.quantity * mango.SOL_DECIMAL_DIVISOR) amount_to_transfer = int(args.quantity * mango.SOL_DECIMAL_DIVISOR)
transaction = Transaction() transaction = Transaction()
signers: typing.List[Account] = [wallet.account]
wrapped_sol_account = Account() wrapped_sol_account = Account()
signers.append(wrapped_sol_account) signers: typing.Sequence[Account] = [wallet.account, wrapped_sol_account]
create_instruction = mango.CreateSplAccountInstructionBuilder( create_instruction = mango.CreateSplAccountInstructionBuilder(
context, wallet, wrapped_sol_account.public_key(), amount_to_transfer) context, wallet, wrapped_sol_account.public_key(), amount_to_transfer)

View File

@ -1,10 +1,10 @@
from .account import Account
from .accountflags import AccountFlags
from .accountinfo import AccountInfo from .accountinfo import AccountInfo
from .accountliquidator import AccountLiquidator, NullAccountLiquidator, ActualAccountLiquidator, ForceCancelOrdersAccountLiquidator, ReportingAccountLiquidator from .accountliquidator import AccountLiquidator, NullAccountLiquidator
from .accountscout import ScoutReport, AccountScout from .accountscout import ScoutReport, AccountScout
from .addressableaccount import AddressableAccount from .addressableaccount import AddressableAccount
from .aggregator import AggregatorConfig, Round, Answer, Aggregator
from .balancesheet import BalanceSheet from .balancesheet import BalanceSheet
from .baskettoken import BasketToken
from .constants import SYSTEM_PROGRAM_ADDRESS, SOL_MINT_ADDRESS, SOL_DECIMALS, SOL_DECIMAL_DIVISOR, WARNING_DISCLAIMER_TEXT, MangoConstants from .constants import SYSTEM_PROGRAM_ADDRESS, SOL_MINT_ADDRESS, SOL_DECIMALS, SOL_DECIMAL_DIVISOR, WARNING_DISCLAIMER_TEXT, MangoConstants
from .context import Context, default_cluster, default_cluster_url, default_program_id, default_dex_program_id, default_group_name, default_group_id from .context import Context, default_cluster, default_cluster_url, default_program_id, default_dex_program_id, default_group_name, default_group_id
from .createmarketoperations import create_market_operations from .createmarketoperations import create_market_operations
@ -12,21 +12,15 @@ from .encoding import decode_binary, encode_binary, encode_key, encode_int
from .group import Group from .group import Group
from .idsjsontokenlookup import IdsJsonTokenLookup from .idsjsontokenlookup import IdsJsonTokenLookup
from .idsjsonmarketlookup import IdsJsonMarketLookup from .idsjsonmarketlookup import IdsJsonMarketLookup
from .index import Index from .instructions import InstructionBuilder, CreateSplAccountInstructionBuilder, InitializeSplAccountInstructionBuilder, TransferSplTokensInstructionBuilder, CloseSplAccountInstructionBuilder, CreateSerumOpenOrdersInstructionBuilder, NewOrderV3InstructionBuilder, ConsumeEventsInstructionBuilder, SettleInstructionBuilder
from .instructions import InstructionBuilder, ForceCancelOrdersInstructionBuilder, LiquidateInstructionBuilder, CreateSplAccountInstructionBuilder, InitializeSplAccountInstructionBuilder, TransferSplTokensInstructionBuilder, CloseSplAccountInstructionBuilder, CreateSerumOpenOrdersInstructionBuilder, NewOrderV3InstructionBuilder, ConsumeEventsInstructionBuilder, SettleInstructionBuilder
from .instructiontype import InstructionType from .instructiontype import InstructionType
from .liquidatablereport import LiquidatableState, LiquidatableReport from .liquidatablereport import LiquidatableState, LiquidatableReport
from .liquidationevent import LiquidationEvent from .liquidationevent import LiquidationEvent
from .liquidationprocessor import LiquidationProcessor, LiquidationProcessorState from .liquidationprocessor import LiquidationProcessor, LiquidationProcessorState
from .mangoaccount import MangoAccount
from .mangoaccountflags import MangoAccountFlags
from .marginaccount import MarginAccount
from .market import Market from .market import Market
from .marketlookup import MarketLookup, CompoundMarketLookup from .marketlookup import MarketLookup, CompoundMarketLookup
from .marketmetadata import MarketMetadata
from .marketoperations import MarketOperations, NullMarketOperations from .marketoperations import MarketOperations, NullMarketOperations
from .mangogroup import MangoGroup from .merpsinstructions import build_cancel_perp_order_instructions, build_create_account_instructions, build_place_perp_order_instructions, build_withdraw_instructions
from .merpsinstructions import build_cancel_perp_order_instructions, build_create_margin_account_instructions, build_place_perp_order_instructions, build_withdraw_instructions
from .metadata import Metadata from .metadata import Metadata
from .notification import NotificationTarget, TelegramNotificationTarget, DiscordNotificationTarget, MailjetNotificationTarget, CsvFileNotificationTarget, FilteringNotificationTarget, NotificationHandler, parse_subscription_target from .notification import NotificationTarget, TelegramNotificationTarget, DiscordNotificationTarget, MailjetNotificationTarget, CsvFileNotificationTarget, FilteringNotificationTarget, NotificationHandler, parse_subscription_target
from .observables import PrintingObserverSubscriber, TimestampedPrintingObserverSubscriber, CollectingObserverSubscriber, CaptureFirstItem, FunctionObserver, create_backpressure_skipping_observer, debug_print_item, log_subscription_error, observable_pipeline_error_reporter, EventSource from .observables import PrintingObserverSubscriber, TimestampedPrintingObserverSubscriber, CollectingObserverSubscriber, CaptureFirstItem, FunctionObserver, create_backpressure_skipping_observer, debug_print_item, log_subscription_error, observable_pipeline_error_reporter, EventSource
@ -41,7 +35,6 @@ from .perpmarketinfo import PerpMarketInfo
from .perpmarketoperations import PerpMarketOperations from .perpmarketoperations import PerpMarketOperations
from .retrier import RetryWithPauses, retry_context from .retrier import RetryWithPauses, retry_context
from .rootbank import NodeBank, RootBank from .rootbank import NodeBank, RootBank
from .serumaccountflags import SerumAccountFlags
from .serummarketlookup import SerumMarketLookup from .serummarketlookup import SerumMarketLookup
from .serummarketoperations import SerumMarketOperations from .serummarketoperations import SerumMarketOperations
from .spltokenlookup import SplTokenLookup from .spltokenlookup import SplTokenLookup

View File

@ -23,66 +23,66 @@ from .accountinfo import AccountInfo
from .addressableaccount import AddressableAccount from .addressableaccount import AddressableAccount
from .context import Context from .context import Context
from .encoding import encode_key from .encoding import encode_key
from .group import Group
from .layouts import layouts from .layouts import layouts
from .mangogroup import MangoGroup
from .metadata import Metadata from .metadata import Metadata
from .version import Version from .version import Version
# # 🥭 MangoAccount class # # 🥭 Account class
# #
# `MangoAccount` holds information about the account for a particular user/wallet for a particualr `MangoGroup`. # `Account` holds information about the account for a particular user/wallet for a particualr `Group`.
# #
class MangoAccount(AddressableAccount): class Account(AddressableAccount):
def __init__(self, account_info: AccountInfo, version: Version, def __init__(self, account_info: AccountInfo, version: Version,
meta_data: Metadata, group: PublicKey, owner: PublicKey, in_basket: typing.List[Decimal], meta_data: Metadata, group: PublicKey, owner: PublicKey, in_basket: typing.Sequence[Decimal],
deposits: typing.List[Decimal], borrows: typing.List[Decimal], deposits: typing.Sequence[Decimal], borrows: typing.Sequence[Decimal],
spot_open_orders: typing.List[PublicKey], perp_accounts: typing.List[typing.Any]): spot_open_orders: typing.Sequence[PublicKey], perp_accounts: typing.Sequence[typing.Any]):
super().__init__(account_info) super().__init__(account_info)
self.version: Version = version self.version: Version = version
self.meta_data: Metadata = meta_data self.meta_data: Metadata = meta_data
self.group: PublicKey = group self.group: PublicKey = group
self.owner: PublicKey = owner self.owner: PublicKey = owner
self.in_basket: typing.List[Decimal] = in_basket self.in_basket: typing.Sequence[Decimal] = in_basket
self.deposits: typing.List[Decimal] = deposits self.deposits: typing.Sequence[Decimal] = deposits
self.borrows: typing.List[Decimal] = borrows self.borrows: typing.Sequence[Decimal] = borrows
self.spot_open_orders: typing.List[PublicKey] = spot_open_orders self.spot_open_orders: typing.Sequence[PublicKey] = spot_open_orders
self.perp_accounts: typing.List[layouts.PERP_ACCOUNT] = perp_accounts self.perp_accounts: typing.Sequence[layouts.PERP_ACCOUNT] = perp_accounts
@staticmethod @staticmethod
def from_layout(layout: layouts.MANGO_ACCOUNT, account_info: AccountInfo, version: Version) -> "MangoAccount": def from_layout(layout: layouts.MANGO_ACCOUNT, account_info: AccountInfo, version: Version) -> "Account":
meta_data = Metadata.from_layout(layout.meta_data) meta_data = Metadata.from_layout(layout.meta_data)
group: PublicKey = layout.group group: PublicKey = layout.group
owner: PublicKey = layout.owner owner: PublicKey = layout.owner
in_basket: typing.List[Decimal] = layout.in_basket in_basket: typing.Sequence[Decimal] = layout.in_basket
deposits: typing.List[Decimal] = layout.deposits deposits: typing.Sequence[Decimal] = layout.deposits
borrows: typing.List[Decimal] = layout.borrows borrows: typing.Sequence[Decimal] = layout.borrows
spot_open_orders: typing.List[PublicKey] = layout.spot_open_orders spot_open_orders: typing.Sequence[PublicKey] = layout.spot_open_orders
perp_accounts: typing.List[typing.Any] = layout.perp_accounts perp_accounts: typing.Sequence[typing.Any] = layout.perp_accounts
return MangoAccount(account_info, version, meta_data, group, owner, in_basket, deposits, borrows, spot_open_orders, perp_accounts) return Account(account_info, version, meta_data, group, owner, in_basket, deposits, borrows, spot_open_orders, perp_accounts)
@staticmethod @staticmethod
def parse(context: Context, account_info: AccountInfo) -> "MangoAccount": def parse(context: Context, account_info: AccountInfo) -> "Account":
data = account_info.data data = account_info.data
if len(data) != layouts.MANGO_ACCOUNT.sizeof(): if len(data) != layouts.MANGO_ACCOUNT.sizeof():
raise Exception( raise Exception(
f"MangoAccount data length ({len(data)}) does not match expected size ({layouts.MANGO_ACCOUNT.sizeof()}") f"Account data length ({len(data)}) does not match expected size ({layouts.MANGO_ACCOUNT.sizeof()}")
layout = layouts.MANGO_ACCOUNT.parse(data) layout = layouts.MANGO_ACCOUNT.parse(data)
return MangoAccount.from_layout(layout, account_info, Version.V1) return Account.from_layout(layout, account_info, Version.V1)
@staticmethod @staticmethod
def load(context: Context, address: PublicKey) -> "MangoAccount": def load(context: Context, address: PublicKey) -> "Account":
account_info = AccountInfo.load(context, address) account_info = AccountInfo.load(context, address)
if account_info is None: if account_info is None:
raise Exception(f"MangoAccount account not found at address '{address}'") raise Exception(f"Account account not found at address '{address}'")
return MangoAccount.parse(context, account_info) return Account.parse(context, account_info)
@staticmethod @staticmethod
def load_all_for_owner(context: Context, owner: PublicKey, group: MangoGroup) -> typing.List["MangoAccount"]: def load_all_for_owner(context: Context, owner: PublicKey, group: Group) -> typing.Sequence["Account"]:
# mango_group is just after the METADATA, which is the first entry. # mango_group is just after the METADATA, which is the first entry.
group_offset = layouts.METADATA.sizeof() group_offset = layouts.METADATA.sizeof()
# owner is just after mango_group in the layout, and it's a PublicKey which is 32 bytes. # owner is just after mango_group in the layout, and it's a PublicKey which is 32 bytes.
@ -104,7 +104,7 @@ class MangoAccount(AddressableAccount):
for account_data in response["result"]: for account_data in response["result"]:
address = PublicKey(account_data["pubkey"]) address = PublicKey(account_data["pubkey"])
account_info = AccountInfo._from_response_values(account_data["account"], address) account_info = AccountInfo._from_response_values(account_data["account"], address)
account = MangoAccount.parse(context, account_info) account = Account.parse(context, account_info)
accounts += [account] accounts += [account]
return accounts return accounts

View File

@ -21,12 +21,12 @@ from .layouts import layouts
from .version import Version from .version import Version
# # 🥭 SerumAccountFlags class # # 🥭 AccountFlags class
# #
# The Serum prefix is because there's also `MangoAccountFlags` for the Mango-specific flags. # Encapsulates the Serum AccountFlags data.
# #
class SerumAccountFlags: class AccountFlags:
def __init__(self, version: Version, initialized: bool, market: bool, open_orders: bool, def __init__(self, version: Version, initialized: bool, market: bool, open_orders: bool,
request_queue: bool, event_queue: bool, bids: bool, asks: bool, disabled: bool): request_queue: bool, event_queue: bool, bids: bool, asks: bool, disabled: bool):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
@ -41,10 +41,10 @@ class SerumAccountFlags:
self.disabled: bool = disabled self.disabled: bool = disabled
@staticmethod @staticmethod
def from_layout(layout: layouts.SERUM_ACCOUNT_FLAGS) -> "SerumAccountFlags": def from_layout(layout: layouts.ACCOUNT_FLAGS) -> "AccountFlags":
return SerumAccountFlags(Version.UNSPECIFIED, layout.initialized, layout.market, return AccountFlags(Version.UNSPECIFIED, layout.initialized, layout.market,
layout.open_orders, layout.request_queue, layout.event_queue, layout.open_orders, layout.request_queue, layout.event_queue,
layout.bids, layout.asks, layout.disabled) layout.bids, layout.asks, layout.disabled)
def __str__(self) -> str: def __str__(self) -> str:
flags: typing.List[typing.Optional[str]] = [] flags: typing.List[typing.Optional[str]] = []
@ -57,7 +57,7 @@ class SerumAccountFlags:
flags += ["asks" if self.asks else None] flags += ["asks" if self.asks else None]
flags += ["disabled" if self.disabled else None] flags += ["disabled" if self.disabled else None]
flag_text = " | ".join(flag for flag in flags if flag is not None) or "None" flag_text = " | ".join(flag for flag in flags if flag is not None) or "None"
return f"« SerumAccountFlags: {flag_text} »" return f"« AccountFlags: {flag_text} »"
def __repr__(self) -> str: def __repr__(self) -> str:
return f"{self}" return f"{self}"

View File

@ -40,7 +40,7 @@ class AccountInfo:
self.rent_epoch: Decimal = rent_epoch self.rent_epoch: Decimal = rent_epoch
self.data: bytes = data self.data: bytes = data
def encoded_data(self) -> typing.List: def encoded_data(self) -> typing.Sequence:
return encode_binary(self.data) return encode_binary(self.data)
def __str__(self) -> str: def __str__(self) -> str:
@ -64,12 +64,12 @@ class AccountInfo:
return AccountInfo._from_response_values(result["value"], address) return AccountInfo._from_response_values(result["value"], address)
@staticmethod @staticmethod
def load_multiple(context: Context, addresses: typing.Sequence[PublicKey], chunk_size: int = 100, sleep_between_calls: float = 0.0) -> typing.List["AccountInfo"]: def load_multiple(context: Context, addresses: typing.Sequence[PublicKey], chunk_size: int = 100, sleep_between_calls: float = 0.0) -> typing.Sequence["AccountInfo"]:
# This is a tricky one to get right. # This is a tricky one to get right.
# Some errors this can generate: # Some errors this can generate:
# 413 Client Error: Payload Too Large for url # 413 Client Error: Payload Too Large for url
# Error response from server: 'Too many inputs provided; max 100', code: -32602 # Error response from server: 'Too many inputs provided; max 100', code: -32602
address_strings: typing.List[str] = list(map(PublicKey.__str__, addresses)) address_strings: typing.Sequence[str] = list(map(PublicKey.__str__, addresses))
multiple: typing.List[AccountInfo] = [] multiple: typing.List[AccountInfo] = []
chunks = AccountInfo._split_list_into_chunks(address_strings, chunk_size) chunks = AccountInfo._split_list_into_chunks(address_strings, chunk_size)
for counter, chunk in enumerate(chunks): for counter, chunk in enumerate(chunks):
@ -100,7 +100,7 @@ class AccountInfo:
return AccountInfo._from_response_values(response["result"]["value"], address) return AccountInfo._from_response_values(response["result"]["value"], address)
@staticmethod @staticmethod
def _split_list_into_chunks(to_chunk: typing.List, chunk_size: int = 100) -> typing.List[typing.List]: def _split_list_into_chunks(to_chunk: typing.Sequence, chunk_size: int = 100) -> typing.Sequence[typing.Sequence]:
chunks = [] chunks = []
start = 0 start = 0
while start < len(to_chunk): while start < len(to_chunk):

View File

@ -15,27 +15,16 @@
import abc import abc
import datetime
import logging import logging
import typing import typing
from solana.transaction import Transaction from .instructions import InstructionBuilder
from .context import Context
from .group import Group
from .instructions import ForceCancelOrdersInstructionBuilder, InstructionBuilder, LiquidateInstructionBuilder
from .liquidatablereport import LiquidatableReport from .liquidatablereport import LiquidatableReport
from .liquidationevent import LiquidationEvent
from .marginaccount import MarginAccount
from .observables import EventSource
from .tokenvalue import TokenValue
from .transactionscout import TransactionScout
from .wallet import Wallet
# # 🥭 AccountLiquidator # # 🥭 AccountLiquidator
# #
# An `AccountLiquidator` liquidates a `MarginAccount`, if possible. # An `AccountLiquidator` liquidates an `Account`, if possible.
# #
# The follows the common pattern of having an abstract base class that defines the interface # The follows the common pattern of having an abstract base class that defines the interface
# external code should use, along with a 'null' implementation and at least one full # external code should use, along with a 'null' implementation and at least one full
@ -53,13 +42,12 @@ from .wallet import Wallet
# is just the `liquidate()` method. # is just the `liquidate()` method.
# #
class AccountLiquidator(metaclass=abc.ABCMeta): class AccountLiquidator(metaclass=abc.ABCMeta):
def __init__(self): def __init__(self):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
@abc.abstractmethod @abc.abstractmethod
def prepare_instructions(self, liquidatable_report: LiquidatableReport) -> typing.List[InstructionBuilder]: def prepare_instructions(self, liquidatable_report: LiquidatableReport) -> typing.Sequence[InstructionBuilder]:
raise NotImplementedError("AccountLiquidator.prepare_instructions() is not implemented on the base type.") raise NotImplementedError("AccountLiquidator.prepare_instructions() is not implemented on the base type.")
@abc.abstractmethod @abc.abstractmethod
@ -72,192 +60,13 @@ class AccountLiquidator(metaclass=abc.ABCMeta):
# A 'null', 'no-op', 'dry run' implementation of the `AccountLiquidator` class. # A 'null', 'no-op', 'dry run' implementation of the `AccountLiquidator` class.
# #
class NullAccountLiquidator(AccountLiquidator): class NullAccountLiquidator(AccountLiquidator):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
def prepare_instructions(self, liquidatable_report: LiquidatableReport) -> typing.List[InstructionBuilder]: def prepare_instructions(self, liquidatable_report: LiquidatableReport) -> typing.Sequence[InstructionBuilder]:
return [] return []
def liquidate(self, liquidatable_report: LiquidatableReport) -> typing.Optional[str]: def liquidate(self, liquidatable_report: LiquidatableReport) -> typing.Optional[str]:
self.logger.info(f"Skipping liquidation of margin account [{liquidatable_report.margin_account.address}]") self.logger.info(f"Skipping liquidation of account [{liquidatable_report.account.address}]")
return None return None
# # 💧 ActualAccountLiquidator class
#
# This full implementation takes a `MarginAccount` and liquidates it.
#
# It can also serve as a base class for further derivation. Derived classes may override
# `prepare_instructions()` to extend the liquidation process (for example to cancel
# outstanding orders before liquidating).
#
class ActualAccountLiquidator(AccountLiquidator):
def __init__(self, context: Context, wallet: Wallet):
super().__init__()
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.context = context
self.wallet = wallet
def prepare_instructions(self, liquidatable_report: LiquidatableReport) -> typing.List[InstructionBuilder]:
liquidate_instructions: typing.List[InstructionBuilder] = []
liquidate_instruction = LiquidateInstructionBuilder.from_margin_account_and_market(
self.context, liquidatable_report.group, self.wallet, liquidatable_report.margin_account, liquidatable_report.prices)
if liquidate_instruction is not None:
liquidate_instructions += [liquidate_instruction]
return liquidate_instructions
def liquidate(self, liquidatable_report: LiquidatableReport) -> typing.Optional[str]:
instruction_builders = self.prepare_instructions(liquidatable_report)
if len(instruction_builders) == 0:
return None
transaction = Transaction()
for builder in instruction_builders:
transaction.add(builder.build())
for instruction in transaction.instructions:
self.logger.debug("INSTRUCTION")
self.logger.debug(" Keys:")
for key in instruction.keys:
self.logger.debug(" ", f"{key.pubkey}".ljust(
45), f"{key.is_signer}".ljust(6), f"{key.is_writable}".ljust(6))
self.logger.debug(" Data:", " ".join(f"{x:02x}" for x in instruction.data))
self.logger.debug(" Program ID:", instruction.program_id)
transaction_response = self.context.client.send_transaction(
transaction, self.wallet.account, opts=self.context.transaction_options)
transaction_id = self.context.unwrap_transaction_id_or_raise_exception(transaction_response)
return transaction_id
# # 🌪️ ForceCancelOrdersAccountLiquidator class
#
# When liquidating an account, it's a good idea to ensure it has no open orders that could
# lock funds. This is why Mango allows a liquidator to force-close orders on a liquidatable
# account.
#
# `ForceCancelOrdersAccountLiquidator` overrides `prepare_instructions()` to inject any
# necessary force-cancel instructions before the `PartialLiquidate` instruction.
#
# This is not always necessary. For example, if the liquidator is partially-liquidating a
# large account, then perhaps only the first partial-liquidate needs to check and force-close
# orders, and subsequent partial liquidations can skip this step as an optimisation.
#
# The separation of the regular `AccountLiquidator` and the
# `ForceCancelOrdersAccountLiquidator` classes allows the caller to determine which process
# is used.
#
class ForceCancelOrdersAccountLiquidator(ActualAccountLiquidator):
def __init__(self, context: Context, wallet: Wallet):
super().__init__(context, wallet)
def prepare_instructions(self, liquidatable_report: LiquidatableReport) -> typing.List[InstructionBuilder]:
force_cancel_orders_instructions: typing.List[InstructionBuilder] = []
for index, market_metadata in enumerate(liquidatable_report.group.markets):
open_orders = liquidatable_report.margin_account.open_orders_accounts[index]
if open_orders is not None:
market = market_metadata.fetch_market(self.context)
orders = market.load_orders_for_owner(liquidatable_report.margin_account.owner)
order_count = len(orders)
if order_count > 0:
force_cancel_orders_instructions += ForceCancelOrdersInstructionBuilder.multiple_instructions_from_margin_account_and_market(
self.context, liquidatable_report.group, self.wallet, liquidatable_report.margin_account, market_metadata, order_count)
all_instructions = force_cancel_orders_instructions + super().prepare_instructions(liquidatable_report)
return all_instructions
# 📝 ReportingAccountLiquidator class
#
# This class takes a regular `AccountLiquidator` and wraps its `liquidate()` call in some
# useful reporting.
#
class ReportingAccountLiquidator(AccountLiquidator):
def __init__(self, inner: AccountLiquidator, context: Context, wallet: Wallet, liquidations_publisher: EventSource[LiquidationEvent], liquidator_name: str):
super().__init__()
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.inner: AccountLiquidator = inner
self.context: Context = context
self.wallet: Wallet = wallet
self.liquidations_publisher: EventSource[LiquidationEvent] = liquidations_publisher
self.liquidator_name: str = liquidator_name
def prepare_instructions(self, liquidatable_report: LiquidatableReport) -> typing.List[InstructionBuilder]:
return self.inner.prepare_instructions(liquidatable_report)
def liquidate(self, liquidatable_report: LiquidatableReport) -> typing.Optional[str]:
balances_before = liquidatable_report.group.fetch_balances(self.context, self.wallet.address)
self.logger.info("Wallet balances before:")
TokenValue.report(self.logger.info, balances_before)
self.logger.info(f"Margin account balances before:\n{liquidatable_report.balances}")
self.logger.info(
f"Liquidating margin account: {liquidatable_report.margin_account}\n{liquidatable_report.balance_sheet}")
try:
transaction_id = self.inner.liquidate(liquidatable_report)
except Exception as exception:
# It would be nice if we had a strongly-typed way of checking this.
if "MangoErrorCode::NotLiquidatable" in str(exception):
failed_liquidation_event = LiquidationEvent(datetime.datetime.now(),
self.liquidator_name,
self.context.group_name,
False,
"",
self.wallet.address,
liquidatable_report.margin_account.address,
balances_before,
balances_before)
self.liquidations_publisher.publish(failed_liquidation_event)
return None
else:
raise exception
if transaction_id is None:
self.logger.info("No transaction sent.")
else:
self.logger.info(f"Transaction ID: {transaction_id} - waiting for confirmation...")
response = self.context.wait_for_confirmation(transaction_id)
if response is None:
self.logger.warning(
f"Could not process 'after' liquidation stage - no data for transaction {transaction_id}")
return transaction_id
transaction_scout = TransactionScout.from_transaction_response(self.context, response)
group_after = Group.load(self.context)
margin_account_after_liquidation = MarginAccount.load(
self.context, liquidatable_report.margin_account.address, group_after)
intrinsic_balances_after = margin_account_after_liquidation.get_intrinsic_balances(group_after)
self.logger.info(f"Margin account balances after: {intrinsic_balances_after}")
self.logger.info("Wallet Balances After:")
balances_after = group_after.fetch_balances(self.context, self.wallet.address)
TokenValue.report(self.logger.info, balances_after)
liquidation_event = LiquidationEvent(datetime.datetime.now(),
self.liquidator_name,
self.context.group_name,
transaction_scout.succeeded,
transaction_id,
self.wallet.address,
margin_account_after_liquidation.address,
balances_before,
balances_after)
self.logger.info("Wallet Balances Changes:")
changes = TokenValue.changes(balances_before, balances_after)
TokenValue.report(self.logger.info, changes)
self.liquidations_publisher.publish(liquidation_event)
return transaction_id

View File

@ -18,12 +18,11 @@ import typing
from solana.publickey import PublicKey from solana.publickey import PublicKey
from .account import Account
from .accountinfo import AccountInfo from .accountinfo import AccountInfo
from .constants import SYSTEM_PROGRAM_ADDRESS from .constants import SYSTEM_PROGRAM_ADDRESS
from .context import Context from .context import Context
from .group import Group from .group import Group
from .marginaccount import MarginAccount
from .openorders import OpenOrders
from .tokenaccount import TokenAccount from .tokenaccount import TokenAccount
from .wallet import Wallet from .wallet import Wallet
@ -144,29 +143,19 @@ class AccountScout:
return report return report
# Must have token accounts for each of the tokens in the group's basket. # Must have token accounts for each of the tokens in the group's basket.
for basket_token in group.basket_tokens: for basket_token in group.tokens:
token_accounts = TokenAccount.fetch_all_for_owner_and_token(context, account_address, basket_token.token) if basket_token is not None:
if len(token_accounts) == 0: token_accounts = TokenAccount.fetch_all_for_owner_and_token(
report.add_error( context, account_address, basket_token.token)
f"Account '{account_address}' has no account for token '{basket_token.token.name}', mint '{basket_token.token.mint}'.") if len(token_accounts) == 0:
else: report.add_error(
report.add_detail( f"Account '{account_address}' has no account for token '{basket_token.token.name}', mint '{basket_token.token.mint}'.")
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]}") else:
# 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.)
for market in group.markets:
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)
if len(open_orders) == 0:
report.add_warning(
f"No Serum open orders account for market '{market.base.token.name}/{market.quote.token.name}' [{market.address}]'.")
else:
for open_orders_account in open_orders:
report.add_detail( report.add_detail(
f"Serum open orders account for market '{market.base.token.name}/{market.quote.token.name}': {open_orders_account}") 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]}")
# May have one or more Mango Markets margin account, but it's optional for liquidating # May have one or more Mango Markets margin account, but it's optional for liquidating
margin_accounts = MarginAccount.load_all_for_owner(context, account_address, group) margin_accounts = Account.load_all_for_owner(context, account_address, group)
if len(margin_accounts) == 0: if len(margin_accounts) == 0:
report.add_detail(f"Account '{account_address}' has no Mango Markets margin accounts.") report.add_detail(f"Account '{account_address}' has no Mango Markets margin accounts.")
else: else:

View File

@ -24,7 +24,7 @@ from .accountinfo import AccountInfo
# # 🥭 AddressableAccount class # # 🥭 AddressableAccount class
# #
# Some of our most-used objects (like `Group` or `MarginAccount`) are accounts on Solana # Some of our most-used objects (like `Group` or `Account`) are accounts on Solana
# with packed data. When these are loaded, they're typically loaded by loading the # with packed data. When these are loaded, they're typically loaded by loading the
# `AccountInfo` and parsing it in an object-specific way. # `AccountInfo` and parsing it in an object-specific way.
# #

View File

@ -1,166 +0,0 @@
# # ⚠ 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 datetime
import logging
from decimal import Decimal
from solana.publickey import PublicKey
from .accountinfo import AccountInfo
from .addressableaccount import AddressableAccount
from .context import Context
from .layouts import layouts
from .version import Version
# # 🥭 AggregatorConfig class
#
class AggregatorConfig:
def __init__(self, version: Version, description: str, decimals: Decimal, restart_delay: Decimal,
max_submissions: Decimal, min_submissions: Decimal, reward_amount: Decimal,
reward_token_account: PublicKey):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.version: Version = version
self.description: str = description
self.decimals: Decimal = decimals
self.restart_delay: Decimal = restart_delay
self.max_submissions: Decimal = max_submissions
self.min_submissions: Decimal = min_submissions
self.reward_amount: Decimal = reward_amount
self.reward_token_account: PublicKey = reward_token_account
@staticmethod
def from_layout(layout: layouts.AGGREGATOR_CONFIG) -> "AggregatorConfig":
return AggregatorConfig(Version.UNSPECIFIED, layout.description, layout.decimals,
layout.restart_delay, layout.max_submissions, layout.min_submissions,
layout.reward_amount, layout.reward_token_account)
def __str__(self) -> str:
return f"« AggregatorConfig: '{self.description}', Decimals: {self.decimals} [restart delay: {self.restart_delay}], Max: {self.max_submissions}, Min: {self.min_submissions}, Reward: {self.reward_amount}, Reward Account: {self.reward_token_account} »"
def __repr__(self) -> str:
return f"{self}"
# # 🥭 Round class
#
class Round:
def __init__(self, version: Version, id: Decimal, created_at: datetime.datetime, updated_at: datetime.datetime):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.version: Version = version
self.id: Decimal = id
self.created_at: datetime.datetime = created_at
self.updated_at: datetime.datetime = updated_at
@staticmethod
def from_layout(layout: layouts.ROUND) -> "Round":
return Round(Version.UNSPECIFIED, layout.id, layout.created_at, layout.updated_at)
def __str__(self) -> str:
return f"« Round[{self.id}], Created: {self.updated_at}, Updated: {self.updated_at} »"
def __repr__(self) -> str:
return f"{self}"
# # 🥭 Answer class
#
class Answer:
def __init__(self, version: Version, round_id: Decimal, median: Decimal, created_at: datetime.datetime, updated_at: datetime.datetime):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.version: Version = version
self.round_id: Decimal = round_id
self.median: Decimal = median
self.created_at: datetime.datetime = created_at
self.updated_at: datetime.datetime = updated_at
@staticmethod
def from_layout(layout: layouts.ANSWER) -> "Answer":
return Answer(Version.UNSPECIFIED, layout.round_id, layout.median, layout.created_at, layout.updated_at)
def __str__(self) -> str:
return f"« Answer: Round[{self.round_id}], Median: {self.median:,.8f}, Created: {self.updated_at}, Updated: {self.updated_at} »"
def __repr__(self) -> str:
return f"{self}"
# # 🥭 Aggregator class
#
class Aggregator(AddressableAccount):
def __init__(self, account_info: AccountInfo, version: Version, config: AggregatorConfig,
initialized: bool, name: str, owner: PublicKey, round_: Round,
round_submissions: PublicKey, answer: Answer, answer_submissions: PublicKey):
super().__init__(account_info)
self.version: Version = version
self.config: AggregatorConfig = config
self.initialized: bool = initialized
self.name: str = name
self.owner: PublicKey = owner
self.round: Round = round_
self.round_submissions: PublicKey = round_submissions
self.answer: Answer = answer
self.answer_submissions: PublicKey = answer_submissions
@property
def price(self) -> Decimal:
return self.answer.median / (10 ** self.config.decimals)
@staticmethod
def from_layout(layout: layouts.AGGREGATOR, account_info: AccountInfo, name: str) -> "Aggregator":
config = AggregatorConfig.from_layout(layout.config)
initialized = bool(layout.initialized)
round_ = Round.from_layout(layout.round)
answer = Answer.from_layout(layout.answer)
return Aggregator(account_info, Version.UNSPECIFIED, config, initialized, name, layout.owner,
round_, layout.round_submissions, answer, layout.answer_submissions)
@staticmethod
def parse(context: Context, account_info: AccountInfo) -> "Aggregator":
data = account_info.data
if len(data) != layouts.AGGREGATOR.sizeof():
raise Exception(f"Data length ({len(data)}) does not match expected size ({layouts.AGGREGATOR.sizeof()})")
name = context.lookup_oracle_name(account_info.address)
layout = layouts.AGGREGATOR.parse(data)
return Aggregator.from_layout(layout, account_info, name)
@staticmethod
def load(context: Context, account_address: PublicKey):
account_info = AccountInfo.load(context, account_address)
if account_info is None:
raise Exception(f"Aggregator account not found at address '{account_address}'")
return Aggregator.parse(context, account_info)
def __str__(self) -> str:
return f"""
« Aggregator '{self.name}' [{self.version}]:
Config: {self.config}
Initialized: {self.initialized}
Owner: {self.owner}
Round: {self.round}
Round Submissions: {self.round_submissions}
Answer: {self.answer}
Answer Submissions: {self.answer_submissions}
»
"""

View File

@ -1,80 +0,0 @@
# # ⚠ 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 logging
import typing
from solana.publickey import PublicKey
from .index import Index
from .token import Token
# # 🥭 BasketToken class
#
# `BasketToken` defines aspects of `Token`s that are part of a `Group` basket.
#
class BasketToken:
def __init__(self, token: Token, vault: PublicKey, index: Index):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.token: Token = token
self.vault: PublicKey = vault
self.index: Index = index
@staticmethod
def find_by_symbol(values: typing.List["BasketToken"], symbol: str) -> "BasketToken":
found = [value for value in values if value.token.symbol_matches(symbol)]
if len(found) == 0:
raise Exception(f"Token '{symbol}' not found in token values: {values}")
if len(found) > 1:
raise Exception(f"Token '{symbol}' matched multiple tokens in values: {values}")
return found[0]
@staticmethod
def find_by_mint(values: typing.List["BasketToken"], mint: PublicKey) -> "BasketToken":
found = [value for value in values if value.token.mint == mint]
if len(found) == 0:
raise Exception(f"Token '{mint}' not found in token values: {values}")
if len(found) > 1:
raise Exception(f"Token '{mint}' matched multiple tokens in values: {values}")
return found[0]
@staticmethod
def find_by_token(values: typing.List["BasketToken"], token: Token) -> "BasketToken":
return BasketToken.find_by_mint(values, token.mint)
# BasketTokens are equal if they have the same underlying token.
def __eq__(self, other):
if hasattr(other, 'token'):
return self.token == other.token
return False
def __str__(self) -> str:
index = str(self.index).replace("\n", "\n ")
return f"""« BasketToken:
{self.token}
Vault: {self.vault}
Index: {index}
»"""
def __repr__(self) -> str:
return f"{self}"

View File

@ -83,7 +83,7 @@ class Context:
self.commitment: Commitment = Commitment("processed") self.commitment: Commitment = Commitment("processed")
self.transaction_options: TxOpts = TxOpts(preflight_commitment=self.commitment) self.transaction_options: TxOpts = TxOpts(preflight_commitment=self.commitment)
self.encoding: str = "base64" self.encoding: str = "base64"
ids_json_token_lookup: TokenLookup = IdsJsonTokenLookup(cluster) ids_json_token_lookup: TokenLookup = IdsJsonTokenLookup(cluster, group_name)
spl_token_lookup: TokenLookup = SplTokenLookup.load(token_filename) spl_token_lookup: TokenLookup = SplTokenLookup.load(token_filename)
all_token_lookup: TokenLookup = CompoundTokenLookup( all_token_lookup: TokenLookup = CompoundTokenLookup(
[ids_json_token_lookup, spl_token_lookup]) [ids_json_token_lookup, spl_token_lookup])
@ -96,7 +96,7 @@ class Context:
# kangda said in Discord: https://discord.com/channels/791995070613159966/836239696467591186/847816026245693451 # kangda said in Discord: https://discord.com/channels/791995070613159966/836239696467591186/847816026245693451
# "I think you are better off doing 4,8,16,20,30" # "I think you are better off doing 4,8,16,20,30"
self.retry_pauses: typing.List[Decimal] = [Decimal(4), Decimal( self.retry_pauses: typing.Sequence[Decimal] = [Decimal(4), Decimal(
8), Decimal(16), Decimal(20), Decimal(30)] 8), Decimal(16), Decimal(20), Decimal(30)]
@property @property

View File

@ -16,11 +16,11 @@
import typing import typing
from .account import Account
from .context import Context from .context import Context
from .mangoaccount import MangoAccount from .group import Group
from .market import Market from .market import Market
from .marketoperations import MarketOperations, NullMarketOperations from .marketoperations import MarketOperations, NullMarketOperations
from .mangogroup import MangoGroup
from .perpmarket import PerpMarket from .perpmarket import PerpMarket
from .perpmarketoperations import PerpMarketOperations from .perpmarketoperations import PerpMarketOperations
# from .serummarketoperations import SerumMarketOperations # from .serummarketoperations import SerumMarketOperations
@ -38,8 +38,8 @@ def create_market_operations(context: Context, wallet: Wallet, dry_run: bool, ma
elif isinstance(market, SpotMarket): elif isinstance(market, SpotMarket):
# return SerumMarketOperations(context, wallet, market, reporter) # return SerumMarketOperations(context, wallet, market, reporter)
# elif isinstance(market, PerpMarket): # elif isinstance(market, PerpMarket):
group = MangoGroup.load(context, context.group_id) group = Group.load(context, context.group_id)
margin_accounts = MangoAccount.load_all_for_owner(context, wallet.address, group) margin_accounts = Account.load_all_for_owner(context, wallet.address, group)
perp_market_info = group.perp_markets[0] perp_market_info = group.perp_markets[0]
if perp_market_info is None: if perp_market_info is None:
raise Exception("Perp market not found at index 0.") raise Exception("Perp market not found at index 0.")

View File

@ -38,7 +38,7 @@ from solana.publickey import PublicKey
# `decode_binary()` decodes the data properly based on which encoding was used. # `decode_binary()` decodes the data properly based on which encoding was used.
def decode_binary(encoded: typing.List) -> bytes: def decode_binary(encoded: typing.Sequence) -> bytes:
if isinstance(encoded, str): if isinstance(encoded, str):
return base58.b58decode(encoded) return base58.b58decode(encoded)
elif encoded[1] == "base64": elif encoded[1] == "base64":
@ -53,7 +53,7 @@ def decode_binary(encoded: typing.List) -> bytes:
# #
def encode_binary(decoded: bytes) -> typing.List: def encode_binary(decoded: bytes) -> typing.Sequence:
return [base64.b64encode(decoded), "base64"] return [base64.b64encode(decoded), "base64"]

View File

@ -13,8 +13,6 @@
# [Github](https://github.com/blockworks-foundation) # [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation) # [Email](mailto:hello@blockworks.foundation)
import construct
import time
import typing import typing
from decimal import Decimal from decimal import Decimal
@ -22,203 +20,126 @@ from solana.publickey import PublicKey
from .accountinfo import AccountInfo from .accountinfo import AccountInfo
from .addressableaccount import AddressableAccount from .addressableaccount import AddressableAccount
from .aggregator import Aggregator
from .baskettoken import BasketToken
from .context import Context from .context import Context
from .index import Index
from .layouts import layouts from .layouts import layouts
from .mangoaccountflags import MangoAccountFlags
from .marketmetadata import MarketMetadata
from .marketlookup import MarketLookup from .marketlookup import MarketLookup
from .token import SolToken, Token from .metadata import Metadata
from .perpmarketinfo import PerpMarketInfo
from .spotmarketinfo import SpotMarketInfo
from .token import SolToken
from .tokeninfo import TokenInfo
from .tokenlookup import TokenLookup from .tokenlookup import TokenLookup
from .tokenvalue import TokenValue from .tokenvalue import TokenValue
from .version import Version from .version import Version
# # 🥭 Group class # # 🥭 Group class
# #
# The `Group` class encapsulates the data for the Mango Group - the cross-margined basket # `Group` defines root functionality for Mango Markets.
# of tokens with lending. #
class Group(AddressableAccount): class Group(AddressableAccount):
def __init__(self, account_info: AccountInfo, version: Version, name: str, def __init__(self, account_info: AccountInfo, version: Version, name: str,
account_flags: MangoAccountFlags, basket_tokens: typing.List[BasketToken], meta_data: Metadata, tokens: typing.Sequence[typing.Optional[TokenInfo]],
markets: typing.List[MarketMetadata], spot_markets: typing.Sequence[typing.Optional[SpotMarketInfo]],
signer_nonce: Decimal, signer_key: PublicKey, dex_program_id: PublicKey, perp_markets: typing.Sequence[typing.Optional[PerpMarketInfo]],
total_deposits: typing.List[TokenValue], total_borrows: typing.List[TokenValue], oracles: typing.Sequence[PublicKey], signer_nonce: Decimal, signer_key: PublicKey,
maint_coll_ratio: Decimal, init_coll_ratio: Decimal, srm_vault: PublicKey, admin: PublicKey, dex_program_id: PublicKey, cache: PublicKey, valid_interval: Decimal):
admin: PublicKey, borrow_limits: typing.List[TokenValue]):
super().__init__(account_info) super().__init__(account_info)
self.version: Version = version self.version: Version = version
self.name: str = name self.name: str = name
self.account_flags: MangoAccountFlags = account_flags
self.basket_tokens: typing.List[BasketToken] = basket_tokens self.meta_data: Metadata = meta_data
self.markets: typing.List[MarketMetadata] = markets self.tokens: typing.Sequence[typing.Optional[TokenInfo]] = tokens
self.spot_markets: typing.Sequence[typing.Optional[SpotMarketInfo]] = spot_markets
self.perp_markets: typing.Sequence[typing.Optional[PerpMarketInfo]] = perp_markets
self.oracles: typing.Sequence[PublicKey] = oracles
self.signer_nonce: Decimal = signer_nonce self.signer_nonce: Decimal = signer_nonce
self.signer_key: PublicKey = signer_key self.signer_key: PublicKey = signer_key
self.dex_program_id: PublicKey = dex_program_id
self.total_deposits: typing.List[TokenValue] = total_deposits
self.total_borrows: typing.List[TokenValue] = total_borrows
self.maint_coll_ratio: Decimal = maint_coll_ratio
self.init_coll_ratio: Decimal = init_coll_ratio
self.srm_vault: PublicKey = srm_vault
self.admin: PublicKey = admin self.admin: PublicKey = admin
self.borrow_limits: typing.List[TokenValue] = borrow_limits self.dex_program_id: PublicKey = dex_program_id
self.cache: PublicKey = cache
self.valid_interval: Decimal = valid_interval
@property @property
def shared_quote_token(self) -> BasketToken: def shared_quote_token(self) -> TokenInfo:
return self.basket_tokens[-1] quote = self.tokens[-1]
if quote is None:
raise Exception(f"Could not find shared quote token for group '{self.name}'.")
return quote
@property @property
def base_tokens(self) -> typing.List[BasketToken]: def base_tokens(self) -> typing.Sequence[typing.Optional[TokenInfo]]:
return self.basket_tokens[:-1] return self.tokens[:-1]
# When loading from a layout, we ignore mint_decimals. In this Discord from Daffy:
# https://discord.com/channels/791995070613159966/818978757648842782/851481660049850388
# he says it's:
# > same as what is stored on on chain Mint
# > Cached on the group so we don't have to pass in the mint every time
#
# Since we already have that value from our `Token` we don't need to use it and we can
# stick with passing around `Token` objects.
#
@staticmethod @staticmethod
def from_layout(layout: construct.Struct, name: str, account_info: AccountInfo, version: Version, token_lookup: TokenLookup, market_lookup: MarketLookup) -> "Group": def from_layout(layout: layouts.GROUP, name: str, account_info: AccountInfo, version: Version, token_lookup: TokenLookup, market_lookup: MarketLookup) -> "Group":
account_flags: MangoAccountFlags = MangoAccountFlags.from_layout(layout.account_flags) meta_data = Metadata.from_layout(layout.meta_data)
num_oracles = layout.num_oracles
tokens = [TokenInfo.from_layout_or_none(t, token_lookup) for t in layout.tokens]
spot_markets = [SpotMarketInfo.from_layout_or_none(m, market_lookup) for m in layout.spot_markets]
perp_markets = [PerpMarketInfo.from_layout_or_none(p) for p in layout.perp_markets]
oracles = list(layout.oracles)[:int(num_oracles)]
signer_nonce = layout.signer_nonce
signer_key = layout.signer_key
admin = layout.admin
dex_program_id = layout.dex_program_id
cache = layout.cache
valid_interval = layout.valid_interval
basket_tokens: typing.List[BasketToken] = [] return Group(account_info, version, name, meta_data, tokens, spot_markets, perp_markets, oracles, signer_nonce, signer_key, admin, dex_program_id, cache, valid_interval)
total_deposits: typing.List[TokenValue] = []
total_borrows: typing.List[TokenValue] = []
borrow_limits: typing.List[TokenValue] = []
for index, token_address in enumerate(layout.tokens):
static_token_data = token_lookup.find_by_mint(token_address)
if static_token_data is None:
raise Exception(f"Could not find token with mint '{token_address}'.")
# We create a new Token object here specifically to force the use of our own decimals
token = Token(static_token_data.symbol, static_token_data.name, token_address, layout.mint_decimals[index])
token_index = Index.from_layout(layout.indexes[index], token)
basket_token = BasketToken(token, layout.vaults[index], token_index)
basket_tokens += [basket_token]
total_deposits += [TokenValue(token, token.shift_to_decimals(layout.total_deposits[index]))]
total_borrows += [TokenValue(token, token.shift_to_decimals(layout.total_borrows[index]))]
borrow_limits += [TokenValue(token, token.shift_to_decimals(layout.borrow_limits[index]))]
markets: typing.List[MarketMetadata] = []
for index, market_address in enumerate(layout.spot_markets):
spot_market = market_lookup.find_by_address(market_address)
if spot_market is None:
raise Exception(f"Could not find spot market with address '{market_address}'.")
base_token = BasketToken.find_by_mint(basket_tokens, spot_market.base.mint)
quote_token = BasketToken.find_by_mint(basket_tokens, spot_market.quote.mint)
market = MarketMetadata(spot_market.symbol, market_address, base_token, quote_token,
spot_market, layout.oracles[index], layout.oracle_decimals[index])
markets += [market]
maint_coll_ratio = layout.maint_coll_ratio.quantize(Decimal('.01'))
init_coll_ratio = layout.init_coll_ratio.quantize(Decimal('.01'))
return Group(account_info, version, name, account_flags, basket_tokens, markets,
layout.signer_nonce, layout.signer_key, layout.dex_program_id, total_deposits,
total_borrows, maint_coll_ratio, init_coll_ratio, layout.srm_vault,
layout.admin, borrow_limits)
@staticmethod @staticmethod
def parse(context: Context, account_info: AccountInfo) -> "Group": def parse(context: Context, account_info: AccountInfo) -> "Group":
data = account_info.data data = account_info.data
if len(data) == layouts.GROUP_V1.sizeof(): if len(data) != layouts.GROUP.sizeof():
layout = layouts.GROUP_V1.parse(data)
version: Version = Version.V1
elif len(data) == layouts.GROUP_V2.sizeof():
version = Version.V2
layout = layouts.GROUP_V2.parse(data)
else:
raise Exception( raise Exception(
f"Group data length ({len(data)}) does not match expected size ({layouts.GROUP_V1.sizeof()} or {layouts.GROUP_V2.sizeof()})") f"Group data length ({len(data)}) does not match expected size ({layouts.GROUP.sizeof()}")
return Group.from_layout(layout, context.group_name, account_info, version, context.token_lookup, context.market_lookup) layout = layouts.GROUP.parse(data)
return Group.from_layout(layout, "merps_test_v3", account_info, Version.V1, context.token_lookup, context.market_lookup)
@staticmethod @staticmethod
def load(context: Context): def load(context: Context, address: typing.Optional[PublicKey] = None) -> "Group":
account_info = AccountInfo.load(context, context.group_id) group_address: PublicKey = address or context.group_id
account_info = AccountInfo.load(context, group_address)
if account_info is None: if account_info is None:
raise Exception(f"Group account not found at address '{context.group_id}'") raise Exception(f"Group account not found at address '{group_address}'")
return Group.parse(context, account_info) return Group.parse(context, account_info)
def price_index_of_token(self, token: Token) -> int: def fetch_balances(self, context: Context, root_address: PublicKey) -> typing.Sequence[TokenValue]:
for index, existing in enumerate(self.basket_tokens):
if existing.token == token:
return index
return -1
def fetch_token_prices(self, context: Context) -> typing.List[TokenValue]:
started_at = time.time()
# Note: we can just load the oracle data in a simpler way, with:
# oracles = map(lambda market: Aggregator.load(context, market.oracle), self.markets)
# but that makes a network request for every oracle. We can reduce that to just one request
# if we use AccountInfo.load_multiple() and parse the data ourselves.
#
# This seems to halve the time this function takes.
oracle_addresses = list([market.oracle for market in self.markets])
oracle_account_infos = AccountInfo.load_multiple(context, oracle_addresses)
oracles = map(lambda oracle_account_info: Aggregator.parse(context, oracle_account_info), oracle_account_infos)
prices = list(map(lambda oracle: oracle.price, oracles)) + [Decimal(1)]
token_prices = []
for index, price in enumerate(prices):
token_prices += [TokenValue(self.basket_tokens[index].token, price)]
time_taken = time.time() - started_at
self.logger.info(f"Fetching prices complete. Time taken: {time_taken:.2f} seconds.")
return token_prices
@staticmethod
def load_with_prices(context: Context) -> typing.Tuple["Group", typing.List[TokenValue]]:
group = Group.load(context)
prices = group.fetch_token_prices(context)
return group, prices
def fetch_balances(self, context: Context, root_address: PublicKey) -> typing.List[TokenValue]:
balances: typing.List[TokenValue] = [] balances: typing.List[TokenValue] = []
sol_balance = context.fetch_sol_balance(root_address) sol_balance = context.fetch_sol_balance(root_address)
balances += [TokenValue(SolToken, sol_balance)] balances += [TokenValue(SolToken, sol_balance)]
for basket_token in self.basket_tokens: for basket_token in self.tokens:
balance = TokenValue.fetch_total_value(context, root_address, basket_token.token) if basket_token is not None and basket_token.token is not None:
balances += [balance] balance = TokenValue.fetch_total_value(context, root_address, basket_token.token)
balances += [balance]
return balances return balances
def __str__(self) -> str: def __str__(self):
total_deposits = "\n ".join(map(str, self.total_deposits)) tokens = "\n ".join([f"{token}".replace("\n", "\n ")
total_borrows = "\n ".join(map(str, self.total_borrows)) for token in self.tokens if token is not None])
borrow_limits = "\n ".join(map(str, self.borrow_limits)) spot_markets = "\n ".join([f"{spot_market}".replace("\n", "\n ")
shared_quote_token = str(self.shared_quote_token).replace("\n", "\n ") for spot_market in self.spot_markets if spot_market is not None])
base_tokens = "\n ".join([f"{tok}".replace("\n", "\n ") for tok in self.base_tokens]) perp_markets = "\n ".join([f"{perp_market}".replace("\n", "\n ")
markets = "\n ".join([f"{mkt}".replace("\n", "\n ") for mkt in self.markets]) for perp_market in self.perp_markets if perp_market is not None])
return f""" oracles = "\n ".join([f"{oracle}" for oracle in self.oracles])
« Group [{self.version} - {self.name}] {self.address}: return f"""« 𝙶𝚛𝚘𝚞𝚙 {self.version} [{self.address}]
Flags: {self.account_flags} {self.meta_data}
Base Tokens: Name: {self.name}
{base_tokens} Signer [Nonce: {self.signer_nonce}]: {self.signer_key}
Quote Token: Admin: {self.admin}
{shared_quote_token} DEX Program ID: {self.dex_program_id}
Markets: Merps Cache: {self.cache}
{markets} Valid Interval: {self.valid_interval}
DEX Program ID: « {self.dex_program_id} » Tokens:
SRM Vault: « {self.srm_vault} » {tokens}
Admin: « {self.admin} » Spot Markets:
Signer Nonce: {self.signer_nonce} {spot_markets}
Signer Key: « {self.signer_key} » Perp Markets:
Initial Collateral Ratio: {self.init_coll_ratio} {perp_markets}
Maintenance Collateral Ratio: {self.maint_coll_ratio} Oracles:
Total Deposits: {oracles}
{total_deposits} »"""
Total Borrows:
{total_borrows}
Borrow Limits:
{borrow_limits}
»
"""

View File

@ -29,13 +29,14 @@ from .tokenlookup import TokenLookup
# #
class IdsJsonTokenLookup(TokenLookup): class IdsJsonTokenLookup(TokenLookup):
def __init__(self, cluster: str) -> None: def __init__(self, cluster: str, group_name: str) -> None:
super().__init__() super().__init__()
self.cluster: str = cluster self.cluster: str = cluster
self.group_name: str = group_name
def find_by_symbol(self, symbol: str) -> typing.Optional[Token]: def find_by_symbol(self, symbol: str) -> typing.Optional[Token]:
for group in MangoConstants["groups"]: for group in MangoConstants["groups"]:
if group["cluster"] == self.cluster: if group["cluster"] == self.cluster and group["name"] == self.group_name:
for token in group["tokens"]: for token in group["tokens"]:
if token["symbol"] == symbol: if token["symbol"] == symbol:
return Token(token["symbol"], token["symbol"], PublicKey(token["mintKey"]), Decimal(token["decimals"])) return Token(token["symbol"], token["symbol"], PublicKey(token["mintKey"]), Decimal(token["decimals"]))
@ -44,7 +45,7 @@ class IdsJsonTokenLookup(TokenLookup):
def find_by_mint(self, mint: PublicKey) -> typing.Optional[Token]: def find_by_mint(self, mint: PublicKey) -> typing.Optional[Token]:
mint_str = str(mint) mint_str = str(mint)
for group in MangoConstants["groups"]: for group in MangoConstants["groups"]:
if group["cluster"] == self.cluster: if group["cluster"] == self.cluster and group["name"] == self.group_name:
for token in group["tokens"]: for token in group["tokens"]:
if token["mintKey"] == mint_str: if token["mintKey"] == mint_str:
return Token(token["symbol"], token["symbol"], PublicKey(token["mintKey"]), Decimal(token["decimals"])) return Token(token["symbol"], token["symbol"], PublicKey(token["mintKey"]), Decimal(token["decimals"]))

View File

@ -1,53 +0,0 @@
# # ⚠ 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 datetime
import logging
from decimal import Decimal
from .layouts import layouts
from .token import Token
from .tokenvalue import TokenValue
from .version import Version
# # 🥭 Index class
#
class Index:
def __init__(self, version: Version, token: Token, last_update: datetime.datetime, borrow: TokenValue, deposit: TokenValue):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.version: Version = version
self.token: Token = token
self.last_update: datetime.datetime = last_update
self.borrow: TokenValue = borrow
self.deposit: TokenValue = deposit
@staticmethod
def from_layout(layout: layouts.INDEX, token: Token) -> "Index":
borrow = TokenValue(token, layout.borrow / Decimal(10 ** token.decimals))
deposit = TokenValue(token, layout.deposit / Decimal(10 ** token.decimals))
return Index(Version.UNSPECIFIED, token, layout.last_update, borrow, deposit)
def __str__(self) -> str:
return f"""« Index [{self.token.symbol}] ({self.last_update}):
Borrow: {self.borrow},
Deposit: {self.deposit} »"""
def __repr__(self) -> str:
return f"{self}"

View File

@ -16,7 +16,6 @@
import abc import abc
import logging import logging
import struct
import typing import typing
from decimal import Decimal from decimal import Decimal
@ -28,20 +27,12 @@ from solana.account import Account
from solana.publickey import PublicKey from solana.publickey import PublicKey
from solana.system_program import CreateAccountParams, create_account from solana.system_program import CreateAccountParams, create_account
from solana.transaction import AccountMeta, TransactionInstruction from solana.transaction import AccountMeta, TransactionInstruction
from solana.sysvar import SYSVAR_CLOCK_PUBKEY
from spl.token.constants import ACCOUNT_LEN, TOKEN_PROGRAM_ID from spl.token.constants import ACCOUNT_LEN, TOKEN_PROGRAM_ID
from spl.token.instructions import CloseAccountParams, InitializeAccountParams, Transfer2Params, close_account, initialize_account, transfer2 from spl.token.instructions import CloseAccountParams, InitializeAccountParams, Transfer2Params, close_account, initialize_account, transfer2
from .baskettoken import BasketToken
from .constants import SYSTEM_PROGRAM_ADDRESS
from .context import Context from .context import Context
from .group import Group
from .layouts import layouts from .layouts import layouts
from .marginaccount import MarginAccount
from .marketmetadata import MarketMetadata
from .token import Token from .token import Token
from .tokenaccount import TokenAccount
from .tokenvalue import TokenValue
from .wallet import Wallet from .wallet import Wallet
@ -69,394 +60,6 @@ class InstructionBuilder(metaclass=abc.ABCMeta):
return f"{self}" return f"{self}"
# # 🥭 ForceCancelOrdersInstructionBuilder class
#
#
# ## Rust Interface
#
# This is what the `force_cancel_orders` instruction looks like in the [Mango Rust](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs) code:
# ```
# pub fn force_cancel_orders(
# program_id: &Pubkey,
# mango_group_pk: &Pubkey,
# liqor_pk: &Pubkey,
# liqee_margin_account_acc: &Pubkey,
# base_vault_pk: &Pubkey,
# quote_vault_pk: &Pubkey,
# spot_market_pk: &Pubkey,
# bids_pk: &Pubkey,
# asks_pk: &Pubkey,
# signer_pk: &Pubkey,
# dex_event_queue_pk: &Pubkey,
# dex_base_pk: &Pubkey,
# dex_quote_pk: &Pubkey,
# dex_signer_pk: &Pubkey,
# dex_prog_id: &Pubkey,
# open_orders_pks: &[Pubkey],
# oracle_pks: &[Pubkey],
# limit: u8
# ) -> Result<Instruction, ProgramError>
# ```
#
# ## Client API call
#
# This is how it is built using the Mango Markets client API:
# ```
# const keys = [
# { isSigner: false, isWritable: true, pubkey: mangoGroup },
# { isSigner: true, isWritable: false, pubkey: liqor },
# { isSigner: false, isWritable: true, pubkey: liqeeMarginAccount },
# { isSigner: false, isWritable: true, pubkey: baseVault },
# { isSigner: false, isWritable: true, pubkey: quoteVault },
# { isSigner: false, isWritable: true, pubkey: spotMarket },
# { isSigner: false, isWritable: true, pubkey: bids },
# { isSigner: false, isWritable: true, pubkey: asks },
# { isSigner: false, isWritable: false, pubkey: signerKey },
# { isSigner: false, isWritable: true, pubkey: dexEventQueue },
# { isSigner: false, isWritable: true, pubkey: dexBaseVault },
# { isSigner: false, isWritable: true, pubkey: dexQuoteVault },
# { isSigner: false, isWritable: false, pubkey: dexSigner },
# { isSigner: false, isWritable: false, pubkey: TOKEN_PROGRAM_ID },
# { isSigner: false, isWritable: false, pubkey: dexProgramId },
# { isSigner: false, isWritable: false, pubkey: SYSVAR_CLOCK_PUBKEY },
# ...openOrders.map((pubkey) => ({
# isSigner: false,
# isWritable: true,
# pubkey,
# })),
# ...oracles.map((pubkey) => ({
# isSigner: false,
# isWritable: false,
# pubkey,
# })),
# ];
#
# const data = encodeMangoInstruction({ ForceCancelOrders: { limit } });
# return new TransactionInstruction({ keys, data, programId });
# ```
#
class ForceCancelOrdersInstructionBuilder(InstructionBuilder):
# We can create up to a maximum of max_instructions instructions. I'm not sure of the reason
# for this threshold but it's what's in the original liquidator source code and I'm assuming
# it's there for a good reason.
max_instructions: int = 10
# We cancel up to max_cancels_per_instruction orders with each instruction.
max_cancels_per_instruction: int = 5
def __init__(self, context: Context, group: Group, wallet: Wallet, margin_account: MarginAccount, market_metadata: MarketMetadata, market: Market, oracles: typing.List[PublicKey], dex_signer: PublicKey):
super().__init__(context)
self.group = group
self.wallet = wallet
self.margin_account = margin_account
self.market_metadata = market_metadata
self.market = market
self.oracles = oracles
self.dex_signer = dex_signer
def build(self) -> TransactionInstruction:
transaction = TransactionInstruction(
keys=[
AccountMeta(is_signer=False, is_writable=True, pubkey=self.group.address),
AccountMeta(is_signer=True, is_writable=False, pubkey=self.wallet.address),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.margin_account.address),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market_metadata.base.vault),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market_metadata.quote.vault),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market_metadata.spot.address),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market.state.bids()),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market.state.asks()),
AccountMeta(is_signer=False, is_writable=False, pubkey=self.group.signer_key),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market.state.event_queue()),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market.state.base_vault()),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market.state.quote_vault()),
AccountMeta(is_signer=False, is_writable=False, pubkey=self.dex_signer),
AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID),
AccountMeta(is_signer=False, is_writable=False, pubkey=self.context.dex_program_id),
AccountMeta(is_signer=False, is_writable=False, pubkey=SYSVAR_CLOCK_PUBKEY),
*list([AccountMeta(is_signer=False, is_writable=oo_address is None, pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS)
for oo_address in self.margin_account.open_orders]),
*list([AccountMeta(is_signer=False, is_writable=False, pubkey=oracle_address) for oracle_address in self.oracles])
],
program_id=self.context.program_id,
data=layouts.FORCE_CANCEL_ORDERS.build(
{"limit": ForceCancelOrdersInstructionBuilder.max_cancels_per_instruction})
)
self.logger.debug(f"Built transaction: {transaction}")
return transaction
@staticmethod
def from_margin_account_and_market(context: Context, group: Group, wallet: Wallet, margin_account: MarginAccount, market_metadata: MarketMetadata) -> "ForceCancelOrdersInstructionBuilder":
market = market_metadata.fetch_market(context)
nonce = struct.pack("<Q", market.state.vault_signer_nonce())
dex_signer = PublicKey.create_program_address(
[bytes(market_metadata.spot.address), nonce], context.dex_program_id)
oracles = list([mkt.oracle for mkt in group.markets])
return ForceCancelOrdersInstructionBuilder(context, group, wallet, margin_account, market_metadata, market, oracles, dex_signer)
@classmethod
def multiple_instructions_from_margin_account_and_market(cls, context: Context, group: Group, wallet: Wallet, margin_account: MarginAccount, market_metadata: MarketMetadata, at_least_this_many_cancellations: int) -> typing.List["ForceCancelOrdersInstructionBuilder"]:
logger: logging.Logger = logging.getLogger(cls.__name__)
# We cancel up to max_cancels_per_instruction orders with each instruction, but if
# we have more than cancel_limit we create more instructions (each handling up to
# 5 orders)
calculated_instruction_count = int(
at_least_this_many_cancellations / ForceCancelOrdersInstructionBuilder.max_cancels_per_instruction) + 1
# We create a maximum of max_instructions instructions.
instruction_count = min(calculated_instruction_count, ForceCancelOrdersInstructionBuilder.max_instructions)
instructions: typing.List[ForceCancelOrdersInstructionBuilder] = []
for counter in range(instruction_count):
instructions += [ForceCancelOrdersInstructionBuilder.from_margin_account_and_market(
context, group, wallet, margin_account, market_metadata)]
logger.debug(f"Built {len(instructions)} transaction(s).")
return instructions
def __str__(self) -> str:
# Print the members out using the Rust parameter order and names.
return f"""« ForceCancelOrdersInstructionBuilder:
program_id: &Pubkey: {self.context.program_id},
mango_group_pk: &Pubkey: {self.group.address},
liqor_pk: &Pubkey: {self.wallet.address},
liqee_margin_account_acc: &Pubkey: {self.margin_account.address},
base_vault_pk: &Pubkey: {self.market_metadata.base.vault},
quote_vault_pk: &Pubkey: {self.market_metadata.quote.vault},
spot_market_pk: &Pubkey: {self.market_metadata.spot.address},
bids_pk: &Pubkey: {self.market.state.bids()},
asks_pk: &Pubkey: {self.market.state.asks()},
signer_pk: &Pubkey: {self.group.signer_key},
dex_event_queue_pk: &Pubkey: {self.market.state.event_queue()},
dex_base_pk: &Pubkey: {self.market.state.base_vault()},
dex_quote_pk: &Pubkey: {self.market.state.quote_vault()},
dex_signer_pk: &Pubkey: {self.dex_signer},
dex_prog_id: &Pubkey: {self.context.dex_program_id},
open_orders_pks: &[Pubkey]: {self.margin_account.open_orders},
oracle_pks: &[Pubkey]: {self.oracles},
limit: u8: {ForceCancelOrdersInstructionBuilder.max_cancels_per_instruction}
»"""
# # 🥭 LiquidateInstructionBuilder class
#
# This is the `Instruction` we send to Solana to perform the (partial) liquidation.
#
# We take care to pass the proper high-level parameters to the `LiquidateInstructionBuilder`
# constructor so that `build_transaction()` is straightforward. That tends to push
# complexities to `from_margin_account_and_market()` though.
#
# ## Rust Interface
#
# This is what the `partial_liquidate` instruction looks like in the [Mango Rust](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs) code:
# ```
# /// Take over a MarginAccount that is below init_coll_ratio by depositing funds
# ///
# /// Accounts expected by this instruction (10 + 2 * NUM_MARKETS):
# ///
# /// 0. `[writable]` mango_group_acc - MangoGroup that this margin account is for
# /// 1. `[signer]` liqor_acc - liquidator's solana account
# /// 2. `[writable]` liqor_in_token_acc - liquidator's token account to deposit
# /// 3. `[writable]` liqor_out_token_acc - liquidator's token account to withdraw into
# /// 4. `[writable]` liqee_margin_account_acc - MarginAccount of liquidatee
# /// 5. `[writable]` in_vault_acc - Mango vault of in_token
# /// 6. `[writable]` out_vault_acc - Mango vault of out_token
# /// 7. `[]` signer_acc
# /// 8. `[]` token_prog_acc - Token program id
# /// 9. `[]` clock_acc - Clock sysvar account
# /// 10..10+NUM_MARKETS `[]` open_orders_accs - open orders for each of the spot market
# /// 10+NUM_MARKETS..10+2*NUM_MARKETS `[]`
# /// oracle_accs - flux aggregator feed accounts
# ```
#
# ```
# pub fn partial_liquidate(
# program_id: &Pubkey,
# mango_group_pk: &Pubkey,
# liqor_pk: &Pubkey,
# liqor_in_token_pk: &Pubkey,
# liqor_out_token_pk: &Pubkey,
# liqee_margin_account_acc: &Pubkey,
# in_vault_pk: &Pubkey,
# out_vault_pk: &Pubkey,
# signer_pk: &Pubkey,
# open_orders_pks: &[Pubkey],
# oracle_pks: &[Pubkey],
# max_deposit: u64
# ) -> Result<Instruction, ProgramError>
# ```
#
# ## Client API call
#
# This is how it is built using the Mango Markets client API:
# ```
# const keys = [
# { isSigner: false, isWritable: true, pubkey: mangoGroup },
# { isSigner: true, isWritable: false, pubkey: liqor },
# { isSigner: false, isWritable: true, pubkey: liqorInTokenWallet },
# { isSigner: false, isWritable: true, pubkey: liqorOutTokenWallet },
# { isSigner: false, isWritable: true, pubkey: liqeeMarginAccount },
# { isSigner: false, isWritable: true, pubkey: inTokenVault },
# { isSigner: false, isWritable: true, pubkey: outTokenVault },
# { isSigner: false, isWritable: false, pubkey: signerKey },
# { isSigner: false, isWritable: false, pubkey: TOKEN_PROGRAM_ID },
# { isSigner: false, isWritable: false, pubkey: SYSVAR_CLOCK_PUBKEY },
# ...openOrders.map((pubkey) => ({
# isSigner: false,
# isWritable: false,
# pubkey,
# })),
# ...oracles.map((pubkey) => ({
# isSigner: false,
# isWritable: false,
# pubkey,
# })),
# ];
# const data = encodeMangoInstruction({ PartialLiquidate: { maxDeposit } });
#
# return new TransactionInstruction({ keys, data, programId });
# ```
#
# ## from_margin_account_and_market() function
#
# `from_margin_account_and_market()` merits a bit of explaining.
#
# `from_margin_account_and_market()` takes (among other things) a `Wallet` and a
# `MarginAccount`. The idea is that the `MarginAccount` has some assets in one token, and
# some liabilities in some different token.
#
# To liquidate the account, we want to:
# * supply tokens from the `Wallet` in the token currency that has the greatest liability
# value in the `MarginAccount`
# * receive tokens in the `Wallet` in the token currency that has the greatest asset value
# in the `MarginAccount`
#
# So we calculate the token currencies from the largest liabilities and assets in the
# `MarginAccount`, but we use those token types to get the correct `Wallet` accounts.
# * `input_token` is the `BasketToken` of the currency the `Wallet` is _paying_ and the
# `MarginAccount` is _receiving_ to pay off its largest liability.
# * `output_token` is the `BasketToken` of the currency the `Wallet` is _receiving_ and the
# `MarginAccount` is _paying_ from its largest asset.
#
class LiquidateInstructionBuilder(InstructionBuilder):
def __init__(self, context: Context, group: Group, wallet: Wallet, margin_account: MarginAccount, oracles: typing.List[PublicKey], input_token: BasketToken, output_token: BasketToken, wallet_input_token_account: TokenAccount, wallet_output_token_account: TokenAccount, maximum_input_amount: Decimal):
super().__init__(context)
self.group: Group = group
self.wallet: Wallet = wallet
self.margin_account: MarginAccount = margin_account
self.oracles: typing.List[PublicKey] = oracles
self.input_token: BasketToken = input_token
self.output_token: BasketToken = output_token
self.wallet_input_token_account: TokenAccount = wallet_input_token_account
self.wallet_output_token_account: TokenAccount = wallet_output_token_account
self.maximum_input_amount: Decimal = maximum_input_amount
def build(self) -> TransactionInstruction:
transaction = TransactionInstruction(
keys=[
AccountMeta(is_signer=False, is_writable=True, pubkey=self.group.address),
AccountMeta(is_signer=True, is_writable=False, pubkey=self.wallet.address),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.wallet_input_token_account.address),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.wallet_output_token_account.address),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.margin_account.address),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.input_token.vault),
AccountMeta(is_signer=False, is_writable=True, pubkey=self.output_token.vault),
AccountMeta(is_signer=False, is_writable=False, pubkey=self.group.signer_key),
AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID),
AccountMeta(is_signer=False, is_writable=False, pubkey=SYSVAR_CLOCK_PUBKEY),
*list([AccountMeta(is_signer=False, is_writable=oo_address is None, pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS)
for oo_address in self.margin_account.open_orders]),
*list([AccountMeta(is_signer=False, is_writable=False, pubkey=oracle_address) for oracle_address in self.oracles])
],
program_id=self.context.program_id,
data=layouts.PARTIAL_LIQUIDATE.build({"max_deposit": int(self.maximum_input_amount)})
)
self.logger.debug(f"Built transaction: {transaction}")
return transaction
@classmethod
def from_margin_account_and_market(cls, context: Context, group: Group, wallet: Wallet, margin_account: MarginAccount, prices: typing.List[TokenValue]) -> typing.Optional["LiquidateInstructionBuilder"]:
logger: logging.Logger = logging.getLogger(cls.__name__)
oracles = list([mkt.oracle for mkt in group.markets])
balance_sheets = margin_account.get_priced_balance_sheets(group, prices)
sorted_by_assets = sorted(balance_sheets, key=lambda sheet: sheet.assets, reverse=True)
sorted_by_liabilities = sorted(balance_sheets, key=lambda sheet: sheet.liabilities, reverse=True)
most_assets = sorted_by_assets[0]
most_liabilities = sorted_by_liabilities[0]
if most_assets.token == most_liabilities.token:
# If there's a weirdness where the account with the biggest assets is also the one
# with the biggest liabilities, pick the next-best one by assets.
logger.info(
f"Switching asset token from {most_assets.token.name} to {sorted_by_assets[1].token.name} because {most_liabilities.token.name} is the token with most liabilities.")
most_assets = sorted_by_assets[1]
logger.info(f"Most assets: {most_assets}")
logger.info(f"Most liabilities: {most_liabilities}")
most_assets_basket_token = BasketToken.find_by_token(group.basket_tokens, most_assets.token)
most_liabilities_basket_token = BasketToken.find_by_token(group.basket_tokens, most_liabilities.token)
logger.info(f"Most assets basket token: {most_assets_basket_token}")
logger.info(f"Most liabilities basket token: {most_liabilities_basket_token}")
if most_assets.value == Decimal(0):
logger.warning(f"Margin account {margin_account.address} has no assets to take.")
return None
if most_liabilities.value == Decimal(0):
logger.warning(f"Margin account {margin_account.address} has no liabilities to fund.")
return None
wallet_input_token_account = TokenAccount.fetch_largest_for_owner_and_token(
context, wallet.address, most_liabilities.token)
if wallet_input_token_account is None:
raise Exception(f"Could not load wallet input token account for mint '{most_liabilities.token.mint}'")
if wallet_input_token_account.value.value == Decimal(0):
logger.warning(
f"Wallet token account {wallet_input_token_account.address} has no tokens to send that could fund a liquidation.")
return None
wallet_output_token_account = TokenAccount.fetch_largest_for_owner_and_token(
context, wallet.address, most_assets.token)
if wallet_output_token_account is None:
raise Exception(f"Could not load wallet output token account for mint '{most_assets.token.mint}'")
# Convert the token amount to the native representation
maximum_input_value = wallet_input_token_account.value
maximum_input_amount = maximum_input_value.token.shift_to_native(maximum_input_value.value)
return LiquidateInstructionBuilder(context, group, wallet, margin_account, oracles,
most_liabilities_basket_token, most_assets_basket_token,
wallet_input_token_account,
wallet_output_token_account,
maximum_input_amount)
def __str__(self) -> str:
# Print the members out using the Rust parameter order and names.
return f"""« LiquidateInstructionBuilder:
program_id: &Pubkey: {self.context.program_id},
mango_group_pk: &Pubkey: {self.group.address},
liqor_pk: &Pubkey: {self.wallet.address},
liqor_in_token_pk: &Pubkey: {self.wallet_input_token_account.address},
liqor_out_token_pk: &Pubkey: {self.wallet_output_token_account.address},
liqee_margin_account_acc: &Pubkey: {self.margin_account.address},
in_vault_pk: &Pubkey: {self.input_token.vault},
out_vault_pk: &Pubkey: {self.output_token.vault},
signer_pk: &Pubkey: {self.group.signer_key},
open_orders_pks: &[Pubkey]: {self.margin_account.open_orders},
oracle_pks: &[Pubkey]: {self.oracles},
max_deposit: u64: : {self.maximum_input_amount}
»"""
# # 🥭 CreateSplAccountInstructionBuilder class # # 🥭 CreateSplAccountInstructionBuilder class
# #
# Creates an SPL token account. Can't do much with it without following by an # Creates an SPL token account. Can't do much with it without following by an
@ -616,11 +219,11 @@ class NewOrderV3InstructionBuilder(InstructionBuilder):
# Creates an event-consuming 'crank' instruction. # Creates an event-consuming 'crank' instruction.
# #
class ConsumeEventsInstructionBuilder(InstructionBuilder): class ConsumeEventsInstructionBuilder(InstructionBuilder):
def __init__(self, context: Context, wallet: Wallet, market: Market, open_orders_addresses: typing.List[PublicKey], limit: int = 32): def __init__(self, context: Context, wallet: Wallet, market: Market, open_orders_addresses: typing.Sequence[PublicKey], limit: int = 32):
super().__init__(context) super().__init__(context)
self.wallet: Wallet = wallet self.wallet: Wallet = wallet
self.market: Market = market self.market: Market = market
self.open_orders_addresses: typing.List[PublicKey] = open_orders_addresses self.open_orders_addresses: typing.Sequence[PublicKey] = open_orders_addresses
self.limit: int = limit self.limit: int = limit
def build(self) -> TransactionInstruction: def build(self) -> TransactionInstruction:

View File

@ -43,4 +43,4 @@ class InstructionType(enum.IntEnum):
PartialLiquidate = 16 PartialLiquidate = 16
def __str__(self): def __str__(self):
return self.name return self.value

View File

@ -32,7 +32,6 @@
import construct import construct
import datetime import datetime
import itertools
import typing import typing
from decimal import Decimal from decimal import Decimal
@ -249,9 +248,7 @@ class OrderBookNodeAdapter(construct.Adapter):
# # Layout Structs # # Layout Structs
# ## SERUM_ACCOUNT_FLAGS # ## ACCOUNT_FLAGS
#
# The SERUM_ prefix is because there's also `MANGO_ACCOUNT_FLAGS`.
# #
# Here's the [Serum Rust structure](https://github.com/project-serum/serum-dex/blob/master/dex/src/state.rs): # Here's the [Serum Rust structure](https://github.com/project-serum/serum-dex/blob/master/dex/src/state.rs):
# ``` # ```
@ -270,7 +267,7 @@ class OrderBookNodeAdapter(construct.Adapter):
# ``` # ```
SERUM_ACCOUNT_FLAGS = construct.BitsSwapped( ACCOUNT_FLAGS = construct.BitsSwapped(
construct.BitStruct( construct.BitStruct(
"initialized" / construct.Flag, "initialized" / construct.Flag,
"market" / construct.Flag, "market" / construct.Flag,
@ -285,316 +282,6 @@ SERUM_ACCOUNT_FLAGS = construct.BitsSwapped(
) )
# ## MANGO_ACCOUNT_FLAGS
#
# The MANGO_ prefix is because there's also `SERUM_ACCOUNT_FLAGS`.
#
# The MANGO_ACCOUNT_FLAGS should be exactly 8 bytes.
#
# Here's the [Mango Rust structure](https://github.com/blockworks-foundation/mango/blob/master/program/src/state.rs):
# ```
# #[derive(Copy, Clone, BitFlags, Debug, Eq, PartialEq)]
# #[repr(u64)]
# pub enum AccountFlag {
# Initialized = 1u64 << 0,
# MangoGroup = 1u64 << 1,
# MarginAccount = 1u64 << 2,
# MangoSrmAccount = 1u64 << 3
# }
# ```
MANGO_ACCOUNT_FLAGS = construct.BitsSwapped(
construct.BitStruct(
"initialized" / construct.Flag,
"group" / construct.Flag,
"margin_account" / construct.Flag,
"srm_account" / construct.Flag,
construct.Padding(4 + (7 * 8))
)
)
# ## INDEX
#
# Here's the [Mango Rust structure](https://github.com/blockworks-foundation/mango/blob/master/program/src/state.rs):
# ```
# #[derive(Copy, Clone)]
# #[repr(C)]
# pub struct MangoIndex {
# pub last_update: u64,
# pub borrow: U64F64,
# pub deposit: U64F64
# }
# ```
INDEX = construct.Struct(
"last_update" / DatetimeAdapter(),
"borrow" / FloatAdapter(),
"deposit" / FloatAdapter()
)
# ## AGGREGATOR_CONFIG
#
# Here's the [Flux Rust structure](https://github.com/blockworks-foundation/solana-flux-aggregator/blob/master/program/src/state.rs):
# ```
# #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Default, PartialEq)]
# pub struct AggregatorConfig {
# /// description
# pub description: [u8; 32],
#
# /// decimals for this feed
# pub decimals: u8,
#
# /// oracle cannot start a new round until after `restart_relay` rounds
# pub restart_delay: u8,
#
# /// max number of submissions in a round
# pub max_submissions: u8,
#
# /// min number of submissions in a round to resolve an answer
# pub min_submissions: u8,
#
# /// amount of tokens oracles are reward per submission
# pub reward_amount: u64,
#
# /// SPL token account from which to withdraw rewards
# pub reward_token_account: PublicKey,
# }
# ```
AGGREGATOR_CONFIG = construct.Struct(
"description" / construct.PaddedString(32, "utf8"),
"decimals" / DecimalAdapter(1),
"restart_delay" / DecimalAdapter(1),
"max_submissions" / DecimalAdapter(1),
"min_submissions" / DecimalAdapter(1),
"reward_amount" / DecimalAdapter(),
"reward_token_account" / PublicKeyAdapter()
)
# ## ROUND
#
# Here's the [Flux Rust structure](https://github.com/blockworks-foundation/solana-flux-aggregator/blob/master/program/src/state.rs):
# ```
# #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Default, PartialEq)]
# pub struct Round {
# pub id: u64,
# pub created_at: u64,
# pub updated_at: u64,
# }
# ```
ROUND = construct.Struct(
"id" / DecimalAdapter(),
"created_at" / DecimalAdapter(),
"updated_at" / DecimalAdapter()
)
# ## ANSWER
#
# Here's the [Flux Rust structure](https://github.com/blockworks-foundation/solana-flux-aggregator/blob/master/program/src/state.rs):
# ```
# #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Default, PartialEq)]
# pub struct Answer {
# pub round_id: u64,
# pub median: u64,
# pub created_at: u64,
# pub updated_at: u64,
# }
# ```
ANSWER = construct.Struct(
"round_id" / DecimalAdapter(),
"median" / DecimalAdapter(),
"created_at" / DatetimeAdapter(),
"updated_at" / DatetimeAdapter()
)
# ## AGGREGATOR
#
# Here's the [Flux Rust structure](https://github.com/blockworks-foundation/solana-flux-aggregator/blob/master/program/src/state.rs):
# ```
# #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Default, PartialEq)]
# pub struct Aggregator {
# pub config: AggregatorConfig,
# /// is initialized
# pub is_initialized: bool,
# /// authority
# pub owner: PublicKey,
# /// current round accepting oracle submissions
# pub round: Round,
# pub round_submissions: PublicKey, // has_one: Submissions
# /// the latest answer resolved
# pub answer: Answer,
# pub answer_submissions: PublicKey, // has_one: Submissions
# }
# ```
AGGREGATOR = construct.Struct(
"config" / AGGREGATOR_CONFIG,
"initialized" / DecimalAdapter(1),
"owner" / PublicKeyAdapter(),
"round" / ROUND,
"round_submissions" / PublicKeyAdapter(),
"answer" / ANSWER,
"answer_submissions" / PublicKeyAdapter()
)
# ## GROUP_V1
#
# Groups have a common quote currency, and it's always the last token in the tokens.
#
# That means the number of markets is number_of_tokens - 1.
#
# Here's the [Mango Rust structure](https://github.com/blockworks-foundation/mango/blob/master/program/src/state.rs):
# ```
# #[derive(Copy, Clone)]
# #[repr(C)]
# pub struct MangoGroup {
# pub account_flags: u64,
# pub tokens: [Pubkey; NUM_TOKENS], // Last token is shared quote currency
# pub vaults: [Pubkey; NUM_TOKENS], // where funds are stored
# pub indexes: [MangoIndex; NUM_TOKENS], // to keep track of interest
# pub spot_markets: [Pubkey; NUM_MARKETS], // pubkeys to MarketState of serum dex
# pub oracles: [Pubkey; NUM_MARKETS], // oracles that give price of each base currency in quote currency
# pub signer_nonce: u64,
# pub signer_key: Pubkey,
# pub dex_program_id: Pubkey, // serum dex program id
#
# // denominated in Mango index adjusted terms
# pub total_deposits: [U64F64; NUM_TOKENS],
# pub total_borrows: [U64F64; NUM_TOKENS],
#
# pub maint_coll_ratio: U64F64, // 1.10
# pub init_coll_ratio: U64F64, // 1.20
#
# pub srm_vault: Pubkey, // holds users SRM for fee reduction
#
# /// This admin key is only for alpha release and the only power it has is to amend borrow limits
# /// If users borrow too much too quickly before liquidators are able to handle the volume,
# /// lender funds will be at risk. Hence these borrow limits will be raised slowly
# pub admin: Pubkey,
# pub borrow_limits: [u64; NUM_TOKENS],
#
# pub mint_decimals: [u8; NUM_TOKENS],
# pub oracle_decimals: [u8; NUM_MARKETS],
# pub padding: [u8; MANGO_GROUP_PADDING]
# }
# impl_loadable!(MangoGroup);
# ```
GROUP_V1_NUM_TOKENS = 3
GROUP_V1_NUM_MARKETS = GROUP_V1_NUM_TOKENS - 1
GROUP_V1_PADDING = 8 - (GROUP_V1_NUM_TOKENS + GROUP_V1_NUM_MARKETS) % 8
GROUP_V1 = construct.Struct(
"account_flags" / MANGO_ACCOUNT_FLAGS,
"tokens" / construct.Array(GROUP_V1_NUM_TOKENS, PublicKeyAdapter()),
"vaults" / construct.Array(GROUP_V1_NUM_TOKENS, PublicKeyAdapter()),
"indexes" / construct.Array(GROUP_V1_NUM_TOKENS, INDEX),
"spot_markets" / construct.Array(GROUP_V1_NUM_MARKETS, PublicKeyAdapter()),
"oracles" / construct.Array(GROUP_V1_NUM_MARKETS, PublicKeyAdapter()),
"signer_nonce" / DecimalAdapter(),
"signer_key" / PublicKeyAdapter(),
"dex_program_id" / PublicKeyAdapter(),
"total_deposits" / construct.Array(GROUP_V1_NUM_TOKENS, FloatAdapter()),
"total_borrows" / construct.Array(GROUP_V1_NUM_TOKENS, FloatAdapter()),
"maint_coll_ratio" / FloatAdapter(),
"init_coll_ratio" / FloatAdapter(),
"srm_vault" / PublicKeyAdapter(),
"admin" / PublicKeyAdapter(),
"borrow_limits" / construct.Array(GROUP_V1_NUM_TOKENS, DecimalAdapter()),
"mint_decimals" / construct.Array(GROUP_V1_NUM_TOKENS, DecimalAdapter(1)),
"oracle_decimals" / construct.Array(GROUP_V1_NUM_MARKETS, DecimalAdapter(1)),
"padding" / construct.Array(GROUP_V1_PADDING, construct.Padding(1))
)
# ## GROUP_V2
#
# Groups have a common quote currency, and it's always the last token in the tokens.
#
# That means the number of markets is number_of_tokens - 1.
#
# There is no difference between the V1 and V2 structures except for the number of tokens.
# We handle things this way to be consistent with how we handle V1 and V2 `MarginAccount`s.
#
# Here's the [Mango Rust structure](https://github.com/blockworks-foundation/mango/blob/master/program/src/state.rs):
# ```
# #[derive(Copy, Clone)]
# #[repr(C)]
# pub struct MangoGroup {
# pub account_flags: u64,
# pub tokens: [Pubkey; NUM_TOKENS], // Last token is shared quote currency
# pub vaults: [Pubkey; NUM_TOKENS], // where funds are stored
# pub indexes: [MangoIndex; NUM_TOKENS], // to keep track of interest
# pub spot_markets: [Pubkey; NUM_MARKETS], // pubkeys to MarketState of serum dex
# pub oracles: [Pubkey; NUM_MARKETS], // oracles that give price of each base currency in quote currency
# pub signer_nonce: u64,
# pub signer_key: Pubkey,
# pub dex_program_id: Pubkey, // serum dex program id
#
# // denominated in Mango index adjusted terms
# pub total_deposits: [U64F64; NUM_TOKENS],
# pub total_borrows: [U64F64; NUM_TOKENS],
#
# pub maint_coll_ratio: U64F64, // 1.10
# pub init_coll_ratio: U64F64, // 1.20
#
# pub srm_vault: Pubkey, // holds users SRM for fee reduction
#
# /// This admin key is only for alpha release and the only power it has is to amend borrow limits
# /// If users borrow too much too quickly before liquidators are able to handle the volume,
# /// lender funds will be at risk. Hence these borrow limits will be raised slowly
# /// UPDATE: 4/15/2021 - this admin key is now useless, borrow limits are removed
# pub admin: Pubkey,
# pub borrow_limits: [u64; NUM_TOKENS],
#
# pub mint_decimals: [u8; NUM_TOKENS],
# pub oracle_decimals: [u8; NUM_MARKETS],
# pub padding: [u8; MANGO_GROUP_PADDING]
# }
#
# ```
GROUP_V2_NUM_TOKENS = 5
GROUP_V2_NUM_MARKETS = GROUP_V2_NUM_TOKENS - 1
GROUP_V2_PADDING = 8 - (GROUP_V2_NUM_TOKENS + GROUP_V2_NUM_MARKETS) % 8
GROUP_V2 = construct.Struct(
"account_flags" / MANGO_ACCOUNT_FLAGS,
"tokens" / construct.Array(GROUP_V2_NUM_TOKENS, PublicKeyAdapter()),
"vaults" / construct.Array(GROUP_V2_NUM_TOKENS, PublicKeyAdapter()),
"indexes" / construct.Array(GROUP_V2_NUM_TOKENS, INDEX),
"spot_markets" / construct.Array(GROUP_V2_NUM_MARKETS, PublicKeyAdapter()),
"oracles" / construct.Array(GROUP_V2_NUM_MARKETS, PublicKeyAdapter()),
"signer_nonce" / DecimalAdapter(),
"signer_key" / PublicKeyAdapter(),
"dex_program_id" / PublicKeyAdapter(),
"total_deposits" / construct.Array(GROUP_V2_NUM_TOKENS, FloatAdapter()),
"total_borrows" / construct.Array(GROUP_V2_NUM_TOKENS, FloatAdapter()),
"maint_coll_ratio" / FloatAdapter(),
"init_coll_ratio" / FloatAdapter(),
"srm_vault" / PublicKeyAdapter(),
"admin" / PublicKeyAdapter(),
"borrow_limits" / construct.Array(GROUP_V2_NUM_TOKENS, DecimalAdapter()),
"mint_decimals" / construct.Array(GROUP_V2_NUM_TOKENS, DecimalAdapter(1)),
"oracle_decimals" / construct.Array(GROUP_V2_NUM_MARKETS, DecimalAdapter(1)),
"padding" / construct.Array(GROUP_V2_PADDING, construct.Padding(1))
)
# ## TOKEN_ACCOUNT # ## TOKEN_ACCOUNT
@ -615,7 +302,7 @@ TOKEN_ACCOUNT = construct.Struct(
OPEN_ORDERS = construct.Struct( OPEN_ORDERS = construct.Struct(
construct.Padding(5), construct.Padding(5),
"account_flags" / SERUM_ACCOUNT_FLAGS, "account_flags" / ACCOUNT_FLAGS,
"market" / PublicKeyAdapter(), "market" / PublicKeyAdapter(),
"owner" / PublicKeyAdapter(), "owner" / PublicKeyAdapter(),
"base_token_free" / DecimalAdapter(), "base_token_free" / DecimalAdapter(),
@ -631,496 +318,6 @@ OPEN_ORDERS = construct.Struct(
) )
# ## MARGIN_ACCOUNT_V1
#
# Here's the V1 [Mango Rust structure](https://github.com/blockworks-foundation/mango/blob/master/program/src/state.rs):
# ```
# #[derive(Copy, Clone)]
# #[repr(C)]
# pub struct MarginAccount {
# pub account_flags: u64,
# pub mango_group: Pubkey,
# pub owner: Pubkey, // solana pubkey of owner
#
# // assets and borrows are denominated in Mango adjusted terms
# pub deposits: [U64F64; NUM_TOKENS], // assets being lent out and gaining interest, including collateral
#
# // this will be incremented every time an order is opened and decremented when order is closed
# pub borrows: [U64F64; NUM_TOKENS], // multiply by current index to get actual value
#
# pub open_orders: [Pubkey; NUM_MARKETS], // owned by Mango
#
# pub being_liquidated: bool,
# pub padding: [u8; 7] // padding to make compatible with previous MarginAccount size
# // TODO add has_borrows field for easy memcmp fetching
# }
# ```
MARGIN_ACCOUNT_V1_NUM_TOKENS = 3
MARGIN_ACCOUNT_V1_NUM_MARKETS = MARGIN_ACCOUNT_V1_NUM_TOKENS - 1
MARGIN_ACCOUNT_V1 = construct.Struct(
"account_flags" / MANGO_ACCOUNT_FLAGS,
"mango_group" / PublicKeyAdapter(),
"owner" / PublicKeyAdapter(),
"deposits" / construct.Array(MARGIN_ACCOUNT_V1_NUM_TOKENS, FloatAdapter()),
"borrows" / construct.Array(MARGIN_ACCOUNT_V1_NUM_TOKENS, FloatAdapter()),
"open_orders" / construct.Array(MARGIN_ACCOUNT_V1_NUM_MARKETS, PublicKeyAdapter()),
"being_liquidated" / DecimalAdapter(1),
"padding" / construct.Padding(7)
)
# ## MARGIN_ACCOUNT_V2
#
# Here's the V2 [Mango Rust structure](https://github.com/blockworks-foundation/mango/blob/master/program/src/state.rs):
# ```
# #[derive(Copy, Clone)]
# #[repr(C)]
# pub struct MarginAccount {
# pub account_flags: u64,
# pub mango_group: Pubkey,
# pub owner: Pubkey, // solana pubkey of owner
#
# // assets and borrows are denominated in Mango adjusted terms
# pub deposits: [U64F64; NUM_TOKENS], // assets being lent out and gaining interest, including collateral
#
# // this will be incremented every time an order is opened and decremented when order is closed
# pub borrows: [U64F64; NUM_TOKENS], // multiply by current index to get actual value
#
# pub open_orders: [Pubkey; NUM_MARKETS], // owned by Mango
#
# pub being_liquidated: bool,
# pub has_borrows: bool, // does the account have any open borrows? set by checked_add_borrow and checked_sub_borrow
# pub padding: [u8; 64] // padding
# }
# ```
MARGIN_ACCOUNT_V2_NUM_TOKENS = 5
MARGIN_ACCOUNT_V2_NUM_MARKETS = MARGIN_ACCOUNT_V2_NUM_TOKENS - 1
MARGIN_ACCOUNT_V2 = construct.Struct(
"account_flags" / MANGO_ACCOUNT_FLAGS,
"mango_group" / PublicKeyAdapter(),
"owner" / PublicKeyAdapter(),
"deposits" / construct.Array(MARGIN_ACCOUNT_V2_NUM_TOKENS, FloatAdapter()),
"borrows" / construct.Array(MARGIN_ACCOUNT_V2_NUM_TOKENS, FloatAdapter()),
"open_orders" / construct.Array(MARGIN_ACCOUNT_V2_NUM_MARKETS, PublicKeyAdapter()),
"being_liquidated" / DecimalAdapter(1),
"has_borrows" / DecimalAdapter(1),
"padding" / construct.Padding(70)
)
# ## build_margin_account_parser_for_num_tokens() function
#
# This function builds a `construct.Struct` that can load a `MarginAccount` with a
# specific number of tokens. The number of markets and size of padding are derived
# from the number of tokens.
def build_margin_account_parser_for_num_tokens(num_tokens: int) -> construct.Struct:
num_markets = num_tokens - 1
return construct.Struct(
"account_flags" / MANGO_ACCOUNT_FLAGS,
"mango_group" / PublicKeyAdapter(),
"owner" / PublicKeyAdapter(),
"deposits" / construct.Array(num_tokens, FloatAdapter()),
"borrows" / construct.Array(num_tokens, FloatAdapter()),
"open_orders" / construct.Array(num_markets, PublicKeyAdapter()),
"padding" / construct.Padding(8)
)
# ## build_margin_account_parser_for_length() function
#
# This function takes a data length (presumably the size of the structure returned from
# the `AccountInfo`) and returns a `MarginAccount` structure that can parse it.
#
# If the size doesn't _exactly_ match the size of the `Struct`, and Exception is raised.
def build_margin_account_parser_for_length(length: int) -> construct.Struct:
tried_sizes: typing.List[int] = []
for num_tokens in itertools.count(start=2):
parser = build_margin_account_parser_for_num_tokens(num_tokens)
if parser.sizeof() == length:
return parser
tried_sizes += [parser.sizeof()]
if parser.sizeof() > length:
raise Exception(
f"Could not create MarginAccount parser for length ({length}) - tried sizes ({tried_sizes})")
# # Instruction Structs
# ## MANGO_INSTRUCTION_VARIANT_FINDER
#
# The 'variant' of the instruction is held in the first 4 bytes. The remainder of the data
# is per-instruction.
#
# This `struct` loads only the first 4 bytes, as an `int`, so we know which specific parser
# has to be used to load the rest of the data.
MANGO_INSTRUCTION_VARIANT_FINDER = construct.Struct(
"variant" / construct.BytesInteger(4, swapped=True)
)
# ## Variant 0: INIT_MANGO_GROUP
#
# Instruction variant 1. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
#
# > Initialize a group of lending pools that can be cross margined
# ```
# InitMangoGroup {
# signer_nonce: u64,
# maint_coll_ratio: U64F64,
# init_coll_ratio: U64F64,
# borrow_limits: [u64; NUM_TOKENS]
# },
# ```
INIT_MANGO_GROUP = construct.Struct(
"variant" / construct.Const(0x0, construct.BytesInteger(4, swapped=True)),
"signer_nonce" / DecimalAdapter(),
"maint_coll_ratio" / FloatAdapter(),
"init_coll_ratio" / FloatAdapter(),
# "borrow_limits" / construct.Array(NUM_TOKENS, DecimalAdapter()) # This is inconsistently available
)
# ## Variant 1: INIT_MARGIN_ACCOUNT
#
# Instruction variant 1. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
#
# > Initialize a margin account for a user
# ```
# InitMarginAccount,
# ```
INIT_MARGIN_ACCOUNT = construct.Struct(
"variant" / construct.Const(0x1, construct.BytesInteger(4, swapped=True)),
)
# ## Variant 2: DEPOSIT
#
# Instruction variant 2. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
#
# > Deposit funds into margin account to be used as collateral and earn interest.
# ```
# Deposit {
# quantity: u64
# },
# ```
DEPOSIT = construct.Struct(
"variant" / construct.Const(0x2, construct.BytesInteger(4, swapped=True)),
"quantity" / DecimalAdapter()
)
# ## Variant 3: WITHDRAW
#
# Instruction variant 3. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
#
# > Withdraw funds that were deposited earlier.
# ```
# Withdraw {
# quantity: u64
# },
# ```
WITHDRAW = construct.Struct(
"variant" / construct.Const(0x3, construct.BytesInteger(4, swapped=True)),
"quantity" / DecimalAdapter()
)
# ## Variant 4: BORROW
#
# Instruction variant 4. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
#
# > Borrow by incrementing MarginAccount.borrows given collateral ratio is below init_coll_rat
# ```
# Borrow {
# token_index: usize,
# quantity: u64
# },
# ```
BORROW = construct.Struct(
"variant" / construct.Const(0x4, construct.BytesInteger(4, swapped=True)),
"token_index" / DecimalAdapter(),
"quantity" / DecimalAdapter()
)
# ## Variant 5: SETTLE_BORROW
#
# Instruction variant 5. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
#
# > Use this token's position and deposit to reduce borrows
# ```
# SettleBorrow {
# token_index: usize,
# quantity: u64
# },
# ```
SETTLE_BORROW = construct.Struct(
"variant" / construct.Const(0x5, construct.BytesInteger(4, swapped=True)),
"token_index" / DecimalAdapter(),
"quantity" / DecimalAdapter()
)
# ## Variant 6: LIQUIDATE
#
# Instruction variant 6. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
#
# > Take over a MarginAccount that is below init_coll_ratio by depositing funds
# ```
# Liquidate {
# /// Quantity of each token liquidator is depositing in order to bring account above maint
# deposit_quantities: [u64; NUM_TOKENS]
# },
# ```
_LIQUIDATE_NUM_TOKENS = 3 # Liquidate is deprecated and was only used with 3 tokens.
LIQUIDATE = construct.Struct(
"variant" / construct.Const(0x6, construct.BytesInteger(4, swapped=True)),
"deposit_quantities" / construct.Array(_LIQUIDATE_NUM_TOKENS, DecimalAdapter())
)
# ## Variant 7: DEPOSIT_SRM
#
# Instruction variant 7. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
#
# > Deposit SRM into the SRM vault for MangoGroup
# >
# > These SRM are not at risk and are not counted towards collateral or any margin calculations
# >
# > Depositing SRM is a strictly altruistic act with no upside and no downside
# ```
# DepositSrm {
# quantity: u64
# },
# ```
DEPOSIT_SRM = construct.Struct(
"variant" / construct.Const(0x7, construct.BytesInteger(4, swapped=True)),
"quantity" / DecimalAdapter()
)
# ## Variant 8: WITHDRAW_SRM
#
# Instruction variant 8. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
#
# > Withdraw SRM owed to this MarginAccount
# ```
# WithdrawSrm {
# quantity: u64
# },
# ```
WITHDRAW_SRM = construct.Struct(
"variant" / construct.Const(0x8, construct.BytesInteger(4, swapped=True)),
"quantity" / DecimalAdapter()
)
# ## Variant 9: PLACE_ORDER
#
# Instruction variant 9. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
#
# > Place an order on the Serum Dex using Mango margin facilities
# ```
# PlaceOrder {
# order: serum_dex::instruction::NewOrderInstructionV3
# },
# ```
PLACE_ORDER = construct.Struct(
"variant" / construct.Const(0x9, construct.BytesInteger(4, swapped=True)),
"order" / construct.Padding(1) # Actual type is: serum_dex::instruction::NewOrderInstructionV3
)
# ## Variant 10: SETTLE_FUNDS
#
# Instruction variant 10. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
#
# > Settle all funds from serum dex open orders into MarginAccount positions
# ```
# SettleFunds,
# ```
SETTLE_FUNDS = construct.Struct(
"variant" / construct.Const(0xa, construct.BytesInteger(4, swapped=True)),
)
# ## Variant 11: CANCEL_ORDER
#
# Instruction variant 11. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
#
# > Cancel an order using dex instruction
# ```
# CancelOrder {
# order: serum_dex::instruction::CancelOrderInstructionV2
# },
# ```
CANCEL_ORDER = construct.Struct(
"variant" / construct.Const(0xb, construct.BytesInteger(4, swapped=True)),
"order" / construct.Padding(1) # Actual type is: serum_dex::instruction::CancelOrderInstructionV2
)
# ## Variant 12: CANCEL_ORDER_BY_CLIENT_ID
#
# Instruction variant 12. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
#
# > Cancel an order using client_id
# ```
# CancelOrderByClientId {
# client_id: u64
# },
# ```
CANCEL_ORDER_BY_CLIENT_ID = construct.Struct(
"variant" / construct.Const(0xc, construct.BytesInteger(4, swapped=True)),
"client_id" / DecimalAdapter()
)
# ## Variant 13: CHANGE_BORROW_LIMIT
#
# Instruction variant 13. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs).
#
# > Change the borrow limit using admin key. This will not affect any open positions on any MarginAccount
# >
# > This is intended to be an instruction only in alpha stage while liquidity is slowly improved"_
# ```
# ChangeBorrowLimit {
# token_index: usize,
# borrow_limit: u64
# },
# ```
CHANGE_BORROW_LIMIT = construct.Struct(
"variant" / construct.Const(0xd, construct.BytesInteger(4, swapped=True)),
"token_index" / DecimalAdapter(),
"borrow_limit" / DecimalAdapter()
)
# ## Variant 14: PLACE_AND_SETTLE
#
# Instruction variant 14. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs).
#
# > Place an order on the Serum Dex and settle funds from the open orders account
# ```
# PlaceAndSettle {
# order: serum_dex::instruction::NewOrderInstructionV3
# },
# ```
PLACE_AND_SETTLE = construct.Struct(
"variant" / construct.Const(0xe, construct.BytesInteger(4, swapped=True)),
"order" / construct.Padding(1) # Actual type is: serum_dex::instruction::NewOrderInstructionV3
)
# ## Variant 15: FORCE_CANCEL_ORDERS
#
# Instruction variant 15. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
#
# > Allow a liquidator to cancel open orders and settle to recoup funds for partial liquidation
#
# ```
# ForceCancelOrders {
# /// Max orders to cancel -- could be useful to lower this if running into compute limits
# /// Recommended: 5
# limit: u8
# },
# ```
FORCE_CANCEL_ORDERS = construct.Struct(
"variant" / construct.Const(0xf, construct.BytesInteger(4, swapped=True)),
"limit" / DecimalAdapter(1)
)
# ## Variant 16: PARTIAL_LIQUIDATE
#
# Instruction variant 16. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
#
# > Take over a MarginAccount that is below init_coll_ratio by depositing funds
#
# ```
# PartialLiquidate {
# /// Quantity of the token being deposited to repay borrows
# max_deposit: u64
# },
# ```
PARTIAL_LIQUIDATE = construct.Struct(
"variant" / construct.Const(0x10, construct.BytesInteger(4, swapped=True)),
"max_deposit" / DecimalAdapter()
)
# ## InstructionParsersByVariant dictionary
#
# This dictionary provides an easy way for us to access the specific parser for a given variant.
InstructionParsersByVariant = {
0: INIT_MANGO_GROUP,
1: INIT_MARGIN_ACCOUNT,
2: DEPOSIT,
3: WITHDRAW,
4: BORROW,
5: SETTLE_BORROW,
6: LIQUIDATE,
7: DEPOSIT_SRM,
8: WITHDRAW_SRM,
9: PLACE_ORDER,
10: SETTLE_FUNDS,
11: CANCEL_ORDER,
12: CANCEL_ORDER_BY_CLIENT_ID,
13: CHANGE_BORROW_LIMIT,
14: PLACE_AND_SETTLE,
15: FORCE_CANCEL_ORDERS,
16: PARTIAL_LIQUIDATE
}
MAX_TOKENS: int = 32 MAX_TOKENS: int = 32
MAX_PAIRS: int = MAX_TOKENS - 1 MAX_PAIRS: int = MAX_TOKENS - 1
MAX_NODE_BANKS: int = 8 MAX_NODE_BANKS: int = 8
@ -1128,7 +325,7 @@ QUOTE_INDEX: int = MAX_TOKENS - 1
OPEN_ORDERS_MAX_ORDERS: int = 32 OPEN_ORDERS_MAX_ORDERS: int = 32
MAX_BOOK_NODES: int = 1024 MAX_BOOK_NODES: int = 1024
DATA_TYPE = construct.Enum(construct.Int8ul, Group=0, MarginAccount=1, RootBank=2, DATA_TYPE = construct.Enum(construct.Int8ul, Group=0, Account=1, RootBank=2,
NodeBank=3, PerpMarket=4, Bids=5, Asks=6, Cache=7, EventQueue=8) NodeBank=3, PerpMarket=4, Bids=5, Asks=6, Cache=7, EventQueue=8)
METADATA = construct.Struct( METADATA = construct.Struct(
@ -1168,7 +365,7 @@ PERP_MARKET_INFO = construct.Struct(
) )
# usize is a u64 on Solana, so a regular DecimalAdapter() works # usize is a u64 on Solana, so a regular DecimalAdapter() works
MANGO_GROUP = construct.Struct( GROUP = construct.Struct(
"meta_data" / METADATA, "meta_data" / METADATA,
"num_oracles" / DecimalAdapter(), "num_oracles" / DecimalAdapter(),
"tokens" / construct.Array(MAX_TOKENS, TOKEN_INFO), "tokens" / construct.Array(MAX_TOKENS, TOKEN_INFO),
@ -1333,6 +530,20 @@ ORDERBOOK_SIDE = construct.Struct(
"nodes" / construct.Array(MAX_BOOK_NODES, OrderBookNodeAdapter()) "nodes" / construct.Array(MAX_BOOK_NODES, OrderBookNodeAdapter())
) )
# # Instruction Structs
# ## MANGO_INSTRUCTION_VARIANT_FINDER
#
# The 'variant' of the instruction is held in the first 4 bytes. The remainder of the data
# is per-instruction.
#
# This `struct` loads only the first 4 bytes, as an `int`, so we know which specific parser
# has to be used to load the rest of the data.
MANGO_INSTRUCTION_VARIANT_FINDER = construct.Struct(
"variant" / construct.BytesInteger(4, swapped=True)
)
# /// Place an order on a perp market # /// Place an order on a perp market
# /// Accounts expected by this instruction (6): # /// Accounts expected by this instruction (6):
@ -1397,7 +608,7 @@ WITHDRAW_V3 = construct.Struct(
) )
MerpsInstructionParsersByVariant = { InstructionParsersByVariant = {
0: None, # INIT_MANGO_GROUP, 0: None, # INIT_MANGO_GROUP,
1: INIT_MANGO_ACCOUNT, # INIT_MANGO_ACCOUNT, 1: INIT_MANGO_ACCOUNT, # INIT_MANGO_ACCOUNT,
2: None, # DEPOSIT, 2: None, # DEPOSIT,

View File

@ -20,9 +20,8 @@ import typing
from decimal import Decimal from decimal import Decimal
from .balancesheet import BalanceSheet from .account import Account
from .group import Group from .group import Group
from .marginaccount import MarginAccount
from .tokenvalue import TokenValue from .tokenvalue import TokenValue
@ -44,40 +43,14 @@ class LiquidatableState(enum.Flag):
# #
class LiquidatableReport: class LiquidatableReport:
def __init__(self, group: Group, prices: typing.List[TokenValue], margin_account: MarginAccount, balance_sheet: BalanceSheet, balances: typing.List[TokenValue], state: LiquidatableState, worthwhile_threshold: Decimal): def __init__(self, group: Group, prices: typing.Sequence[TokenValue], account: Account, state: LiquidatableState, worthwhile_threshold: Decimal):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.group: Group = group self.group: Group = group
self.prices: typing.List[TokenValue] = prices self.prices: typing.Sequence[TokenValue] = prices
self.margin_account: MarginAccount = margin_account self.account: Account = account
self.balance_sheet: BalanceSheet = balance_sheet
self.balances: typing.List[TokenValue] = balances
self.state: LiquidatableState = state self.state: LiquidatableState = state
self.worthwhile_threshold: Decimal = worthwhile_threshold self.worthwhile_threshold: Decimal = worthwhile_threshold
@staticmethod @staticmethod
def build(group: Group, prices: typing.List[TokenValue], margin_account: MarginAccount, worthwhile_threshold: Decimal) -> "LiquidatableReport": def build(group: Group, prices: typing.Sequence[TokenValue], account: Account, worthwhile_threshold: Decimal) -> "LiquidatableReport":
balance_sheet = margin_account.get_balance_sheet_totals(group, prices) return LiquidatableReport(group, prices, account, LiquidatableState.UNSET, worthwhile_threshold)
balances = margin_account.get_intrinsic_balances(group)
state = LiquidatableState.UNSET
if balance_sheet.collateral_ratio != Decimal(0):
if balance_sheet.collateral_ratio <= group.init_coll_ratio:
state |= LiquidatableState.RIPE
if balance_sheet.collateral_ratio <= group.maint_coll_ratio:
state |= LiquidatableState.LIQUIDATABLE
if balance_sheet.collateral_ratio > Decimal(1):
state |= LiquidatableState.ABOVE_WATER
if balance_sheet.assets - balance_sheet.liabilities > worthwhile_threshold:
state |= LiquidatableState.WORTHWHILE
# If a liquidation is ongoing, their account may be above the `maint_coll_ratio` but still
# be liquidatable until it reaches `init_coll_ratio`.
if margin_account.being_liquidated:
state |= LiquidatableState.BEING_LIQUIDATED
state |= LiquidatableState.LIQUIDATABLE
return LiquidatableReport(group, prices, margin_account, balance_sheet, balances, state, worthwhile_threshold)

View File

@ -25,7 +25,7 @@ from .tokenvalue import TokenValue
class LiquidationEvent: class LiquidationEvent:
def __init__(self, timestamp: datetime.datetime, liquidator_name: str, group_name: str, succeeded: bool, signature: str, wallet_address: PublicKey, margin_account_address: PublicKey, balances_before: typing.List[TokenValue], balances_after: typing.List[TokenValue]): def __init__(self, timestamp: datetime.datetime, liquidator_name: str, group_name: str, succeeded: bool, signature: str, wallet_address: PublicKey, margin_account_address: PublicKey, balances_before: typing.Sequence[TokenValue], balances_after: typing.Sequence[TokenValue]):
self.timestamp: datetime.datetime = timestamp self.timestamp: datetime.datetime = timestamp
self.liquidator_name: str = liquidator_name self.liquidator_name: str = liquidator_name
self.group_name: str = group_name self.group_name: str = group_name
@ -33,9 +33,9 @@ class LiquidationEvent:
self.signature: str = signature self.signature: str = signature
self.wallet_address: PublicKey = wallet_address self.wallet_address: PublicKey = wallet_address
self.margin_account_address: PublicKey = margin_account_address self.margin_account_address: PublicKey = margin_account_address
self.balances_before: typing.List[TokenValue] = balances_before self.balances_before: typing.Sequence[TokenValue] = balances_before
self.balances_after: typing.List[TokenValue] = balances_after self.balances_after: typing.Sequence[TokenValue] = balances_after
self.changes: typing.List[TokenValue] = TokenValue.changes(balances_before, balances_after) self.changes: typing.Sequence[TokenValue] = TokenValue.changes(balances_before, balances_after)
def __str__(self) -> str: def __str__(self) -> str:
result = "" if self.succeeded else "" result = "" if self.succeeded else ""

View File

@ -21,12 +21,12 @@ import typing
from datetime import datetime, timedelta from datetime import datetime, timedelta
from decimal import Decimal from decimal import Decimal
from .account import Account
from .accountliquidator import AccountLiquidator from .accountliquidator import AccountLiquidator
from .context import Context from .context import Context
from .group import Group from .group import Group
from .liquidatablereport import LiquidatableReport, LiquidatableState from .liquidatablereport import LiquidatableReport, LiquidatableState
from .liquidationevent import LiquidationEvent from .liquidationevent import LiquidationEvent
from .marginaccount import MarginAccount
from .observables import EventSource from .observables import EventSource
from .tokenvalue import TokenValue from .tokenvalue import TokenValue
from .walletbalancer import WalletBalancer from .walletbalancer import WalletBalancer
@ -54,8 +54,8 @@ class LiquidationProcessorState(enum.Enum):
# # 💧 LiquidationProcessor class # # 💧 LiquidationProcessor class
# #
# An `AccountLiquidator` liquidates a `MarginAccount`. A `LiquidationProcessor` processes a # An `AccountLiquidator` liquidates a `Account`. A `LiquidationProcessor` processes a
# list of `MarginAccount`s, determines if they're liquidatable, and calls an # list of `Account`s, determines if they're liquidatable, and calls an
# `AccountLiquidator` to do the work. # `AccountLiquidator` to do the work.
# #
@ -72,13 +72,13 @@ class LiquidationProcessor:
self.wallet_balancer: WalletBalancer = wallet_balancer self.wallet_balancer: WalletBalancer = wallet_balancer
self.worthwhile_threshold: Decimal = worthwhile_threshold self.worthwhile_threshold: Decimal = worthwhile_threshold
self.liquidations: EventSource[LiquidationEvent] = EventSource[LiquidationEvent]() self.liquidations: EventSource[LiquidationEvent] = EventSource[LiquidationEvent]()
self.ripe_accounts: typing.Optional[typing.List[MarginAccount]] = None self.ripe_accounts: typing.Optional[typing.Sequence[Account]] = None
self.ripe_accounts_updated_at: datetime = datetime.now() self.ripe_accounts_updated_at: datetime = datetime.now()
self.prices_updated_at: datetime = datetime.now() self.prices_updated_at: datetime = datetime.now()
self.state: LiquidationProcessorState = LiquidationProcessorState.STARTING self.state: LiquidationProcessorState = LiquidationProcessorState.STARTING
self.state_change: EventSource[LiquidationProcessor] = EventSource[LiquidationProcessor]() self.state_change: EventSource[LiquidationProcessor] = EventSource[LiquidationProcessor]()
def update_margin_accounts(self, ripe_margin_accounts: typing.List[MarginAccount]): def update_margin_accounts(self, ripe_margin_accounts: typing.Sequence[Account]):
self.logger.info( self.logger.info(
f"Received {len(ripe_margin_accounts)} ripe 🥭 margin accounts to process - prices last updated {self.prices_updated_at:%Y-%m-%d %H:%M:%S}") f"Received {len(ripe_margin_accounts)} ripe 🥭 margin accounts to process - prices last updated {self.prices_updated_at:%Y-%m-%d %H:%M:%S}")
self._check_update_recency("prices", self.prices_updated_at) self._check_update_recency("prices", self.prices_updated_at)
@ -127,17 +127,19 @@ class LiquidationProcessor:
time_taken = time.time() - started_at time_taken = time.time() - started_at
self.logger.info(f"Check of all ripe 🥭 accounts complete. Time taken: {time_taken:.2f} seconds.") self.logger.info(f"Check of all ripe 🥭 accounts complete. Time taken: {time_taken:.2f} seconds.")
def _liquidate_all(self, group: Group, prices: typing.List[TokenValue], to_liquidate: typing.List[LiquidatableReport]): def _liquidate_all(self, group: Group, prices: typing.Sequence[TokenValue], to_liquidate: typing.Sequence[LiquidatableReport]):
to_process = to_liquidate to_process = list(to_liquidate)
while len(to_process) > 0: while len(to_process) > 0:
highest_first = sorted(to_process, # TODO - sort this when LiquidationReport has the proper details for V3.
key=lambda report: report.balance_sheet.assets - report.balance_sheet.liabilities, reverse=True) # highest_first = sorted(to_process,
# key=lambda report: report.balance_sheet.assets - report.balance_sheet.liabilities, reverse=True)
highest_first = to_process
highest = highest_first[0] highest = highest_first[0]
try: try:
self.account_liquidator.liquidate(highest) self.account_liquidator.liquidate(highest)
self.wallet_balancer.balance(prices) self.wallet_balancer.balance(prices)
updated_margin_account = MarginAccount.load(self.context, highest.margin_account.address, group) updated_margin_account = Account.load(self.context, highest.account.address)
updated_report = LiquidatableReport.build( updated_report = LiquidatableReport.build(
group, prices, updated_margin_account, highest.worthwhile_threshold) group, prices, updated_margin_account, highest.worthwhile_threshold)
if not (updated_report.state & LiquidatableState.WORTHWHILE): if not (updated_report.state & LiquidatableState.WORTHWHILE):
@ -149,7 +151,7 @@ class LiquidationProcessor:
to_process += [updated_report] to_process += [updated_report]
except Exception as exception: except Exception as exception:
self.logger.error( self.logger.error(
f"Liquidator '{self.name}' - failed to liquidate account '{highest.margin_account.address}' - {exception}.") f"Liquidator '{self.name}' - failed to liquidate account '{highest.account.address}' - {exception}.")
finally: finally:
# highest should always be in to_process, but we're outside the try-except block # highest should always be in to_process, but we're outside the try-except block
# so let's be a little paranoid about it. # so let's be a little paranoid about it.

View File

@ -1,53 +0,0 @@
# # ⚠ 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 logging
import typing
from .version import Version
from .layouts import layouts
# # 🥭 MangoAccountFlags class
#
# The Mango prefix is because there's also `SerumAccountFlags` for the standard Serum flags.
#
class MangoAccountFlags:
def __init__(self, version: Version, initialized: bool, group: bool, margin_account: bool, srm_account: bool):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.version: Version = version
self.initialized = initialized
self.group = group
self.margin_account = margin_account
self.srm_account = srm_account
@staticmethod
def from_layout(layout: layouts.MANGO_ACCOUNT_FLAGS) -> "MangoAccountFlags":
return MangoAccountFlags(Version.UNSPECIFIED, layout.initialized, layout.group,
layout.margin_account, layout.srm_account)
def __str__(self) -> str:
flags: typing.List[typing.Optional[str]] = []
flags += ["initialized" if self.initialized else None]
flags += ["group" if self.group else None]
flags += ["margin_account" if self.margin_account else None]
flags += ["srm_account" if self.srm_account else None]
flag_text = " | ".join(flag for flag in flags if flag is not None) or "None"
return f"« MangoAccountFlags: {flag_text} »"
def __repr__(self) -> str:
return f"{self}"

View File

@ -1,145 +0,0 @@
# # ⚠ 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 decimal import Decimal
from solana.publickey import PublicKey
from .accountinfo import AccountInfo
from .addressableaccount import AddressableAccount
from .context import Context
from .layouts import layouts
from .marketlookup import MarketLookup
from .metadata import Metadata
from .perpmarketinfo import PerpMarketInfo
from .spotmarketinfo import SpotMarketInfo
from .token import SolToken
from .tokeninfo import TokenInfo
from .tokenlookup import TokenLookup
from .tokenvalue import TokenValue
from .version import Version
# # 🥭 MangoGroup class
#
# `MangoGroup` defines root functionality for Mango Markets.
#
class MangoGroup(AddressableAccount):
def __init__(self, account_info: AccountInfo, version: Version, name: str,
meta_data: Metadata, tokens: typing.Sequence[typing.Optional[TokenInfo]],
spot_markets: typing.Sequence[typing.Optional[SpotMarketInfo]],
perp_markets: typing.Sequence[typing.Optional[PerpMarketInfo]],
oracles: typing.Sequence[PublicKey], signer_nonce: Decimal, signer_key: PublicKey,
admin: PublicKey, dex_program_id: PublicKey, cache: PublicKey, valid_interval: Decimal):
super().__init__(account_info)
self.version: Version = version
self.name: str = name
self.meta_data: Metadata = meta_data
self.tokens: typing.Sequence[typing.Optional[TokenInfo]] = tokens
self.spot_markets: typing.Sequence[typing.Optional[SpotMarketInfo]] = spot_markets
self.perp_markets: typing.Sequence[typing.Optional[PerpMarketInfo]] = perp_markets
self.oracles: typing.Sequence[PublicKey] = oracles
self.signer_nonce: Decimal = signer_nonce
self.signer_key: PublicKey = signer_key
self.admin: PublicKey = admin
self.dex_program_id: PublicKey = dex_program_id
self.cache: PublicKey = cache
self.valid_interval: Decimal = valid_interval
@property
def shared_quote_token(self) -> TokenInfo:
quote = self.tokens[-1]
if quote is None:
raise Exception(f"Could not find shared quote token for group '{self.name}'.")
return quote
@property
def base_tokens(self) -> typing.Sequence[typing.Optional[TokenInfo]]:
return self.tokens[:-1]
@staticmethod
def from_layout(layout: layouts.MANGO_GROUP, name: str, account_info: AccountInfo, version: Version, token_lookup: TokenLookup, market_lookup: MarketLookup) -> "MangoGroup":
meta_data = Metadata.from_layout(layout.meta_data)
num_oracles = layout.num_oracles
tokens = [TokenInfo.from_layout_or_none(t, token_lookup) for t in layout.tokens]
spot_markets = [SpotMarketInfo.from_layout_or_none(m, market_lookup) for m in layout.spot_markets]
perp_markets = [PerpMarketInfo.from_layout_or_none(p) for p in layout.perp_markets]
oracles = list(layout.oracles)[:int(num_oracles)]
signer_nonce = layout.signer_nonce
signer_key = layout.signer_key
admin = layout.admin
dex_program_id = layout.dex_program_id
cache = layout.cache
valid_interval = layout.valid_interval
return MangoGroup(account_info, version, name, meta_data, tokens, spot_markets, perp_markets, oracles, signer_nonce, signer_key, admin, dex_program_id, cache, valid_interval)
@staticmethod
def parse(context: Context, account_info: AccountInfo) -> "MangoGroup":
data = account_info.data
if len(data) != layouts.MANGO_GROUP.sizeof():
raise Exception(
f"MangoGroup data length ({len(data)}) does not match expected size ({layouts.MANGO_GROUP.sizeof()}")
layout = layouts.MANGO_GROUP.parse(data)
return MangoGroup.from_layout(layout, "merps_test_v3", account_info, Version.V1, context.token_lookup, context.market_lookup)
@staticmethod
def load(context: Context, address: typing.Optional[PublicKey] = None) -> "MangoGroup":
group_address: PublicKey = address or context.group_id
account_info = AccountInfo.load(context, group_address)
if account_info is None:
raise Exception(f"MangoGroup account not found at address '{group_address}'")
return MangoGroup.parse(context, account_info)
def fetch_balances(self, context: Context, root_address: PublicKey) -> typing.List[TokenValue]:
balances: typing.List[TokenValue] = []
sol_balance = context.fetch_sol_balance(root_address)
balances += [TokenValue(SolToken, sol_balance)]
for basket_token in self.tokens:
if basket_token is not None and basket_token.token is not None:
balance = TokenValue.fetch_total_value(context, root_address, basket_token.token)
balances += [balance]
return balances
def __str__(self):
tokens = "\n ".join([f"{token}".replace("\n", "\n ")
for token in self.tokens if token is not None])
spot_markets = "\n ".join([f"{spot_market}".replace("\n", "\n ")
for spot_market in self.spot_markets if spot_market is not None])
perp_markets = "\n ".join([f"{perp_market}".replace("\n", "\n ")
for perp_market in self.perp_markets if perp_market is not None])
oracles = "\n ".join([f"{oracle}" for oracle in self.oracles])
return f"""« 𝙼𝚊𝚗𝚐𝚘𝙶𝚛𝚘𝚞𝚙 {self.version} [{self.address}]
{self.meta_data}
Name: {self.name}
Signer [Nonce: {self.signer_nonce}]: {self.signer_key}
Admin: {self.admin}
DEX Program ID: {self.dex_program_id}
Merps Cache: {self.cache}
Valid Interval: {self.valid_interval}
Tokens:
{tokens}
Spot Markets:
{spot_markets}
Perp Markets:
{perp_markets}
Oracles:
{oracles}
»"""

View File

@ -1,393 +0,0 @@
# # ⚠ 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 construct
import logging
import time
import typing
from decimal import Decimal
from solana.publickey import PublicKey
from solana.rpc.types import MemcmpOpts
from .accountinfo import AccountInfo
from .addressableaccount import AddressableAccount
from .balancesheet import BalanceSheet
from .constants import SYSTEM_PROGRAM_ADDRESS
from .context import Context
from .encoding import encode_int, encode_key
from .group import Group
from .layouts import layouts
from .mangoaccountflags import MangoAccountFlags
from .openorders import OpenOrders
from .token import Token
from .tokenvalue import TokenValue
from .version import Version
# # 🥭 MarginAccount class
#
class MarginAccount(AddressableAccount):
def __init__(self, account_info: AccountInfo, version: Version, account_flags: MangoAccountFlags,
has_borrows: bool, mango_group: PublicKey, owner: PublicKey, being_liquidated: bool,
deposits: typing.List[TokenValue], borrows: typing.List[TokenValue],
open_orders: typing.List[typing.Optional[PublicKey]]):
super().__init__(account_info)
self.version: Version = version
self.account_flags: MangoAccountFlags = account_flags
self.has_borrows: bool = has_borrows
self.mango_group: PublicKey = mango_group
self.owner: PublicKey = owner
self.being_liquidated: bool = being_liquidated
self.deposits: typing.List[TokenValue] = deposits
self.borrows: typing.List[TokenValue] = borrows
self.open_orders: typing.List[typing.Optional[PublicKey]] = open_orders
self.open_orders_accounts: typing.List[typing.Optional[OpenOrders]] = [None] * len(open_orders)
@staticmethod
def from_layout(layout: construct.Struct, account_info: AccountInfo, version: Version, group: Group) -> "MarginAccount":
if group.address != layout.mango_group:
raise Exception(
f"Margin account belongs to Group ID '{group.address}', not Group ID '{layout.mango_group}'")
if version == Version.V1:
# This is an old-style margin account, with no borrows flag
has_borrows = False
else:
# This is a new-style margin account where we can depend on the presence of the borrows flag
has_borrows = bool(layout.has_borrows)
account_flags: MangoAccountFlags = MangoAccountFlags.from_layout(layout.account_flags)
deposits: typing.List[TokenValue] = []
for index, deposit in enumerate(layout.deposits):
token = group.basket_tokens[index].token
rebased_deposit = deposit * group.basket_tokens[index].index.deposit.value
token_value = TokenValue(token, rebased_deposit)
deposits += [token_value]
borrows: typing.List[TokenValue] = []
for index, borrow in enumerate(layout.borrows):
token = group.basket_tokens[index].token
rebased_borrow = borrow * group.basket_tokens[index].index.borrow.value
token_value = TokenValue(token, rebased_borrow)
borrows += [token_value]
return MarginAccount(account_info, version, account_flags, has_borrows, layout.mango_group,
layout.owner, layout.being_liquidated, deposits, borrows, list(layout.open_orders))
@staticmethod
def parse(account_info: AccountInfo, group: Group) -> "MarginAccount":
data = account_info.data
if len(data) == layouts.MARGIN_ACCOUNT_V1.sizeof():
layout = layouts.MARGIN_ACCOUNT_V1.parse(data)
version: Version = Version.V1
elif len(data) == layouts.MARGIN_ACCOUNT_V2.sizeof():
version = Version.V2
layout = layouts.MARGIN_ACCOUNT_V2.parse(data)
else:
raise Exception(
f"Data length ({len(data)}) does not match expected size ({layouts.MARGIN_ACCOUNT_V1.sizeof()} or {layouts.MARGIN_ACCOUNT_V2.sizeof()})")
return MarginAccount.from_layout(layout, account_info, version, group)
@staticmethod
def load(context: Context, margin_account_address: PublicKey, group: Group) -> "MarginAccount":
account_info = AccountInfo.load(context, margin_account_address)
if account_info is None:
raise Exception(f"MarginAccount account not found at address '{margin_account_address}'")
margin_account = MarginAccount.parse(account_info, group)
margin_account.load_open_orders_accounts(context, group)
return margin_account
@staticmethod
def load_all_for_group(context: Context, program_id: PublicKey, group: Group) -> typing.List["MarginAccount"]:
filters = [
MemcmpOpts(
offset=layouts.MANGO_ACCOUNT_FLAGS.sizeof(), # mango_group is just after the MangoAccountFlags, which is the first entry
bytes=encode_key(group.address)
)
]
if group.version == Version.V1:
parser = layouts.MARGIN_ACCOUNT_V1
else:
parser = layouts.MARGIN_ACCOUNT_V2
response = context.client.get_program_accounts(
program_id, data_size=parser.sizeof(), memcmp_opts=filters, commitment=context.commitment, encoding="base64")
margin_accounts = []
for margin_account_data in response["result"]:
address = PublicKey(margin_account_data["pubkey"])
account = AccountInfo._from_response_values(margin_account_data["account"], address)
margin_account = MarginAccount.parse(account, group)
margin_accounts += [margin_account]
return margin_accounts
@staticmethod
def load_all_for_group_with_open_orders(context: Context, program_id: PublicKey, group: Group) -> typing.List["MarginAccount"]:
margin_accounts = MarginAccount.load_all_for_group(context, program_id, group)
open_orders = OpenOrders.load_raw_open_orders_account_infos(context, group)
for margin_account in margin_accounts:
margin_account.install_open_orders_accounts(group, open_orders)
return margin_accounts
@staticmethod
def load_all_for_owner(context: Context, owner: PublicKey, group: typing.Optional[Group] = None) -> typing.List["MarginAccount"]:
if group is None:
group = Group.load(context)
# mango_group is just after the MangoAccountFlags, which is the first entry.
mango_group_offset = layouts.MANGO_ACCOUNT_FLAGS.sizeof()
# owner is just after mango_group in the layout, and it's a PublicKey which is 32 bytes.
owner_offset = mango_group_offset + 32
filters = [
MemcmpOpts(
offset=mango_group_offset,
bytes=encode_key(group.address)
),
MemcmpOpts(
offset=owner_offset,
bytes=encode_key(owner)
)
]
response = context.client.get_program_accounts(
context.program_id, memcmp_opts=filters, commitment=context.commitment, encoding="base64")
margin_accounts = []
for margin_account_data in response["result"]:
address = PublicKey(margin_account_data["pubkey"])
account = AccountInfo._from_response_values(margin_account_data["account"], address)
margin_account = MarginAccount.parse(account, group)
margin_account.load_open_orders_accounts(context, group)
margin_accounts += [margin_account]
return margin_accounts
@classmethod
def filter_out_unripe(cls, margin_accounts: typing.List["MarginAccount"], group: Group, prices: typing.List[TokenValue]) -> typing.List["MarginAccount"]:
logger: logging.Logger = logging.getLogger(cls.__name__)
ripe_accounts: typing.List[MarginAccount] = []
for margin_account in margin_accounts:
balance_sheet = margin_account.get_balance_sheet_totals(group, prices)
if balance_sheet.collateral_ratio > 0:
if balance_sheet.collateral_ratio <= group.init_coll_ratio:
ripe_accounts += [margin_account]
logger.info(
f"Of those {len(margin_accounts)}, {len(ripe_accounts)} have a collateral ratio greater than zero but less than the initial collateral ratio of: {group.init_coll_ratio}.")
return ripe_accounts
def load_open_orders_accounts(self, context: Context, group: Group) -> None:
for index, oo in enumerate(self.open_orders):
key = oo
if key is not None:
self.open_orders_accounts[index] = OpenOrders.load(
context, key, group.basket_tokens[index].token.decimals, group.shared_quote_token.token.decimals)
def install_open_orders_accounts(self, group: Group, all_open_orders_by_address: typing.Dict[str, AccountInfo]) -> None:
for index, oo in enumerate(self.open_orders):
key = str(oo)
if key in all_open_orders_by_address:
open_orders_account_info = all_open_orders_by_address[key]
open_orders = OpenOrders.parse(open_orders_account_info,
group.basket_tokens[index].token.decimals,
group.shared_quote_token.token.decimals)
self.open_orders_accounts[index] = open_orders
def get_intrinsic_balance_sheets(self, group: Group) -> typing.List[BalanceSheet]:
settled_assets: typing.List[Decimal] = [Decimal(0)] * len(group.basket_tokens)
liabilities: typing.List[Decimal] = [Decimal(0)] * len(group.basket_tokens)
for index, token in enumerate(group.basket_tokens):
settled_assets[index] = self.deposits[index].value
liabilities[index] = self.borrows[index].value
unsettled_assets: typing.List[Decimal] = [Decimal(0)] * len(group.basket_tokens)
for index, open_orders_account in enumerate(self.open_orders_accounts):
if open_orders_account is not None:
unsettled_assets[index] += open_orders_account.base_token_total
unsettled_assets[-1] += open_orders_account.quote_token_total + \
open_orders_account.referrer_rebate_accrued
balance_sheets: typing.List[BalanceSheet] = []
for index, token in enumerate(group.basket_tokens):
balance_sheets += [BalanceSheet(token.token, liabilities[index],
settled_assets[index], unsettled_assets[index])]
return balance_sheets
def get_priced_balance_sheets(self, group: Group, prices: typing.List[TokenValue]) -> typing.List[BalanceSheet]:
priced: typing.List[BalanceSheet] = []
balance_sheets = self.get_intrinsic_balance_sheets(group)
for balance_sheet in balance_sheets:
price = TokenValue.find_by_token(prices, balance_sheet.token)
liabilities = balance_sheet.liabilities * price.value
settled_assets = balance_sheet.settled_assets * price.value
unsettled_assets = balance_sheet.unsettled_assets * price.value
priced += [BalanceSheet(
price.token,
price.token.round(liabilities),
price.token.round(settled_assets),
price.token.round(unsettled_assets)
)]
return priced
def get_balance_sheet_totals(self, group: Group, prices: typing.List[TokenValue]) -> BalanceSheet:
liabilities = Decimal(0)
settled_assets = Decimal(0)
unsettled_assets = Decimal(0)
balance_sheets = self.get_priced_balance_sheets(group, prices)
for balance_sheet in balance_sheets:
if balance_sheet is not None:
liabilities += balance_sheet.liabilities
settled_assets += balance_sheet.settled_assets
unsettled_assets += balance_sheet.unsettled_assets
# A BalanceSheet must have a token - it's a pain to make it a typing.Optional[Token].
# So in this one case, we produce a 'fake' token whose symbol is a summary of all token
# symbols that went into it.
#
# If this becomes more painful than typing.Optional[Token], we can go with making
# Token optional.
summary_name = "-".join([bal.token.name for bal in balance_sheets])
summary_token = Token(summary_name, f"{summary_name} Summary", SYSTEM_PROGRAM_ADDRESS, Decimal(0))
return BalanceSheet(summary_token, liabilities, settled_assets, unsettled_assets)
def get_intrinsic_balances(self, group: Group) -> typing.List[TokenValue]:
balance_sheets = self.get_intrinsic_balance_sheets(group)
balances: typing.List[TokenValue] = []
for index, balance_sheet in enumerate(balance_sheets):
if balance_sheet.token is None:
raise Exception(f"Intrinsic balance sheet with index [{index}] has no token.")
balances += [TokenValue(balance_sheet.token, balance_sheet.value)]
return balances
# The old way of fetching ripe margin accounts was to fetch them all then inspect them to see
# if they were ripe. That was a big performance problem - fetching all groups was quite a penalty.
#
# This is still how it's done in load_ripe_v1().
#
# The newer mechanism is to look for the has_borrows flag in the ManrginAccount. That should
# mean fewer MarginAccounts need to be fetched.
#
# This newer method is implemented in load_ripe_v2()
@staticmethod
def load_ripe(context: Context, group: Group) -> typing.List["MarginAccount"]:
if group.version == Version.V1:
return MarginAccount._load_ripe_v1(context, group)
else:
return MarginAccount._load_ripe_v2(context, group)
@classmethod
def _load_ripe_v1(cls, context: Context, group: Group) -> typing.List["MarginAccount"]:
started_at = time.time()
logger: logging.Logger = logging.getLogger(cls.__name__)
margin_accounts = MarginAccount.load_all_for_group_with_open_orders(context, context.program_id, group)
logger.info(f"Fetched {len(margin_accounts)} V1 margin accounts to process.")
prices = group.fetch_token_prices(context)
ripe_accounts = MarginAccount.filter_out_unripe(margin_accounts, group, prices)
time_taken = time.time() - started_at
logger.info(f"Loading ripe 🥭 accounts complete. Time taken: {time_taken:.2f} seconds.")
return ripe_accounts
@classmethod
def _load_ripe_v2(cls, context: Context, group: Group) -> typing.List["MarginAccount"]:
started_at = time.time()
logger: logging.Logger = logging.getLogger(cls.__name__)
filters = [
# 'has_borrows' offset is: 8 + 32 + 32 + (5 * 16) + (5 * 16) + (4 * 32) + 1
# = 361
MemcmpOpts(
offset=361,
bytes=encode_int(1)
),
MemcmpOpts(
offset=layouts.MANGO_ACCOUNT_FLAGS.sizeof(), # mango_group is just after the MangoAccountFlags, which is the first entry
bytes=encode_key(group.address)
)
]
data_size = layouts.MARGIN_ACCOUNT_V2.sizeof()
response = context.client.get_program_accounts(
context.program_id, data_size=data_size, memcmp_opts=filters, commitment=context.commitment, encoding="base64")
result = context.unwrap_or_raise_exception(response)
margin_accounts = []
open_orders_addresses = []
for margin_account_data in result:
address = PublicKey(margin_account_data["pubkey"])
account = AccountInfo._from_response_values(margin_account_data["account"], address)
margin_account = MarginAccount.parse(account, group)
open_orders_addresses += margin_account.open_orders
margin_accounts += [margin_account]
logger.info(f"Fetched {len(margin_accounts)} V2 margin accounts to process.")
# It looks like this will be more efficient - just specify only the addresses we
# need, and install them.
#
# Unfortunately there's a limit of 100 for the getMultipleAccounts() RPC call,
# and doing it repeatedly requires some pauses because of rate limits.
#
# It's quicker (so far) to bring back every openorders account for the group.
#
# open_orders_addresses = [oo for oo in open_orders_addresses if oo is not None]
# open_orders_account_infos = AccountInfo.load_multiple(self.context, open_orders_addresses)
# open_orders_account_infos_by_address = {key: value for key, value in [(str(account_info.address), account_info) for account_info in open_orders_account_infos]}
# for margin_account in margin_accounts:
# margin_account.install_open_orders_accounts(self, open_orders_account_infos_by_address)
# This just fetches every openorder account for the group.
open_orders = OpenOrders.load_raw_open_orders_account_infos(context, group)
logger.info(f"Fetched {len(open_orders)} openorders accounts.")
for margin_account in margin_accounts:
margin_account.install_open_orders_accounts(group, open_orders)
prices = group.fetch_token_prices(context)
ripe_accounts = MarginAccount.filter_out_unripe(margin_accounts, group, prices)
time_taken = time.time() - started_at
logger.info(f"Loading ripe 🥭 accounts complete. Time taken: {time_taken:.2f} seconds.")
return ripe_accounts
def __str__(self) -> str:
deposits = "\n ".join([f"{item}" for item in self.deposits])
borrows = "\n ".join([f"{item:}" for item in self.borrows])
if all(oo is None for oo in self.open_orders_accounts):
open_orders = f"{self.open_orders}"
else:
open_orders_unindented = f"{self.open_orders_accounts}"
open_orders = open_orders_unindented.replace("\n", "\n ")
return f"""« MarginAccount: {self.address}
Flags: {self.account_flags}
Has Borrows: {self.has_borrows}
Owner: {self.owner}
Mango Group: {self.mango_group}
Deposits:
{deposits}
Borrows:
{borrows}
Mango Open Orders: {open_orders}
»"""

View File

@ -128,7 +128,7 @@ class SimpleMarketMaker:
for order in orders: for order in orders:
self.market_operations.cancel_order(order) self.market_operations.cancel_order(order)
def fetch_inventory(self) -> typing.List[mango.TokenValue]: def fetch_inventory(self) -> typing.Sequence[mango.TokenValue]:
return [ return [
mango.TokenValue.fetch_total_value(self.context, self.wallet.address, self.market.base), mango.TokenValue.fetch_total_value(self.context, self.wallet.address, self.market.base),
mango.TokenValue.fetch_total_value(self.context, self.wallet.address, self.market.quote) mango.TokenValue.fetch_total_value(self.context, self.wallet.address, self.market.quote)
@ -140,7 +140,7 @@ class SimpleMarketMaker:
return (bid, ask) return (bid, ask)
def calculate_order_sizes(self, price: mango.Price, inventory: typing.List[mango.TokenValue]): def calculate_order_sizes(self, price: mango.Price, inventory: typing.Sequence[mango.TokenValue]):
base_tokens: typing.Optional[mango.TokenValue] = mango.TokenValue.find_by_token(inventory, price.market.base) base_tokens: typing.Optional[mango.TokenValue] = mango.TokenValue.find_by_token(inventory, price.market.base)
if base_tokens is None: if base_tokens is None:
raise Exception(f"Could not find market-maker base token {price.market.base.symbol} in inventory.") raise Exception(f"Could not find market-maker base token {price.market.base.symbol} in inventory.")
@ -149,7 +149,7 @@ class SimpleMarketMaker:
sell_size = base_tokens.value * self.position_size_ratio sell_size = base_tokens.value * self.position_size_ratio
return (buy_size, sell_size) return (buy_size, sell_size)
def orders_require_action(self, orders: typing.List[mango.Order], price: Decimal, size: Decimal) -> bool: def orders_require_action(self, orders: typing.Sequence[mango.Order], price: Decimal, size: Decimal) -> bool:
# for order in orders: # for order in orders:
# price_tolerance = order.price * self.existing_order_tolerance # price_tolerance = order.price * self.existing_order_tolerance
# size_tolerance = order.size * self.existing_order_tolerance # size_tolerance = order.size * self.existing_order_tolerance

View File

@ -1,64 +0,0 @@
# # ⚠ 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 logging
from decimal import Decimal
from pyserum.market import Market as PySerumMarket
from solana.publickey import PublicKey
from .baskettoken import BasketToken
from .context import Context
from .market import Market
from .spotmarket import SpotMarket
# # 🥭 MarketMetadata class
#
class MarketMetadata:
def __init__(self, name: str, address: PublicKey, base: BasketToken, quote: BasketToken,
spot: Market, oracle: PublicKey, decimals: Decimal):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.name: str = name
self.address: PublicKey = address
self.base: BasketToken = base
self.quote: BasketToken = quote
if not isinstance(spot, SpotMarket):
raise Exception(f"Spot '{spot}' is not a spot market.")
self.spot: SpotMarket = spot
self.oracle: PublicKey = oracle
self.decimals: Decimal = decimals
self.symbol: str = f"{base.token.symbol}/{quote.token.symbol}"
self._market = None
def fetch_market(self, context: Context) -> PySerumMarket:
if self._market is None:
self._market = PySerumMarket.load(context.client, self.spot.address)
return self._market
def __str__(self) -> str:
base = f"{self.base}".replace("\n", "\n ")
quote = f"{self.quote}".replace("\n", "\n ")
return f"""« MarketMetadata '{self.name}' [{self.address}/{self.spot.address}]:
Base: {base}
Quote: {quote}
Oracle: {self.oracle} ({self.decimals} decimals)
»"""
def __repr__(self) -> str:
return f"{self}"

View File

@ -64,11 +64,11 @@ class MarketOperations(metaclass=abc.ABCMeta):
raise NotImplementedError("MarketOperations.place_order() is not implemented on the base type.") raise NotImplementedError("MarketOperations.place_order() is not implemented on the base type.")
@abc.abstractmethod @abc.abstractmethod
def load_orders(self) -> typing.List[Order]: def load_orders(self) -> typing.Sequence[Order]:
raise NotImplementedError("MarketOperations.load_orders() is not implemented on the base type.") raise NotImplementedError("MarketOperations.load_orders() is not implemented on the base type.")
@abc.abstractmethod @abc.abstractmethod
def load_my_orders(self) -> typing.List[Order]: def load_my_orders(self) -> typing.Sequence[Order]:
raise NotImplementedError("MarketOperations.load_my_orders() is not implemented on the base type.") raise NotImplementedError("MarketOperations.load_my_orders() is not implemented on the base type.")
def __repr__(self) -> str: def __repr__(self) -> str:
@ -99,10 +99,10 @@ class NullMarketOperations(MarketOperations):
self.reporter(report) self.reporter(report)
return Order(id=0, side=side, price=price, size=size, client_id=0, owner=SYSTEM_PROGRAM_ADDRESS) return Order(id=0, side=side, price=price, size=size, client_id=0, owner=SYSTEM_PROGRAM_ADDRESS)
def load_orders(self) -> typing.List[Order]: def load_orders(self) -> typing.Sequence[Order]:
return [] return []
def load_my_orders(self) -> typing.List[Order]: def load_my_orders(self) -> typing.Sequence[Order]:
return [] return []
def __str__(self) -> str: def __str__(self) -> str:

View File

@ -16,17 +16,17 @@
import typing import typing
from decimal import Decimal from decimal import Decimal
from solana.account import Account from solana.account import Account as SolanaAccount
from solana.system_program import CreateAccountParams, create_account from solana.system_program import CreateAccountParams, create_account
from solana.sysvar import SYSVAR_CLOCK_PUBKEY from solana.sysvar import SYSVAR_CLOCK_PUBKEY
from solana.transaction import AccountMeta, TransactionInstruction from solana.transaction import AccountMeta, TransactionInstruction
from spl.token.constants import TOKEN_PROGRAM_ID from spl.token.constants import TOKEN_PROGRAM_ID
from .account import Account
from .constants import SYSTEM_PROGRAM_ADDRESS from .constants import SYSTEM_PROGRAM_ADDRESS
from .context import Context from .context import Context
from .group import Group
from .layouts import layouts from .layouts import layouts
from .mangoaccount import MangoAccount
from .mangogroup import MangoGroup
from .orders import Order, OrderType, Side from .orders import Order, OrderType, Side
from .perpmarket import PerpMarket from .perpmarket import PerpMarket
from .rootbank import NodeBank, RootBank from .rootbank import NodeBank, RootBank
@ -39,7 +39,7 @@ from .wallet import Wallet
# #
def build_cancel_perp_order_instructions(context: Context, wallet: Wallet, margin_account: MangoAccount, perp_market: PerpMarket, order: Order) -> typing.Sequence[TransactionInstruction]: def build_cancel_perp_order_instructions(context: Context, wallet: Wallet, margin_account: Account, perp_market: PerpMarket, order: Order) -> typing.Sequence[TransactionInstruction]:
# { buy: 0, sell: 1 } # { buy: 0, sell: 1 }
raw_side: int = 1 if order.side == Side.SELL else 0 raw_side: int = 1 if order.side == Side.SELL else 0
@ -73,7 +73,7 @@ def build_cancel_perp_order_instructions(context: Context, wallet: Wallet, margi
] ]
def build_place_perp_order_instructions(context: Context, wallet: Wallet, group: MangoGroup, margin_account: MangoAccount, perp_market: PerpMarket, price: Decimal, quantity: Decimal, client_order_id: int, side: Side, order_type: OrderType) -> typing.Sequence[TransactionInstruction]: def build_place_perp_order_instructions(context: Context, wallet: Wallet, group: Group, margin_account: Account, perp_market: PerpMarket, price: Decimal, quantity: Decimal, client_order_id: int, side: Side, order_type: OrderType) -> typing.Sequence[TransactionInstruction]:
# { buy: 0, sell: 1 } # { buy: 0, sell: 1 }
raw_side: int = 1 if side == Side.SELL else 0 raw_side: int = 1 if side == Side.SELL else 0
# { limit: 0, ioc: 1, postOnly: 2 } # { limit: 0, ioc: 1, postOnly: 2 }
@ -125,14 +125,14 @@ def build_place_perp_order_instructions(context: Context, wallet: Wallet, group:
] ]
def build_create_margin_account_instructions(context: Context, wallet: Wallet, group: MangoGroup, new_account: Account) -> typing.Sequence[TransactionInstruction]: def build_create_account_instructions(context: Context, wallet: Wallet, group: Group, new_account: SolanaAccount) -> typing.Sequence[TransactionInstruction]:
mango_account_address = new_account.public_key() mango_account_address = new_account.public_key()
minimum_balance_response = context.client.get_minimum_balance_for_rent_exemption(layouts.MANGO_ACCOUNT.sizeof()) minimum_balance_response = context.client.get_minimum_balance_for_rent_exemption(layouts.MANGO_ACCOUNT.sizeof())
minimum_balance = context.unwrap_or_raise_exception(minimum_balance_response) minimum_balance = context.unwrap_or_raise_exception(minimum_balance_response)
create = create_account( create = create_account(
CreateAccountParams(wallet.address, mango_account_address, minimum_balance, layouts.MANGO_ACCOUNT.sizeof(), context.program_id)) CreateAccountParams(wallet.address, mango_account_address, minimum_balance, layouts.MANGO_ACCOUNT.sizeof(), context.program_id))
# /// 0. `[]` mango_group_ai - MangoGroup that this mango account is for # /// 0. `[]` mango_group_ai - Group that this mango account is for
# /// 1. `[writable]` mango_account_ai - the mango account data # /// 1. `[writable]` mango_account_ai - the mango account data
# /// 2. `[signer]` owner_ai - Solana account of owner of the mango account # /// 2. `[signer]` owner_ai - Solana account of owner of the mango account
# /// 3. `[]` rent_ai - Rent sysvar account # /// 3. `[]` rent_ai - Rent sysvar account
@ -169,7 +169,7 @@ def build_create_margin_account_instructions(context: Context, wallet: Wallet, g
# quantity: u64, # quantity: u64,
# allow_borrow: bool, # allow_borrow: bool,
# }, # },
def build_withdraw_instructions(context: Context, wallet: Wallet, group: MangoGroup, margin_account: MangoAccount, root_bank: RootBank, node_bank: NodeBank, token_account: TokenAccount, allow_borrow: bool) -> typing.Sequence[TransactionInstruction]: def build_withdraw_instructions(context: Context, wallet: Wallet, group: Group, margin_account: Account, root_bank: RootBank, node_bank: NodeBank, token_account: TokenAccount, allow_borrow: bool) -> typing.Sequence[TransactionInstruction]:
value = token_account.value.shift_to_native().value value = token_account.value.shift_to_native().value
withdraw = TransactionInstruction( withdraw = TransactionInstruction(
keys=[ keys=[

View File

@ -21,13 +21,13 @@ from pyserum.open_orders_account import OpenOrdersAccount
from solana.publickey import PublicKey from solana.publickey import PublicKey
from solana.rpc.types import MemcmpOpts from solana.rpc.types import MemcmpOpts
from .accountflags import AccountFlags
from .accountinfo import AccountInfo from .accountinfo import AccountInfo
from .addressableaccount import AddressableAccount from .addressableaccount import AddressableAccount
from .context import Context from .context import Context
from .encoding import encode_key from .encoding import encode_key
from .group import Group from .group import Group
from .layouts import layouts from .layouts import layouts
from .serumaccountflags import SerumAccountFlags
from .version import Version from .version import Version
# # 🥭 OpenOrders class # # 🥭 OpenOrders class
@ -36,15 +36,15 @@ from .version import Version
class OpenOrders(AddressableAccount): class OpenOrders(AddressableAccount):
def __init__(self, account_info: AccountInfo, version: Version, program_id: PublicKey, def __init__(self, account_info: AccountInfo, version: Version, program_id: PublicKey,
account_flags: SerumAccountFlags, market: PublicKey, owner: PublicKey, account_flags: AccountFlags, market: PublicKey, owner: PublicKey,
base_token_free: Decimal, base_token_total: Decimal, quote_token_free: Decimal, base_token_free: Decimal, base_token_total: Decimal, quote_token_free: Decimal,
quote_token_total: Decimal, free_slot_bits: Decimal, is_bid_bits: Decimal, quote_token_total: Decimal, free_slot_bits: Decimal, is_bid_bits: Decimal,
orders: typing.List[Decimal], client_ids: typing.List[Decimal], orders: typing.Sequence[Decimal], client_ids: typing.Sequence[Decimal],
referrer_rebate_accrued: Decimal): referrer_rebate_accrued: Decimal):
super().__init__(account_info) super().__init__(account_info)
self.version: Version = version self.version: Version = version
self.program_id: PublicKey = program_id self.program_id: PublicKey = program_id
self.account_flags: SerumAccountFlags = account_flags self.account_flags: AccountFlags = account_flags
self.market: PublicKey = market self.market: PublicKey = market
self.owner: PublicKey = owner self.owner: PublicKey = owner
self.base_token_free: Decimal = base_token_free self.base_token_free: Decimal = base_token_free
@ -53,8 +53,8 @@ class OpenOrders(AddressableAccount):
self.quote_token_total: Decimal = quote_token_total self.quote_token_total: Decimal = quote_token_total
self.free_slot_bits: Decimal = free_slot_bits self.free_slot_bits: Decimal = free_slot_bits
self.is_bid_bits: Decimal = is_bid_bits self.is_bid_bits: Decimal = is_bid_bits
self.orders: typing.List[Decimal] = orders self.orders: typing.Sequence[Decimal] = orders
self.client_ids: typing.List[Decimal] = client_ids self.client_ids: typing.Sequence[Decimal] = client_ids
self.referrer_rebate_accrued: Decimal = referrer_rebate_accrued self.referrer_rebate_accrued: Decimal = referrer_rebate_accrued
# Sometimes pyserum wants to take its own OpenOrdersAccount as a parameter (e.g. in settle_funds()) # Sometimes pyserum wants to take its own OpenOrdersAccount as a parameter (e.g. in settle_funds())
@ -64,7 +64,7 @@ class OpenOrders(AddressableAccount):
@staticmethod @staticmethod
def from_layout(layout: layouts.OPEN_ORDERS, account_info: AccountInfo, def from_layout(layout: layouts.OPEN_ORDERS, account_info: AccountInfo,
base_decimals: Decimal, quote_decimals: Decimal) -> "OpenOrders": base_decimals: Decimal, quote_decimals: Decimal) -> "OpenOrders":
account_flags = SerumAccountFlags.from_layout(layout.account_flags) account_flags = AccountFlags.from_layout(layout.account_flags)
program_id = account_info.owner program_id = account_info.owner
base_divisor = 10 ** base_decimals base_divisor = 10 ** base_decimals
@ -73,8 +73,8 @@ class OpenOrders(AddressableAccount):
base_token_total: Decimal = layout.base_token_total / base_divisor base_token_total: Decimal = layout.base_token_total / base_divisor
quote_token_free: Decimal = layout.quote_token_free / quote_divisor quote_token_free: Decimal = layout.quote_token_free / quote_divisor
quote_token_total: Decimal = layout.quote_token_total / quote_divisor quote_token_total: Decimal = layout.quote_token_total / quote_divisor
nonzero_orders: typing.List[Decimal] = list([order for order in layout.orders if order != 0]) nonzero_orders: typing.Sequence[Decimal] = list([order for order in layout.orders if order != 0])
nonzero_client_ids: typing.List[Decimal] = list( nonzero_client_ids: typing.Sequence[Decimal] = list(
[client_id for client_id in layout.client_ids if client_id != 0]) [client_id for client_id in layout.client_ids if client_id != 0])
return OpenOrders(account_info, Version.UNSPECIFIED, program_id, account_flags, layout.market, return OpenOrders(account_info, Version.UNSPECIFIED, program_id, account_flags, layout.market,
@ -95,7 +95,7 @@ class OpenOrders(AddressableAccount):
def load_raw_open_orders_account_infos(context: Context, group: Group) -> typing.Dict[str, AccountInfo]: def load_raw_open_orders_account_infos(context: Context, group: Group) -> typing.Dict[str, AccountInfo]:
filters = [ filters = [
MemcmpOpts( MemcmpOpts(
offset=layouts.SERUM_ACCOUNT_FLAGS.sizeof() + 37, offset=layouts.ACCOUNT_FLAGS.sizeof() + 37,
bytes=encode_key(group.signer_key) bytes=encode_key(group.signer_key)
) )
] ]
@ -119,11 +119,11 @@ class OpenOrders(AddressableAccount):
def load_for_market_and_owner(context: Context, market: PublicKey, owner: PublicKey, program_id: PublicKey, base_decimals: Decimal, quote_decimals: Decimal): def load_for_market_and_owner(context: Context, market: PublicKey, owner: PublicKey, program_id: PublicKey, base_decimals: Decimal, quote_decimals: Decimal):
filters = [ filters = [
MemcmpOpts( MemcmpOpts(
offset=layouts.SERUM_ACCOUNT_FLAGS.sizeof() + 5, offset=layouts.ACCOUNT_FLAGS.sizeof() + 5,
bytes=encode_key(market) bytes=encode_key(market)
), ),
MemcmpOpts( MemcmpOpts(
offset=layouts.SERUM_ACCOUNT_FLAGS.sizeof() + 37, offset=layouts.ACCOUNT_FLAGS.sizeof() + 37,
bytes=encode_key(owner) bytes=encode_key(owner)
) )
] ]

View File

@ -120,7 +120,7 @@ class PythOracleProvider(OracleProvider):
fixed_usdt = re.sub('USDT$', 'USD', normalised) fixed_usdt = re.sub('USDT$', 'USD', normalised)
return re.sub('USDC$', 'USD', fixed_usdt) return re.sub('USDC$', 'USD', fixed_usdt)
def _pyth_symbol_to_market_symbols(self, symbol: str) -> typing.List[str]: def _pyth_symbol_to_market_symbols(self, symbol: str) -> typing.Sequence[str]:
if symbol.endswith("USD"): if symbol.endswith("USD"):
return [f"{symbol}C", f"{symbol}T"] return [f"{symbol}C", f"{symbol}T"]
return [symbol] return [symbol]
@ -141,7 +141,7 @@ class PythOracleProvider(OracleProvider):
return mapping return mapping
def _fetch_all_pyth_products(self, context: Context, address: PublicKey) -> typing.List[typing.Any]: def _fetch_all_pyth_products(self, context: Context, address: PublicKey) -> typing.Sequence[typing.Any]:
mapping = self._load_pyth_mapping(context, address) mapping = self._load_pyth_mapping(context, address)
all_product_addresses = mapping.products[0:int(mapping.num)] all_product_addresses = mapping.products[0:int(mapping.num)]
product_account_infos = AccountInfo.load_multiple(context, all_product_addresses) product_account_infos = AccountInfo.load_multiple(context, all_product_addresses)

View File

@ -35,7 +35,7 @@ class OwnedTokenValue:
self.token_value = token_value self.token_value = token_value
@staticmethod @staticmethod
def find_by_owner(values: typing.List["OwnedTokenValue"], owner: PublicKey) -> "OwnedTokenValue": def find_by_owner(values: typing.Sequence["OwnedTokenValue"], owner: PublicKey) -> "OwnedTokenValue":
found = [value for value in values if value.owner == owner] found = [value for value in values if value.owner == owner]
if len(found) == 0: if len(found) == 0:
raise Exception(f"Owner '{owner}' not found in: {values}") raise Exception(f"Owner '{owner}' not found in: {values}")
@ -46,7 +46,7 @@ class OwnedTokenValue:
return found[0] return found[0]
@staticmethod @staticmethod
def changes(before: typing.List["OwnedTokenValue"], after: typing.List["OwnedTokenValue"]) -> typing.List["OwnedTokenValue"]: def changes(before: typing.Sequence["OwnedTokenValue"], after: typing.Sequence["OwnedTokenValue"]) -> typing.Sequence["OwnedTokenValue"]:
changes: typing.List[OwnedTokenValue] = [] changes: typing.List[OwnedTokenValue] = []
for before_value in before: for before_value in before:
after_value = OwnedTokenValue.find_by_owner(after, before_value.owner) after_value = OwnedTokenValue.find_by_owner(after, before_value.owner)

View File

@ -20,8 +20,8 @@ from solana.publickey import PublicKey
from .accountinfo import AccountInfo from .accountinfo import AccountInfo
from .addressableaccount import AddressableAccount from .addressableaccount import AddressableAccount
from .context import Context from .context import Context
from .group import Group
from .layouts import layouts from .layouts import layouts
from .mangogroup import MangoGroup
from .metadata import Metadata from .metadata import Metadata
from .tokeninfo import TokenInfo from .tokeninfo import TokenInfo
from .version import Version from .version import Version
@ -34,7 +34,7 @@ from .version import Version
class PerpMarket(AddressableAccount): class PerpMarket(AddressableAccount):
def __init__(self, account_info: AccountInfo, version: Version, def __init__(self, account_info: AccountInfo, version: Version,
meta_data: Metadata, group: MangoGroup, bids: PublicKey, asks: PublicKey, meta_data: Metadata, group: Group, bids: PublicKey, asks: PublicKey,
event_queue: PublicKey, long_funding: Decimal, short_funding: Decimal, event_queue: PublicKey, long_funding: Decimal, short_funding: Decimal,
open_interest: Decimal, quote_lot_size: Decimal, index_oracle: PublicKey, open_interest: Decimal, quote_lot_size: Decimal, index_oracle: PublicKey,
last_updated: datetime, seq_num: Decimal, contract_size: Decimal last_updated: datetime, seq_num: Decimal, contract_size: Decimal
@ -43,7 +43,7 @@ class PerpMarket(AddressableAccount):
self.version: Version = version self.version: Version = version
self.meta_data: Metadata = meta_data self.meta_data: Metadata = meta_data
self.group: MangoGroup = group self.group: Group = group
self.bids: PublicKey = bids self.bids: PublicKey = bids
self.asks: PublicKey = asks self.asks: PublicKey = asks
self.event_queue: PublicKey = event_queue self.event_queue: PublicKey = event_queue
@ -74,7 +74,7 @@ class PerpMarket(AddressableAccount):
self.quote_token: TokenInfo = quote_token self.quote_token: TokenInfo = quote_token
@staticmethod @staticmethod
def from_layout(layout: layouts.PERP_MARKET, account_info: AccountInfo, version: Version, group: MangoGroup) -> "PerpMarket": def from_layout(layout: layouts.PERP_MARKET, account_info: AccountInfo, version: Version, group: Group) -> "PerpMarket":
meta_data = Metadata.from_layout(layout.meta_data) meta_data = Metadata.from_layout(layout.meta_data)
bids: PublicKey = layout.bids bids: PublicKey = layout.bids
asks: PublicKey = layout.asks asks: PublicKey = layout.asks
@ -91,7 +91,7 @@ class PerpMarket(AddressableAccount):
return PerpMarket(account_info, version, meta_data, group, bids, asks, event_queue, long_funding, short_funding, open_interest, quote_lot_size, index_oracle, last_updated, seq_num, contract_size) return PerpMarket(account_info, version, meta_data, group, bids, asks, event_queue, long_funding, short_funding, open_interest, quote_lot_size, index_oracle, last_updated, seq_num, contract_size)
@staticmethod @staticmethod
def parse(account_info: AccountInfo, group: MangoGroup) -> "PerpMarket": def parse(account_info: AccountInfo, group: Group) -> "PerpMarket":
data = account_info.data data = account_info.data
if len(data) != layouts.PERP_MARKET.sizeof(): if len(data) != layouts.PERP_MARKET.sizeof():
raise Exception( raise Exception(
@ -101,7 +101,7 @@ class PerpMarket(AddressableAccount):
return PerpMarket.from_layout(layout, account_info, Version.V1, group) return PerpMarket.from_layout(layout, account_info, Version.V1, group)
@staticmethod @staticmethod
def load(context: Context, group: MangoGroup, address: PublicKey) -> "PerpMarket": def load(context: Context, group: Group, address: PublicKey) -> "PerpMarket":
account_info = AccountInfo.load(context, address) account_info = AccountInfo.load(context, address)
if account_info is None: if account_info is None:
raise Exception(f"PerpMarket account not found at address '{address}'") raise Exception(f"PerpMarket account not found at address '{address}'")

View File

@ -17,13 +17,13 @@
import typing import typing
from decimal import Decimal from decimal import Decimal
from solana.account import Account from solana.account import Account as SolanaAccount
from solana.publickey import PublicKey from solana.publickey import PublicKey
from solana.transaction import Transaction from solana.transaction import Transaction
from .account import Account
from .accountinfo import AccountInfo from .accountinfo import AccountInfo
from .context import Context from .context import Context
from .mangoaccount import MangoAccount
from .marketoperations import MarketOperations from .marketoperations import MarketOperations
from .merpsinstructions import build_cancel_perp_order_instructions, build_place_perp_order_instructions from .merpsinstructions import build_cancel_perp_order_instructions, build_place_perp_order_instructions
from .orderbookside import OrderBookSide from .orderbookside import OrderBookSide
@ -40,13 +40,13 @@ from .wallet import Wallet
class PerpMarketOperations(MarketOperations): class PerpMarketOperations(MarketOperations):
def __init__(self, market_name: str, context: Context, wallet: Wallet, def __init__(self, market_name: str, context: Context, wallet: Wallet,
margin_account: MangoAccount, perp_market: PerpMarket, margin_account: Account, perp_market: PerpMarket,
reporter: typing.Callable[[str], None] = None): reporter: typing.Callable[[str], None] = None):
super().__init__() super().__init__()
self.market_name: str = market_name self.market_name: str = market_name
self.context: Context = context self.context: Context = context
self.wallet: Wallet = wallet self.wallet: Wallet = wallet
self.margin_account: MangoAccount = margin_account self.margin_account: Account = margin_account
self.perp_market: PerpMarket = perp_market self.perp_market: PerpMarket = perp_market
self.reporter = reporter or (lambda _: None) self.reporter = reporter or (lambda _: None)
@ -55,7 +55,7 @@ class PerpMarketOperations(MarketOperations):
self.logger.info(report) self.logger.info(report)
self.reporter(report) self.reporter(report)
signers: typing.List[Account] = [self.wallet.account] signers: typing.Sequence[SolanaAccount] = [self.wallet.account]
transaction = Transaction() transaction = Transaction()
cancel_instructions = build_cancel_perp_order_instructions( cancel_instructions = build_cancel_perp_order_instructions(
self.context, self.wallet, self.margin_account, self.perp_market, order) self.context, self.wallet, self.margin_account, self.perp_market, order)
@ -69,7 +69,7 @@ class PerpMarketOperations(MarketOperations):
self.logger.info(report) self.logger.info(report)
self.reporter(report) self.reporter(report)
signers: typing.List[Account] = [self.wallet.account] signers: typing.Sequence[SolanaAccount] = [self.wallet.account]
transaction = Transaction() transaction = Transaction()
place_instructions = build_place_perp_order_instructions( place_instructions = build_place_perp_order_instructions(
self.context, self.wallet, self.perp_market.group, self.margin_account, self.perp_market, price, size, client_order_id, side, order_type) self.context, self.wallet, self.perp_market.group, self.margin_account, self.perp_market, price, size, client_order_id, side, order_type)
@ -79,7 +79,7 @@ class PerpMarketOperations(MarketOperations):
return Order(id=0, side=side, price=price, size=size, client_id=client_order_id, owner=self.margin_account.address) return Order(id=0, side=side, price=price, size=size, client_id=client_order_id, owner=self.margin_account.address)
def load_orders(self) -> typing.List[Order]: def load_orders(self) -> typing.Sequence[Order]:
bids_address: PublicKey = self.perp_market.bids bids_address: PublicKey = self.perp_market.bids
asks_address: PublicKey = self.perp_market.asks asks_address: PublicKey = self.perp_market.asks
[bids, asks] = AccountInfo.load_multiple(self.context, [bids_address, asks_address]) [bids, asks] = AccountInfo.load_multiple(self.context, [bids_address, asks_address])
@ -88,7 +88,7 @@ class PerpMarketOperations(MarketOperations):
return [*bid_side.orders(), *ask_side.orders()] return [*bid_side.orders(), *ask_side.orders()]
def load_my_orders(self) -> typing.List[Order]: def load_my_orders(self) -> typing.Sequence[Order]:
orders = self.load_orders() orders = self.load_orders()
mine = [] mine = []
for order in orders: for order in orders:

View File

@ -42,11 +42,11 @@ from decimal import Decimal
class RetryWithPauses: class RetryWithPauses:
def __init__(self, name: str, func: typing.Callable, pauses: typing.List[Decimal]) -> None: def __init__(self, name: str, func: typing.Callable, pauses: typing.Sequence[Decimal]) -> None:
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.name: str = name self.name: str = name
self.func: typing.Callable = func self.func: typing.Callable = func
self.pauses: typing.List[Decimal] = pauses self.pauses: typing.Sequence[Decimal] = pauses
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
captured_exception: Exception = None captured_exception: Exception = None
@ -98,5 +98,5 @@ class RetryWithPauses:
# ``` # ```
@contextmanager @contextmanager
def retry_context(name: str, func: typing.Callable, pauses: typing.List[Decimal]) -> typing.Iterator[RetryWithPauses]: def retry_context(name: str, func: typing.Callable, pauses: typing.Sequence[Decimal]) -> typing.Iterator[RetryWithPauses]:
yield RetryWithPauses(name, func, pauses) yield RetryWithPauses(name, func, pauses)

View File

@ -99,7 +99,7 @@ class SerumMarketOperations(MarketOperations):
client_id=serum_order.client_id, owner=serum_order.open_order_address) client_id=serum_order.client_id, owner=serum_order.open_order_address)
return order return order
def load_orders(self) -> typing.List[Order]: def load_orders(self) -> typing.Sequence[Order]:
asks = self.market.load_asks() asks = self.market.load_asks()
orders: typing.List[Order] = [] orders: typing.List[Order] = []
for serum_order in asks: for serum_order in asks:
@ -111,7 +111,7 @@ class SerumMarketOperations(MarketOperations):
return orders return orders
def load_my_orders(self) -> typing.List[Order]: def load_my_orders(self) -> typing.Sequence[Order]:
serum_orders = self.market.load_orders_for_owner(self.wallet.address) serum_orders = self.market.load_orders_for_owner(self.wallet.address)
orders: typing.List[Order] = [] orders: typing.List[Order] = []
for serum_order in serum_orders: for serum_order in serum_orders:

View File

@ -52,7 +52,7 @@ class Token:
return self.symbol.upper() == symbol.upper() return self.symbol.upper() == symbol.upper()
@staticmethod @staticmethod
def find_by_symbol(values: typing.List["Token"], symbol: str) -> "Token": def find_by_symbol(values: typing.Sequence["Token"], symbol: str) -> "Token":
found = [value for value in values if value.symbol_matches(symbol)] found = [value for value in values if value.symbol_matches(symbol)]
if len(found) == 0: if len(found) == 0:
raise Exception(f"Token '{symbol}' not found in token values: {values}") raise Exception(f"Token '{symbol}' not found in token values: {values}")
@ -63,7 +63,7 @@ class Token:
return found[0] return found[0]
@staticmethod @staticmethod
def find_by_mint(values: typing.List["Token"], mint: PublicKey) -> "Token": def find_by_mint(values: typing.Sequence["Token"], mint: PublicKey) -> "Token":
found = [value for value in values if value.mint == mint] found = [value for value in values if value.mint == mint]
if len(found) == 0: if len(found) == 0:
raise Exception(f"Token '{mint}' not found in token values: {values}") raise Exception(f"Token '{mint}' not found in token values: {values}")

View File

@ -30,19 +30,21 @@ from .tokenlookup import TokenLookup
class TokenInfo(): class TokenInfo():
def __init__(self, token: typing.Optional[Token], root_bank: PublicKey, decimals: Decimal): def __init__(self, token: Token, root_bank: PublicKey, decimals: Decimal):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.token: typing.Optional[Token] = token self.token: Token = token
self.root_bank: PublicKey = root_bank self.root_bank: PublicKey = root_bank
self.decimals: Decimal = decimals self.decimals: Decimal = decimals
def from_layout(layout: layouts.TOKEN_INFO, token_lookup: TokenLookup) -> "TokenInfo": def from_layout(layout: layouts.TOKEN_INFO, token_lookup: TokenLookup) -> "TokenInfo":
token = token_lookup.find_by_mint(layout.mint) token = token_lookup.find_by_mint(layout.mint)
# TODO - this should be resolved. The decimals should match, but the mixture of token lookups is getting messy. if token is None:
# if token is not None: raise Exception(f"Token with mint {layout.mint} could not be found.")
# if layout.decimals != token.decimals:
# raise Exception( if layout.decimals != token.decimals:
# f"Conflict between number of decimals in token static data {token.decimals} and group {layout.decimals} for token {token.symbol}.") raise Exception(
f"Conflict between number of decimals in token static data {token.decimals} and group {layout.decimals} for token {token.symbol}.")
return TokenInfo(token, layout.root_bank, layout.decimals) return TokenInfo(token, layout.root_bank, layout.decimals)
def from_layout_or_none(layout: layouts.TOKEN_INFO, token_lookup: TokenLookup) -> typing.Optional["TokenInfo"]: def from_layout_or_none(layout: layouts.TOKEN_INFO, token_lookup: TokenLookup) -> typing.Optional["TokenInfo"]:

View File

@ -69,12 +69,12 @@ class TokenValue:
return value return value
@staticmethod @staticmethod
def report(reporter: typing.Callable[[str], None], values: typing.List["TokenValue"]) -> None: def report(reporter: typing.Callable[[str], None], values: typing.Sequence["TokenValue"]) -> None:
for value in values: for value in values:
reporter(f"{value.value:>18,.8f} {value.token.name}") reporter(f"{value.value:>18,.8f} {value.token.name}")
@staticmethod @staticmethod
def find_by_symbol(values: typing.List["TokenValue"], symbol: str) -> "TokenValue": def find_by_symbol(values: typing.Sequence["TokenValue"], symbol: str) -> "TokenValue":
found = [value for value in values if value.token.symbol_matches(symbol)] found = [value for value in values if value.token.symbol_matches(symbol)]
if len(found) == 0: if len(found) == 0:
raise Exception(f"Token '{symbol}' not found in token values: {values}") raise Exception(f"Token '{symbol}' not found in token values: {values}")
@ -85,7 +85,7 @@ class TokenValue:
return found[0] return found[0]
@staticmethod @staticmethod
def find_by_mint(values: typing.List["TokenValue"], mint: PublicKey) -> "TokenValue": def find_by_mint(values: typing.Sequence["TokenValue"], mint: PublicKey) -> "TokenValue":
found = [value for value in values if value.token.mint == mint] found = [value for value in values if value.token.mint == mint]
if len(found) == 0: if len(found) == 0:
raise Exception(f"Token '{mint}' not found in token values: {values}") raise Exception(f"Token '{mint}' not found in token values: {values}")
@ -96,11 +96,11 @@ class TokenValue:
return found[0] return found[0]
@staticmethod @staticmethod
def find_by_token(values: typing.List["TokenValue"], token: Token) -> "TokenValue": def find_by_token(values: typing.Sequence["TokenValue"], token: Token) -> "TokenValue":
return TokenValue.find_by_mint(values, token.mint) return TokenValue.find_by_mint(values, token.mint)
@staticmethod @staticmethod
def changes(before: typing.List["TokenValue"], after: typing.List["TokenValue"]) -> typing.List["TokenValue"]: def changes(before: typing.Sequence["TokenValue"], after: typing.Sequence["TokenValue"]) -> typing.Sequence["TokenValue"]:
changes: typing.List[TokenValue] = [] changes: typing.List[TokenValue] = []
for before_balance in before: for before_balance in before:
after_balance = TokenValue.find_by_token(after, before_balance.token) after_balance = TokenValue.find_by_token(after, before_balance.token)

View File

@ -133,7 +133,7 @@ _token_out_indices: typing.Dict[InstructionType, int] = {
class MangoInstruction: class MangoInstruction:
def __init__(self, instruction_type: InstructionType, instruction_data: typing.Any, accounts: typing.List[PublicKey]): def __init__(self, instruction_type: InstructionType, instruction_data: typing.Any, accounts: typing.Sequence[PublicKey]):
self.instruction_type = instruction_type self.instruction_type = instruction_type
self.instruction_data = instruction_data self.instruction_data = instruction_data
self.accounts = accounts self.accounts = accounts
@ -203,7 +203,7 @@ class MangoInstruction:
return additional_data return additional_data
@staticmethod @staticmethod
def from_response(context: Context, all_accounts: typing.List[PublicKey], instruction_data: typing.Dict) -> typing.Optional["MangoInstruction"]: def from_response(context: Context, all_accounts: typing.Sequence[PublicKey], instruction_data: typing.Dict) -> typing.Optional["MangoInstruction"]:
program_account_index = instruction_data["programIdIndex"] program_account_index = instruction_data["programIdIndex"]
if all_accounts[program_account_index] != context.program_id: if all_accounts[program_account_index] != context.program_id:
# It's an instruction, it's just not a Mango one. # It's an instruction, it's just not a Mango one.
@ -215,6 +215,8 @@ class MangoInstruction:
decoded = base58.b58decode(data) decoded = base58.b58decode(data)
initial = layouts.MANGO_INSTRUCTION_VARIANT_FINDER.parse(decoded) initial = layouts.MANGO_INSTRUCTION_VARIANT_FINDER.parse(decoded)
parser = layouts.InstructionParsersByVariant[initial.variant] parser = layouts.InstructionParsersByVariant[initial.variant]
if parser is None:
raise Exception(f"Could not find instruction parser for variant {initial.variant}.")
# A whole bunch of accounts are listed for a transaction. Some (or all) of them apply # A whole bunch of accounts are listed for a transaction. Some (or all) of them apply
# to this instruction. The instruction data gives the index of each account it uses, # to this instruction. The instruction data gives the index of each account it uses,
@ -244,20 +246,20 @@ class MangoInstruction:
class TransactionScout: class TransactionScout:
def __init__(self, timestamp: datetime.datetime, signatures: typing.List[str], def __init__(self, timestamp: datetime.datetime, signatures: typing.Sequence[str],
succeeded: bool, group_name: str, accounts: typing.List[PublicKey], succeeded: bool, group_name: str, accounts: typing.Sequence[PublicKey],
instructions: typing.List[typing.Any], messages: typing.List[str], instructions: typing.Sequence[typing.Any], messages: typing.Sequence[str],
pre_token_balances: typing.List[OwnedTokenValue], pre_token_balances: typing.Sequence[OwnedTokenValue],
post_token_balances: typing.List[OwnedTokenValue]): post_token_balances: typing.Sequence[OwnedTokenValue]):
self.timestamp: datetime.datetime = timestamp self.timestamp: datetime.datetime = timestamp
self.signatures: typing.List[str] = signatures self.signatures: typing.Sequence[str] = signatures
self.succeeded: bool = succeeded self.succeeded: bool = succeeded
self.group_name: str = group_name self.group_name: str = group_name
self.accounts: typing.List[PublicKey] = accounts self.accounts: typing.Sequence[PublicKey] = accounts
self.instructions: typing.List[typing.Any] = instructions self.instructions: typing.Sequence[typing.Any] = instructions
self.messages: typing.List[str] = messages self.messages: typing.Sequence[str] = messages
self.pre_token_balances: typing.List[OwnedTokenValue] = pre_token_balances self.pre_token_balances: typing.Sequence[OwnedTokenValue] = pre_token_balances
self.post_token_balances: typing.List[OwnedTokenValue] = post_token_balances self.post_token_balances: typing.Sequence[OwnedTokenValue] = post_token_balances
@property @property
def summary(self) -> str: def summary(self) -> str:
@ -309,7 +311,7 @@ class TransactionScout:
@staticmethod @staticmethod
def from_transaction_response(context: Context, response: typing.Dict) -> "TransactionScout": def from_transaction_response(context: Context, response: typing.Dict) -> "TransactionScout":
def balance_to_token_value(accounts: typing.List[PublicKey], balance: typing.Dict) -> OwnedTokenValue: def balance_to_token_value(accounts: typing.Sequence[PublicKey], balance: typing.Dict) -> OwnedTokenValue:
mint = PublicKey(balance["mint"]) mint = PublicKey(balance["mint"])
account = accounts[balance["accountIndex"]] account = accounts[balance["accountIndex"]]
amount = Decimal(balance["uiTokenAmount"]["amount"]) amount = Decimal(balance["uiTokenAmount"]["amount"])
@ -352,7 +354,7 @@ class TransactionScout:
raise Exception(f"Exception fetching transaction '{signature}'", exception) raise Exception(f"Exception fetching transaction '{signature}'", exception)
def __str__(self) -> str: def __str__(self) -> str:
def format_tokens(account_token_values: typing.List[OwnedTokenValue]) -> str: def format_tokens(account_token_values: typing.Sequence[OwnedTokenValue]) -> str:
if len(account_token_values) == 0: if len(account_token_values) == 0:
return "None" return "None"
return "\n ".join([f"{atv}" for atv in account_token_values]) return "\n ".join([f"{atv}" for atv in account_token_values])
@ -396,7 +398,7 @@ class TransactionScout:
# # 🥭 fetch_all_recent_transaction_signatures function # # 🥭 fetch_all_recent_transaction_signatures function
# #
def fetch_all_recent_transaction_signatures(context: Context, in_the_last: datetime.timedelta = datetime.timedelta(days=1)) -> typing.List[str]: def fetch_all_recent_transaction_signatures(context: Context, in_the_last: datetime.timedelta = datetime.timedelta(days=1)) -> typing.Sequence[str]:
now = datetime.datetime.now() now = datetime.datetime.now()
recency_cutoff = now - in_the_last recency_cutoff = now - in_the_last
recency_cutoff_timestamp = recency_cutoff.timestamp() recency_cutoff_timestamp = recency_cutoff.timestamp()

View File

@ -133,7 +133,7 @@ class PercentageTargetBalance(TargetBalance):
# #
class TargetBalanceParser: class TargetBalanceParser:
def __init__(self, tokens: typing.List[Token]): def __init__(self, tokens: typing.Sequence[Token]):
self.tokens = tokens self.tokens = tokens
def parse(self, to_parse: str) -> TargetBalance: def parse(self, to_parse: str) -> TargetBalance:
@ -176,7 +176,7 @@ class TargetBalanceParser:
# into account for this sorting but we don't need to now so we don't.) # into account for this sorting but we don't need to now so we don't.)
# #
def sort_changes_for_trades(changes: typing.List[TokenValue]) -> typing.List[TokenValue]: def sort_changes_for_trades(changes: typing.Sequence[TokenValue]) -> typing.Sequence[TokenValue]:
return sorted(changes, key=lambda change: change.value) return sorted(changes, key=lambda change: change.value)
@ -186,7 +186,7 @@ def sort_changes_for_trades(changes: typing.List[TokenValue]) -> typing.List[Tok
# #
def calculate_required_balance_changes(current_balances: typing.List[TokenValue], desired_balances: typing.List[TokenValue]) -> typing.List[TokenValue]: def calculate_required_balance_changes(current_balances: typing.Sequence[TokenValue], desired_balances: typing.Sequence[TokenValue]) -> typing.Sequence[TokenValue]:
changes: typing.List[TokenValue] = [] changes: typing.List[TokenValue] = []
for desired in desired_balances: for desired in desired_balances:
current = TokenValue.find_by_token(current_balances, desired.token) current = TokenValue.find_by_token(current_balances, desired.token)
@ -211,7 +211,7 @@ def calculate_required_balance_changes(current_balances: typing.List[TokenValue]
class FilterSmallChanges: class FilterSmallChanges:
def __init__(self, action_threshold: Decimal, balances: typing.List[TokenValue], prices: typing.List[TokenValue]): def __init__(self, action_threshold: Decimal, balances: typing.Sequence[TokenValue], prices: typing.Sequence[TokenValue]):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.prices: typing.Dict[str, TokenValue] = {} self.prices: typing.Dict[str, TokenValue] = {}
total = Decimal(0) total = Decimal(0)
@ -258,7 +258,7 @@ class FilterSmallChanges:
class WalletBalancer(metaclass=abc.ABCMeta): class WalletBalancer(metaclass=abc.ABCMeta):
@abc.abstractmethod @abc.abstractmethod
def balance(self, prices: typing.List[TokenValue]): def balance(self, prices: typing.Sequence[TokenValue]):
raise NotImplementedError("WalletBalancer.balance() is not implemented on the base type.") raise NotImplementedError("WalletBalancer.balance() is not implemented on the base type.")
@ -270,7 +270,7 @@ class WalletBalancer(metaclass=abc.ABCMeta):
class NullWalletBalancer(WalletBalancer): class NullWalletBalancer(WalletBalancer):
def balance(self, prices: typing.List[TokenValue]): def balance(self, prices: typing.Sequence[TokenValue]):
pass pass
@ -280,17 +280,17 @@ class NullWalletBalancer(WalletBalancer):
# #
class LiveWalletBalancer(WalletBalancer): class LiveWalletBalancer(WalletBalancer):
def __init__(self, context: Context, wallet: Wallet, group: Group, trade_executor: TradeExecutor, action_threshold: Decimal, tokens: typing.List[Token], target_balances: typing.List[TargetBalance]): def __init__(self, context: Context, wallet: Wallet, group: Group, trade_executor: TradeExecutor, action_threshold: Decimal, tokens: typing.Sequence[Token], target_balances: typing.Sequence[TargetBalance]):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.context: Context = context self.context: Context = context
self.wallet: Wallet = wallet self.wallet: Wallet = wallet
self.group: Group = group self.group: Group = group
self.trade_executor: TradeExecutor = trade_executor self.trade_executor: TradeExecutor = trade_executor
self.action_threshold: Decimal = action_threshold self.action_threshold: Decimal = action_threshold
self.tokens: typing.List[Token] = tokens self.tokens: typing.Sequence[Token] = tokens
self.target_balances: typing.List[TargetBalance] = target_balances self.target_balances: typing.Sequence[TargetBalance] = target_balances
def balance(self, prices: typing.List[TokenValue]): def balance(self, prices: typing.Sequence[TokenValue]):
padding = "\n " padding = "\n "
def balances_report(balances) -> str: def balances_report(balances) -> str:
@ -323,7 +323,7 @@ class LiveWalletBalancer(WalletBalancer):
updated_balances = self._fetch_balances() updated_balances = self._fetch_balances()
self.logger.info(f"Finishing balances: {padding}{balances_report(updated_balances)}") self.logger.info(f"Finishing balances: {padding}{balances_report(updated_balances)}")
def _make_changes(self, balance_changes: typing.List[TokenValue]): def _make_changes(self, balance_changes: typing.Sequence[TokenValue]):
self.logger.info(f"Balance changes to make: {balance_changes}") self.logger.info(f"Balance changes to make: {balance_changes}")
quote = self.group.shared_quote_token.token.symbol quote = self.group.shared_quote_token.token.symbol
for change in balance_changes: for change in balance_changes:
@ -333,7 +333,7 @@ class LiveWalletBalancer(WalletBalancer):
else: else:
self.trade_executor.buy(market_symbol, change.value.copy_abs()) self.trade_executor.buy(market_symbol, change.value.copy_abs())
def _fetch_balances(self) -> typing.List[TokenValue]: def _fetch_balances(self) -> typing.Sequence[TokenValue]:
balances: typing.List[TokenValue] = [] balances: typing.List[TokenValue] = []
for token in self.tokens: for token in self.tokens:
balance = TokenValue.fetch_total_value(self.context, self.wallet.address, token) balance = TokenValue.fetch_total_value(self.context, self.wallet.address, token)

View File

@ -1,5 +1,3 @@
import datetime
from decimal import Decimal from decimal import Decimal
from typing import NamedTuple from typing import NamedTuple
from pyserum import market from pyserum import market
@ -37,6 +35,11 @@ def fake_token() -> mango.Token:
return mango.Token("FAKE", "Fake Token", fake_seeded_public_key("fake token"), Decimal(6)) return mango.Token("FAKE", "Fake Token", fake_seeded_public_key("fake token"), Decimal(6))
def fake_token_info() -> mango.TokenInfo:
token = fake_token()
return mango.TokenInfo(token, fake_seeded_public_key("root bank"), Decimal(7))
def fake_context() -> mango.Context: def fake_context() -> mango.Context:
context = mango.Context(cluster="test", context = mango.Context(cluster="test",
cluster_url="http://localhost", cluster_url="http://localhost",
@ -48,13 +51,6 @@ def fake_context() -> mango.Context:
return context return context
def fake_index() -> mango.Index:
token = fake_token()
borrow = mango.TokenValue(token, Decimal(0))
deposit = mango.TokenValue(token, Decimal(0))
return mango.Index(mango.Version.V1, token, datetime.datetime.now(), borrow, deposit)
def fake_market() -> market.Market: def fake_market() -> market.Market:
Container = NamedTuple("Container", [("own_address", PublicKey), ("vault_signer_nonce", int)]) Container = NamedTuple("Container", [("own_address", PublicKey), ("vault_signer_nonce", int)])
container = Container(own_address=fake_seeded_public_key("market address"), vault_signer_nonce=2) container = Container(own_address=fake_seeded_public_key("market address"), vault_signer_nonce=2)

File diff suppressed because one or more lines are too long

View File

@ -3,9 +3,8 @@ from .fakes import fake_account_info, fake_context, fake_seeded_public_key
import typing import typing
from datetime import datetime
from decimal import Decimal from decimal import Decimal
from solana.publickey import PublicKey from mango.layouts import layouts
# #
# Mocks are more involved than fakes, but do tend to allow more introspection. # Mocks are more involved than fakes, but do tend to allow more introspection.
@ -22,52 +21,26 @@ USDC = token_lookup.find_by_symbol_or_raise("USDC")
def mock_group(): def mock_group():
account_info = fake_account_info() account_info = fake_account_info()
name = "FAKE_GROUP" name = "FAKE_GROUP"
account_flags = mango.MangoAccountFlags(mango.Version.V1, True, False, True, False) meta_data = mango.Metadata(layouts.DATA_TYPE.Group, mango.Version.V1, True)
btc_info = mango.TokenInfo(BTC, fake_seeded_public_key("root bank"), Decimal(6))
def index(token): usdc_info = mango.TokenInfo(USDC, fake_seeded_public_key("root bank"), Decimal(6))
borrow = mango.TokenValue(token, Decimal(1)) token_infos = [btc_info, None, usdc_info]
deposit = mango.TokenValue(token, Decimal(1)) spot_markets = []
return mango.Index(mango.Version.V1, token, datetime.now(), borrow, deposit) perp_markets = []
oracles = []
basket_tokens = [
mango.BasketToken(ETH, fake_seeded_public_key("ETH vault"), index(ETH)),
mango.BasketToken(BTC, fake_seeded_public_key("BTC vault"), index(ETH)),
mango.BasketToken(SOL, fake_seeded_public_key("SOL vault"), index(ETH)),
mango.BasketToken(SRM, fake_seeded_public_key("SRM vault"), index(ETH)),
mango.BasketToken(USDC, fake_seeded_public_key("USDC vault"), index(ETH))
]
markets = []
signer_nonce = Decimal(1) signer_nonce = Decimal(1)
signer_key = fake_seeded_public_key("signer key") signer_key = fake_seeded_public_key("signer key")
admin_key = fake_seeded_public_key("admin key")
dex_program_id = fake_seeded_public_key("DEX program ID") dex_program_id = fake_seeded_public_key("DEX program ID")
total_deposits = [ cache_key = fake_seeded_public_key("cache key")
mango.TokenValue(ETH, Decimal(1000)), valid_interval = Decimal(7)
mango.TokenValue(BTC, Decimal(1000)),
mango.TokenValue(SOL, Decimal(1000)),
mango.TokenValue(SRM, Decimal(1000)),
mango.TokenValue(USDC, Decimal(1000)),
]
total_borrows = [
mango.TokenValue(ETH, Decimal(0)),
mango.TokenValue(BTC, Decimal(0)),
mango.TokenValue(SOL, Decimal(0)),
mango.TokenValue(SRM, Decimal(0)),
mango.TokenValue(USDC, Decimal(0)),
]
maint_coll_ratio = Decimal("1.1")
init_coll_ratio = Decimal("1.2")
srm_vault = fake_seeded_public_key("SRM vault")
admin = fake_seeded_public_key("admin")
borrow_limits = [Decimal(10), Decimal(10), Decimal(10), Decimal(10), Decimal(10)]
return mango.Group(account_info, mango.Version.V1, name, account_flags, return mango.Group(account_info, mango.Version.V1, name, meta_data, token_infos,
basket_tokens, markets, signer_nonce, signer_key, dex_program_id, spot_markets, perp_markets, oracles, signer_nonce, signer_key,
total_deposits, total_borrows, maint_coll_ratio, init_coll_ratio, admin_key, dex_program_id, cache_key, valid_interval)
srm_vault, admin, borrow_limits)
def mock_prices(prices: typing.List[str]): def mock_prices(prices: typing.Sequence[str]):
eth, btc, sol, srm, usdc = prices eth, btc, sol, srm, usdc = prices
return [ return [
mango.TokenValue(ETH, Decimal(eth)), mango.TokenValue(ETH, Decimal(eth)),
@ -78,48 +51,13 @@ def mock_prices(prices: typing.List[str]):
] ]
def mock_margin_account(group: mango.Group, deposits: typing.List[str], borrows: typing.List[str], openorders: typing.List[typing.Optional[mango.OpenOrders]]):
eth, btc, sol, srm, usdc = deposits
token_deposits = [
mango.TokenValue(ETH, Decimal(eth)),
mango.TokenValue(BTC, Decimal(btc)),
mango.TokenValue(SOL, Decimal(sol)),
mango.TokenValue(SRM, Decimal(srm)),
mango.TokenValue(USDC, Decimal(usdc)),
]
eth, btc, sol, srm, usdc = borrows
token_borrows = [
mango.TokenValue(ETH, Decimal(eth)),
mango.TokenValue(BTC, Decimal(btc)),
mango.TokenValue(SOL, Decimal(sol)),
mango.TokenValue(SRM, Decimal(srm)),
mango.TokenValue(USDC, Decimal(usdc)),
]
account_flags = mango.MangoAccountFlags(mango.Version.V1, True, False, True, False)
has_borrows = False
owner = fake_seeded_public_key("owner")
being_liquidated = False
open_orders_keys: typing.List[typing.Optional[PublicKey]] = []
for oo in openorders:
if oo is None:
open_orders_keys += [None]
else:
open_orders_keys += [oo.address]
margin_account = mango.MarginAccount(fake_account_info(), mango.Version.V1, account_flags,
has_borrows, group.address, owner, being_liquidated,
token_deposits, token_borrows, open_orders_keys)
margin_account.open_orders_accounts = openorders
return margin_account
def mock_open_orders(base_token_free: Decimal = Decimal(0), base_token_total: Decimal = Decimal(0), quote_token_free: Decimal = Decimal(0), quote_token_total: Decimal = Decimal(0), referrer_rebate_accrued: Decimal = Decimal(0)): def mock_open_orders(base_token_free: Decimal = Decimal(0), base_token_total: Decimal = Decimal(0), quote_token_free: Decimal = Decimal(0), quote_token_total: Decimal = Decimal(0), referrer_rebate_accrued: Decimal = Decimal(0)):
account_info = fake_account_info() account_info = fake_account_info()
program_id = fake_seeded_public_key("program ID") program_id = fake_seeded_public_key("program ID")
market = fake_seeded_public_key("market") market = fake_seeded_public_key("market")
owner = fake_seeded_public_key("owner") owner = fake_seeded_public_key("owner")
flags = mango.SerumAccountFlags(mango.Version.V1, True, False, True, False, False, False, False, False) flags = mango.AccountFlags(mango.Version.V1, True, False, True, False, False, False, False, False)
return mango.OpenOrders(account_info, mango.Version.V1, program_id, flags, market, return mango.OpenOrders(account_info, mango.Version.V1, program_id, flags, market,
owner, base_token_free, base_token_total, quote_token_free, owner, base_token_free, base_token_total, quote_token_free,
quote_token_total, Decimal(0), Decimal(0), [], [], quote_token_total, Decimal(0), Decimal(0), [], [],

35
tests/test_account.py Normal file
View File

@ -0,0 +1,35 @@
from .context import mango
from .fakes import fake_account_info, fake_seeded_public_key
from decimal import Decimal
from mango.layouts import layouts
def test_construction():
account_info = fake_account_info()
meta_data = mango.Metadata(layouts.DATA_TYPE.Group, mango.Version.V1, True)
group = fake_seeded_public_key("group")
owner = fake_seeded_public_key("owner")
in_basket = [Decimal(1), Decimal(0), Decimal(3)]
deposits = [Decimal(10), Decimal(0), Decimal(5)]
borrows = [Decimal(0), Decimal(0), Decimal(0)]
spot_open_orders = [fake_seeded_public_key("spot1"), fake_seeded_public_key(
"spot2"), fake_seeded_public_key("spot3")]
# TODO - this isn't right.
perp_accounts = [fake_seeded_public_key("perp1"), fake_seeded_public_key("perp2"), fake_seeded_public_key("perp3")]
actual = mango.Account(account_info, mango.Version.V1, meta_data, group, owner,
in_basket, deposits, borrows, spot_open_orders, perp_accounts)
assert actual is not None
assert actual.logger is not None
assert actual.version == mango.Version.V1
assert actual.meta_data == meta_data
assert actual.group == group
assert actual.owner == owner
assert actual.in_basket == in_basket
assert actual.deposits == deposits
assert actual.borrows == borrows
assert actual.spot_open_orders == spot_open_orders
assert actual.perp_accounts == perp_accounts

View File

@ -10,8 +10,8 @@ def test_constructor():
bids: bool = True bids: bool = True
asks: bool = True asks: bool = True
disabled: bool = True disabled: bool = True
actual = mango.SerumAccountFlags(mango.Version.V1, initialized, market, open_orders, actual = mango.AccountFlags(mango.Version.V1, initialized, market, open_orders,
request_queue, event_queue, bids, asks, disabled) request_queue, event_queue, bids, asks, disabled)
assert actual is not None assert actual is not None
assert actual.logger is not None assert actual.logger is not None
assert actual.version == mango.Version.V1 assert actual.version == mango.Version.V1
@ -24,9 +24,9 @@ def test_constructor():
assert actual.asks == asks assert actual.asks == asks
assert actual.disabled == disabled assert actual.disabled == disabled
actual2 = mango.SerumAccountFlags(mango.Version.V2, not initialized, not market, actual2 = mango.AccountFlags(mango.Version.V2, not initialized, not market,
not open_orders, not request_queue, not event_queue, not open_orders, not request_queue, not event_queue,
not bids, not asks, not disabled) not bids, not asks, not disabled)
assert actual2 is not None assert actual2 is not None
assert actual2.logger is not None assert actual2.logger is not None
assert actual2.version == mango.Version.V2 assert actual2.version == mango.Version.V2

View File

@ -1,5 +1,4 @@
from .context import mango from .context import mango
from .fakes import fake_context
def test_account_liquidator_constructor(): def test_account_liquidator_constructor():
@ -16,39 +15,3 @@ def test_null_account_liquidator_constructor():
actual = mango.NullAccountLiquidator() actual = mango.NullAccountLiquidator()
assert actual is not None assert actual is not None
assert actual.logger is not None assert actual.logger is not None
def test_actual_account_liquidator_constructor():
context: mango.Context = fake_context()
wallet: mango.Wallet = {"fake": "Walle"}
actual = mango.ActualAccountLiquidator(context, wallet)
assert actual is not None
assert actual.logger is not None
assert actual.context == context
assert actual.wallet == wallet
def test_force_cancel_orders_account_liquidator_constructor():
context: mango.Context = fake_context()
wallet: mango.Wallet = {"fake": "Walle"}
actual = mango.ForceCancelOrdersAccountLiquidator(context, wallet)
assert actual is not None
assert actual.logger is not None
assert actual.context == context
assert actual.wallet == wallet
def test_reporting_account_liquidator_constructor():
inner = mango.NullAccountLiquidator()
context: mango.Context = fake_context()
wallet: mango.Wallet = {"fake": "Walle"}
liquidations_publisher: mango.EventSource[mango.LiquidationEvent] = mango.EventSource()
liquidator_name = "Test"
actual = mango.ReportingAccountLiquidator(inner, context, wallet, liquidations_publisher, liquidator_name)
assert actual is not None
assert actual.logger is not None
assert actual.inner == inner
assert actual.context == context
assert actual.wallet == wallet
assert actual.liquidations_publisher == liquidations_publisher
assert actual.liquidator_name == liquidator_name

View File

@ -1,102 +0,0 @@
from .context import mango
from .fakes import fake_account_info, fake_seeded_public_key
from datetime import datetime, timedelta
from decimal import Decimal
from solana.publickey import PublicKey
def test_aggregator_config_constructor():
description: str = "Test Aggregator Config"
decimals: Decimal = Decimal(5)
restart_delay: Decimal = Decimal(30)
max_submissions: Decimal = Decimal(10)
min_submissions: Decimal = Decimal(2)
reward_amount: Decimal = Decimal(30)
reward_token_account: PublicKey = fake_seeded_public_key("reward token account")
actual = mango.AggregatorConfig(mango.Version.V1, description, decimals, restart_delay,
max_submissions, min_submissions, reward_amount, reward_token_account)
assert actual is not None
assert actual.logger is not None
assert actual.version == mango.Version.V1
assert actual.description == description
assert actual.decimals == decimals
assert actual.restart_delay == restart_delay
assert actual.max_submissions == max_submissions
assert actual.min_submissions == min_submissions
assert actual.reward_amount == reward_amount
assert actual.reward_token_account == reward_token_account
def test_round_constructor():
id: Decimal = Decimal(85)
updated_at: datetime = datetime.now()
created_at: datetime = updated_at - timedelta(minutes=5)
actual = mango.Round(mango.Version.V1, id, created_at, updated_at)
assert actual is not None
assert actual.logger is not None
assert actual.version == mango.Version.V1
assert actual.id == id
assert actual.created_at == created_at
assert actual.updated_at == updated_at
# def __init__(self, version: Version, round_id: Decimal, median: Decimal, created_at: datetime.datetime, updated_at: datetime.datetime):
def test_answer_constructor():
round_id: Decimal = Decimal(85)
median: Decimal = Decimal(25)
updated_at: datetime = datetime.now()
created_at: datetime = updated_at - timedelta(minutes=5)
actual = mango.Answer(mango.Version.V1, round_id, median, created_at, updated_at)
assert actual is not None
assert actual.logger is not None
assert actual.version == mango.Version.V1
assert actual.round_id == round_id
assert actual.median == median
assert actual.created_at == created_at
assert actual.updated_at == updated_at
def test_aggregator_constructor():
account_info = fake_account_info()
description: str = "Test Aggregator Config"
decimals: Decimal = Decimal(5)
restart_delay: Decimal = Decimal(30)
max_submissions: Decimal = Decimal(10)
min_submissions: Decimal = Decimal(2)
reward_amount: Decimal = Decimal(30)
reward_token_account: PublicKey = fake_seeded_public_key("reward token account")
config = mango.AggregatorConfig(mango.Version.V1, description, decimals, restart_delay,
max_submissions, min_submissions, reward_amount, reward_token_account)
initialized = True
name = "Test Aggregator"
owner = fake_seeded_public_key("owner")
id: Decimal = Decimal(85)
updated_at: datetime = datetime.now()
created_at: datetime = updated_at - timedelta(minutes=5)
round = mango.Round(mango.Version.V1, id, created_at, updated_at)
round_submissions = fake_seeded_public_key("round submissions")
round_id: Decimal = Decimal(85)
median: Decimal = Decimal(25)
answer = mango.Answer(mango.Version.V1, round_id, median, created_at, updated_at)
answer_submissions = fake_seeded_public_key("answer submissions")
actual = mango.Aggregator(account_info, mango.Version.V1, config, initialized,
name, owner, round, round_submissions, answer, answer_submissions)
assert actual is not None
assert actual.logger is not None
assert actual.account_info == account_info
assert actual.version == mango.Version.V1
assert actual.config == config
assert actual.initialized == initialized
assert actual.name == name
assert actual.owner == owner
assert actual.round == round
assert actual.round_submissions == round_submissions
assert actual.answer == answer
assert actual.answer_submissions == answer_submissions

View File

@ -1,14 +0,0 @@
from .context import mango
from .fakes import fake_index, fake_seeded_public_key, fake_token
def test_constructor():
token = fake_token()
vault = fake_seeded_public_key("vault")
index = fake_index()
actual = mango.BasketToken(token, vault, index)
assert actual is not None
assert actual.logger is not None
assert actual.token == token
assert actual.vault == vault
assert actual.index == index

File diff suppressed because one or more lines are too long

View File

@ -1,19 +0,0 @@
from .context import mango
from .fakes import fake_token
from decimal import Decimal
import datetime
def test_constructor():
last_update = datetime.datetime.now()
token = fake_token()
borrow = mango.TokenValue(token, Decimal(27))
deposit = mango.TokenValue(token, Decimal(62))
actual = mango.Index(mango.Version.V1, token, last_update, borrow, deposit)
assert actual is not None
assert actual.logger is not None
assert actual.last_update == last_update
assert actual.borrow == borrow
assert actual.deposit == deposit

View File

@ -1,7 +1,7 @@
import typing import typing
from .context import mango from .context import mango
from .fakes import fake_account_info, fake_context, fake_index, fake_market, fake_seeded_public_key, fake_token from .fakes import fake_context, fake_market, fake_seeded_public_key, fake_token
from decimal import Decimal from decimal import Decimal
from pyserum.enums import OrderType, Side from pyserum.enums import OrderType, Side
@ -20,64 +20,6 @@ def test_instruction_builder_constructor():
assert succeeded assert succeeded
def test_force_cancel_orders_instruction_builder_constructor():
context: mango.Context = fake_context()
group: mango.Group = {"fake": "Group"}
wallet: mango.Wallet = {"fake": "Wallet"}
margin_account: mango.MarginAccount = {"fake": "MarginAccount"}
market_metadata: mango.MarketMetadata = {"fake": "MarketMetadata"}
market: Market = {"fake": "Market"}
oracles: mango.typing.List[PublicKey] = [fake_seeded_public_key("oracle")]
dex_signer: mango.PublicKey = fake_seeded_public_key("DEX signer")
actual = mango.ForceCancelOrdersInstructionBuilder(context, group, wallet, margin_account,
market_metadata, market, oracles,
dex_signer)
assert actual is not None
assert actual.logger is not None
assert actual.context == context
assert actual.group == group
assert actual.wallet == wallet
assert actual.margin_account == margin_account
assert actual.market_metadata == market_metadata
assert actual.market == market
assert actual.oracles == oracles
assert actual.dex_signer == dex_signer
def test_liquidate_instruction_builder_constructor():
context: mango.Context = fake_context()
group: mango.Group = {"fake": "Group"}
wallet: mango.Wallet = {"fake": "Wallet"}
margin_account: mango.MarginAccount = {"fake": "MarginAccount"}
oracles: mango.typing.List[PublicKey] = [fake_seeded_public_key("oracle")]
input_token = mango.BasketToken(fake_token(), fake_seeded_public_key("vault"), fake_index())
input_token_value = mango.TokenValue(input_token.token, Decimal(30))
output_token = mango.BasketToken(fake_token(), fake_seeded_public_key("vault"), fake_index())
output_token_value = mango.TokenValue(output_token.token, Decimal(40))
wallet_input_token_account = mango.TokenAccount(
fake_account_info(), mango.Version.V1, fake_seeded_public_key("owner"), input_token_value)
wallet_output_token_account = mango.TokenAccount(
fake_account_info(), mango.Version.V1, fake_seeded_public_key("owner"), output_token_value)
maximum_input_amount = Decimal(50)
actual = mango.LiquidateInstructionBuilder(context, group, wallet, margin_account,
oracles, input_token, output_token,
wallet_input_token_account,
wallet_output_token_account,
maximum_input_amount)
assert actual is not None
assert actual.logger is not None
assert actual.context == context
assert actual.group == group
assert actual.wallet == wallet
assert actual.margin_account == margin_account
assert actual.oracles == oracles
assert actual.input_token == input_token
assert actual.output_token == output_token
assert actual.wallet_input_token_account == wallet_input_token_account
assert actual.wallet_output_token_account == wallet_output_token_account
assert actual.maximum_input_amount == maximum_input_amount
def test_create_spl_account_instruction_builder_constructor(): def test_create_spl_account_instruction_builder_constructor():
context: mango.Context = fake_context() context: mango.Context = fake_context()
wallet: mango.Wallet = {"fake": "Wallet"} wallet: mango.Wallet = {"fake": "Wallet"}
@ -181,7 +123,7 @@ def test_consume_events_instruction_builder_constructor():
context: mango.Context = fake_context() context: mango.Context = fake_context()
wallet: mango.Wallet = {"fake": "Wallet"} wallet: mango.Wallet = {"fake": "Wallet"}
market: Market = {"fake": "Market"} market: Market = {"fake": "Market"}
open_orders_addresses: typing.List[PublicKey] = [fake_seeded_public_key("open orders account")] open_orders_addresses: typing.Sequence[PublicKey] = [fake_seeded_public_key("open orders account")]
limit: int = 64 limit: int = 64
actual = mango.ConsumeEventsInstructionBuilder(context, wallet, market, open_orders_addresses, limit) actual = mango.ConsumeEventsInstructionBuilder(context, wallet, market, open_orders_addresses, limit)
assert actual is not None assert actual is not None

View File

@ -1,406 +1,406 @@
from .context import mango # from .context import mango
from .mocks import mock_group, mock_prices, mock_margin_account, mock_open_orders # from .mocks import mock_group, mock_prices, mock_margin_account, mock_open_orders
import typing # import typing
from decimal import Decimal # from decimal import Decimal
def test_constructor(): # def test_constructor():
group = mock_group() # group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"]) # prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "1000"] # deposits = ["0", "0", "0", "0", "1000"]
borrows = ["0", "0", "0", "0", "0"] # borrows = ["0", "0", "0", "0", "0"]
margin_account = mock_margin_account(group, deposits, borrows, []) # margin_account = mock_margin_account(group, deposits, borrows, [])
balance_sheet = margin_account.get_balance_sheet_totals(group, prices) # balance_sheet = margin_account.get_balance_sheet_totals(group, prices)
balances = margin_account.get_intrinsic_balances(group) # balances = margin_account.get_intrinsic_balances(group)
state = mango.LiquidatableState.RIPE | mango.LiquidatableState.LIQUIDATABLE | mango.LiquidatableState.ABOVE_WATER # state = mango.LiquidatableState.RIPE | mango.LiquidatableState.LIQUIDATABLE | mango.LiquidatableState.ABOVE_WATER
worthwhile_threshold = Decimal(1) # worthwhile_threshold = Decimal(1)
actual = mango.LiquidatableReport(group, prices, margin_account, balance_sheet, # actual = mango.LiquidatableReport(group, prices, margin_account, balance_sheet,
balances, state, worthwhile_threshold) # balances, state, worthwhile_threshold)
assert actual is not None # assert actual is not None
assert actual.logger is not None # assert actual.logger is not None
assert actual.balance_sheet == balance_sheet # assert actual.balance_sheet == balance_sheet
assert actual.balances == balances # assert actual.balances == balances
assert actual.state != mango.LiquidatableState.UNSET # assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE # assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE # assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert actual.state & mango.LiquidatableState.ABOVE_WATER # assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert not (actual.state & mango.LiquidatableState.WORTHWHILE) # assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
assert actual.worthwhile_threshold == worthwhile_threshold # assert actual.worthwhile_threshold == worthwhile_threshold
def test_non_ripe_account_no_openorders(): # def test_non_ripe_account_no_openorders():
group = mock_group() # group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"]) # prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "1000"] # deposits = ["0", "0", "0", "0", "1000"]
borrows = ["0", "0", "0", "0", "0"] # borrows = ["0", "0", "0", "0", "0"]
margin_account = mock_margin_account(group, deposits, borrows, []) # margin_account = mock_margin_account(group, deposits, borrows, [])
worthwhile_threshold = Decimal("0.01") # worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold) # actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state == mango.LiquidatableState.UNSET # assert actual.state == mango.LiquidatableState.UNSET
assert not (actual.state & mango.LiquidatableState.RIPE) # assert not (actual.state & mango.LiquidatableState.RIPE)
assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE) # assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
assert not (actual.state & mango.LiquidatableState.ABOVE_WATER) # assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
assert not (actual.state & mango.LiquidatableState.WORTHWHILE) # assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
def test_ripe_account_no_openorders(): # def test_ripe_account_no_openorders():
group = mock_group() # group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"]) # prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "1000"] # deposits = ["0", "0", "0", "0", "1000"]
# 170 @ 5 = 850 for a collateral ratio of 1000/850 = 117% - ripe but not liquidatable # # 170 @ 5 = 850 for a collateral ratio of 1000/850 = 117% - ripe but not liquidatable
borrows = ["0", "0", "0", "170", "0"] # borrows = ["0", "0", "0", "170", "0"]
margin_account = mock_margin_account(group, deposits, borrows, []) # margin_account = mock_margin_account(group, deposits, borrows, [])
worthwhile_threshold = Decimal("0.01") # worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold) # actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET # assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE # assert actual.state & mango.LiquidatableState.RIPE
assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE) # assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
assert actual.state & mango.LiquidatableState.ABOVE_WATER # assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert actual.state & mango.LiquidatableState.WORTHWHILE # assert actual.state & mango.LiquidatableState.WORTHWHILE
def test_liquidatable_account_no_openorders(): # def test_liquidatable_account_no_openorders():
group = mock_group() # group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"]) # prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "1000"] # deposits = ["0", "0", "0", "0", "1000"]
# 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water # # 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water
borrows = ["0", "0", "0", "200", "0"] # borrows = ["0", "0", "0", "200", "0"]
margin_account = mock_margin_account(group, deposits, borrows, []) # margin_account = mock_margin_account(group, deposits, borrows, [])
worthwhile_threshold = Decimal("0.01") # worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold) # actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET # assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE # assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE # assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert not (actual.state & mango.LiquidatableState.ABOVE_WATER) # assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
assert not (actual.state & mango.LiquidatableState.WORTHWHILE) # assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
def test_above_water_account_no_openorders(): # def test_above_water_account_no_openorders():
group = mock_group() # group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"]) # prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "1000"] # deposits = ["0", "0", "0", "0", "1000"]
# 199.998 @ 5 = 999.99 for a collateral ratio of 1000/999.99 = 100.001% - liquidatable and above # # 199.998 @ 5 = 999.99 for a collateral ratio of 1000/999.99 = 100.001% - liquidatable and above
# water but not worthwhile because the $0.01 available is not greater than the whorthwhile_threshold. # # water but not worthwhile because the $0.01 available is not greater than the whorthwhile_threshold.
borrows = ["0", "0", "0", "199.998", "0"] # borrows = ["0", "0", "0", "199.998", "0"]
margin_account = mock_margin_account(group, deposits, borrows, []) # margin_account = mock_margin_account(group, deposits, borrows, [])
worthwhile_threshold = Decimal("0.01") # worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold) # actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET # assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE # assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE # assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert actual.state & mango.LiquidatableState.ABOVE_WATER # assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert not (actual.state & mango.LiquidatableState.WORTHWHILE) # assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
def test_worthwhile_account_no_openorders(): # def test_worthwhile_account_no_openorders():
group = mock_group() # group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"]) # prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "1000"] # deposits = ["0", "0", "0", "0", "1000"]
# 199.99 @ 5 = 999.95 for a collateral ratio of 1000/999.99 = 100.005% - liquidatable, above water # # 199.99 @ 5 = 999.95 for a collateral ratio of 1000/999.99 = 100.005% - liquidatable, above water
# and worthwhile because the $0.05 available is greater than the whorthwhile_threshold. # # and worthwhile because the $0.05 available is greater than the whorthwhile_threshold.
borrows = ["0", "0", "0", "199.99", "0"] # borrows = ["0", "0", "0", "199.99", "0"]
margin_account = mock_margin_account(group, deposits, borrows, []) # margin_account = mock_margin_account(group, deposits, borrows, [])
worthwhile_threshold = Decimal("0.01") # worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold) # actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET # assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE # assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE # assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert actual.state & mango.LiquidatableState.ABOVE_WATER # assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert actual.state & mango.LiquidatableState.WORTHWHILE # assert actual.state & mango.LiquidatableState.WORTHWHILE
def test_non_ripe_account_openorders_base(): # def test_non_ripe_account_openorders_base():
group = mock_group() # group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"]) # prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "1000"] # deposits = ["0", "0", "0", "0", "1000"]
borrows = ["0", "0", "0", "100", "0"] # borrows = ["0", "0", "0", "100", "0"]
open_orders: typing.List[mango.OpenOrders] = [None, # open_orders: typing.Sequence[mango.OpenOrders] = [None,
None, # None,
None, # None,
mock_open_orders(base_token_total=Decimal(0)), # mock_open_orders(base_token_total=Decimal(0)),
None] # None]
margin_account = mock_margin_account(group, deposits, borrows, open_orders) # margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01") # worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold) # actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET # assert actual.state != mango.LiquidatableState.UNSET
assert not (actual.state & mango.LiquidatableState.RIPE) # assert not (actual.state & mango.LiquidatableState.RIPE)
assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE) # assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
assert actual.state & mango.LiquidatableState.ABOVE_WATER # assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert actual.state & mango.LiquidatableState.WORTHWHILE # assert actual.state & mango.LiquidatableState.WORTHWHILE
# The idea of these OpenOrders tests is that the deposits and borrows remain the same and the only # # The idea of these OpenOrders tests is that the deposits and borrows remain the same and the only
# differences are through changes in OpenOrders balances. # # differences are through changes in OpenOrders balances.
def test_ripe_account_openorders_base(): # def test_ripe_account_openorders_base():
group = mock_group() # group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"]) # prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "900"] # deposits = ["0", "0", "0", "0", "900"]
borrows = ["0", "0", "0", "200", "0"] # borrows = ["0", "0", "0", "200", "0"]
# 1150/1000 = 115% - ripe but not liquidatable # # 1150/1000 = 115% - ripe but not liquidatable
open_orders: typing.List[mango.OpenOrders] = [None, # open_orders: typing.Sequence[mango.OpenOrders] = [None,
None, # None,
None, # None,
mock_open_orders(base_token_total=Decimal(50)), # mock_open_orders(base_token_total=Decimal(50)),
None # None
] # ]
margin_account = mock_margin_account(group, deposits, borrows, open_orders) # margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01") # worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold) # actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET # assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE # assert actual.state & mango.LiquidatableState.RIPE
assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE) # assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
assert actual.state & mango.LiquidatableState.ABOVE_WATER # assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert actual.state & mango.LiquidatableState.WORTHWHILE # assert actual.state & mango.LiquidatableState.WORTHWHILE
def test_liquidatable_account_openorders_base(): # def test_liquidatable_account_openorders_base():
group = mock_group() # group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"]) # prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "900"] # deposits = ["0", "0", "0", "0", "900"]
borrows = ["0", "0", "0", "200", "0"] # borrows = ["0", "0", "0", "200", "0"]
# 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water # # 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water
open_orders: typing.List[mango.OpenOrders] = [None, # open_orders: typing.Sequence[mango.OpenOrders] = [None,
None, # None,
None, # None,
mock_open_orders(base_token_total=Decimal(20)), # mock_open_orders(base_token_total=Decimal(20)),
None] # None]
margin_account = mock_margin_account(group, deposits, borrows, open_orders) # margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01") # worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold) # actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET # assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE # assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE # assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert not (actual.state & mango.LiquidatableState.ABOVE_WATER) # assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
assert not (actual.state & mango.LiquidatableState.WORTHWHILE) # assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
def test_above_water_account_openorders_base(): # def test_above_water_account_openorders_base():
group = mock_group() # group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"]) # prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "900"] # deposits = ["0", "0", "0", "0", "900"]
borrows = ["0", "0", "0", "200", "0"] # borrows = ["0", "0", "0", "200", "0"]
# 900 + (20.002 @ 5) = 1000.01 for a collateral ratio of 1000.01/1000 = 100.001% - liquidatable and above # # 900 + (20.002 @ 5) = 1000.01 for a collateral ratio of 1000.01/1000 = 100.001% - liquidatable and above
# water but not worthwhile because the $0.01 available is not greater than the whorthwhile_threshold. # # water but not worthwhile because the $0.01 available is not greater than the whorthwhile_threshold.
open_orders: typing.List[mango.OpenOrders] = [None, # open_orders: typing.Sequence[mango.OpenOrders] = [None,
None, # None,
None, # None,
mock_open_orders(base_token_total=Decimal("20.002")), # mock_open_orders(base_token_total=Decimal("20.002")),
None] # None]
margin_account = mock_margin_account(group, deposits, borrows, open_orders) # margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01") # worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold) # actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET # assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE # assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE # assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert actual.state & mango.LiquidatableState.ABOVE_WATER # assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert not (actual.state & mango.LiquidatableState.WORTHWHILE) # assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
def test_worthwhile_account_openorders_base(): # def test_worthwhile_account_openorders_base():
group = mock_group() # group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"]) # prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "900"] # deposits = ["0", "0", "0", "0", "900"]
borrows = ["0", "0", "0", "200", "0"] # borrows = ["0", "0", "0", "200", "0"]
# 900 + (20.003 @ 5) = 1000.015 for a collateral ratio of 1000.015/1000 = 100.0015% - liquidatable, above water # # 900 + (20.003 @ 5) = 1000.015 for a collateral ratio of 1000.015/1000 = 100.0015% - liquidatable, above water
# and worthwhile because the $0.015 available is greater than the whorthwhile_threshold. # # and worthwhile because the $0.015 available is greater than the whorthwhile_threshold.
open_orders: typing.List[mango.OpenOrders] = [None, # open_orders: typing.Sequence[mango.OpenOrders] = [None,
None, # None,
None, # None,
mock_open_orders(base_token_total=Decimal("20.003")), # mock_open_orders(base_token_total=Decimal("20.003")),
None] # None]
margin_account = mock_margin_account(group, deposits, borrows, open_orders) # margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01") # worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold) # actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET # assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE # assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE # assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert actual.state & mango.LiquidatableState.ABOVE_WATER # assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert actual.state & mango.LiquidatableState.WORTHWHILE # assert actual.state & mango.LiquidatableState.WORTHWHILE
def test_non_ripe_account_openorders_quote(): # def test_non_ripe_account_openorders_quote():
group = mock_group() # group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"]) # prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "1000"] # deposits = ["0", "0", "0", "0", "1000"]
borrows = ["0", "0", "0", "100", "0"] # borrows = ["0", "0", "0", "100", "0"]
open_orders: typing.List[mango.OpenOrders] = [None, # open_orders: typing.Sequence[mango.OpenOrders] = [None,
None, # None,
None, # None,
mock_open_orders(quote_token_total=Decimal(0)), # mock_open_orders(quote_token_total=Decimal(0)),
None] # None]
margin_account = mock_margin_account(group, deposits, borrows, open_orders) # margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01") # worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold) # actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET # assert actual.state != mango.LiquidatableState.UNSET
assert not (actual.state & mango.LiquidatableState.RIPE) # assert not (actual.state & mango.LiquidatableState.RIPE)
assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE) # assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
assert actual.state & mango.LiquidatableState.ABOVE_WATER # assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert actual.state & mango.LiquidatableState.WORTHWHILE # assert actual.state & mango.LiquidatableState.WORTHWHILE
# The idea of these OpenOrders tests is that the deposits and borrows remain the same and the only # # The idea of these OpenOrders tests is that the deposits and borrows remain the same and the only
# differences are through changes in OpenOrders balances. # # differences are through changes in OpenOrders balances.
def test_ripe_account_openorders_quote(): # def test_ripe_account_openorders_quote():
group = mock_group() # group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"]) # prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "900"] # deposits = ["0", "0", "0", "0", "900"]
borrows = ["0", "0", "0", "200", "0"] # borrows = ["0", "0", "0", "200", "0"]
# 1150/1000 = 115% - ripe but not liquidatable # # 1150/1000 = 115% - ripe but not liquidatable
open_orders: typing.List[mango.OpenOrders] = [None, # open_orders: typing.Sequence[mango.OpenOrders] = [None,
None, # None,
None, # None,
mock_open_orders(quote_token_total=Decimal(250)), # mock_open_orders(quote_token_total=Decimal(250)),
None # None
] # ]
margin_account = mock_margin_account(group, deposits, borrows, open_orders) # margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01") # worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold) # actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET # assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE # assert actual.state & mango.LiquidatableState.RIPE
assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE) # assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
assert actual.state & mango.LiquidatableState.ABOVE_WATER # assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert actual.state & mango.LiquidatableState.WORTHWHILE # assert actual.state & mango.LiquidatableState.WORTHWHILE
def test_liquidatable_account_openorders_quote(): # def test_liquidatable_account_openorders_quote():
group = mock_group() # group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"]) # prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "900"] # deposits = ["0", "0", "0", "0", "900"]
borrows = ["0", "0", "0", "200", "0"] # borrows = ["0", "0", "0", "200", "0"]
# 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water # # 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water
open_orders: typing.List[mango.OpenOrders] = [None, # open_orders: typing.Sequence[mango.OpenOrders] = [None,
None, # None,
None, # None,
mock_open_orders(quote_token_total=Decimal(100)), # mock_open_orders(quote_token_total=Decimal(100)),
None] # None]
margin_account = mock_margin_account(group, deposits, borrows, open_orders) # margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01") # worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold) # actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET # assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE # assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE # assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert not (actual.state & mango.LiquidatableState.ABOVE_WATER) # assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
assert not (actual.state & mango.LiquidatableState.WORTHWHILE) # assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
def test_above_water_account_openorders_quote(): # def test_above_water_account_openorders_quote():
group = mock_group() # group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"]) # prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "900"] # deposits = ["0", "0", "0", "0", "900"]
borrows = ["0", "0", "0", "200", "0"] # borrows = ["0", "0", "0", "200", "0"]
# 900 + 100.01 for a collateral ratio of 1000.01/1000 = 100.001% - liquidatable and above # # 900 + 100.01 for a collateral ratio of 1000.01/1000 = 100.001% - liquidatable and above
# water but not worthwhile because the $0.01 available is not greater than the whorthwhile_threshold. # # water but not worthwhile because the $0.01 available is not greater than the whorthwhile_threshold.
open_orders: typing.List[mango.OpenOrders] = [None, # open_orders: typing.Sequence[mango.OpenOrders] = [None,
None, # None,
None, # None,
mock_open_orders(quote_token_total=Decimal("100.01")), # mock_open_orders(quote_token_total=Decimal("100.01")),
None] # None]
margin_account = mock_margin_account(group, deposits, borrows, open_orders) # margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01") # worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold) # actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET # assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE # assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE # assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert actual.state & mango.LiquidatableState.ABOVE_WATER # assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert not (actual.state & mango.LiquidatableState.WORTHWHILE) # assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
def test_worthwhile_account_openorders_quote(): # def test_worthwhile_account_openorders_quote():
group = mock_group() # group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"]) # prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "900"] # deposits = ["0", "0", "0", "0", "900"]
borrows = ["0", "0", "0", "200", "0"] # borrows = ["0", "0", "0", "200", "0"]
# 199.99 @ 5 = 999.95 for a collateral ratio of 1000/999.99 = 100.005% - liquidatable, above water # # 199.99 @ 5 = 999.95 for a collateral ratio of 1000/999.99 = 100.005% - liquidatable, above water
# and worthwhile because the $0.05 available is greater than the whorthwhile_threshold. # # and worthwhile because the $0.05 available is greater than the whorthwhile_threshold.
open_orders: typing.List[mango.OpenOrders] = [None, # open_orders: typing.Sequence[mango.OpenOrders] = [None,
None, # None,
None, # None,
mock_open_orders(quote_token_total=Decimal(101)), # mock_open_orders(quote_token_total=Decimal(101)),
None] # None]
margin_account = mock_margin_account(group, deposits, borrows, open_orders) # margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01") # worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold) # actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET # assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE # assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE # assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert actual.state & mango.LiquidatableState.ABOVE_WATER # assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert actual.state & mango.LiquidatableState.WORTHWHILE # assert actual.state & mango.LiquidatableState.WORTHWHILE
def test_liquidatable_account_referrer_fee(): # def test_liquidatable_account_referrer_fee():
group = mock_group() # group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"]) # prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "900"] # deposits = ["0", "0", "0", "0", "900"]
borrows = ["0", "0", "0", "200", "0"] # borrows = ["0", "0", "0", "200", "0"]
# 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water # # 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water
open_orders: typing.List[mango.OpenOrders] = [None, # open_orders: typing.Sequence[mango.OpenOrders] = [None,
None, # None,
None, # None,
mock_open_orders(quote_token_total=Decimal(100)), # mock_open_orders(quote_token_total=Decimal(100)),
None] # None]
margin_account = mock_margin_account(group, deposits, borrows, open_orders) # margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01") # worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold) # actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET # assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE # assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE # assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert not (actual.state & mango.LiquidatableState.ABOVE_WATER) # assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
assert not (actual.state & mango.LiquidatableState.WORTHWHILE) # assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
# Exactly the same scenario as above, but with referrer_rebate_accrued=Decimal(1) in one OpenOrders. # # Exactly the same scenario as above, but with referrer_rebate_accrued=Decimal(1) in one OpenOrders.
# This gives a collateral ratio of 1.001, which is ripe, liquidatable, above water and worthwhile. # # This gives a collateral ratio of 1.001, which is ripe, liquidatable, above water and worthwhile.
open_orders: typing.List[mango.OpenOrders] = [None, # open_orders: typing.Sequence[mango.OpenOrders] = [None,
None, # None,
None, # None,
mock_open_orders(quote_token_total=Decimal(100), # mock_open_orders(quote_token_total=Decimal(100),
referrer_rebate_accrued=Decimal(1)), # referrer_rebate_accrued=Decimal(1)),
None] # None]
margin_account = mock_margin_account(group, deposits, borrows, open_orders) # margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01") # worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold) # actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET # assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE # assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE # assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert actual.state & mango.LiquidatableState.ABOVE_WATER # assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert actual.state & mango.LiquidatableState.WORTHWHILE # assert actual.state & mango.LiquidatableState.WORTHWHILE

View File

@ -1,8 +1,8 @@
from .context import mango from .context import mango
from .fakes import fake_context from .fakes import fake_context
from .mocks import mock_group, mock_prices, mock_margin_account, mock_open_orders # from .mocks import mock_group, mock_prices, mock_open_orders
import typing # import typing
from decimal import Decimal from decimal import Decimal
@ -33,250 +33,250 @@ def test_constructor():
# additional code is common and shared across test functions. # additional code is common and shared across test functions.
# #
class LiquidateMock: # class LiquidateMock:
def __init__(self, liquidation_processor: mango.LiquidationProcessor): # def __init__(self, liquidation_processor: mango.LiquidationProcessor):
self.liquidation_processor = liquidation_processor # self.liquidation_processor = liquidation_processor
self.captured_group: typing.Optional[mango.Group] = None # self.captured_group: typing.Optional[mango.Group] = None
self.captured_prices: typing.Optional[typing.List[mango.TokenValue]] = None # self.captured_prices: typing.Optional[typing.Sequence[mango.TokenValue]] = None
self.captured_to_liquidate: typing.Optional[typing.List[mango.LiquidatableReport]] = None # self.captured_to_liquidate: typing.Optional[typing.Sequence[mango.LiquidatableReport]] = None
# This monkeypatch is a bit nasty. It would be better to make the LiquidationProcessor # # This monkeypatch is a bit nasty. It would be better to make the LiquidationProcessor
# a bit more test-friendly. # # a bit more test-friendly.
liquidation_processor._liquidate_all = self.liquidate_capture # type: ignore # liquidation_processor._liquidate_all = self.liquidate_capture # type: ignore
def liquidate_capture(self, group: mango.Group, prices: typing.List[mango.TokenValue], to_liquidate: typing.List[mango.LiquidatableReport]): # def liquidate_capture(self, group: mango.Group, prices: typing.Sequence[mango.TokenValue], to_liquidate: typing.Sequence[mango.LiquidatableReport]):
self.captured_group = group # self.captured_group = group
self.captured_prices = prices # self.captured_prices = prices
self.captured_to_liquidate = to_liquidate # self.captured_to_liquidate = to_liquidate
def capturing_liquidation_processor() -> LiquidateMock: # def capturing_liquidation_processor() -> LiquidateMock:
context: mango.Context = fake_context() # context: mango.Context = fake_context()
name: str = "Test Liquidator" # name: str = "Test Liquidator"
account_liquidator: mango.AccountLiquidator = mango.NullAccountLiquidator() # account_liquidator: mango.AccountLiquidator = mango.NullAccountLiquidator()
wallet_balancer: mango.WalletBalancer = mango.NullWalletBalancer() # wallet_balancer: mango.WalletBalancer = mango.NullWalletBalancer()
worthwhile_threshold: Decimal = Decimal("0.1") # worthwhile_threshold: Decimal = Decimal("0.1")
actual = mango.LiquidationProcessor(context, name, account_liquidator, wallet_balancer, worthwhile_threshold) # actual = mango.LiquidationProcessor(context, name, account_liquidator, wallet_balancer, worthwhile_threshold)
return LiquidateMock(actual) # return LiquidateMock(actual)
def validate_liquidation_results(deposits: typing.List[str], borrows: typing.List[str], openorders: typing.List[typing.Optional[mango.OpenOrders]], price_iterations: typing.List[typing.Tuple[typing.List[str], str, bool]]): # def validate_liquidation_results(deposits: typing.Sequence[str], borrows: typing.Sequence[str], openorders: typing.Sequence[typing.Optional[mango.OpenOrders]], price_iterations: typing.Sequence[typing.Tuple[typing.Sequence[str], str, bool]]):
group = mock_group() # group = mock_group()
capturer = capturing_liquidation_processor() # capturer = capturing_liquidation_processor()
margin_account = mock_margin_account(group, # margin_account = mock_margin_account(group,
deposits, # deposits,
borrows, # borrows,
openorders) # openorders)
for (prices, calculated_balance, liquidatable) in price_iterations: # for (prices, calculated_balance, liquidatable) in price_iterations:
token_prices = mock_prices(prices) # token_prices = mock_prices(prices)
balance_sheet = margin_account.get_balance_sheet_totals(group, token_prices) # balance_sheet = margin_account.get_balance_sheet_totals(group, token_prices)
assert balance_sheet.assets - balance_sheet.liabilities == Decimal(calculated_balance) # assert balance_sheet.assets - balance_sheet.liabilities == Decimal(calculated_balance)
capturer.liquidation_processor.update_margin_accounts([margin_account]) # capturer.liquidation_processor.update_margin_accounts([margin_account])
capturer.liquidation_processor.update_prices(group, token_prices) # capturer.liquidation_processor.update_prices(group, token_prices)
if liquidatable: # if liquidatable:
assert (capturer.captured_to_liquidate is not None) and (len(capturer.captured_to_liquidate) == 1) # assert (capturer.captured_to_liquidate is not None) and (len(capturer.captured_to_liquidate) == 1)
assert capturer.captured_to_liquidate[0].margin_account == margin_account # assert capturer.captured_to_liquidate[0].margin_account == margin_account
else: # else:
assert (capturer.captured_to_liquidate is not None) and (len(capturer.captured_to_liquidate) == 0) # assert (capturer.captured_to_liquidate is not None) and (len(capturer.captured_to_liquidate) == 0)
def test_non_liquidatable_account(): # def test_non_liquidatable_account():
# A simple case - no borrows # # A simple case - no borrows
validate_liquidation_results( # validate_liquidation_results(
["0", "0", "0", "0", "1000"], # ["0", "0", "0", "0", "1000"],
["0", "0", "0", "0", "0"], # ["0", "0", "0", "0", "0"],
[None, None, None, None, None], # [None, None, None, None, None],
[ # [
( # (
["2000", "30000", "40", "5", "1"], # ["2000", "30000", "40", "5", "1"],
"1000", # "1000",
False # False
) # )
] # ]
) # )
def test_liquidatable_account(): # def test_liquidatable_account():
# A simple case with no currency conversions - 1000 USDC and (somehow) borrowing # # A simple case with no currency conversions - 1000 USDC and (somehow) borrowing
# 950 USDC # # 950 USDC
validate_liquidation_results( # validate_liquidation_results(
["0", "0", "0", "0", "1000"], # ["0", "0", "0", "0", "1000"],
["0", "0", "0", "0", "950"], # ["0", "0", "0", "0", "950"],
[None, None, None, None, None], # [None, None, None, None, None],
[ # [
( # (
["2000", "30000", "40", "5", "1"], # ["2000", "30000", "40", "5", "1"],
"50", # "50",
True # True
) # )
] # ]
) # )
def test_converted_balance_not_liquidatable(): # def test_converted_balance_not_liquidatable():
# A more realistic case. 1000 USDC, borrowed 180 SRM now @ 5 USDC # # A more realistic case. 1000 USDC, borrowed 180 SRM now @ 5 USDC
validate_liquidation_results( # validate_liquidation_results(
["0", "0", "0", "0", "1000"], # ["0", "0", "0", "0", "1000"],
["0", "0", "0", "180", "0"], # ["0", "0", "0", "180", "0"],
[None, None, None, None, None], # [None, None, None, None, None],
[ # [
( # (
["2000", "30000", "40", "5", "1"], # ["2000", "30000", "40", "5", "1"],
"100", # "100",
False # False
) # )
] # ]
) # )
def test_converted_balance_liquidatable(): # def test_converted_balance_liquidatable():
# 1000 USDC, borrowed 190 SRM now @ 5 USDC # # 1000 USDC, borrowed 190 SRM now @ 5 USDC
validate_liquidation_results( # validate_liquidation_results(
["0", "0", "0", "0", "1000"], # ["0", "0", "0", "0", "1000"],
["0", "0", "0", "190", "0"], # ["0", "0", "0", "190", "0"],
[None, None, None, None, None], # [None, None, None, None, None],
[ # [
( # (
["2000", "30000", "40", "5", "1"], # ["2000", "30000", "40", "5", "1"],
"50", # "50",
True # True
) # )
] # ]
) # )
def test_converted_balance_not_liquidatable_becomes_liquidatable_on_price_change(): # def test_converted_balance_not_liquidatable_becomes_liquidatable_on_price_change():
# 1000 USDC, borrowed 180 SRM @ 5 USDC, price goes to 5.2 USDC # # 1000 USDC, borrowed 180 SRM @ 5 USDC, price goes to 5.2 USDC
validate_liquidation_results( # validate_liquidation_results(
["0", "0", "0", "0", "1000"], # ["0", "0", "0", "0", "1000"],
["0", "0", "0", "180", "0"], # ["0", "0", "0", "180", "0"],
[None, None, None, None, None], # [None, None, None, None, None],
[ # [
( # (
["2000", "30000", "40", "5", "1"], # ["2000", "30000", "40", "5", "1"],
"100", # "100",
False # False
), # ),
( # (
["2000", "30000", "40", "5.2", "1"], # ["2000", "30000", "40", "5.2", "1"],
"64", # "64",
True # True
) # )
] # ]
) # )
def test_converted_balance_liquidatable_becomes_not_liquidatable_on_price_change(): # def test_converted_balance_liquidatable_becomes_not_liquidatable_on_price_change():
# 1000 USDC, borrowed 180 SRM, price goes to 5.2 USDC (and account becomes liquidatable), # # 1000 USDC, borrowed 180 SRM, price goes to 5.2 USDC (and account becomes liquidatable),
# then SRM price falls to 5 USDC (and account becomes non-liqudatable). Can margin # # then SRM price falls to 5 USDC (and account becomes non-liqudatable). Can margin
# accounts switch from liquidatable (but not liquidated) to non-liquidatable? Yes - if # # accounts switch from liquidatable (but not liquidated) to non-liquidatable? Yes - if
# something causes an error on the liquidation attempt, it's skipped until the next # # something causes an error on the liquidation attempt, it's skipped until the next
# round (with fresh prices). If no-one else tries to liquidate it (unlikely), it'll # # round (with fresh prices). If no-one else tries to liquidate it (unlikely), it'll
# appear in the next round as non-liquidatable. # # appear in the next round as non-liquidatable.
validate_liquidation_results( # validate_liquidation_results(
["0", "0", "0", "0", "1000"], # ["0", "0", "0", "0", "1000"],
["0", "0", "0", "180", "0"], # ["0", "0", "0", "180", "0"],
[None, None, None, None, None], # [None, None, None, None, None],
[ # [
( # (
["2000", "30000", "40", "5.2", "1"], # ["2000", "30000", "40", "5.2", "1"],
"64", # "64",
True # True
), # ),
( # (
["2000", "30000", "40", "5", "1"], # ["2000", "30000", "40", "5", "1"],
"100", # "100",
False # False
) # )
] # ]
) # )
def test_open_orders_balance_not_liquidatable(): # def test_open_orders_balance_not_liquidatable():
# SRM OO account has 10 SRM in it # # SRM OO account has 10 SRM in it
validate_liquidation_results( # validate_liquidation_results(
["0", "0", "0", "0", "1000"], # ["0", "0", "0", "0", "1000"],
["0", "0", "0", "190", "0"], # ["0", "0", "0", "190", "0"],
[None, None, None, mock_open_orders(base_token_total=Decimal(10)), None], # [None, None, None, mock_open_orders(base_token_total=Decimal(10)), None],
[ # [
( # (
["2000", "30000", "40", "5", "1"], # ["2000", "30000", "40", "5", "1"],
"100", # "100",
False # False
) # )
] # ]
) # )
def test_open_orders_balance_liquidatable(): # def test_open_orders_balance_liquidatable():
# SRM OO account has only 9 SRM in it. # # SRM OO account has only 9 SRM in it.
# Assets (1045) / Liabiities (950) = collateral ratio of exactly 1.1 # # Assets (1045) / Liabiities (950) = collateral ratio of exactly 1.1
validate_liquidation_results( # validate_liquidation_results(
["0", "0", "0", "0", "1000"], # ["0", "0", "0", "0", "1000"],
["0", "0", "0", "190", "0"], # ["0", "0", "0", "190", "0"],
[None, None, None, mock_open_orders(base_token_total=Decimal(9)), None], # [None, None, None, mock_open_orders(base_token_total=Decimal(9)), None],
[ # [
( # (
["2000", "30000", "40", "5", "1"], # ["2000", "30000", "40", "5", "1"],
"95", # "95",
True # True
) # )
] # ]
) # )
def test_open_orders_referral_fee_not_liquidatable(): # def test_open_orders_referral_fee_not_liquidatable():
# Figures are exactly the same as the test_open_orders_balance_liquidatable() test above, # # Figures are exactly the same as the test_open_orders_balance_liquidatable() test above,
# except for the referrer_rebate_accrued value. If it's not taken into account, the # # except for the referrer_rebate_accrued value. If it's not taken into account, the
# margin account is liquidatable. # # margin account is liquidatable.
validate_liquidation_results( # validate_liquidation_results(
["0", "0", "0", "0", "1000"], # ["0", "0", "0", "0", "1000"],
["0", "0", "0", "190", "0"], # ["0", "0", "0", "190", "0"],
[None, None, None, mock_open_orders(base_token_total=Decimal(9), referrer_rebate_accrued=Decimal("0.1")), None], # [None, None, None, mock_open_orders(base_token_total=Decimal(9), referrer_rebate_accrued=Decimal("0.1")), None],
[ # [
( # (
["2000", "30000", "40", "5", "1"], # ["2000", "30000", "40", "5", "1"],
"95.1", # The 0.1 referrer rebate is the difference between non-liquidation and iquidation. # "95.1", # The 0.1 referrer rebate is the difference between non-liquidation and iquidation.
False # False
) # )
] # ]
) # )
def test_open_orders_bigger_referral_fee_not_liquidatable(): # def test_open_orders_bigger_referral_fee_not_liquidatable():
# 900 USDC + 100.1 USDC referrer rebate should be equivalent to the above # # 900 USDC + 100.1 USDC referrer rebate should be equivalent to the above
# test_open_orders_referral_fee_not_liquidatable test. # # test_open_orders_referral_fee_not_liquidatable test.
validate_liquidation_results( # validate_liquidation_results(
["0", "0", "0", "0", "900"], # ["0", "0", "0", "0", "900"],
["0", "0", "0", "190", "0"], # ["0", "0", "0", "190", "0"],
[None, None, None, mock_open_orders(base_token_total=Decimal( # [None, None, None, mock_open_orders(base_token_total=Decimal(
9), referrer_rebate_accrued=Decimal("100.1")), None], # 9), referrer_rebate_accrued=Decimal("100.1")), None],
[ # [
( # (
["2000", "30000", "40", "5", "1"], # ["2000", "30000", "40", "5", "1"],
"95.1", # 0.1 of the referrer rebate is the difference between non-liquidation and iquidation. # "95.1", # 0.1 of the referrer rebate is the difference between non-liquidation and iquidation.
False # False
) # )
] # ]
) # )
def test_open_orders_bigger_referral_fee_liquidatable(): # def test_open_orders_bigger_referral_fee_liquidatable():
# 900 USDC + 100.1 USDC referrer rebate should be equivalent to the above # # 900 USDC + 100.1 USDC referrer rebate should be equivalent to the above
# test_open_orders_referral_fee_not_liquidatable test. # # test_open_orders_referral_fee_not_liquidatable test.
validate_liquidation_results( # validate_liquidation_results(
["0", "0", "0", "0", "900"], # ["0", "0", "0", "0", "900"],
["0", "0", "0", "190", "0"], # ["0", "0", "0", "190", "0"],
[None, None, None, mock_open_orders(base_token_total=Decimal( # [None, None, None, mock_open_orders(base_token_total=Decimal(
9), referrer_rebate_accrued=Decimal("100")), None], # 9), referrer_rebate_accrued=Decimal("100")), None],
[ # [
( # (
["2000", "30000", "40", "5", "1"], # ["2000", "30000", "40", "5", "1"],
"95", # Just 0.1 more in the referrer rebate would make it non-liquidatable. # "95", # Just 0.1 more in the referrer rebate would make it non-liquidatable.
True # True
) # )
] # ]
) # )

View File

@ -1,27 +0,0 @@
from .context import mango
def test_constructor():
initialized: bool = True
group: bool = True
margin_account: bool = True
srm_account: bool = True
actual = mango.MangoAccountFlags(mango.Version.V1, initialized, group,
margin_account, srm_account)
assert actual is not None
assert actual.logger is not None
assert actual.version == mango.Version.V1
assert actual.initialized == initialized
assert actual.group == group
assert actual.margin_account == margin_account
assert actual.srm_account == srm_account
actual2 = mango.MangoAccountFlags(mango.Version.V2, not initialized, not group,
not margin_account, not srm_account)
assert actual2 is not None
assert actual2.logger is not None
assert actual2.version == mango.Version.V2
assert actual2.initialized == (not initialized)
assert actual2.group == (not group)
assert actual2.margin_account == (not margin_account)
assert actual2.srm_account == (not srm_account)

View File

@ -1,31 +0,0 @@
from .context import mango
from .fakes import fake_account_info, fake_seeded_public_key, fake_token
from decimal import Decimal
def test_construction():
account_flags = mango.MangoAccountFlags(mango.Version.V1, True, False, True, False)
has_borrows = False
mango_group = fake_seeded_public_key("mango group")
owner = fake_seeded_public_key("owner")
being_liquidated = False
token = fake_token()
deposits = [mango.TokenValue(token, Decimal(0)), mango.TokenValue(
token, Decimal(0)), mango.TokenValue(token, Decimal(0))]
borrows = [mango.TokenValue(token, Decimal(0)), mango.TokenValue(
token, Decimal(0)), mango.TokenValue(token, Decimal(0))]
open_orders = [None, None]
actual = mango.MarginAccount(fake_account_info(), mango.Version.V1, account_flags,
has_borrows, mango_group, owner, being_liquidated,
deposits, borrows, open_orders)
assert actual is not None
assert actual.logger is not None
assert actual.version == mango.Version.V1
assert actual.account_flags == account_flags
assert actual.has_borrows == has_borrows
assert actual.mango_group == mango_group
assert actual.owner == owner
assert actual.deposits == deposits
assert actual.borrows == borrows
assert actual.open_orders == open_orders

View File

@ -1,14 +0,0 @@
from .context import mango
from .fakes import fake_index, fake_seeded_public_key, fake_token
from decimal import Decimal
def test_constructor():
base = mango.BasketToken(fake_token(), fake_seeded_public_key("base vault"), fake_index())
quote = mango.BasketToken(fake_token(), fake_seeded_public_key("quote vault"), fake_index())
spot_market = mango.SpotMarket(fake_seeded_public_key("spot market"), base, quote)
actual = mango.MarketMetadata("FAKE/MKT", fake_seeded_public_key("market metadata"),
base, quote, spot_market, fake_seeded_public_key("oracle"), Decimal(7))
assert actual is not None
assert actual.logger is not None

View File

@ -10,7 +10,7 @@ def test_constructor():
market = fake_public_key() market = fake_public_key()
owner = fake_public_key() owner = fake_public_key()
flags = mango.SerumAccountFlags(mango.Version.V1, True, False, True, False, False, False, False, False) flags = mango.AccountFlags(mango.Version.V1, True, False, True, False, False, False, False, False)
actual = mango.OpenOrders(account_info, mango.Version.V1, program_id, flags, market, actual = mango.OpenOrders(account_info, mango.Version.V1, program_id, flags, market,
owner, Decimal(0), Decimal(0), Decimal(0), Decimal(0), owner, Decimal(0), Decimal(0), Decimal(0), Decimal(0),
Decimal(0), Decimal(0), [], [], Decimal(0)) Decimal(0), Decimal(0), [], [], Decimal(0))

View File

@ -8,7 +8,7 @@ import typing
def test_constructor(): def test_constructor():
name: str = "Test" name: str = "Test"
func: typing.Callable = lambda: 1 func: typing.Callable = lambda: 1
pauses: typing.List[Decimal] = [Decimal(2)] pauses: typing.Sequence[Decimal] = [Decimal(2)]
actual = mango.RetryWithPauses(name, func, pauses) actual = mango.RetryWithPauses(name, func, pauses)
assert actual is not None assert actual is not None
assert actual.logger is not None assert actual.logger is not None
@ -23,7 +23,7 @@ def test_0_retry():
# Number of retries is the number of pauses - 1. # Number of retries is the number of pauses - 1.
# The retrier only pauses if an exception is raised. # The retrier only pauses if an exception is raised.
pauses: typing.List[Decimal] = [Decimal(0)] pauses: typing.Sequence[Decimal] = [Decimal(0)]
class FuncScope: class FuncScope:
called: int = 0 called: int = 0
@ -48,7 +48,7 @@ def test_1_retry():
# Number of retries is the number of pauses - 1. # Number of retries is the number of pauses - 1.
# The retrier only pauses if an exception is raised. # The retrier only pauses if an exception is raised.
pauses: typing.List[Decimal] = [Decimal(0), Decimal(0)] pauses: typing.Sequence[Decimal] = [Decimal(0), Decimal(0)]
class FuncScope: class FuncScope:
called: int = 0 called: int = 0
@ -73,7 +73,7 @@ def test_3_retries():
# Number of retries is the number of pauses - 1. # Number of retries is the number of pauses - 1.
# The retrier only pauses if an exception is raised. # The retrier only pauses if an exception is raised.
pauses: typing.List[Decimal] = [Decimal(0), Decimal(0), Decimal(0), Decimal(0)] pauses: typing.Sequence[Decimal] = [Decimal(0), Decimal(0), Decimal(0), Decimal(0)]
class FuncScope: class FuncScope:
called: int = 0 called: int = 0
@ -98,7 +98,7 @@ def test_with_context():
# Number of retries is the number of pauses - 1. # Number of retries is the number of pauses - 1.
# The retrier only pauses if an exception is raised. # The retrier only pauses if an exception is raised.
pauses: typing.List[Decimal] = [Decimal(0), Decimal(0), Decimal(0), Decimal(0)] pauses: typing.Sequence[Decimal] = [Decimal(0), Decimal(0), Decimal(0), Decimal(0)]
class FuncScope: class FuncScope:
called: int = 0 called: int = 0

View File

@ -24,21 +24,21 @@ def test_transaction_instruction_constructor():
def test_transaction_scout_constructor(): def test_transaction_scout_constructor():
timestamp: datetime = datetime.now() timestamp: datetime = datetime.now()
signatures: typing.List[str] = ["Signature1", "Signature2"] signatures: typing.Sequence[str] = ["Signature1", "Signature2"]
succeeded: bool = True succeeded: bool = True
group_name: str = "BTC_ETH_USDT" group_name: str = "BTC_ETH_USDT"
account1: PublicKey = fake_seeded_public_key("account 1") account1: PublicKey = fake_seeded_public_key("account 1")
account2: PublicKey = fake_seeded_public_key("account 2") account2: PublicKey = fake_seeded_public_key("account 2")
account3: PublicKey = fake_seeded_public_key("account 3") account3: PublicKey = fake_seeded_public_key("account 3")
accounts: typing.List[PublicKey] = [account1, account2, account3] accounts: typing.Sequence[PublicKey] = [account1, account2, account3]
instructions: typing.List[str] = ["Instruction"] instructions: typing.Sequence[str] = ["Instruction"]
messages: typing.List[str] = ["Message 1", "Message 2"] messages: typing.Sequence[str] = ["Message 1", "Message 2"]
token = fake_token() token = fake_token()
token_value = mango.TokenValue(token, Decimal(28)) token_value = mango.TokenValue(token, Decimal(28))
owner = fake_seeded_public_key("owner") owner = fake_seeded_public_key("owner")
owned_token_value = mango.OwnedTokenValue(owner, token_value) owned_token_value = mango.OwnedTokenValue(owner, token_value)
pre_token_balances: typing.List[mango.OwnedTokenValue] = [owned_token_value] pre_token_balances: typing.Sequence[mango.OwnedTokenValue] = [owned_token_value]
post_token_balances: typing.List[mango.OwnedTokenValue] = [owned_token_value] post_token_balances: typing.Sequence[mango.OwnedTokenValue] = [owned_token_value]
actual = mango.TransactionScout(timestamp, signatures, succeeded, group_name, accounts, actual = mango.TransactionScout(timestamp, signatures, succeeded, group_name, accounts,
instructions, messages, pre_token_balances, post_token_balances) instructions, messages, pre_token_balances, post_token_balances)
assert actual is not None assert actual is not None