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 \
cp $${file} .tmplintdir/$${file##*/}.py ; \
done
-mypy mango tests .tmplintdir
-mypy --no-incremental --cache-dir=/dev/null mango tests .tmplintdir
rm -rf .tmplintdir
flake8:
flake8 --extend-ignore E402,E501,E722,W291,W391 . tests/* bin/*
flake8 --extend-ignore E402,E501,E722,W291,W391 . bin/*
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.")
transaction = Transaction()
signers: typing.List[Account] = [wallet.account]
signers: typing.Sequence[Account] = [wallet.account]
payer = wallet.address
close_instruction = mango.CloseSplAccountInstructionBuilder(context, wallet, args.address)

View File

@ -6,6 +6,7 @@ import os
import os.path
import sys
import traceback
import typing
from decimal import Decimal
@ -42,13 +43,15 @@ try:
logging.info(f"Wallet address: {wallet.address}")
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)
targets = list(map(balance_parser.parse, args.target))
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}")
if args.dry_run:

View File

@ -36,7 +36,7 @@ try:
logging.info(f"Context: {context}")
logging.info(f"Address: {address}")
group = mango.MangoGroup.load(context)
group = mango.Group.load(context)
balances = group.fetch_balances(context, address)
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")
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()
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.instructions.extend(init)

View File

@ -78,20 +78,17 @@ try:
item, mango.LiquidationEvent) and not item.succeeded)
liquidations_publisher.subscribe(on_next=filtering.send)
# TODO: Add proper liquidator classes here when they're written for V3
if args.dry_run:
account_liquidator: mango.AccountLiquidator = mango.NullAccountLiquidator()
else:
intermediate = mango.ForceCancelOrdersAccountLiquidator(context, wallet)
account_liquidator = mango.ReportingAccountLiquidator(intermediate,
context,
wallet,
liquidations_publisher,
liquidator_name)
account_liquidator = mango.NullAccountLiquidator()
prices = group.fetch_token_prices(context)
margin_account = mango.MarginAccount.load(context, margin_account_address, group)
# TODO - fetch prices when available for V3.
# 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.
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)
if transaction_id is None:
print("No transaction sent.")

View File

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

View File

@ -73,27 +73,23 @@ try:
item, mango.LiquidationEvent) and not item.succeeded)
liquidations_publisher.subscribe(on_next=filtering.send)
# TODO: Add proper liquidator classes here when they're written for V3
if args.dry_run:
account_liquidator: mango.AccountLiquidator = mango.NullAccountLiquidator()
else:
intermediate = mango.ForceCancelOrdersAccountLiquidator(context, wallet)
account_liquidator = mango.ReportingAccountLiquidator(intermediate,
context,
wallet,
liquidations_publisher,
liquidator_name)
account_liquidator = mango.NullAccountLiquidator()
wallet_balancer = mango.NullWalletBalancer()
liquidation_processor = mango.LiquidationProcessor(context, liquidator_name, account_liquidator, wallet_balancer)
started_at = time.time()
ripe = group.load_ripe_margin_accounts()
liquidation_processor.update_margin_accounts(ripe)
# ripe = group.load_ripe_margin_accounts()
liquidation_processor.update_margin_accounts([])
group = mango.Group.load(context) # Refresh group data
prices = group.fetch_token_prices(context)
liquidation_processor.update_prices(group, prices)
# prices = group.fetch_token_prices(context)
liquidation_processor.update_prices(group, [])
time_taken = time.time() - started_at
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.")
mango.Context.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,
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")
@ -40,18 +40,18 @@ try:
logging.info(f"Context: {context}")
logging.info(f"Wallet address: {wallet.address}")
group = mango.Group.load(context)
group_basket_token = mango.BasketToken.find_by_symbol(group.basket_tokens, args.token_symbol)
group_token = group_basket_token.token
token = context.token_lookup.find_by_symbol(args.symbol)
if token is None:
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_account = source_accounts["result"]["value"][0]
source = PublicKey(source_account["pubkey"])
# 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)
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.
destination: PublicKey = args.address
else:
@ -63,11 +63,11 @@ try:
destination = PublicKey(destination_account["pubkey"])
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"]
["info"]["tokenAmount"]["uiAmountString"], group_token.name)
text_amount = f"{amount} {group_token.name} (@ {group_token.decimals} decimal places)"
["info"]["tokenAmount"]["uiAmountString"], token.name)
text_amount = f"{amount} {token.name} (@ {token.decimals} decimal places)"
print(f"Sending {text_amount}")
print(f" From: {source}")
print(f" To: {destination}")
@ -82,7 +82,7 @@ try:
updated_balance = spl_token.get_balance(source)
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:
logging.critical(f"send-token stopped because of exception: {exception} - {traceback.format_exc()}")
except:

View File

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

View File

@ -11,8 +11,6 @@ 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 Group.")
mango.Context.add_command_line_parameters(parser)
args = parser.parse_args()
@ -23,7 +21,7 @@ logging.warning(mango.WARNING_DISCLAIMER_TEXT)
try:
context = mango.Context.from_command_line_parameters(args)
group = mango.MangoGroup.load(context)
group = mango.Group.load(context)
print(group)
except Exception as exception:
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}.")
transaction = Transaction()
signers: typing.List[Account] = [wallet.account]
wrapped_sol_account = Account()
signers.append(wrapped_sol_account)
signers: typing.Sequence[Account] = [wallet.account, wrapped_sol_account]
create_instruction = mango.CreateSplAccountInstructionBuilder(
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)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.MangoGroup.load(context, context.group_id)
margin_accounts = mango.MangoAccount.load_all_for_owner(context, wallet.address, group)
if len(margin_accounts) == 0:
group = mango.Group.load(context, context.group_id)
accounts = mango.Account.load_all_for_owner(context, wallet.address, group)
if len(accounts) == 0:
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)
print(token)
@ -58,7 +58,7 @@ node_bank = root_bank.pick_node_bank(context)
withdraw = mango.build_withdraw_instructions(
context, wallet, group, margin_account, root_bank, node_bank, withdrawal_token_account, args.allow_borrow)
print(withdraw)
signers: typing.List[Account] = [wallet.account]
signers: typing.Sequence[Account] = [wallet.account]
transaction = Transaction()
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)
transaction = Transaction()
signers: typing.List[Account] = [wallet.account]
wrapped_sol_account = Account()
signers.append(wrapped_sol_account)
signers: typing.Sequence[Account] = [wallet.account, wrapped_sol_account]
create_instruction = mango.CreateSplAccountInstructionBuilder(
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 .accountliquidator import AccountLiquidator, NullAccountLiquidator, ActualAccountLiquidator, ForceCancelOrdersAccountLiquidator, ReportingAccountLiquidator
from .accountliquidator import AccountLiquidator, NullAccountLiquidator
from .accountscout import ScoutReport, AccountScout
from .addressableaccount import AddressableAccount
from .aggregator import AggregatorConfig, Round, Answer, Aggregator
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 .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
@ -12,21 +12,15 @@ from .encoding import decode_binary, encode_binary, encode_key, encode_int
from .group import Group
from .idsjsontokenlookup import IdsJsonTokenLookup
from .idsjsonmarketlookup import IdsJsonMarketLookup
from .index import Index
from .instructions import InstructionBuilder, ForceCancelOrdersInstructionBuilder, LiquidateInstructionBuilder, CreateSplAccountInstructionBuilder, InitializeSplAccountInstructionBuilder, TransferSplTokensInstructionBuilder, CloseSplAccountInstructionBuilder, CreateSerumOpenOrdersInstructionBuilder, NewOrderV3InstructionBuilder, ConsumeEventsInstructionBuilder, SettleInstructionBuilder
from .instructions import InstructionBuilder, CreateSplAccountInstructionBuilder, InitializeSplAccountInstructionBuilder, TransferSplTokensInstructionBuilder, CloseSplAccountInstructionBuilder, CreateSerumOpenOrdersInstructionBuilder, NewOrderV3InstructionBuilder, ConsumeEventsInstructionBuilder, SettleInstructionBuilder
from .instructiontype import InstructionType
from .liquidatablereport import LiquidatableState, LiquidatableReport
from .liquidationevent import LiquidationEvent
from .liquidationprocessor import LiquidationProcessor, LiquidationProcessorState
from .mangoaccount import MangoAccount
from .mangoaccountflags import MangoAccountFlags
from .marginaccount import MarginAccount
from .market import Market
from .marketlookup import MarketLookup, CompoundMarketLookup
from .marketmetadata import MarketMetadata
from .marketoperations import MarketOperations, NullMarketOperations
from .mangogroup import MangoGroup
from .merpsinstructions import build_cancel_perp_order_instructions, build_create_margin_account_instructions, build_place_perp_order_instructions, build_withdraw_instructions
from .merpsinstructions import build_cancel_perp_order_instructions, build_create_account_instructions, build_place_perp_order_instructions, build_withdraw_instructions
from .metadata import Metadata
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
@ -41,7 +35,6 @@ from .perpmarketinfo import PerpMarketInfo
from .perpmarketoperations import PerpMarketOperations
from .retrier import RetryWithPauses, retry_context
from .rootbank import NodeBank, RootBank
from .serumaccountflags import SerumAccountFlags
from .serummarketlookup import SerumMarketLookup
from .serummarketoperations import SerumMarketOperations
from .spltokenlookup import SplTokenLookup

View File

@ -23,66 +23,66 @@ from .accountinfo import AccountInfo
from .addressableaccount import AddressableAccount
from .context import Context
from .encoding import encode_key
from .group import Group
from .layouts import layouts
from .mangogroup import MangoGroup
from .metadata import Metadata
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,
meta_data: Metadata, group: PublicKey, owner: PublicKey, in_basket: typing.List[Decimal],
deposits: typing.List[Decimal], borrows: typing.List[Decimal],
spot_open_orders: typing.List[PublicKey], perp_accounts: typing.List[typing.Any]):
meta_data: Metadata, group: PublicKey, owner: PublicKey, in_basket: typing.Sequence[Decimal],
deposits: typing.Sequence[Decimal], borrows: typing.Sequence[Decimal],
spot_open_orders: typing.Sequence[PublicKey], perp_accounts: typing.Sequence[typing.Any]):
super().__init__(account_info)
self.version: Version = version
self.meta_data: Metadata = meta_data
self.group: PublicKey = group
self.owner: PublicKey = owner
self.in_basket: typing.List[Decimal] = in_basket
self.deposits: typing.List[Decimal] = deposits
self.borrows: typing.List[Decimal] = borrows
self.spot_open_orders: typing.List[PublicKey] = spot_open_orders
self.perp_accounts: typing.List[layouts.PERP_ACCOUNT] = perp_accounts
self.in_basket: typing.Sequence[Decimal] = in_basket
self.deposits: typing.Sequence[Decimal] = deposits
self.borrows: typing.Sequence[Decimal] = borrows
self.spot_open_orders: typing.Sequence[PublicKey] = spot_open_orders
self.perp_accounts: typing.Sequence[layouts.PERP_ACCOUNT] = perp_accounts
@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)
group: PublicKey = layout.group
owner: PublicKey = layout.owner
in_basket: typing.List[Decimal] = layout.in_basket
deposits: typing.List[Decimal] = layout.deposits
borrows: typing.List[Decimal] = layout.borrows
spot_open_orders: typing.List[PublicKey] = layout.spot_open_orders
perp_accounts: typing.List[typing.Any] = layout.perp_accounts
in_basket: typing.Sequence[Decimal] = layout.in_basket
deposits: typing.Sequence[Decimal] = layout.deposits
borrows: typing.Sequence[Decimal] = layout.borrows
spot_open_orders: typing.Sequence[PublicKey] = layout.spot_open_orders
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
def parse(context: Context, account_info: AccountInfo) -> "MangoAccount":
def parse(context: Context, account_info: AccountInfo) -> "Account":
data = account_info.data
if len(data) != layouts.MANGO_ACCOUNT.sizeof():
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)
return MangoAccount.from_layout(layout, account_info, Version.V1)
return Account.from_layout(layout, account_info, Version.V1)
@staticmethod
def load(context: Context, address: PublicKey) -> "MangoAccount":
def load(context: Context, address: PublicKey) -> "Account":
account_info = AccountInfo.load(context, address)
if account_info is None:
raise Exception(f"MangoAccount account not found at address '{address}'")
return MangoAccount.parse(context, account_info)
raise Exception(f"Account account not found at address '{address}'")
return Account.parse(context, account_info)
@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.
group_offset = layouts.METADATA.sizeof()
# 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"]:
address = PublicKey(account_data["pubkey"])
account_info = AccountInfo._from_response_values(account_data["account"], address)
account = MangoAccount.parse(context, account_info)
account = Account.parse(context, account_info)
accounts += [account]
return accounts

View File

@ -21,12 +21,12 @@ from .layouts import layouts
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,
request_queue: bool, event_queue: bool, bids: bool, asks: bool, disabled: bool):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
@ -41,10 +41,10 @@ class SerumAccountFlags:
self.disabled: bool = disabled
@staticmethod
def from_layout(layout: layouts.SERUM_ACCOUNT_FLAGS) -> "SerumAccountFlags":
return SerumAccountFlags(Version.UNSPECIFIED, layout.initialized, layout.market,
layout.open_orders, layout.request_queue, layout.event_queue,
layout.bids, layout.asks, layout.disabled)
def from_layout(layout: layouts.ACCOUNT_FLAGS) -> "AccountFlags":
return AccountFlags(Version.UNSPECIFIED, layout.initialized, layout.market,
layout.open_orders, layout.request_queue, layout.event_queue,
layout.bids, layout.asks, layout.disabled)
def __str__(self) -> str:
flags: typing.List[typing.Optional[str]] = []
@ -57,7 +57,7 @@ class SerumAccountFlags:
flags += ["asks" if self.asks else None]
flags += ["disabled" if self.disabled else 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:
return f"{self}"

View File

@ -40,7 +40,7 @@ class AccountInfo:
self.rent_epoch: Decimal = rent_epoch
self.data: bytes = data
def encoded_data(self) -> typing.List:
def encoded_data(self) -> typing.Sequence:
return encode_binary(self.data)
def __str__(self) -> str:
@ -64,12 +64,12 @@ class AccountInfo:
return AccountInfo._from_response_values(result["value"], address)
@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.
# Some errors this can generate:
# 413 Client Error: Payload Too Large for url
# 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] = []
chunks = AccountInfo._split_list_into_chunks(address_strings, chunk_size)
for counter, chunk in enumerate(chunks):
@ -100,7 +100,7 @@ class AccountInfo:
return AccountInfo._from_response_values(response["result"]["value"], address)
@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 = []
start = 0
while start < len(to_chunk):

View File

@ -15,27 +15,16 @@
import abc
import datetime
import logging
import typing
from solana.transaction import Transaction
from .context import Context
from .group import Group
from .instructions import ForceCancelOrdersInstructionBuilder, InstructionBuilder, LiquidateInstructionBuilder
from .instructions import InstructionBuilder
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
#
# 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
# 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.
#
class AccountLiquidator(metaclass=abc.ABCMeta):
def __init__(self):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
@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.")
@abc.abstractmethod
@ -72,192 +60,13 @@ class AccountLiquidator(metaclass=abc.ABCMeta):
# A 'null', 'no-op', 'dry run' implementation of the `AccountLiquidator` class.
#
class NullAccountLiquidator(AccountLiquidator):
def __init__(self):
super().__init__()
def prepare_instructions(self, liquidatable_report: LiquidatableReport) -> typing.List[InstructionBuilder]:
def prepare_instructions(self, liquidatable_report: LiquidatableReport) -> typing.Sequence[InstructionBuilder]:
return []
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
# # 💧 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 .account import Account
from .accountinfo import AccountInfo
from .constants import SYSTEM_PROGRAM_ADDRESS
from .context import Context
from .group import Group
from .marginaccount import MarginAccount
from .openorders import OpenOrders
from .tokenaccount import TokenAccount
from .wallet import Wallet
@ -144,29 +143,19 @@ class AccountScout:
return report
# Must have token accounts for each of the tokens in the group's basket.
for basket_token in group.basket_tokens:
token_accounts = TokenAccount.fetch_all_for_owner_and_token(context, account_address, basket_token.token)
if len(token_accounts) == 0:
report.add_error(
f"Account '{account_address}' has no account for token '{basket_token.token.name}', mint '{basket_token.token.mint}'.")
else:
report.add_detail(
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]}")
# 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:
for basket_token in group.tokens:
if basket_token is not None:
token_accounts = TokenAccount.fetch_all_for_owner_and_token(
context, account_address, basket_token.token)
if len(token_accounts) == 0:
report.add_error(
f"Account '{account_address}' has no account for token '{basket_token.token.name}', mint '{basket_token.token.mint}'.")
else:
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
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:
report.add_detail(f"Account '{account_address}' has no Mango Markets margin accounts.")
else:

View File

@ -24,7 +24,7 @@ from .accountinfo import AccountInfo
# # 🥭 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
# `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.transaction_options: TxOpts = TxOpts(preflight_commitment=self.commitment)
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)
all_token_lookup: TokenLookup = CompoundTokenLookup(
[ids_json_token_lookup, spl_token_lookup])
@ -96,7 +96,7 @@ class Context:
# kangda said in Discord: https://discord.com/channels/791995070613159966/836239696467591186/847816026245693451
# "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)]
@property

View File

@ -16,11 +16,11 @@
import typing
from .account import Account
from .context import Context
from .mangoaccount import MangoAccount
from .group import Group
from .market import Market
from .marketoperations import MarketOperations, NullMarketOperations
from .mangogroup import MangoGroup
from .perpmarket import PerpMarket
from .perpmarketoperations import PerpMarketOperations
# from .serummarketoperations import SerumMarketOperations
@ -38,8 +38,8 @@ def create_market_operations(context: Context, wallet: Wallet, dry_run: bool, ma
elif isinstance(market, SpotMarket):
# return SerumMarketOperations(context, wallet, market, reporter)
# elif isinstance(market, PerpMarket):
group = MangoGroup.load(context, context.group_id)
margin_accounts = MangoAccount.load_all_for_owner(context, wallet.address, group)
group = Group.load(context, context.group_id)
margin_accounts = Account.load_all_for_owner(context, wallet.address, group)
perp_market_info = group.perp_markets[0]
if perp_market_info is None:
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.
def decode_binary(encoded: typing.List) -> bytes:
def decode_binary(encoded: typing.Sequence) -> bytes:
if isinstance(encoded, str):
return base58.b58decode(encoded)
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"]

View File

@ -13,8 +13,6 @@
# [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation)
import construct
import time
import typing
from decimal import Decimal
@ -22,203 +20,126 @@ from solana.publickey import PublicKey
from .accountinfo import AccountInfo
from .addressableaccount import AddressableAccount
from .aggregator import Aggregator
from .baskettoken import BasketToken
from .context import Context
from .index import Index
from .layouts import layouts
from .mangoaccountflags import MangoAccountFlags
from .marketmetadata import MarketMetadata
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 .tokenvalue import TokenValue
from .version import Version
# # 🥭 Group class
#
# The `Group` class encapsulates the data for the Mango Group - the cross-margined basket
# of tokens with lending.
# `Group` defines root functionality for Mango Markets.
#
class Group(AddressableAccount):
def __init__(self, account_info: AccountInfo, version: Version, name: str,
account_flags: MangoAccountFlags, basket_tokens: typing.List[BasketToken],
markets: typing.List[MarketMetadata],
signer_nonce: Decimal, signer_key: PublicKey, dex_program_id: PublicKey,
total_deposits: typing.List[TokenValue], total_borrows: typing.List[TokenValue],
maint_coll_ratio: Decimal, init_coll_ratio: Decimal, srm_vault: PublicKey,
admin: PublicKey, borrow_limits: typing.List[TokenValue]):
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.account_flags: MangoAccountFlags = account_flags
self.basket_tokens: typing.List[BasketToken] = basket_tokens
self.markets: typing.List[MarketMetadata] = markets
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.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.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
def shared_quote_token(self) -> BasketToken:
return self.basket_tokens[-1]
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.List[BasketToken]:
return self.basket_tokens[:-1]
def base_tokens(self) -> typing.Sequence[typing.Optional[TokenInfo]]:
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
def from_layout(layout: construct.Struct, name: str, account_info: AccountInfo, version: Version, token_lookup: TokenLookup, market_lookup: MarketLookup) -> "Group":
account_flags: MangoAccountFlags = MangoAccountFlags.from_layout(layout.account_flags)
def from_layout(layout: layouts.GROUP, name: str, account_info: AccountInfo, version: Version, token_lookup: TokenLookup, market_lookup: MarketLookup) -> "Group":
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] = []
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)
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)
@staticmethod
def parse(context: Context, account_info: AccountInfo) -> "Group":
data = account_info.data
if len(data) == layouts.GROUP_V1.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:
if len(data) != layouts.GROUP.sizeof():
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
def load(context: Context):
account_info = AccountInfo.load(context, context.group_id)
def load(context: Context, address: typing.Optional[PublicKey] = None) -> "Group":
group_address: PublicKey = address or context.group_id
account_info = AccountInfo.load(context, group_address)
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)
def price_index_of_token(self, token: Token) -> int:
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]:
def fetch_balances(self, context: Context, root_address: PublicKey) -> typing.Sequence[TokenValue]:
balances: typing.List[TokenValue] = []
sol_balance = context.fetch_sol_balance(root_address)
balances += [TokenValue(SolToken, sol_balance)]
for basket_token in self.basket_tokens:
balance = TokenValue.fetch_total_value(context, root_address, basket_token.token)
balances += [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) -> str:
total_deposits = "\n ".join(map(str, self.total_deposits))
total_borrows = "\n ".join(map(str, self.total_borrows))
borrow_limits = "\n ".join(map(str, self.borrow_limits))
shared_quote_token = str(self.shared_quote_token).replace("\n", "\n ")
base_tokens = "\n ".join([f"{tok}".replace("\n", "\n ") for tok in self.base_tokens])
markets = "\n ".join([f"{mkt}".replace("\n", "\n ") for mkt in self.markets])
return f"""
« Group [{self.version} - {self.name}] {self.address}:
Flags: {self.account_flags}
Base Tokens:
{base_tokens}
Quote Token:
{shared_quote_token}
Markets:
{markets}
DEX Program ID: « {self.dex_program_id} »
SRM Vault: « {self.srm_vault} »
Admin: « {self.admin} »
Signer Nonce: {self.signer_nonce}
Signer Key: « {self.signer_key} »
Initial Collateral Ratio: {self.init_coll_ratio}
Maintenance Collateral Ratio: {self.maint_coll_ratio}
Total Deposits:
{total_deposits}
Total Borrows:
{total_borrows}
Borrow Limits:
{borrow_limits}
»
"""
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

@ -29,13 +29,14 @@ from .tokenlookup import TokenLookup
#
class IdsJsonTokenLookup(TokenLookup):
def __init__(self, cluster: str) -> None:
def __init__(self, cluster: str, group_name: str) -> None:
super().__init__()
self.cluster: str = cluster
self.group_name: str = group_name
def find_by_symbol(self, symbol: str) -> typing.Optional[Token]:
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"]:
if token["symbol"] == symbol:
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]:
mint_str = str(mint)
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"]:
if token["mintKey"] == mint_str:
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 logging
import struct
import typing
from decimal import Decimal
@ -28,20 +27,12 @@ from solana.account import Account
from solana.publickey import PublicKey
from solana.system_program import CreateAccountParams, create_account
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.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 .group import Group
from .layouts import layouts
from .marginaccount import MarginAccount
from .marketmetadata import MarketMetadata
from .token import Token
from .tokenaccount import TokenAccount
from .tokenvalue import TokenValue
from .wallet import Wallet
@ -69,394 +60,6 @@ class InstructionBuilder(metaclass=abc.ABCMeta):
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
#
# 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.
#
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)
self.wallet: Wallet = wallet
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
def build(self) -> TransactionInstruction:

View File

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

View File

@ -32,7 +32,6 @@
import construct
import datetime
import itertools
import typing
from decimal import Decimal
@ -249,9 +248,7 @@ class OrderBookNodeAdapter(construct.Adapter):
# # Layout Structs
# ## SERUM_ACCOUNT_FLAGS
#
# The SERUM_ prefix is because there's also `MANGO_ACCOUNT_FLAGS`.
# ## ACCOUNT_FLAGS
#
# 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(
"initialized" / 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
@ -615,7 +302,7 @@ TOKEN_ACCOUNT = construct.Struct(
OPEN_ORDERS = construct.Struct(
construct.Padding(5),
"account_flags" / SERUM_ACCOUNT_FLAGS,
"account_flags" / ACCOUNT_FLAGS,
"market" / PublicKeyAdapter(),
"owner" / PublicKeyAdapter(),
"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_PAIRS: int = MAX_TOKENS - 1
MAX_NODE_BANKS: int = 8
@ -1128,7 +325,7 @@ QUOTE_INDEX: int = MAX_TOKENS - 1
OPEN_ORDERS_MAX_ORDERS: int = 32
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)
METADATA = construct.Struct(
@ -1168,7 +365,7 @@ PERP_MARKET_INFO = construct.Struct(
)
# usize is a u64 on Solana, so a regular DecimalAdapter() works
MANGO_GROUP = construct.Struct(
GROUP = construct.Struct(
"meta_data" / METADATA,
"num_oracles" / DecimalAdapter(),
"tokens" / construct.Array(MAX_TOKENS, TOKEN_INFO),
@ -1333,6 +530,20 @@ ORDERBOOK_SIDE = construct.Struct(
"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
# /// Accounts expected by this instruction (6):
@ -1397,7 +608,7 @@ WITHDRAW_V3 = construct.Struct(
)
MerpsInstructionParsersByVariant = {
InstructionParsersByVariant = {
0: None, # INIT_MANGO_GROUP,
1: INIT_MANGO_ACCOUNT, # INIT_MANGO_ACCOUNT,
2: None, # DEPOSIT,

View File

@ -20,9 +20,8 @@ import typing
from decimal import Decimal
from .balancesheet import BalanceSheet
from .account import Account
from .group import Group
from .marginaccount import MarginAccount
from .tokenvalue import TokenValue
@ -44,40 +43,14 @@ class LiquidatableState(enum.Flag):
#
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.group: Group = group
self.prices: typing.List[TokenValue] = prices
self.margin_account: MarginAccount = margin_account
self.balance_sheet: BalanceSheet = balance_sheet
self.balances: typing.List[TokenValue] = balances
self.prices: typing.Sequence[TokenValue] = prices
self.account: Account = account
self.state: LiquidatableState = state
self.worthwhile_threshold: Decimal = worthwhile_threshold
@staticmethod
def build(group: Group, prices: typing.List[TokenValue], margin_account: MarginAccount, worthwhile_threshold: Decimal) -> "LiquidatableReport":
balance_sheet = margin_account.get_balance_sheet_totals(group, prices)
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)
def build(group: Group, prices: typing.Sequence[TokenValue], account: Account, worthwhile_threshold: Decimal) -> "LiquidatableReport":
return LiquidatableReport(group, prices, account, LiquidatableState.UNSET, worthwhile_threshold)

View File

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

View File

@ -21,12 +21,12 @@ import typing
from datetime import datetime, timedelta
from decimal import Decimal
from .account import Account
from .accountliquidator import AccountLiquidator
from .context import Context
from .group import Group
from .liquidatablereport import LiquidatableReport, LiquidatableState
from .liquidationevent import LiquidationEvent
from .marginaccount import MarginAccount
from .observables import EventSource
from .tokenvalue import TokenValue
from .walletbalancer import WalletBalancer
@ -54,8 +54,8 @@ class LiquidationProcessorState(enum.Enum):
# # 💧 LiquidationProcessor class
#
# An `AccountLiquidator` liquidates a `MarginAccount`. A `LiquidationProcessor` processes a
# list of `MarginAccount`s, determines if they're liquidatable, and calls an
# An `AccountLiquidator` liquidates a `Account`. A `LiquidationProcessor` processes a
# list of `Account`s, determines if they're liquidatable, and calls an
# `AccountLiquidator` to do the work.
#
@ -72,13 +72,13 @@ class LiquidationProcessor:
self.wallet_balancer: WalletBalancer = wallet_balancer
self.worthwhile_threshold: Decimal = worthwhile_threshold
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.prices_updated_at: datetime = datetime.now()
self.state: LiquidationProcessorState = LiquidationProcessorState.STARTING
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(
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)
@ -127,17 +127,19 @@ class LiquidationProcessor:
time_taken = time.time() - started_at
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]):
to_process = to_liquidate
def _liquidate_all(self, group: Group, prices: typing.Sequence[TokenValue], to_liquidate: typing.Sequence[LiquidatableReport]):
to_process = list(to_liquidate)
while len(to_process) > 0:
highest_first = sorted(to_process,
key=lambda report: report.balance_sheet.assets - report.balance_sheet.liabilities, reverse=True)
# TODO - sort this when LiquidationReport has the proper details for V3.
# 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]
try:
self.account_liquidator.liquidate(highest)
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(
group, prices, updated_margin_account, highest.worthwhile_threshold)
if not (updated_report.state & LiquidatableState.WORTHWHILE):
@ -149,7 +151,7 @@ class LiquidationProcessor:
to_process += [updated_report]
except Exception as exception:
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:
# highest should always be in to_process, but we're outside the try-except block
# 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:
self.market_operations.cancel_order(order)
def fetch_inventory(self) -> typing.List[mango.TokenValue]:
def fetch_inventory(self) -> typing.Sequence[mango.TokenValue]:
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.quote)
@ -140,7 +140,7 @@ class SimpleMarketMaker:
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)
if base_tokens is None:
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
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:
# price_tolerance = order.price * 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.")
@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.")
@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.")
def __repr__(self) -> str:
@ -99,10 +99,10 @@ class NullMarketOperations(MarketOperations):
self.reporter(report)
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 []
def load_my_orders(self) -> typing.List[Order]:
def load_my_orders(self) -> typing.Sequence[Order]:
return []
def __str__(self) -> str:

View File

@ -16,17 +16,17 @@
import typing
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.sysvar import SYSVAR_CLOCK_PUBKEY
from solana.transaction import AccountMeta, TransactionInstruction
from spl.token.constants import TOKEN_PROGRAM_ID
from .account import Account
from .constants import SYSTEM_PROGRAM_ADDRESS
from .context import Context
from .group import Group
from .layouts import layouts
from .mangoaccount import MangoAccount
from .mangogroup import MangoGroup
from .orders import Order, OrderType, Side
from .perpmarket import PerpMarket
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 }
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 }
raw_side: int = 1 if side == Side.SELL else 0
# { 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()
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)
create = create_account(
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
# /// 2. `[signer]` owner_ai - Solana account of owner of the mango account
# /// 3. `[]` rent_ai - Rent sysvar account
@ -169,7 +169,7 @@ def build_create_margin_account_instructions(context: Context, wallet: Wallet, g
# quantity: u64,
# 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
withdraw = TransactionInstruction(
keys=[

View File

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

View File

@ -120,7 +120,7 @@ class PythOracleProvider(OracleProvider):
fixed_usdt = re.sub('USDT$', 'USD', normalised)
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"):
return [f"{symbol}C", f"{symbol}T"]
return [symbol]
@ -141,7 +141,7 @@ class PythOracleProvider(OracleProvider):
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)
all_product_addresses = mapping.products[0:int(mapping.num)]
product_account_infos = AccountInfo.load_multiple(context, all_product_addresses)

View File

@ -35,7 +35,7 @@ class OwnedTokenValue:
self.token_value = token_value
@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]
if len(found) == 0:
raise Exception(f"Owner '{owner}' not found in: {values}")
@ -46,7 +46,7 @@ class OwnedTokenValue:
return found[0]
@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] = []
for before_value in before:
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 .addressableaccount import AddressableAccount
from .context import Context
from .group import Group
from .layouts import layouts
from .mangogroup import MangoGroup
from .metadata import Metadata
from .tokeninfo import TokenInfo
from .version import Version
@ -34,7 +34,7 @@ from .version import Version
class PerpMarket(AddressableAccount):
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,
open_interest: Decimal, quote_lot_size: Decimal, index_oracle: PublicKey,
last_updated: datetime, seq_num: Decimal, contract_size: Decimal
@ -43,7 +43,7 @@ class PerpMarket(AddressableAccount):
self.version: Version = version
self.meta_data: Metadata = meta_data
self.group: MangoGroup = group
self.group: Group = group
self.bids: PublicKey = bids
self.asks: PublicKey = asks
self.event_queue: PublicKey = event_queue
@ -74,7 +74,7 @@ class PerpMarket(AddressableAccount):
self.quote_token: TokenInfo = quote_token
@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)
bids: PublicKey = layout.bids
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)
@staticmethod
def parse(account_info: AccountInfo, group: MangoGroup) -> "PerpMarket":
def parse(account_info: AccountInfo, group: Group) -> "PerpMarket":
data = account_info.data
if len(data) != layouts.PERP_MARKET.sizeof():
raise Exception(
@ -101,7 +101,7 @@ class PerpMarket(AddressableAccount):
return PerpMarket.from_layout(layout, account_info, Version.V1, group)
@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)
if account_info is None:
raise Exception(f"PerpMarket account not found at address '{address}'")

View File

@ -17,13 +17,13 @@
import typing
from decimal import Decimal
from solana.account import Account
from solana.account import Account as SolanaAccount
from solana.publickey import PublicKey
from solana.transaction import Transaction
from .account import Account
from .accountinfo import AccountInfo
from .context import Context
from .mangoaccount import MangoAccount
from .marketoperations import MarketOperations
from .merpsinstructions import build_cancel_perp_order_instructions, build_place_perp_order_instructions
from .orderbookside import OrderBookSide
@ -40,13 +40,13 @@ from .wallet import Wallet
class PerpMarketOperations(MarketOperations):
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):
super().__init__()
self.market_name: str = market_name
self.context: Context = context
self.wallet: Wallet = wallet
self.margin_account: MangoAccount = margin_account
self.margin_account: Account = margin_account
self.perp_market: PerpMarket = perp_market
self.reporter = reporter or (lambda _: None)
@ -55,7 +55,7 @@ class PerpMarketOperations(MarketOperations):
self.logger.info(report)
self.reporter(report)
signers: typing.List[Account] = [self.wallet.account]
signers: typing.Sequence[SolanaAccount] = [self.wallet.account]
transaction = Transaction()
cancel_instructions = build_cancel_perp_order_instructions(
self.context, self.wallet, self.margin_account, self.perp_market, order)
@ -69,7 +69,7 @@ class PerpMarketOperations(MarketOperations):
self.logger.info(report)
self.reporter(report)
signers: typing.List[Account] = [self.wallet.account]
signers: typing.Sequence[SolanaAccount] = [self.wallet.account]
transaction = Transaction()
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)
@ -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)
def load_orders(self) -> typing.List[Order]:
def load_orders(self) -> typing.Sequence[Order]:
bids_address: PublicKey = self.perp_market.bids
asks_address: PublicKey = self.perp_market.asks
[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()]
def load_my_orders(self) -> typing.List[Order]:
def load_my_orders(self) -> typing.Sequence[Order]:
orders = self.load_orders()
mine = []
for order in orders:

View File

@ -42,11 +42,11 @@ from decimal import Decimal
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.name: str = name
self.func: typing.Callable = func
self.pauses: typing.List[Decimal] = pauses
self.pauses: typing.Sequence[Decimal] = pauses
def run(self, *args, **kwargs):
captured_exception: Exception = None
@ -98,5 +98,5 @@ class RetryWithPauses:
# ```
@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)

View File

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

View File

@ -52,7 +52,7 @@ class Token:
return self.symbol.upper() == symbol.upper()
@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)]
if len(found) == 0:
raise Exception(f"Token '{symbol}' not found in token values: {values}")
@ -63,7 +63,7 @@ class Token:
return found[0]
@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]
if len(found) == 0:
raise Exception(f"Token '{mint}' not found in token values: {values}")

View File

@ -30,19 +30,21 @@ from .tokenlookup import TokenLookup
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.token: typing.Optional[Token] = token
self.token: Token = token
self.root_bank: PublicKey = root_bank
self.decimals: Decimal = decimals
def from_layout(layout: layouts.TOKEN_INFO, token_lookup: TokenLookup) -> "TokenInfo":
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 not None:
# if layout.decimals != token.decimals:
# raise Exception(
# f"Conflict between number of decimals in token static data {token.decimals} and group {layout.decimals} for token {token.symbol}.")
if token is None:
raise Exception(f"Token with mint {layout.mint} could not be found.")
if layout.decimals != token.decimals:
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)
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
@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:
reporter(f"{value.value:>18,.8f} {value.token.name}")
@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)]
if len(found) == 0:
raise Exception(f"Token '{symbol}' not found in token values: {values}")
@ -85,7 +85,7 @@ class TokenValue:
return found[0]
@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]
if len(found) == 0:
raise Exception(f"Token '{mint}' not found in token values: {values}")
@ -96,11 +96,11 @@ class TokenValue:
return found[0]
@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)
@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] = []
for before_balance in before:
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:
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_data = instruction_data
self.accounts = accounts
@ -203,7 +203,7 @@ class MangoInstruction:
return additional_data
@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"]
if all_accounts[program_account_index] != context.program_id:
# It's an instruction, it's just not a Mango one.
@ -215,6 +215,8 @@ class MangoInstruction:
decoded = base58.b58decode(data)
initial = layouts.MANGO_INSTRUCTION_VARIANT_FINDER.parse(decoded)
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
# to this instruction. The instruction data gives the index of each account it uses,
@ -244,20 +246,20 @@ class MangoInstruction:
class TransactionScout:
def __init__(self, timestamp: datetime.datetime, signatures: typing.List[str],
succeeded: bool, group_name: str, accounts: typing.List[PublicKey],
instructions: typing.List[typing.Any], messages: typing.List[str],
pre_token_balances: typing.List[OwnedTokenValue],
post_token_balances: typing.List[OwnedTokenValue]):
def __init__(self, timestamp: datetime.datetime, signatures: typing.Sequence[str],
succeeded: bool, group_name: str, accounts: typing.Sequence[PublicKey],
instructions: typing.Sequence[typing.Any], messages: typing.Sequence[str],
pre_token_balances: typing.Sequence[OwnedTokenValue],
post_token_balances: typing.Sequence[OwnedTokenValue]):
self.timestamp: datetime.datetime = timestamp
self.signatures: typing.List[str] = signatures
self.signatures: typing.Sequence[str] = signatures
self.succeeded: bool = succeeded
self.group_name: str = group_name
self.accounts: typing.List[PublicKey] = accounts
self.instructions: typing.List[typing.Any] = instructions
self.messages: typing.List[str] = messages
self.pre_token_balances: typing.List[OwnedTokenValue] = pre_token_balances
self.post_token_balances: typing.List[OwnedTokenValue] = post_token_balances
self.accounts: typing.Sequence[PublicKey] = accounts
self.instructions: typing.Sequence[typing.Any] = instructions
self.messages: typing.Sequence[str] = messages
self.pre_token_balances: typing.Sequence[OwnedTokenValue] = pre_token_balances
self.post_token_balances: typing.Sequence[OwnedTokenValue] = post_token_balances
@property
def summary(self) -> str:
@ -309,7 +311,7 @@ class TransactionScout:
@staticmethod
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"])
account = accounts[balance["accountIndex"]]
amount = Decimal(balance["uiTokenAmount"]["amount"])
@ -352,7 +354,7 @@ class TransactionScout:
raise Exception(f"Exception fetching transaction '{signature}'", exception)
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:
return "None"
return "\n ".join([f"{atv}" for atv in account_token_values])
@ -396,7 +398,7 @@ class TransactionScout:
# # 🥭 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()
recency_cutoff = now - in_the_last
recency_cutoff_timestamp = recency_cutoff.timestamp()

View File

@ -133,7 +133,7 @@ class PercentageTargetBalance(TargetBalance):
#
class TargetBalanceParser:
def __init__(self, tokens: typing.List[Token]):
def __init__(self, tokens: typing.Sequence[Token]):
self.tokens = tokens
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.)
#
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)
@ -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] = []
for desired in desired_balances:
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:
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.prices: typing.Dict[str, TokenValue] = {}
total = Decimal(0)
@ -258,7 +258,7 @@ class FilterSmallChanges:
class WalletBalancer(metaclass=abc.ABCMeta):
@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.")
@ -270,7 +270,7 @@ class WalletBalancer(metaclass=abc.ABCMeta):
class NullWalletBalancer(WalletBalancer):
def balance(self, prices: typing.List[TokenValue]):
def balance(self, prices: typing.Sequence[TokenValue]):
pass
@ -280,17 +280,17 @@ class NullWalletBalancer(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.context: Context = context
self.wallet: Wallet = wallet
self.group: Group = group
self.trade_executor: TradeExecutor = trade_executor
self.action_threshold: Decimal = action_threshold
self.tokens: typing.List[Token] = tokens
self.target_balances: typing.List[TargetBalance] = target_balances
self.tokens: typing.Sequence[Token] = tokens
self.target_balances: typing.Sequence[TargetBalance] = target_balances
def balance(self, prices: typing.List[TokenValue]):
def balance(self, prices: typing.Sequence[TokenValue]):
padding = "\n "
def balances_report(balances) -> str:
@ -323,7 +323,7 @@ class LiveWalletBalancer(WalletBalancer):
updated_balances = self._fetch_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}")
quote = self.group.shared_quote_token.token.symbol
for change in balance_changes:
@ -333,7 +333,7 @@ class LiveWalletBalancer(WalletBalancer):
else:
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] = []
for token in self.tokens:
balance = TokenValue.fetch_total_value(self.context, self.wallet.address, token)

View File

@ -1,5 +1,3 @@
import datetime
from decimal import Decimal
from typing import NamedTuple
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))
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:
context = mango.Context(cluster="test",
cluster_url="http://localhost",
@ -48,13 +51,6 @@ def fake_context() -> mango.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:
Container = NamedTuple("Container", [("own_address", PublicKey), ("vault_signer_nonce", int)])
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
from datetime import datetime
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.
@ -22,52 +21,26 @@ USDC = token_lookup.find_by_symbol_or_raise("USDC")
def mock_group():
account_info = fake_account_info()
name = "FAKE_GROUP"
account_flags = mango.MangoAccountFlags(mango.Version.V1, True, False, True, False)
def index(token):
borrow = mango.TokenValue(token, Decimal(1))
deposit = mango.TokenValue(token, Decimal(1))
return mango.Index(mango.Version.V1, token, datetime.now(), borrow, deposit)
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 = []
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))
usdc_info = mango.TokenInfo(USDC, fake_seeded_public_key("root bank"), Decimal(6))
token_infos = [btc_info, None, usdc_info]
spot_markets = []
perp_markets = []
oracles = []
signer_nonce = Decimal(1)
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")
total_deposits = [
mango.TokenValue(ETH, Decimal(1000)),
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)]
cache_key = fake_seeded_public_key("cache key")
valid_interval = Decimal(7)
return mango.Group(account_info, mango.Version.V1, name, account_flags,
basket_tokens, markets, signer_nonce, signer_key, dex_program_id,
total_deposits, total_borrows, maint_coll_ratio, init_coll_ratio,
srm_vault, admin, borrow_limits)
return mango.Group(account_info, mango.Version.V1, name, meta_data, token_infos,
spot_markets, perp_markets, oracles, signer_nonce, signer_key,
admin_key, dex_program_id, cache_key, valid_interval)
def mock_prices(prices: typing.List[str]):
def mock_prices(prices: typing.Sequence[str]):
eth, btc, sol, srm, usdc = prices
return [
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)):
account_info = fake_account_info()
program_id = fake_seeded_public_key("program ID")
market = fake_seeded_public_key("market")
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,
owner, base_token_free, base_token_total, quote_token_free,
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
asks: bool = True
disabled: bool = True
actual = mango.SerumAccountFlags(mango.Version.V1, initialized, market, open_orders,
request_queue, event_queue, bids, asks, disabled)
actual = mango.AccountFlags(mango.Version.V1, initialized, market, open_orders,
request_queue, event_queue, bids, asks, disabled)
assert actual is not None
assert actual.logger is not None
assert actual.version == mango.Version.V1
@ -24,9 +24,9 @@ def test_constructor():
assert actual.asks == asks
assert actual.disabled == disabled
actual2 = mango.SerumAccountFlags(mango.Version.V2, not initialized, not market,
not open_orders, not request_queue, not event_queue,
not bids, not asks, not disabled)
actual2 = mango.AccountFlags(mango.Version.V2, not initialized, not market,
not open_orders, not request_queue, not event_queue,
not bids, not asks, not disabled)
assert actual2 is not None
assert actual2.logger is not None
assert actual2.version == mango.Version.V2

View File

@ -1,5 +1,4 @@
from .context import mango
from .fakes import fake_context
def test_account_liquidator_constructor():
@ -16,39 +15,3 @@ def test_null_account_liquidator_constructor():
actual = mango.NullAccountLiquidator()
assert actual 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
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 pyserum.enums import OrderType, Side
@ -20,64 +20,6 @@ def test_instruction_builder_constructor():
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():
context: mango.Context = fake_context()
wallet: mango.Wallet = {"fake": "Wallet"}
@ -181,7 +123,7 @@ def test_consume_events_instruction_builder_constructor():
context: mango.Context = fake_context()
wallet: mango.Wallet = {"fake": "Wallet"}
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
actual = mango.ConsumeEventsInstructionBuilder(context, wallet, market, open_orders_addresses, limit)
assert actual is not None

View File

@ -1,406 +1,406 @@
from .context import mango
from .mocks import mock_group, mock_prices, mock_margin_account, mock_open_orders
import typing
from decimal import Decimal
def test_constructor():
group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "1000"]
borrows = ["0", "0", "0", "0", "0"]
margin_account = mock_margin_account(group, deposits, borrows, [])
balance_sheet = margin_account.get_balance_sheet_totals(group, prices)
balances = margin_account.get_intrinsic_balances(group)
state = mango.LiquidatableState.RIPE | mango.LiquidatableState.LIQUIDATABLE | mango.LiquidatableState.ABOVE_WATER
worthwhile_threshold = Decimal(1)
actual = mango.LiquidatableReport(group, prices, margin_account, balance_sheet,
balances, state, worthwhile_threshold)
assert actual is not None
assert actual.logger is not None
assert actual.balance_sheet == balance_sheet
assert actual.balances == balances
assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
assert actual.worthwhile_threshold == worthwhile_threshold
def test_non_ripe_account_no_openorders():
group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "1000"]
borrows = ["0", "0", "0", "0", "0"]
margin_account = mock_margin_account(group, deposits, borrows, [])
worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state == mango.LiquidatableState.UNSET
assert not (actual.state & mango.LiquidatableState.RIPE)
assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
def test_ripe_account_no_openorders():
group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "1000"]
# 170 @ 5 = 850 for a collateral ratio of 1000/850 = 117% - ripe but not liquidatable
borrows = ["0", "0", "0", "170", "0"]
margin_account = mock_margin_account(group, deposits, borrows, [])
worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE
assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert actual.state & mango.LiquidatableState.WORTHWHILE
def test_liquidatable_account_no_openorders():
group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "1000"]
# 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water
borrows = ["0", "0", "0", "200", "0"]
margin_account = mock_margin_account(group, deposits, borrows, [])
worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
def test_above_water_account_no_openorders():
group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "1000"]
# 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.
borrows = ["0", "0", "0", "199.998", "0"]
margin_account = mock_margin_account(group, deposits, borrows, [])
worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
def test_worthwhile_account_no_openorders():
group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "1000"]
# 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.
borrows = ["0", "0", "0", "199.99", "0"]
margin_account = mock_margin_account(group, deposits, borrows, [])
worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert actual.state & mango.LiquidatableState.WORTHWHILE
def test_non_ripe_account_openorders_base():
group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "1000"]
borrows = ["0", "0", "0", "100", "0"]
open_orders: typing.List[mango.OpenOrders] = [None,
None,
None,
mock_open_orders(base_token_total=Decimal(0)),
None]
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET
assert not (actual.state & mango.LiquidatableState.RIPE)
assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert actual.state & mango.LiquidatableState.WORTHWHILE
# 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.
def test_ripe_account_openorders_base():
group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "900"]
borrows = ["0", "0", "0", "200", "0"]
# 1150/1000 = 115% - ripe but not liquidatable
open_orders: typing.List[mango.OpenOrders] = [None,
None,
None,
mock_open_orders(base_token_total=Decimal(50)),
None
]
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE
assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert actual.state & mango.LiquidatableState.WORTHWHILE
def test_liquidatable_account_openorders_base():
group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "900"]
borrows = ["0", "0", "0", "200", "0"]
# 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water
open_orders: typing.List[mango.OpenOrders] = [None,
None,
None,
mock_open_orders(base_token_total=Decimal(20)),
None]
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
def test_above_water_account_openorders_base():
group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "900"]
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
# water but not worthwhile because the $0.01 available is not greater than the whorthwhile_threshold.
open_orders: typing.List[mango.OpenOrders] = [None,
None,
None,
mock_open_orders(base_token_total=Decimal("20.002")),
None]
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
def test_worthwhile_account_openorders_base():
group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "900"]
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
# and worthwhile because the $0.015 available is greater than the whorthwhile_threshold.
open_orders: typing.List[mango.OpenOrders] = [None,
None,
None,
mock_open_orders(base_token_total=Decimal("20.003")),
None]
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert actual.state & mango.LiquidatableState.WORTHWHILE
def test_non_ripe_account_openorders_quote():
group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "1000"]
borrows = ["0", "0", "0", "100", "0"]
open_orders: typing.List[mango.OpenOrders] = [None,
None,
None,
mock_open_orders(quote_token_total=Decimal(0)),
None]
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET
assert not (actual.state & mango.LiquidatableState.RIPE)
assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert actual.state & mango.LiquidatableState.WORTHWHILE
# 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.
def test_ripe_account_openorders_quote():
group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "900"]
borrows = ["0", "0", "0", "200", "0"]
# 1150/1000 = 115% - ripe but not liquidatable
open_orders: typing.List[mango.OpenOrders] = [None,
None,
None,
mock_open_orders(quote_token_total=Decimal(250)),
None
]
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE
assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert actual.state & mango.LiquidatableState.WORTHWHILE
def test_liquidatable_account_openorders_quote():
group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "900"]
borrows = ["0", "0", "0", "200", "0"]
# 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water
open_orders: typing.List[mango.OpenOrders] = [None,
None,
None,
mock_open_orders(quote_token_total=Decimal(100)),
None]
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
def test_above_water_account_openorders_quote():
group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "900"]
borrows = ["0", "0", "0", "200", "0"]
# 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.
open_orders: typing.List[mango.OpenOrders] = [None,
None,
None,
mock_open_orders(quote_token_total=Decimal("100.01")),
None]
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
def test_worthwhile_account_openorders_quote():
group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "900"]
borrows = ["0", "0", "0", "200", "0"]
# 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.
open_orders: typing.List[mango.OpenOrders] = [None,
None,
None,
mock_open_orders(quote_token_total=Decimal(101)),
None]
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert actual.state & mango.LiquidatableState.WORTHWHILE
def test_liquidatable_account_referrer_fee():
group = mock_group()
prices = mock_prices(["2000", "30000", "40", "5", "1"])
deposits = ["0", "0", "0", "0", "900"]
borrows = ["0", "0", "0", "200", "0"]
# 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water
open_orders: typing.List[mango.OpenOrders] = [None,
None,
None,
mock_open_orders(quote_token_total=Decimal(100)),
None]
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
# 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.
open_orders: typing.List[mango.OpenOrders] = [None,
None,
None,
mock_open_orders(quote_token_total=Decimal(100),
referrer_rebate_accrued=Decimal(1)),
None]
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
worthwhile_threshold = Decimal("0.01")
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
assert actual.state != mango.LiquidatableState.UNSET
assert actual.state & mango.LiquidatableState.RIPE
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
assert actual.state & mango.LiquidatableState.ABOVE_WATER
assert actual.state & mango.LiquidatableState.WORTHWHILE
# from .context import mango
# from .mocks import mock_group, mock_prices, mock_margin_account, mock_open_orders
# import typing
# from decimal import Decimal
# def test_constructor():
# group = mock_group()
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
# deposits = ["0", "0", "0", "0", "1000"]
# borrows = ["0", "0", "0", "0", "0"]
# margin_account = mock_margin_account(group, deposits, borrows, [])
# balance_sheet = margin_account.get_balance_sheet_totals(group, prices)
# balances = margin_account.get_intrinsic_balances(group)
# state = mango.LiquidatableState.RIPE | mango.LiquidatableState.LIQUIDATABLE | mango.LiquidatableState.ABOVE_WATER
# worthwhile_threshold = Decimal(1)
# actual = mango.LiquidatableReport(group, prices, margin_account, balance_sheet,
# balances, state, worthwhile_threshold)
# assert actual is not None
# assert actual.logger is not None
# assert actual.balance_sheet == balance_sheet
# assert actual.balances == balances
# assert actual.state != mango.LiquidatableState.UNSET
# assert actual.state & mango.LiquidatableState.RIPE
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
# assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
# assert actual.worthwhile_threshold == worthwhile_threshold
# def test_non_ripe_account_no_openorders():
# group = mock_group()
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
# deposits = ["0", "0", "0", "0", "1000"]
# borrows = ["0", "0", "0", "0", "0"]
# margin_account = mock_margin_account(group, deposits, borrows, [])
# worthwhile_threshold = Decimal("0.01")
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
# assert actual.state == mango.LiquidatableState.UNSET
# assert not (actual.state & mango.LiquidatableState.RIPE)
# assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
# assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
# assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
# def test_ripe_account_no_openorders():
# group = mock_group()
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
# deposits = ["0", "0", "0", "0", "1000"]
# # 170 @ 5 = 850 for a collateral ratio of 1000/850 = 117% - ripe but not liquidatable
# borrows = ["0", "0", "0", "170", "0"]
# margin_account = mock_margin_account(group, deposits, borrows, [])
# worthwhile_threshold = Decimal("0.01")
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
# assert actual.state != mango.LiquidatableState.UNSET
# assert actual.state & mango.LiquidatableState.RIPE
# assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
# assert actual.state & mango.LiquidatableState.WORTHWHILE
# def test_liquidatable_account_no_openorders():
# group = mock_group()
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
# deposits = ["0", "0", "0", "0", "1000"]
# # 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water
# borrows = ["0", "0", "0", "200", "0"]
# margin_account = mock_margin_account(group, deposits, borrows, [])
# worthwhile_threshold = Decimal("0.01")
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
# assert actual.state != mango.LiquidatableState.UNSET
# assert actual.state & mango.LiquidatableState.RIPE
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
# assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
# assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
# def test_above_water_account_no_openorders():
# group = mock_group()
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
# deposits = ["0", "0", "0", "0", "1000"]
# # 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.
# borrows = ["0", "0", "0", "199.998", "0"]
# margin_account = mock_margin_account(group, deposits, borrows, [])
# worthwhile_threshold = Decimal("0.01")
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
# assert actual.state != mango.LiquidatableState.UNSET
# assert actual.state & mango.LiquidatableState.RIPE
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
# assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
# def test_worthwhile_account_no_openorders():
# group = mock_group()
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
# deposits = ["0", "0", "0", "0", "1000"]
# # 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.
# borrows = ["0", "0", "0", "199.99", "0"]
# margin_account = mock_margin_account(group, deposits, borrows, [])
# worthwhile_threshold = Decimal("0.01")
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
# assert actual.state != mango.LiquidatableState.UNSET
# assert actual.state & mango.LiquidatableState.RIPE
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
# assert actual.state & mango.LiquidatableState.WORTHWHILE
# def test_non_ripe_account_openorders_base():
# group = mock_group()
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
# deposits = ["0", "0", "0", "0", "1000"]
# borrows = ["0", "0", "0", "100", "0"]
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
# None,
# None,
# mock_open_orders(base_token_total=Decimal(0)),
# None]
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
# worthwhile_threshold = Decimal("0.01")
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
# assert actual.state != mango.LiquidatableState.UNSET
# assert not (actual.state & mango.LiquidatableState.RIPE)
# assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
# assert actual.state & mango.LiquidatableState.WORTHWHILE
# # 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.
# def test_ripe_account_openorders_base():
# group = mock_group()
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
# deposits = ["0", "0", "0", "0", "900"]
# borrows = ["0", "0", "0", "200", "0"]
# # 1150/1000 = 115% - ripe but not liquidatable
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
# None,
# None,
# mock_open_orders(base_token_total=Decimal(50)),
# None
# ]
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
# worthwhile_threshold = Decimal("0.01")
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
# assert actual.state != mango.LiquidatableState.UNSET
# assert actual.state & mango.LiquidatableState.RIPE
# assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
# assert actual.state & mango.LiquidatableState.WORTHWHILE
# def test_liquidatable_account_openorders_base():
# group = mock_group()
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
# deposits = ["0", "0", "0", "0", "900"]
# borrows = ["0", "0", "0", "200", "0"]
# # 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
# None,
# None,
# mock_open_orders(base_token_total=Decimal(20)),
# None]
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
# worthwhile_threshold = Decimal("0.01")
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
# assert actual.state != mango.LiquidatableState.UNSET
# assert actual.state & mango.LiquidatableState.RIPE
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
# assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
# assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
# def test_above_water_account_openorders_base():
# group = mock_group()
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
# deposits = ["0", "0", "0", "0", "900"]
# 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
# # water but not worthwhile because the $0.01 available is not greater than the whorthwhile_threshold.
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
# None,
# None,
# mock_open_orders(base_token_total=Decimal("20.002")),
# None]
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
# worthwhile_threshold = Decimal("0.01")
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
# assert actual.state != mango.LiquidatableState.UNSET
# assert actual.state & mango.LiquidatableState.RIPE
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
# assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
# def test_worthwhile_account_openorders_base():
# group = mock_group()
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
# deposits = ["0", "0", "0", "0", "900"]
# 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
# # and worthwhile because the $0.015 available is greater than the whorthwhile_threshold.
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
# None,
# None,
# mock_open_orders(base_token_total=Decimal("20.003")),
# None]
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
# worthwhile_threshold = Decimal("0.01")
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
# assert actual.state != mango.LiquidatableState.UNSET
# assert actual.state & mango.LiquidatableState.RIPE
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
# assert actual.state & mango.LiquidatableState.WORTHWHILE
# def test_non_ripe_account_openorders_quote():
# group = mock_group()
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
# deposits = ["0", "0", "0", "0", "1000"]
# borrows = ["0", "0", "0", "100", "0"]
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
# None,
# None,
# mock_open_orders(quote_token_total=Decimal(0)),
# None]
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
# worthwhile_threshold = Decimal("0.01")
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
# assert actual.state != mango.LiquidatableState.UNSET
# assert not (actual.state & mango.LiquidatableState.RIPE)
# assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
# assert actual.state & mango.LiquidatableState.WORTHWHILE
# # 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.
# def test_ripe_account_openorders_quote():
# group = mock_group()
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
# deposits = ["0", "0", "0", "0", "900"]
# borrows = ["0", "0", "0", "200", "0"]
# # 1150/1000 = 115% - ripe but not liquidatable
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
# None,
# None,
# mock_open_orders(quote_token_total=Decimal(250)),
# None
# ]
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
# worthwhile_threshold = Decimal("0.01")
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
# assert actual.state != mango.LiquidatableState.UNSET
# assert actual.state & mango.LiquidatableState.RIPE
# assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
# assert actual.state & mango.LiquidatableState.WORTHWHILE
# def test_liquidatable_account_openorders_quote():
# group = mock_group()
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
# deposits = ["0", "0", "0", "0", "900"]
# borrows = ["0", "0", "0", "200", "0"]
# # 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
# None,
# None,
# mock_open_orders(quote_token_total=Decimal(100)),
# None]
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
# worthwhile_threshold = Decimal("0.01")
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
# assert actual.state != mango.LiquidatableState.UNSET
# assert actual.state & mango.LiquidatableState.RIPE
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
# assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
# assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
# def test_above_water_account_openorders_quote():
# group = mock_group()
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
# deposits = ["0", "0", "0", "0", "900"]
# borrows = ["0", "0", "0", "200", "0"]
# # 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.
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
# None,
# None,
# mock_open_orders(quote_token_total=Decimal("100.01")),
# None]
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
# worthwhile_threshold = Decimal("0.01")
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
# assert actual.state != mango.LiquidatableState.UNSET
# assert actual.state & mango.LiquidatableState.RIPE
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
# assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
# def test_worthwhile_account_openorders_quote():
# group = mock_group()
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
# deposits = ["0", "0", "0", "0", "900"]
# borrows = ["0", "0", "0", "200", "0"]
# # 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.
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
# None,
# None,
# mock_open_orders(quote_token_total=Decimal(101)),
# None]
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
# worthwhile_threshold = Decimal("0.01")
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
# assert actual.state != mango.LiquidatableState.UNSET
# assert actual.state & mango.LiquidatableState.RIPE
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
# assert actual.state & mango.LiquidatableState.WORTHWHILE
# def test_liquidatable_account_referrer_fee():
# group = mock_group()
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
# deposits = ["0", "0", "0", "0", "900"]
# borrows = ["0", "0", "0", "200", "0"]
# # 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
# None,
# None,
# mock_open_orders(quote_token_total=Decimal(100)),
# None]
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
# worthwhile_threshold = Decimal("0.01")
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
# assert actual.state != mango.LiquidatableState.UNSET
# assert actual.state & mango.LiquidatableState.RIPE
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
# assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
# assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
# # 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.
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
# None,
# None,
# mock_open_orders(quote_token_total=Decimal(100),
# referrer_rebate_accrued=Decimal(1)),
# None]
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
# worthwhile_threshold = Decimal("0.01")
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
# assert actual.state != mango.LiquidatableState.UNSET
# assert actual.state & mango.LiquidatableState.RIPE
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
# assert actual.state & mango.LiquidatableState.WORTHWHILE

View File

@ -1,8 +1,8 @@
from .context import mango
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
@ -33,250 +33,250 @@ def test_constructor():
# additional code is common and shared across test functions.
#
class LiquidateMock:
def __init__(self, liquidation_processor: mango.LiquidationProcessor):
self.liquidation_processor = liquidation_processor
self.captured_group: typing.Optional[mango.Group] = None
self.captured_prices: typing.Optional[typing.List[mango.TokenValue]] = None
self.captured_to_liquidate: typing.Optional[typing.List[mango.LiquidatableReport]] = None
# class LiquidateMock:
# def __init__(self, liquidation_processor: mango.LiquidationProcessor):
# self.liquidation_processor = liquidation_processor
# self.captured_group: typing.Optional[mango.Group] = None
# self.captured_prices: typing.Optional[typing.Sequence[mango.TokenValue]] = 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
# a bit more test-friendly.
liquidation_processor._liquidate_all = self.liquidate_capture # type: ignore
# # This monkeypatch is a bit nasty. It would be better to make the LiquidationProcessor
# # a bit more test-friendly.
# 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]):
self.captured_group = group
self.captured_prices = prices
self.captured_to_liquidate = to_liquidate
# def liquidate_capture(self, group: mango.Group, prices: typing.Sequence[mango.TokenValue], to_liquidate: typing.Sequence[mango.LiquidatableReport]):
# self.captured_group = group
# self.captured_prices = prices
# self.captured_to_liquidate = to_liquidate
def capturing_liquidation_processor() -> LiquidateMock:
context: mango.Context = fake_context()
name: str = "Test Liquidator"
account_liquidator: mango.AccountLiquidator = mango.NullAccountLiquidator()
wallet_balancer: mango.WalletBalancer = mango.NullWalletBalancer()
worthwhile_threshold: Decimal = Decimal("0.1")
actual = mango.LiquidationProcessor(context, name, account_liquidator, wallet_balancer, worthwhile_threshold)
# def capturing_liquidation_processor() -> LiquidateMock:
# context: mango.Context = fake_context()
# name: str = "Test Liquidator"
# account_liquidator: mango.AccountLiquidator = mango.NullAccountLiquidator()
# wallet_balancer: mango.WalletBalancer = mango.NullWalletBalancer()
# worthwhile_threshold: Decimal = Decimal("0.1")
# 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]]):
group = mock_group()
capturer = capturing_liquidation_processor()
margin_account = mock_margin_account(group,
deposits,
borrows,
openorders)
for (prices, calculated_balance, liquidatable) in price_iterations:
token_prices = mock_prices(prices)
balance_sheet = margin_account.get_balance_sheet_totals(group, token_prices)
assert balance_sheet.assets - balance_sheet.liabilities == Decimal(calculated_balance)
# 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()
# capturer = capturing_liquidation_processor()
# margin_account = mock_margin_account(group,
# deposits,
# borrows,
# openorders)
# for (prices, calculated_balance, liquidatable) in price_iterations:
# token_prices = mock_prices(prices)
# balance_sheet = margin_account.get_balance_sheet_totals(group, token_prices)
# assert balance_sheet.assets - balance_sheet.liabilities == Decimal(calculated_balance)
capturer.liquidation_processor.update_margin_accounts([margin_account])
capturer.liquidation_processor.update_prices(group, token_prices)
# capturer.liquidation_processor.update_margin_accounts([margin_account])
# capturer.liquidation_processor.update_prices(group, token_prices)
if liquidatable:
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
else:
assert (capturer.captured_to_liquidate is not None) and (len(capturer.captured_to_liquidate) == 0)
# if liquidatable:
# 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
# else:
# assert (capturer.captured_to_liquidate is not None) and (len(capturer.captured_to_liquidate) == 0)
def test_non_liquidatable_account():
# A simple case - no borrows
validate_liquidation_results(
["0", "0", "0", "0", "1000"],
["0", "0", "0", "0", "0"],
[None, None, None, None, None],
[
(
["2000", "30000", "40", "5", "1"],
"1000",
False
)
]
)
# def test_non_liquidatable_account():
# # A simple case - no borrows
# validate_liquidation_results(
# ["0", "0", "0", "0", "1000"],
# ["0", "0", "0", "0", "0"],
# [None, None, None, None, None],
# [
# (
# ["2000", "30000", "40", "5", "1"],
# "1000",
# False
# )
# ]
# )
def test_liquidatable_account():
# A simple case with no currency conversions - 1000 USDC and (somehow) borrowing
# 950 USDC
validate_liquidation_results(
["0", "0", "0", "0", "1000"],
["0", "0", "0", "0", "950"],
[None, None, None, None, None],
[
(
["2000", "30000", "40", "5", "1"],
"50",
True
)
]
)
# def test_liquidatable_account():
# # A simple case with no currency conversions - 1000 USDC and (somehow) borrowing
# # 950 USDC
# validate_liquidation_results(
# ["0", "0", "0", "0", "1000"],
# ["0", "0", "0", "0", "950"],
# [None, None, None, None, None],
# [
# (
# ["2000", "30000", "40", "5", "1"],
# "50",
# True
# )
# ]
# )
def test_converted_balance_not_liquidatable():
# A more realistic case. 1000 USDC, borrowed 180 SRM now @ 5 USDC
validate_liquidation_results(
["0", "0", "0", "0", "1000"],
["0", "0", "0", "180", "0"],
[None, None, None, None, None],
[
(
["2000", "30000", "40", "5", "1"],
"100",
False
)
]
)
# def test_converted_balance_not_liquidatable():
# # A more realistic case. 1000 USDC, borrowed 180 SRM now @ 5 USDC
# validate_liquidation_results(
# ["0", "0", "0", "0", "1000"],
# ["0", "0", "0", "180", "0"],
# [None, None, None, None, None],
# [
# (
# ["2000", "30000", "40", "5", "1"],
# "100",
# False
# )
# ]
# )
def test_converted_balance_liquidatable():
# 1000 USDC, borrowed 190 SRM now @ 5 USDC
validate_liquidation_results(
["0", "0", "0", "0", "1000"],
["0", "0", "0", "190", "0"],
[None, None, None, None, None],
[
(
["2000", "30000", "40", "5", "1"],
"50",
True
)
]
)
# def test_converted_balance_liquidatable():
# # 1000 USDC, borrowed 190 SRM now @ 5 USDC
# validate_liquidation_results(
# ["0", "0", "0", "0", "1000"],
# ["0", "0", "0", "190", "0"],
# [None, None, None, None, None],
# [
# (
# ["2000", "30000", "40", "5", "1"],
# "50",
# True
# )
# ]
# )
def test_converted_balance_not_liquidatable_becomes_liquidatable_on_price_change():
# 1000 USDC, borrowed 180 SRM @ 5 USDC, price goes to 5.2 USDC
validate_liquidation_results(
["0", "0", "0", "0", "1000"],
["0", "0", "0", "180", "0"],
[None, None, None, None, None],
[
(
["2000", "30000", "40", "5", "1"],
"100",
False
),
(
["2000", "30000", "40", "5.2", "1"],
"64",
True
)
]
)
# def test_converted_balance_not_liquidatable_becomes_liquidatable_on_price_change():
# # 1000 USDC, borrowed 180 SRM @ 5 USDC, price goes to 5.2 USDC
# validate_liquidation_results(
# ["0", "0", "0", "0", "1000"],
# ["0", "0", "0", "180", "0"],
# [None, None, None, None, None],
# [
# (
# ["2000", "30000", "40", "5", "1"],
# "100",
# False
# ),
# (
# ["2000", "30000", "40", "5.2", "1"],
# "64",
# True
# )
# ]
# )
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),
# 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
# 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
# appear in the next round as non-liquidatable.
validate_liquidation_results(
["0", "0", "0", "0", "1000"],
["0", "0", "0", "180", "0"],
[None, None, None, None, None],
[
(
["2000", "30000", "40", "5.2", "1"],
"64",
True
),
(
["2000", "30000", "40", "5", "1"],
"100",
False
)
]
)
# 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),
# # 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
# # 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
# # appear in the next round as non-liquidatable.
# validate_liquidation_results(
# ["0", "0", "0", "0", "1000"],
# ["0", "0", "0", "180", "0"],
# [None, None, None, None, None],
# [
# (
# ["2000", "30000", "40", "5.2", "1"],
# "64",
# True
# ),
# (
# ["2000", "30000", "40", "5", "1"],
# "100",
# False
# )
# ]
# )
def test_open_orders_balance_not_liquidatable():
# SRM OO account has 10 SRM in it
validate_liquidation_results(
["0", "0", "0", "0", "1000"],
["0", "0", "0", "190", "0"],
[None, None, None, mock_open_orders(base_token_total=Decimal(10)), None],
[
(
["2000", "30000", "40", "5", "1"],
"100",
False
)
]
)
# def test_open_orders_balance_not_liquidatable():
# # SRM OO account has 10 SRM in it
# validate_liquidation_results(
# ["0", "0", "0", "0", "1000"],
# ["0", "0", "0", "190", "0"],
# [None, None, None, mock_open_orders(base_token_total=Decimal(10)), None],
# [
# (
# ["2000", "30000", "40", "5", "1"],
# "100",
# False
# )
# ]
# )
def test_open_orders_balance_liquidatable():
# SRM OO account has only 9 SRM in it.
# Assets (1045) / Liabiities (950) = collateral ratio of exactly 1.1
validate_liquidation_results(
["0", "0", "0", "0", "1000"],
["0", "0", "0", "190", "0"],
[None, None, None, mock_open_orders(base_token_total=Decimal(9)), None],
[
(
["2000", "30000", "40", "5", "1"],
"95",
True
)
]
)
# def test_open_orders_balance_liquidatable():
# # SRM OO account has only 9 SRM in it.
# # Assets (1045) / Liabiities (950) = collateral ratio of exactly 1.1
# validate_liquidation_results(
# ["0", "0", "0", "0", "1000"],
# ["0", "0", "0", "190", "0"],
# [None, None, None, mock_open_orders(base_token_total=Decimal(9)), None],
# [
# (
# ["2000", "30000", "40", "5", "1"],
# "95",
# True
# )
# ]
# )
def test_open_orders_referral_fee_not_liquidatable():
# 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
# margin account is liquidatable.
validate_liquidation_results(
["0", "0", "0", "0", "1000"],
["0", "0", "0", "190", "0"],
[None, None, None, mock_open_orders(base_token_total=Decimal(9), referrer_rebate_accrued=Decimal("0.1")), None],
[
(
["2000", "30000", "40", "5", "1"],
"95.1", # The 0.1 referrer rebate is the difference between non-liquidation and iquidation.
False
)
]
)
# def test_open_orders_referral_fee_not_liquidatable():
# # 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
# # margin account is liquidatable.
# validate_liquidation_results(
# ["0", "0", "0", "0", "1000"],
# ["0", "0", "0", "190", "0"],
# [None, None, None, mock_open_orders(base_token_total=Decimal(9), referrer_rebate_accrued=Decimal("0.1")), None],
# [
# (
# ["2000", "30000", "40", "5", "1"],
# "95.1", # The 0.1 referrer rebate is the difference between non-liquidation and iquidation.
# False
# )
# ]
# )
def test_open_orders_bigger_referral_fee_not_liquidatable():
# 900 USDC + 100.1 USDC referrer rebate should be equivalent to the above
# test_open_orders_referral_fee_not_liquidatable test.
validate_liquidation_results(
["0", "0", "0", "0", "900"],
["0", "0", "0", "190", "0"],
[None, None, None, mock_open_orders(base_token_total=Decimal(
9), referrer_rebate_accrued=Decimal("100.1")), None],
[
(
["2000", "30000", "40", "5", "1"],
"95.1", # 0.1 of the referrer rebate is the difference between non-liquidation and iquidation.
False
)
]
)
# def test_open_orders_bigger_referral_fee_not_liquidatable():
# # 900 USDC + 100.1 USDC referrer rebate should be equivalent to the above
# # test_open_orders_referral_fee_not_liquidatable test.
# validate_liquidation_results(
# ["0", "0", "0", "0", "900"],
# ["0", "0", "0", "190", "0"],
# [None, None, None, mock_open_orders(base_token_total=Decimal(
# 9), referrer_rebate_accrued=Decimal("100.1")), None],
# [
# (
# ["2000", "30000", "40", "5", "1"],
# "95.1", # 0.1 of the referrer rebate is the difference between non-liquidation and iquidation.
# False
# )
# ]
# )
def test_open_orders_bigger_referral_fee_liquidatable():
# 900 USDC + 100.1 USDC referrer rebate should be equivalent to the above
# test_open_orders_referral_fee_not_liquidatable test.
validate_liquidation_results(
["0", "0", "0", "0", "900"],
["0", "0", "0", "190", "0"],
[None, None, None, mock_open_orders(base_token_total=Decimal(
9), referrer_rebate_accrued=Decimal("100")), None],
[
(
["2000", "30000", "40", "5", "1"],
"95", # Just 0.1 more in the referrer rebate would make it non-liquidatable.
True
)
]
)
# def test_open_orders_bigger_referral_fee_liquidatable():
# # 900 USDC + 100.1 USDC referrer rebate should be equivalent to the above
# # test_open_orders_referral_fee_not_liquidatable test.
# validate_liquidation_results(
# ["0", "0", "0", "0", "900"],
# ["0", "0", "0", "190", "0"],
# [None, None, None, mock_open_orders(base_token_total=Decimal(
# 9), referrer_rebate_accrued=Decimal("100")), None],
# [
# (
# ["2000", "30000", "40", "5", "1"],
# "95", # Just 0.1 more in the referrer rebate would make it non-liquidatable.
# 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()
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,
owner, 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():
name: str = "Test"
func: typing.Callable = lambda: 1
pauses: typing.List[Decimal] = [Decimal(2)]
pauses: typing.Sequence[Decimal] = [Decimal(2)]
actual = mango.RetryWithPauses(name, func, pauses)
assert actual 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.
# The retrier only pauses if an exception is raised.
pauses: typing.List[Decimal] = [Decimal(0)]
pauses: typing.Sequence[Decimal] = [Decimal(0)]
class FuncScope:
called: int = 0
@ -48,7 +48,7 @@ def test_1_retry():
# Number of retries is the number of pauses - 1.
# 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:
called: int = 0
@ -73,7 +73,7 @@ def test_3_retries():
# Number of retries is the number of pauses - 1.
# 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:
called: int = 0
@ -98,7 +98,7 @@ def test_with_context():
# Number of retries is the number of pauses - 1.
# 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:
called: int = 0

View File

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